From f0e52061e34f3a397dbe1120590d062feb1be0ef Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 5 Sep 2022 13:11:36 -0500 Subject: fix ComparableListWatcher not removing items in zero case --- .../Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs index 0b13434a..b24f4178 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs @@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { if (this.LastValues.Count > 0) { - this.AddedImpl.AddRange(this.LastValues); + this.RemovedImpl.AddRange(this.LastValues); this.LastValues.Clear(); } return; -- cgit From 715b9b09bae846e1f199ad2271283940c8fce7bd Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Sun, 18 Sep 2022 12:05:46 -0400 Subject: Update ModScanner.cs Add a few more files to the ignored files like .7z --- src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index a85ef109..d115810a 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -45,10 +45,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ".png", ".psd", ".tif", + ".xcf", // gimp files // archives ".rar", ".zip", + ".7z", + ".tar", + ".tar.gz" // backup files ".backup", -- cgit From e8da8fff5163eacd6ae7870eaa8c7dbc8285e3e7 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Mon, 26 Sep 2022 15:18:36 -0400 Subject: Initial work on a way for mods to return specific API instances to specific mods. --- .../Framework/ModHelpers/ModRegistryHelper.cs | 53 +++++++++++++++++++--- src/SMAPI/IMod.cs | 6 +++ src/SMAPI/Mod.cs | 6 +++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 348ba225..9ad3e3ae 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModHelpers { @@ -15,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Encapsulates monitoring and logging for the mod. private readonly IMonitor Monitor; - /// The mod IDs for APIs accessed by this instanced. - private readonly HashSet AccessedModApis = new(); + /// The APIs accessed by this instance. + private readonly Dictionary AccessedModApis = new(); /// Generates proxy classes to access mod APIs through an arbitrary interface. private readonly IInterfaceProxyFactory ProxyFactory; @@ -66,11 +68,50 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - // get raw API + // get our cached API if one is available IModMetadata? mod = this.Registry.Get(uniqueID); - if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) - this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); - return mod?.Api; + if (mod == null) + return null; + + if (this.AccessedModApis.ContainsKey(mod.Manifest.UniqueID)) + { + return this.AccessedModApis[mod.Manifest.UniqueID]; + } + + object? api; + + // safely request a specific API instance + try + { + api = mod.Mod?.GetApi(this.Mod.Manifest); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{mod.DisplayName} provided a specific API instance with a non-public type. This isn't currently supported, so the specific API won't be available to the requesting mod.", LogLevel.Warn); + } + + if (api != null) + this.Monitor.Log($"Accessed specific mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}."); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading specific mod-provided API for {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + api = null; + } + + // fall back to the generic API instance + if (api == null) + { + api = mod.Api; + if (api != null) + { + this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); + } + } + + // cache the API instance and return it + this.AccessedModApis[mod.Manifest.UniqueID] = api; + return api; } /// diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index b81ba0e3..6041bf66 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -25,5 +25,11 @@ namespace StardewModdingAPI /// Get an API that other mods can access. This is always called after . object? GetApi(); + + /// Get an API that a specific other mod can access. This method is called the first time the other mod calls for this mod. + /// The other mod's manifest. + /// Returns an API for another mod, or null if the other mod should use the general API returned from . + object? GetApi(IManifest manifest); + } } diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index f764752b..1a5f5594 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -30,6 +30,12 @@ namespace StardewModdingAPI return null; } + /// + public virtual object? GetApi(IManifest manifest) + { + return null; + } + /// Release or reset unmanaged resources. public void Dispose() { -- cgit From c0e31d17a6d3f235c8a251e851c446e00c804cdb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 28 Sep 2022 23:21:12 -0400 Subject: fix handling of GitHub prerelease versions marked as non-prerelease --- docs/release-notes.md | 4 ++++ src/SMAPI.Web/Controllers/ModsApiController.cs | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index ab23b46e..ea459bcb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,10 @@ _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._ --> +## Upcoming release +* For players: + * Fixed update alert shown for a prerelease version on GitHub if it's not marked as prerelease. + ## 3.16.2 Released 31 August 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 401bba4f..71fb42c2 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -159,11 +159,20 @@ namespace StardewModdingAPI.Web.Controllers continue; } + // if there's only a prerelease version (e.g. from GitHub), don't override the main version + ISemanticVersion? curMain = data.Version; + ISemanticVersion? curPreview = data.PreviewVersion; + if (curPreview == null && curMain?.IsPrerelease() == true) + { + curPreview = curMain; + curMain = null; + } + // handle versions - if (this.IsNewer(data.Version, main?.Version)) - main = new ModEntryVersionModel(data.Version, data.Url!); - if (this.IsNewer(data.PreviewVersion, optional?.Version)) - optional = new ModEntryVersionModel(data.PreviewVersion, data.Url!); + if (this.IsNewer(curMain, main?.Version)) + main = new ModEntryVersionModel(curMain, data.Url!); + if (this.IsNewer(curPreview, optional?.Version)) + optional = new ModEntryVersionModel(curPreview, data.Url!); } // get unofficial version -- cgit From c6b3446e9cb1d1e02db9db86f143ecfe75e9908c Mon Sep 17 00:00:00 2001 From: pizzaoverhead Date: Thu, 29 Sep 2022 13:33:45 +0100 Subject: Added checking for alternative Steam library install locations when looking for the Stardew Valley install. --- .../Framework/GameScanning/GameScanner.cs | 36 +++++++++++++++++ .../GameScanning/SteamLibraryCollection.cs | 47 ++++++++++++++++++++++ src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 1 + 3 files changed, 84 insertions(+) create mode 100644 src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 8e1538a5..8e24dcdf 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.Toolkit.Utilities; using System.Reflection; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; +using VdfParser; #endif namespace StardewModdingAPI.Toolkit.Framework.GameScanning @@ -158,7 +159,14 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning // via Steam library path string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); if (steamPath != null) + { yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); + + // Check for Steam libraries in other locations + string? path = this.GetPathFromSteamLibrary(steamPath); + if (!string.IsNullOrWhiteSpace(path)) + yield return path; + } #endif // default GOG/Steam paths @@ -243,6 +251,34 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning using (openKey) return (string?)openKey.GetValue(name); } + + /// Get the game directory path from alternative Steam library locations. + /// The full path to the directory containing steam.exe. + /// The game directory, if found. + private string? GetPathFromSteamLibrary(string? steamPath) + { + string stardewAppId = "413150"; + if (steamPath != null) + { + string? libraryFoldersPath = Path.Combine(steamPath.Replace('/', '\\'), "steamapps\\libraryfolders.vdf"); + using FileStream fs = File.OpenRead(libraryFoldersPath); + VdfDeserializer deserializer = new VdfDeserializer(); + SteamLibraryCollection libraries = deserializer.Deserialize(fs); + if (libraries.libraryfolders != null) + { + var stardewLibrary = libraries.libraryfolders.FirstOrDefault(f => + { + var apps = f.Value?.apps; + return apps != null && apps.Any(a => a.Key.Equals(stardewAppId)); + }); + if (stardewLibrary.Value?.path != null) + { + return Path.Combine(stardewLibrary.Value.path.Replace("\\\\", "\\"), @"steamapps\common\Stardew Valley"); + } + } + } + return null; + } #endif } } diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs b/src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs new file mode 100644 index 00000000..7a186f69 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs @@ -0,0 +1,47 @@ +#if SMAPI_FOR_WINDOWS +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.GameScanning +{ +#pragma warning disable IDE1006 // Model requires lowercase naming. +#pragma warning disable CS8618 // Required for model. + /// Model for Steam's libraryfolders.vdf. + public class SteamLibraryCollection + { + /// Each entry identifies a different location that part of the Steam games library is installed to. + public LibraryFolders libraryfolders { get; set; } + } + + /// A collection of LibraryFolders. Like a dictionary, but has contentstatsid used as an index also. + /// + /// +#pragma warning disable CS8714 // Required for model. + public class LibraryFolders : Dictionary +#pragma warning restore CS8714 + { + /// Index of the library, starting from "0". + public string contentstatsid { get; set; } + } + + /// A Steam library folder, containing information on the location and size of games installed there. + public class LibraryFolder + { + /// The escaped path to this Steam library folder. There will be a steam.exe here, but this may not be the one the player generally launches. + public string path { get; set; } + /// Label for the library, or "" + public string label { get; set; } + /// ~19-digit identifier. + public string contentid { get; set; } + /// Size of the library in bytes. May show 0 when size is non-zero. + public string totalsize { get; set; } + /// Used for downloads. + public string update_clean_bytes_tally { get; set; } + /// Normally "0". + public string time_last_update_corruption { get; set; } + /// List of Steam app IDs, and their current size in bytes. + public Dictionary apps { get; set; } + } +#pragma warning restore IDE1006 +#pragma warning restore CS8618 +} +#endif diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 7b79105f..411fd469 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -14,6 +14,7 @@ + -- cgit From 2c2542657890d896750384bbaa2cb765379bad06 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Oct 2022 00:16:00 -0400 Subject: fix issues with BundleExtraAssemblies --- docs/technical/mod-package.md | 2 ++ src/SMAPI.ModBuildConfig/build/smapi.targets | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index ca78be55..c483754e 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -414,6 +414,8 @@ when you compile it. ## Release notes ### Upcoming release * Switched to the newer crossplatform `portable` debug symbols (thanks to lanturnalis!). +* Fixed `BundleExtraAssemblies` option being partly case-sensitive. +* Fixed `BundleExtraAssemblies` not applying `All` value to game assemblies. ### 4.0.1 Released 14 April 2022. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 12619439..b4fd312e 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -27,8 +27,12 @@ true + + <_BundleExtraAssembliesForGame>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|All\b', RegexOptions.IgnoreCase)) + <_BundleExtraAssembliesForAny>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|System|ThirdParty|All\b', RegexOptions.IgnoreCase)) + - true + true @@ -44,17 +48,17 @@ **********************************************--> - - - - + + + + - - + + - + -- cgit From 5a0d337fcf6d18eed55334361b3eef3021912498 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Oct 2022 00:21:09 -0400 Subject: update FluentHttpClient --- docs/release-notes.md | 3 +++ src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 2 +- src/SMAPI/SMAPI.csproj | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index ea459bcb..04874729 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,9 @@ * For players: * Fixed update alert shown for a prerelease version on GitHub if it's not marked as prerelease. +* For mod authors: + * Updated to [FluentHttpClient](https://github.com/Pathoschild/FluentHttpClient#readme) 4.2.0 (see [changes](https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#420)). + ## 3.16.2 Released 31 August 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 7b79105f..bd4f4e3d 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index d26cb6f8..81b187fe 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 36db0545..e5d8937c 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -26,7 +26,7 @@ - + -- cgit From a7f03abe25128dba78d8c22802370a3f9a8aff11 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 13:16:38 -0400 Subject: change square brackets to round ones in manifest name --- docs/release-notes.md | 1 + src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 53 +++++++++++++++++----- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 04874729..4875d1cd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Fixed update alert shown for a prerelease version on GitHub if it's not marked as prerelease. * For mod authors: + * SMAPI now treats square brackets in the manifest `Name` field as round brackets, to avoid breaking tools which parse log files. * Updated to [FluentHttpClient](https://github.com/Pathoschild/FluentHttpClient#readme) 4.2.0 (see [changes](https://github.com/Pathoschild/FluentHttpClient/blob/develop/RELEASE-NOTES.md#420)). ## 3.16.2 diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index da3ad608..8a449f0a 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; @@ -90,13 +91,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models [JsonConstructor] public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys) { - this.UniqueID = this.NormalizeWhitespace(uniqueId); - this.Name = this.NormalizeWhitespace(name); - this.Author = this.NormalizeWhitespace(author); - this.Description = this.NormalizeWhitespace(description); + this.UniqueID = this.NormalizeField(uniqueId); + this.Name = this.NormalizeField(name, replaceSquareBrackets: true); + this.Author = this.NormalizeField(author); + this.Description = this.NormalizeField(description); this.Version = version; this.MinimumApiVersion = minimumApiVersion; - this.EntryDll = this.NormalizeWhitespace(entryDll); + this.EntryDll = this.NormalizeField(entryDll); this.ContentPackFor = contentPackFor; this.Dependencies = dependencies ?? Array.Empty(); this.UpdateKeys = updateKeys ?? Array.Empty(); @@ -113,17 +114,47 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models /********* ** Private methods *********/ - /// Normalize whitespace in a raw string. + /// Normalize a manifest field to strip newlines, trim whitespace, and optionally strip square brackets. /// The input to strip. + /// Whether to replace square brackets with round ones. This is used in the mod name to avoid breaking the log format. #if NET5_0_OR_GREATER [return: NotNullIfNotNull("input")] #endif - private string? NormalizeWhitespace(string? input) + private string? NormalizeField(string? input, bool replaceSquareBrackets = false) { - return input - ?.Trim() - .Replace("\r", "") - .Replace("\n", ""); + input = input?.Trim(); + + if (!string.IsNullOrEmpty(input)) + { + StringBuilder? builder = null; + + for (int i = 0; i < input.Length; i++) + { + switch (input[i]) + { + case '\r': + case '\n': + builder ??= new StringBuilder(input); + builder[i] = ' '; + break; + + case '[' when replaceSquareBrackets: + builder ??= new StringBuilder(input); + builder[i] = '('; + break; + + case ']' when replaceSquareBrackets: + builder ??= new StringBuilder(input); + builder[i] = ')'; + break; + } + } + + if (builder != null) + input = builder.ToString(); + } + + return input; } } } -- cgit From 7c90385d8df7bbf9469fc468480b26ebb134abd8 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 15 Aug 2022 19:13:24 -0400 Subject: Pre-calculate the strings for log levels. --- src/SMAPI/Framework/Monitor.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 6b53daff..8ba175e6 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -25,7 +25,9 @@ namespace StardewModdingAPI.Framework private readonly LogFileManager LogFile; /// The maximum length of the values. - private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); + private static readonly int MaxLevelLength = (from level in Enum.GetValues() select level.ToString().Length).Max(); + + private static readonly Dictionary LogStrings = Enum.GetValues().ToDictionary(k => k, v => v.ToString().ToUpper().PadRight(MaxLevelLength)); /// A cache of messages that should only be logged once. private readonly HashSet LogOnceCache = new(); @@ -147,7 +149,7 @@ namespace StardewModdingAPI.Framework /// The log level. private string GenerateMessagePrefix(string source, ConsoleLogLevel level) { - string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); + string levelStr = LogStrings[level]; int? playerIndex = this.GetScreenIdForLog(); return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]"; -- cgit From 78643710ce09197dbb5505fd8cc2361c8ada0830 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 15 Aug 2022 19:13:39 -0400 Subject: Use array pools in editing images. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 43 +++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 3393b22f..98d6725a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -47,44 +48,55 @@ namespace StardewModdingAPI.Framework.Content int areaHeight = sourceArea.Value.Height; 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); + } else { - sourceData = new Color[areaWidth * areaHeight]; - int i = 0; - for (int y = areaY, maxY = areaY + areaHeight - 1; y <= maxY; y++) + int pixelCount = areaWidth * areaHeight; + sourceData = ArrayPool.Shared.Rent(pixelCount); + + for (int y = areaY, maxY = areaY + areaHeight; 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]; - } + // avoiding an variable that increments allows the processor to re-arrange here. + 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); + + // return + ArrayPool.Shared.Return(sourceData); } } - - // 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) { + // validate + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + 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."); // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; - Color[] sourceData = GC.AllocateUninitializedArray(pixelCount); + 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); } /// @@ -143,7 +155,7 @@ namespace StardewModdingAPI.Framework.Content if (patchMode == PatchMode.Overlay) { // get target data - Color[] mergedData = GC.AllocateUninitializedArray(pixelCount); + Color[] mergedData = ArrayPool.Shared.Rent(pixelCount); target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels @@ -175,6 +187,7 @@ namespace StardewModdingAPI.Framework.Content } target.SetData(0, targetArea, mergedData, 0, pixelCount); + ArrayPool.Shared.Return(mergedData); } else target.SetData(0, targetArea, sourceData, 0, pixelCount); -- cgit 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 --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 15 ++++---- .../Framework/ContentManagers/ModContentManager.cs | 40 ++++++++++++++-------- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 7 ++-- 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 98d6725a..46c2a22e 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -33,12 +33,12 @@ namespace StardewModdingAPI.Framework.Content /// public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); - - // validate source data + // nullcheck if (source == null) throw new ArgumentNullException(nameof(source), "Can't patch from null source data."); + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + // get the pixels for the source area Color[] sourceData; { @@ -59,7 +59,6 @@ namespace StardewModdingAPI.Framework.Content for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) { - // avoiding an variable that increments allows the processor to re-arrange here. int sourceIndex = (y * source.Width) + areaX; int targetIndex = (y - areaY) * areaWidth; Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); @@ -77,13 +76,13 @@ namespace StardewModdingAPI.Framework.Content /// public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // validate + // nullcheck if (source == null) throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); - // validate source texture + // 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."); @@ -161,8 +160,8 @@ namespace StardewModdingAPI.Framework.Content // merge pixels for (int i = 0; i < pixelCount; i++) { - Color above = sourceData[i]; - Color below = mergedData[i]; + ref Color above = ref sourceData[i]; + ref Color below = ref mergedData[i]; // shortcut transparency if (above.A < MinOpacity) 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; } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 01037870..ae08d972 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// public static Assembly? ResolveAssembly(string name) { - string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) + string shortName = name.Split(',', 2).First(); // get simple name (without version and culture) return AppDomain.CurrentDomain .GetAssemblies() .FirstOrDefault(p => p.GetName().Name == shortName); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 8ba175e6..d33bf259 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -27,10 +27,13 @@ namespace StardewModdingAPI.Framework /// The maximum length of the values. private static readonly int MaxLevelLength = (from level in Enum.GetValues() select level.ToString().Length).Max(); + /// 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); + /// 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; @@ -86,7 +89,7 @@ namespace StardewModdingAPI.Framework /// public void LogOnce(string message, LogLevel level = LogLevel.Trace) { - if (this.LogOnceCache.Add($"{message}|{level}")) + if (this.LogOnceCache.Add(new LogOnceCacheEntry(message, level))) this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } -- cgit From 581763c36392e28ed6e05690ff84b66da5882e78 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:46:10 -0400 Subject: Skip math if above is fully opaque. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 46c2a22e..ea04f57a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -160,13 +160,13 @@ namespace StardewModdingAPI.Framework.Content // merge pixels for (int i = 0; i < pixelCount; i++) { - ref Color above = ref sourceData[i]; - ref Color below = ref mergedData[i]; + Color above = sourceData[i]; + Color below = mergedData[i]; // shortcut transparency if (above.A < MinOpacity) continue; - if (below.A < MinOpacity) + if (below.A < MinOpacity || above.A == byte.MaxValue) mergedData[i] = above; // merge pixels -- 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(-) 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(+) 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(-) 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/Cont