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(-) (limited to 'src') 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(+) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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 (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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') 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') 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 ff523c619a040c8eb2d2f1c1a0d19c8fe83f1c75 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Fri, 19 Aug 2022 11:10:19 -0400 Subject: fix fast-track array copying --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 5f175217..9c71328f 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.Content int sourceIndex = areaY * areaWidth; int targetIndex = 0; - Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, pixelCount); } else -- cgit From ce63efa2f45ee770fdb628a45f5a6b63544b0031 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:32:45 -0400 Subject: Avoid making copy if the source image is just taller than the sourceArea. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 9c71328f..bcdebff6 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -47,10 +47,12 @@ namespace StardewModdingAPI.Framework.Content int areaWidth = sourceArea.Value.Width; int areaHeight = sourceArea.Value.Height; - if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) + if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight <= source.Height) { + // 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); + this.PatchImageImpl(sourceData, areaWidth, areaHeight, sourceArea.Value, targetArea.Value, patchMode); } else { -- cgit From c1d5d19e43a9305ebf71696408fc8a0777794f55 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:35:08 -0400 Subject: Skip transparent rows at the start and end when doing a patch overlay. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 51 +++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index bcdebff6..90468316 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -166,9 +167,51 @@ namespace StardewModdingAPI.Framework.Content if (sourceArea.Size != targetArea.Size) throw new InvalidOperationException("The source and target areas must be the same size."); - // merge data - if (patchMode == PatchMode.Overlay) + if (patchMode == PatchMode.Replace) + target.SetData(0, targetArea, sourceData, 0, 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. + + int startIndex = -1; + for (int i = 0; i < pixelCount; i++) + { + if (sourceData[i].A >= MinOpacity) + { + startIndex = i; + break; + } + } + + if (startIndex == -1) + return; + + int endIndex = -1; + for (int i = pixelCount - 1; i >= startIndex; i--) + { + if (sourceData[i].A >= MinOpacity) + { + endIndex = i; + break; + } + } + + if (endIndex == -1) + return; + + 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); + + pixelCount = targetArea.Width * targetArea.Height; + + int sourceoffset = topoffset * sourceArea.Width; + // get target data Color[] mergedData = ArrayPool.Shared.Rent(pixelCount); target.GetData(0, targetArea, mergedData, 0, pixelCount); @@ -177,7 +220,7 @@ namespace StardewModdingAPI.Framework.Content for (int i = 0; i < pixelCount; i++) { // ref locals here? Not sure. - Color above = sourceData[i]; + Color above = sourceData[sourceoffset + i]; Color below = mergedData[i]; // shortcut transparency @@ -205,8 +248,6 @@ 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 09fd12ddfe89c5d36b1db66167dd741e4f8a7e0f Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 22 Aug 2022 23:44:07 -0400 Subject: use startindex/endindex since I've already calculated those... --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 90468316..70fa369f 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -207,7 +207,6 @@ namespace StardewModdingAPI.Framework.Content // Update target rectangle targetArea = new(targetArea.X, targetArea.Y + topoffset, targetArea.Width, bottomoffset - topoffset + 1); - pixelCount = targetArea.Width * targetArea.Height; int sourceoffset = topoffset * sourceArea.Width; @@ -217,11 +216,11 @@ namespace StardewModdingAPI.Framework.Content target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels - for (int i = 0; i < pixelCount; i++) + for (int i = startIndex; i <= endIndex; i++) { // ref locals here? Not sure. - Color above = sourceData[sourceoffset + i]; - Color below = mergedData[i]; + Color above = sourceData[i]; + Color below = mergedData[i - sourceoffset]; // shortcut transparency if (above.A < MinOpacity) -- cgit From a3b8546ec8975ef1941e7618cef773c41d5c423f Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Mon, 22 Aug 2022 23:50:52 -0400 Subject: cleanup and comments --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 70fa369f..f00d2d4c 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -187,7 +187,7 @@ namespace StardewModdingAPI.Framework.Content } if (startIndex == -1) - return; + return; // apparently a completely blank texture? int endIndex = -1; for (int i = pixelCount - 1; i >= startIndex; i--) @@ -200,8 +200,9 @@ namespace StardewModdingAPI.Framework.Content } if (endIndex == -1) - return; + return; // should never happen + // Calculate new Y bounds int topoffset = startIndex / sourceArea.Width; int bottomoffset = endIndex / sourceArea.Width; -- cgit From 496c438be244c6e51f34cbcf238913edf55a8618 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Tue, 23 Aug 2022 14:34:23 -0400 Subject: fix indexing again, because apparently I'm bad at math now? --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index f00d2d4c..2bbcc60c 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -219,15 +219,17 @@ namespace StardewModdingAPI.Framework.Content // merge pixels for (int i = startIndex; i <= endIndex; i++) { + int targetIndex = i - sourceoffset; + // ref locals here? Not sure. Color above = sourceData[i]; - Color below = mergedData[i - sourceoffset]; + Color below = mergedData[targetIndex]; // shortcut transparency if (above.A < MinOpacity) continue; if (below.A < MinOpacity || above.A == byte.MaxValue) - mergedData[i] = above; + mergedData[targetIndex] = above; // merge pixels else @@ -236,7 +238,7 @@ namespace StardewModdingAPI.Framework.Content // premultiplied by the content pipeline. The formula is derived from // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. float alphaBelow = 1 - (above.A / 255f); - mergedData[i] = new Color( + 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)), -- cgit From 798a56bd2e94e9e60b588222c730e313c3dbe075 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Sun, 28 Aug 2022 16:14:57 -0400 Subject: Avoid copying memory for contingous buffers. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 39 ++++++++++-------------- 1 file changed, 16 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 2bbcc60c..068634b3 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -40,6 +40,11 @@ namespace StardewModdingAPI.Framework.Content 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; { @@ -48,37 +53,24 @@ namespace StardewModdingAPI.Framework.Content int areaWidth = sourceArea.Value.Width; int areaHeight = sourceArea.Value.Height; - if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight <= source.Height) + if (areaWidth == source.Width) { // 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, areaWidth, areaHeight, sourceArea.Value, targetArea.Value, patchMode); + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY); } else { int pixelCount = areaWidth * areaHeight; sourceData = ArrayPool.Shared.Rent(pixelCount); - if (areaX == 0 && areaWidth == 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, pixelCount); - - } - else + // slower copying, line by line + for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++) { - // 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); - } + int sourceIndex = (y * source.Width) + areaX; + int targetIndex = (y - areaY) * areaWidth; + Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth); } // apply @@ -150,10 +142,11 @@ namespace StardewModdingAPI.Framework.Content /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. /// Indicates how an image should be patched. + /// The row to start on, for the sourceData. /// One of the arguments is null. /// The is outside the bounds of the spritesheet. /// The content being read isn't an image. - private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode) + private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode, int startRow = 0) { // get texture Texture2D target = this.Data; @@ -168,7 +161,7 @@ namespace StardewModdingAPI.Framework.Content throw new InvalidOperationException("The source and target areas must be the same size."); if (patchMode == PatchMode.Replace) - target.SetData(0, targetArea, sourceData, 0, pixelCount); + target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount); else { // merge data @@ -177,7 +170,7 @@ namespace StardewModdingAPI.Framework.Content // Adjusting bounds to ignore transparent pixels at the start and end. int startIndex = -1; - for (int i = 0; i < pixelCount; i++) + for (int i = startRow * sourceArea.Width; i < pixelCount; i++) { if (sourceData[i].A >= MinOpacity) { -- cgit From 48d0f70ffd07df9ba364808a71407800834f95d3 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Thu, 6 Oct 2022 08:16:57 -0400 Subject: fix indexing math again. --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 068634b3..636d4a71 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -137,8 +137,8 @@ namespace StardewModdingAPI.Framework.Content /// Overwrite part of the image. /// The image data to patch into the content. - /// The pixel width of the source image. - /// The pixel height of the source image. + /// The pixel width of the original source image. + /// The pixel height of the original source image. /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. /// Indicates how an image should be patched. @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Content return; // apparently a completely blank texture? int endIndex = -1; - for (int i = pixelCount - 1; i >= startIndex; i--) + for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--) { if (sourceData[i].A >= MinOpacity) { -- 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') 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') 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 a565ac9405a95d24f7cf945228935107e91bb89f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 19:59:21 -0400 Subject: make GetApi methods mutually exclusive & improve docs --- .../Framework/ModHelpers/ModRegistryHelper.cs | 56 ++++++++++------------ src/SMAPI/Framework/SCore.cs | 5 ++ src/SMAPI/IMod.cs | 12 +++-- 3 files changed, 37 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9ad3e3ae..8cc73481 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -68,49 +68,43 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - // get our cached API if one is available + // get the target mod IModMetadata? mod = this.Registry.Get(uniqueID); if (mod == null) return null; - if (this.AccessedModApis.ContainsKey(mod.Manifest.UniqueID)) + // fetch API + if (!this.AccessedModApis.TryGetValue(mod.Manifest.UniqueID, out object? api)) { - return this.AccessedModApis[mod.Manifest.UniqueID]; - } - - object? api; + // if the target has a global API, this is mutually exclusive with per-mod APIs + if (mod.Api != null) + api = mod.Api; - // safely request a specific API instance - try - { - api = mod.Mod?.GetApi(this.Mod.Manifest); - if (api != null && !api.GetType().IsPublic) + // else try to get a per-mod API + else { - 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); + try + { + api = mod.Mod?.GetApi(this.Mod.Manifest); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{mod.DisplayName} provides a per-mod API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading the per-mod API instance from {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + api = null; + } } + // cache & log API access + this.AccessedModApis[mod.Manifest.UniqueID] = api; 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}."); - } + this.Monitor.Log($"Accessed mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}."); } - // cache the API instance and return it - this.AccessedModApis[mod.Manifest.UniqueID] = api; return api; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 16ff2537..4ba0dd9c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1779,6 +1779,11 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); } + + // validate mod doesn't implement both GetApi() and GetApi(mod) + if (metadata.Api != null && metadata.Mod!.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IManifest) })!.DeclaringType != typeof(Mod)) + metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IManifest)}), which isn't allowed. The latter will be ignored.", LogLevel.Error); + Context.HeuristicModsRunningCode.TryPop(out _); } diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 6041bf66..4576246a 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -23,13 +23,15 @@ namespace StardewModdingAPI /// Provides simplified APIs for writing mods. void Entry(IModHelper helper); - /// Get an API that other mods can access. This is always called after . + /// Get an API that other mods can access. This is always called after , and is only called once even if multiple mods access it. + /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. + /// Returns the API instance, or null if the mod has no API. 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 . + /// Get an API that other mods can access. This is always called after , and is called once per mod that accesses the API (even if they access it multiple times). + /// The manifest for the mod accessing the API. + /// Returns the API instance, or null if the mod has no API. Note that the manifest is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. + /// object? GetApi(IManifest manifest); - } } -- cgit From 8d6670cfc8abf7e71197d2f621314fb04a0543b8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 20:33:01 -0400 Subject: pass mod info to GetApi instead --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 2 +- src/SMAPI/IMod.cs | 6 +++--- src/SMAPI/Mod.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 8cc73481..93edd597 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { try { - api = mod.Mod?.GetApi(this.Mod.Manifest); + api = mod.Mod?.GetApi(this.Mod); if (api != null && !api.GetType().IsPublic) { api = null; diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 4576246a..19d01311 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -24,14 +24,14 @@ namespace StardewModdingAPI void Entry(IModHelper helper); /// Get an API that other mods can access. This is always called after , and is only called once even if multiple mods access it. - /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. + /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. /// Returns the API instance, or null if the mod has no API. object? GetApi(); /// Get an API that other mods can access. This is always called after , and is called once per mod that accesses the API (even if they access it multiple times). - /// The manifest for the mod accessing the API. + /// The mod accessing the API. /// Returns the API instance, or null if the mod has no API. Note that the manifest is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. /// - object? GetApi(IManifest manifest); + object? GetApi(IModInfo mod); } } diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index 1a5f5594..01157886 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI } /// - public virtual object? GetApi(IManifest manifest) + public virtual object? GetApi(IModInfo mod) { return null; } -- cgit From ab66266b4bc4d7b1a7ae76d05693e9dd30c03989 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 21:32:10 -0400 Subject: update installer for VdfConverter & rework avoid custom models --- build/windows/prepare-install-package.ps1 | 2 +- .../Framework/GameScanning/GameScanner.cs | 44 ++++++++++++-------- .../GameScanning/SteamLibraryCollection.cs | 47 ---------------------- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- 4 files changed, 30 insertions(+), 65 deletions(-) delete mode 100644 src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs (limited to 'src') diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 87a4fe01..002bcbca 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -154,7 +154,7 @@ foreach ($folder in $folders) { cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) { + foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll", "VdfConverter.dll")) { cp "$smapiBin/$name" "$bundlePath/smapi-internal" } diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 8e24dcdf..66465ffe 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -24,6 +24,9 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// The current OS. private readonly Platform Platform; + /// The Steam app ID for Stardew Valley. + private const string SteamAppId = "413150"; + /********* ** Public methods @@ -146,7 +149,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning #if SMAPI_FOR_WINDOWS IDictionary registryKeys = new Dictionary { - [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam + [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App " + GameScanner.SteamAppId] = "InstallLocation", // Steam [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows }; foreach (var pair in registryKeys) @@ -160,9 +163,10 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); if (steamPath != null) { + // conventional path yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); - // Check for Steam libraries in other locations + // from Steam's .vdf file string? path = this.GetPathFromSteamLibrary(steamPath); if (!string.IsNullOrWhiteSpace(path)) yield return path; @@ -257,26 +261,34 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// The game directory, if found. private string? GetPathFromSteamLibrary(string? steamPath) { - string stardewAppId = "413150"; - if (steamPath != null) + if (steamPath == null) + return null; + + // get raw .vdf data + string libraryFoldersPath = Path.Combine(steamPath.Replace('/', '\\'), "steamapps\\libraryfolders.vdf"); + using FileStream fileStream = File.OpenRead(libraryFoldersPath); + VdfDeserializer deserializer = new(); + dynamic libraries = deserializer.Deserialize(fileStream); + if (libraries?.libraryfolders is null) + return null; + + // get path from Stardew Valley app (if any) + foreach (dynamic pair in libraries.libraryfolders) { - 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) + dynamic library = pair.Value; + + foreach (dynamic app in library.apps) { - 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) + string key = app.Key; + if (key == GameScanner.SteamAppId) { - return Path.Combine(stardewLibrary.Value.path.Replace("\\\\", "\\"), @"steamapps\common\Stardew Valley"); + string path = library.path; + + return Path.Combine(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 deleted file mode 100644 index 7a186f69..00000000 --- a/src/SMAPI.Toolkit/Framework/GameScanning/SteamLibraryCollection.cs +++ /dev/null @@ -1,47 +0,0 @@ -#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 411fd469..6080a85e 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -14,7 +14,7 @@ - + -- cgit From a220e14f2d22f5d481c87bfd76d1b9eeaebf04e3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 13:50:24 -0400 Subject: polish recent changes & update release notes --- docs/release-notes.md | 7 ++- .../Framework/ModScanning/ModScanner.cs | 2 +- src/SMAPI/Framework/SCore.cs | 67 ++++++++++++---------- src/SMAPI/IMod.cs | 2 +- 4 files changed, 44 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4875d1cd..75e143e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,11 +9,16 @@ ## Upcoming release * For players: - * Fixed update alert shown for a prerelease version on GitHub if it's not marked as prerelease. + * The SMAPI installer now also detects game folders listed in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). + * Optimized performance and memory usage (thanks to atravita!). + * Added more file extensions to ignore when searching for mod folders: `.7z`, `.tar`, `.tar.gz`, and `.xcf` (thanks to atravita!). + * Fixed update alerts incorrectly shown for prerelease versions on GitHub that aren't marked as prerelease. * For mod authors: + * 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 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)). + * Fixed `LocationListChanged` event not raised & memory leak occurring when a generated mine/volcano is removed (thanks to tylergibbs2!). ## 3.16.2 Released 31 August 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index d115810a..5e9e3c35 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning ".zip", ".7z", ".tar", - ".tar.gz" + ".tar.gz", // backup files ".backup", diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 4ba0dd9c..98eb2803 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1686,12 +1686,16 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Launching mods...", LogLevel.Debug); foreach (IModMetadata metadata in loadedMods) { + IMod mod = + metadata.Mod + ?? throw new InvalidOperationException($"The '{metadata.DisplayName}' mod is not initialized correctly."); // should never happen, but avoids nullability warnings + #if SMAPI_DEPRECATED // add interceptors - if (metadata.Mod?.Helper is ModHelper helper) + if (mod.Helper is ModHelper helper) { // ReSharper disable SuspiciousTypeConversion.Global - if (metadata.Mod is IAssetEditor editor) + if (mod is IAssetEditor editor) { SCore.DeprecationManager.Warn( source: metadata, @@ -1704,7 +1708,7 @@ namespace StardewModdingAPI.Framework this.ContentCore.Editors.Add(new ModLinked(metadata, editor)); } - if (metadata.Mod is IAssetLoader loader) + if (mod is IAssetLoader loader) { SCore.DeprecationManager.Warn( source: metadata, @@ -1749,41 +1753,42 @@ namespace StardewModdingAPI.Framework } #endif - // call entry method + // initialize mod Context.HeuristicModsRunningCode.Push(metadata); - try - { - IMod mod = metadata.Mod!; - mod.Entry(mod.Helper!); - } - catch (Exception ex) - { - metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - // get mod API - try { - object? api = metadata.Mod!.GetApi(); - if (api != null && !api.GetType().IsPublic) + // call entry method + try + { + mod.Entry(mod.Helper!); + } + catch (Exception ex) { - api = null; - this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } - if (api != null) - this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName})."); - metadata.SetApi(api); - } - catch (Exception ex) - { - this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); - } + // get mod API + try + { + object? api = mod.GetApi(); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } - // validate mod doesn't implement both GetApi() and GetApi(mod) - if (metadata.Api != null && metadata.Mod!.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IManifest) })!.DeclaringType != typeof(Mod)) - metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IManifest)}), which isn't allowed. The latter will be ignored.", LogLevel.Error); + if (api != null) + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName})."); + metadata.SetApi(api); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + } + // validate mod doesn't implement both GetApi() and GetApi(mod) + if (metadata.Api != null && mod.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IModInfo) })!.DeclaringType != typeof(Mod)) + metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IModInfo)}), which isn't allowed. The latter will be ignored.", LogLevel.Error); + } Context.HeuristicModsRunningCode.TryPop(out _); } diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 19d01311..87c9880c 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI /// Get an API that other mods can access. This is always called after , and is called once per mod that accesses the API (even if they access it multiple times). /// The mod accessing the API. - /// Returns the API instance, or null if the mod has no API. Note that the manifest is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. + /// Returns the API instance, or null if the mod has no API. Note that is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. /// object? GetApi(IModInfo mod); } -- cgit From 3d10d08a1ac281620b60c0e5fd1d51b2da896a0d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 14:08:16 -0400 Subject: make deprecation warnings a bit stronger for the upcoming 4.0.0 release --- docs/release-notes.md | 3 ++- src/SMAPI/Framework/Deprecations/DeprecationManager.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 75e143e8..4352e2ba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,7 +16,8 @@ * For mod authors: * 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 brackets, to avoid breaking tools which parse log files. + * 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. * 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)). * Fixed `LocationListChanged` event not raised & memory leak occurring when a generated mine/volcano is removed (thanks to tylergibbs2!). diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs index f58f085e..5a5850d1 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework.Deprecations foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase)) { // build message - string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; + string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase}) and will break in the upcoming SMAPI 4.0.0."; // get log level LogLevel level; -- cgit From 8dc12fd01c9274b045bafb04f02ef97fd8999c5d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 14:30:40 -0400 Subject: optimize string splits --- docs/release-notes.md | 1 + .../Framework/Commands/Player/SetColorCommand.cs | 2 +- src/SMAPI.Tests/Utilities/KeybindListTests.cs | 6 +++--- src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs | 4 ++-- src/SMAPI/Framework/Logging/LogManager.cs | 2 +- src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs | 2 +- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Utilities/Keybind.cs | 4 ++-- 8 files changed, 12 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4352e2ba..e4324d40 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For players: * The SMAPI installer now also detects game folders listed in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). * 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!). * Fixed update alerts incorrectly shown for prerelease versions on GitHub that aren't marked as prerelease. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index 12a51bc9..ea9f1d82 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -63,7 +63,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// The color to set. private bool TryParseColor(string input, out Color color) { - string[] colorHexes = input.Split(new[] { ',' }, 3); + string[] colorHexes = input.Split(',', 3); if (int.TryParse(colorHexes[0], out int r) && int.TryParse(colorHexes[1], out int g) && int.TryParse(colorHexes[2], out int b)) { color = new Color(r, g, b); diff --git a/src/SMAPI.Tests/Utilities/KeybindListTests.cs b/src/SMAPI.Tests/Utilities/KeybindListTests.cs index c4c086de..c5fd5daf 100644 --- a/src/SMAPI.Tests/Utilities/KeybindListTests.cs +++ b/src/SMAPI.Tests/Utilities/KeybindListTests.cs @@ -136,11 +136,11 @@ namespace SMAPI.Tests.Utilities foreach (string rawPair in stateMap.Split(',')) { // parse values - string[] parts = rawPair.Split(new[] { ':' }, 2); + string[] parts = rawPair.Split(':', 2, StringSplitOptions.TrimEntries); if (!Enum.TryParse(parts[0], ignoreCase: true, out SButton curButton)) - Assert.Fail($"The state map is invalid: unknown button value '{parts[0].Trim()}'"); + Assert.Fail($"The state map is invalid: unknown button value '{parts[0]}'"); if (!Enum.TryParse(parts[1], ignoreCase: true, out SButtonState state)) - Assert.Fail($"The state map is invalid: unknown state value '{parts[1].Trim()}'"); + Assert.Fail($"The state map is invalid: unknown state value '{parts[1]}'"); // get state if (curButton == button) diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 23b25f95..46c3092c 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -121,10 +121,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus HtmlNode? node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); if (node != null) { - string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); + string[] errorParts = node.InnerText.Trim().Split('\n', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); string errorCode = errorParts[0]; string? errorText = errorParts.Length > 1 ? errorParts[1] : null; - switch (errorCode.Trim().ToLower()) + switch (errorCode.ToLower()) { case "not found": return null; diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index c0b7c0ba..d5b33289 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -223,7 +223,7 @@ namespace StardewModdingAPI.Framework.Logging // show update alert if (File.Exists(Constants.UpdateMarker)) { - string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split(new[] { '|' }, 2); + string[] rawUpdateFound = File.ReadAllText(Constants.UpdateMarker).Split('|', 2); if (SemanticVersion.TryParse(rawUpdateFound[0], out ISemanticVersion? updateFound)) { if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index f5d449c5..4dd9ccc6 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null && methodRef.DeclaringType.FullName == this.FullTypeName && this.MethodNames.Contains(methodRef.Name)) { - string eventName = methodRef.Name.Split(new[] { '_' }, 2)[1]; + string eventName = methodRef.Name.Split('_', 2)[1]; this.MethodNames.Remove($"add_{eventName}"); this.MethodNames.Remove($"remove_{eventName}"); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 98eb2803..114c4bb3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1384,7 +1384,7 @@ namespace StardewModdingAPI.Framework } // check min length for specific types - switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) + switch (fields[SObject.objectInfoTypeIndex].Split(' ', 2)[0]) { case "Cooking": if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs index 3455ce77..3532620d 100644 --- a/src/SMAPI/Utilities/Keybind.cs +++ b/src/SMAPI/Utilities/Keybind.cs @@ -54,12 +54,12 @@ namespace StardewModdingAPI.Utilities } // parse buttons - string[] rawButtons = input.Split('+'); + string[] rawButtons = input.Split('+', StringSplitOptions.TrimEntries); SButton[] buttons = new SButton[rawButtons.Length]; List rawErrors = new List(); for (int i = 0; i < buttons.Length; i++) { - string rawButton = rawButtons[i].Trim(); + string rawButton = rawButtons[i]; if (string.IsNullOrWhiteSpace(rawButton)) rawErrors.Add("Invalid empty button value"); else if (!Enum.TryParse(rawButton, ignoreCase: true, out SButton button)) -- cgit From d0704ef6f0040d10aa89e9cf1ee8d38098bff5df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 14:34:31 -0400 Subject: fix nullability warnings --- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 4 ++-- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index fedadba6..77c0da5f 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -382,11 +382,11 @@ namespace SMAPI.Tests.Utilities { // act string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); - SemanticVersion after = JsonConvert.DeserializeObject(json); + SemanticVersion? after = JsonConvert.DeserializeObject(json); // assert Assert.IsNotNull(after, "The semantic version after deserialization is unexpectedly null."); - Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version."); + Assert.AreEqual(versionStr, after!.ToString(), "The semantic version after deserialization doesn't match the input version."); } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 24fe5fa2..b982bc0c 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -361,7 +361,7 @@ else if (log?.IsValid == true) ContentPacks: contentPacks?.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) == true ? contentPackList : Array.Empty() )) .ToList(); - if (contentPacks?.TryGetValue("", out LogModInfo[] invalidPacks) == true) + if (contentPacks?.TryGetValue("", out LogModInfo[]? invalidPacks) == true) { modsWithContentPacks.Add(( Mod: new LogModInfo(ModType.CodeMod, "", "", "", ""), -- cgit From 42ff20cd92a3a28faca8de0c309396efa147f0e2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 15:01:25 -0400 Subject: suppress Harmony debug mode by default --- docs/release-notes.md | 2 ++ src/SMAPI/Framework/Models/SConfig.cs | 10 ++++++++-- src/SMAPI/Framework/SCore.cs | 9 +++++++++ src/SMAPI/SMAPI.config.json | 14 ++++++++------ 4 files changed, 27 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 89883f87..4de0fa66 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,8 @@ ## Upcoming release * For players: * The SMAPI installer now also detects game folders listed in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). + * SMAPI now prevents mods from enabling Harmony debug mode, which impacts performance and creates a file on your desktop. + _You can allow debug mode by editing `smapi-internal/config.json` in your game folder._ * 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!). diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 9444c046..b3061fba 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -23,7 +23,8 @@ namespace StardewModdingAPI.Framework.Models [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, [nameof(UseRawImageLoading)] = true, - [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux + [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux, + [nameof(SuppressHarmonyDebugMode)] = true }; /// The default values for , to log changes if different. @@ -79,6 +80,9 @@ namespace StardewModdingAPI.Framework.Models /// The colors to use for text written to the SMAPI console. public ColorSchemeConfig ConsoleColors { get; set; } + /// 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. + public bool SuppressHarmonyDebugMode { get; set; } + /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. public HashSet SuppressUpdateChecks { get; set; } @@ -99,8 +103,9 @@ namespace StardewModdingAPI.Framework.Models /// >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, string[]? suppressUpdateChecks) + 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) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -114,6 +119,7 @@ namespace StardewModdingAPI.Framework.Models this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)]; this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)]; this.ConsoleColors = consoleColors; + this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 114c4bb3..3e6cd853 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -501,6 +501,15 @@ namespace StardewModdingAPI.Framework return; } + /********* + ** Prevent Harmony debug mode + *********/ + if (HarmonyLib.Harmony.DEBUG && this.Settings.SuppressHarmonyDebugMode) + { + HarmonyLib.Harmony.DEBUG = false; + this.Monitor.LogOnce("A mod enabled Harmony debug mode, which impacts performance and creates a file on your desktop. SMAPI will try to keep it disabled. (You can allow debug mode by editing the smapi-internal/config.json file.)", LogLevel.Warn); + } + #if SMAPI_DEPRECATED /********* ** Reload assets when interceptors are added/removed diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 97e8e00c..2d4239ba 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 the experimental Pintail API proxying library, instead of the original - * proxying built into SMAPI itself. - */ - "UsePintail": true, - /** * Whether to use raw image data when possible, instead of initializing an XNA Texture2D * instance through the GPU. @@ -138,6 +132,14 @@ copy all the settings, or you may cause bugs due to overridden changes in future } }, + /** + * 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. + * + * If you actually need debug mode to test your own mod, set this to false. + */ + "SuppressHarmonyDebugMode": true, + /** * The mod IDs SMAPI should ignore when performing update checks or validating update keys. */ -- cgit From 9a15da5a173e5e218c16e2e4ef0af0c98968e1cb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 16:59:05 -0400 Subject: add 'strict mode' release with deprecated APIs stripped out --- docs/release-notes.md | 1 + src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 9 +++++++- .../Framework/LogParsing/Models/ParsedLog.cs | 3 +++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 25 ++++++++++++++++++---- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 5 +++++ src/SMAPI/Framework/Logging/LogManager.cs | 10 ++++++++- src/SMAPI/Framework/SCore.cs | 9 +++++--- 7 files changed, 53 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4de0fa66..5bf3b875 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ## Upcoming release * For players: + * You can now download SMAPI 'strict mode' from the [Nexus optional files](https://www.nexusmods.com/stardewvalley/mods/2400/). This removes all deprecated APIs, which may significantly improve performance. However mods which still show deprecation warnings won't work. * The SMAPI installer now also detects game folders listed in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). * SMAPI now prevents mods from enabling Harmony debug mode, which impacts performance and creates a file on your desktop. _You can allow debug mode by editing `smapi-internal/config.json` in your game folder._ diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 18ba754c..5e0dedf3 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -199,8 +199,15 @@ namespace StardewModdingAPI.Web.Framework.LogParsing log.ApiVersion = match.Groups["apiVersion"].Value; log.GameVersion = match.Groups["gameVersion"].Value; log.OperatingSystem = match.Groups["os"].Value; - smapiMod.OverrideVersion(log.ApiVersion); + const string strictModeSuffix = " (strict mode)"; + if (log.ApiVersion.EndsWith(strictModeSuffix)) + { + log.IsStrictMode = true; + log.ApiVersion = log.ApiVersion[..^strictModeSuffix.Length]; + } + + smapiMod.OverrideVersion(log.ApiVersion); log.ApiVersionParsed = smapiMod.GetParsedVersion(); } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index 3f649199..cda0f653 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -25,6 +25,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /**** ** Log data ****/ + /// Whether SMAPI is running in strict mode, which disables all deprecated APIs. + public bool IsStrictMode { get; set; } + /// The SMAPI version. public string? ApiVersion { get; set; } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b982bc0c..28127903 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -40,7 +40,7 @@ } - + @@ -243,7 +243,7 @@ else if (log?.IsValid == true) @if (log?.IsValid == true) {
- @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode) + @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode || log.IsStrictMode) {

Suggested fixes

    @@ -257,7 +257,14 @@ else if (log?.IsValid == true) } @if (isPyTkCompatibilityMode) { -
  • PyTK 1.23.* or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.
  • + if (log.IsStrictMode) + { +
  • PyTK's image scaling isn't compatible with SMAPI strict mode.
  • + } + else + { +
  • PyTK 1.23.* or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.
  • + } } @if (outdatedMods.Any()) { @@ -307,6 +314,10 @@ else if (log?.IsValid == true) } + @if (log.IsStrictMode) + { +
  • SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can reinstall SMAPI to disable it if you run into problems.
  • + }
} @@ -329,7 +340,13 @@ else if (log?.IsValid == true) SMAPI: - @log.ApiVersion + + @log.ApiVersion + @if (log.IsStrictMode) + { + (strict mode) + } + Folder: diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index f136a96f..995f5aa9 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -73,6 +73,11 @@ table caption { margin-bottom: 0.5em; } +#fix-list li.notice { + background: #EEFFEE; + border-color: #080; +} + #fix-list li.important { background: #FCC; border-color: #800; diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index d5b33289..ffffc9c7 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -269,7 +269,11 @@ namespace StardewModdingAPI.Framework.Logging public void LogIntro(string modsPath, IDictionary customSettings) { // log platform - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} " +#if !SMAPI_DEPRECATED + + "(strict mode) " +#endif + + $"with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); // log basic info this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); @@ -280,6 +284,10 @@ namespace StardewModdingAPI.Framework.Logging // log custom settings if (customSettings.Any()) this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}"); + +#if !SMAPI_DEPRECATED + this.Monitor.Log("SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can reinstall SMAPI to disable it if you run into problems.", LogLevel.Info); +#endif } /// Log details for settings that don't match the default. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3e6cd853..4d6deb15 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1681,15 +1681,18 @@ namespace StardewModdingAPI.Framework // initialize translations this.ReloadTranslations(loaded); -#if SMAPI_DEPRECATED // set temporary PyTK compatibility mode // This is part of a three-part fix for PyTK 1.23.* and earlier. When removing this, // search 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. { IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit"); - ModContentManager.EnablePyTkLegacyMode = pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0"); - } + if (pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0")) +#if SMAPI_DEPRECATED + ModContentManager.EnablePyTkLegacyMode = true; +#else + this.Monitor.Log("PyTK's image scaling is not compatible with SMAPI strict mode.", LogLevel.Warn); #endif + } // initialize loaded non-content-pack mods this.Monitor.Log("Launching mods...", LogLevel.Debug); -- 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') 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') 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') 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 From 4d2ad379b4198fa7a341c6ba2bb6455d488b414f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 19:29:18 -0400 Subject: fix package error --- build/windows/prepare-install-package.ps1 | 6 +++++- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 002bcbca..71de1154 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -154,10 +154,14 @@ foreach ($folder in $folders) { cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll", "VdfConverter.dll")) { + foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) { cp "$smapiBin/$name" "$bundlePath/smapi-internal" } + if ($folder -eq "windows") { + cp "$smapiBin/VdfConverter.dll" "$bundlePath/smapi-internal" + } + cp "$smapiBin/SMAPI.config.json" "$bundlePath/smapi-internal/config.json" cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json" if ($folder -eq "linux" -or $folder -eq "macOS") { diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 0086f38a..10f1df70 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -14,7 +14,7 @@ - + -- cgit From ee77efcc976ef1a5ee64933a6174d2fac9c6d0f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Oct 2022 19:42:24 -0400 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 12 +++++++----- docs/technical/mod-package.md | 4 +++- src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj | 2 +- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 8 files changed, 19 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 7fe66fab..02cf69bd 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed. - 3.16.2 + 3.17.0 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index b5a0adaf..7574d62b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,20 +7,22 @@ _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._ --> -## Upcoming release +## 3.17.0 +Released 09 October 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/73090322). + * For players: - * You can now download SMAPI 'strict mode' from the [Nexus optional files](https://www.nexusmods.com/stardewvalley/mods/2400/). This removes all deprecated APIs, which may significantly improve performance. However mods which still show deprecation warnings won't work. - * The SMAPI installer now also detects game folders listed in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). + * You can now download SMAPI 'strict mode' from [Nexus files](https://www.nexusmods.com/stardewvalley/mods/2400/?tab=files), which removes all deprecated APIs. This may significantly improve performance, but mods which still show deprecation warnings won't work. + * The SMAPI installer now also detects game folders in Steam's `.vdf` library data on Windows (thanks to pizzaoverhead!). * SMAPI now prevents mods from enabling Harmony debug mode, which impacts performance and creates a file on your desktop. _You can allow debug mode by editing `smapi-internal/config.json` in your game folder._ * 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. + * Removed transitional `UseRawImageLoading` option added in 3.15.0. 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: - * 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. + * When [providing a mod API in a C# mod](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations), you can now get the mod requesting it as an optional parameter (thanks to KhloeLeclair!). * 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. diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index c483754e..77260a57 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -412,7 +412,9 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi when you compile it. ## Release notes -### Upcoming release +### 4.0.2 +Released 09 October 2022. + * 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. diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index e25da168..cded6f65 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -10,7 +10,7 @@ Pathoschild.Stardew.ModBuildConfig Build package for SMAPI mods - 4.0.1 + 4.0.2 Pathoschild Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later. MIT diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index abe8b334..754e543a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.16.2", + "Version": "3.17.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.16.2" + "MinimumApiVersion": "3.17.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index b6764bc0..8f94e5ab 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.16.2", + "Version": "3.17.0", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.16.2" + "MinimumApiVersion": "3.17.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index a2657168..a9401fc3 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.16.2", + "Version": "3.17.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.16.2" + "MinimumApiVersion": "3.17.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 493aebd9..31dafa8e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -52,7 +52,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "3.16.2"; + internal static string RawApiVersion = "3.17.0"; } /// Contains SMAPI's constants and assumptions. -- cgit