diff options
Diffstat (limited to 'src')
36 files changed, 531 insertions, 395 deletions
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 264932e4..3be9c225 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,9 +6,9 @@ <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> - <PackageReference Include="NUnit" Version="3.13.2" /> - <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> + <PackageReference Include="NUnit" Version="3.13.3" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index c5790186..e25da168 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -24,7 +24,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <!-- This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 564e480e..300de9d2 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.14.7", + "Version": "3.15.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index bfbfd2dc..22e68421 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -2,6 +2,7 @@ using System; using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Internal.Patching; +using StardewModdingAPI.Mods.ErrorHandler.ModPatches; using StardewModdingAPI.Mods.ErrorHandler.Patches; using StardewValley; @@ -29,6 +30,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler // apply patches HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor, + // game patches new DialoguePatcher(monitorForGame, this.Helper.Reflection), new EventPatcher(monitorForGame), new GameLocationPatcher(monitorForGame), @@ -37,7 +39,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler new ObjectPatcher(), new SaveGamePatcher(this.Monitor, this.OnSaveContentRemoved), new SpriteBatchPatcher(), - new UtilityPatcher() + new UtilityPatcher(), + + // mod patches + new PyTkPatcher(helper.ModRegistry) ); // hook events diff --git a/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs b/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs new file mode 100644 index 00000000..9ee864db --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Internal.Patching; + +// +// This is part of a three-part fix for PyTK 1.23.0 and earlier. When removing this, search +// 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. +// + +namespace StardewModdingAPI.Mods.ErrorHandler.ModPatches +{ + /// <summary>Harmony patches for the PyTK mod for compatibility with newer SMAPI versions.</summary> + /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "'Platonymous' is part of the mod ID.")] + internal class PyTkPatcher : BasePatcher + { + /********* + ** Fields + *********/ + /// <summary>The PyTK mod metadata, if it's installed.</summary> + private static IModMetadata? PyTk; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="modRegistry">The mod registry from which to read PyTK metadata.</param> + public PyTkPatcher(IModRegistry modRegistry) + { + IModMetadata? pyTk = (IModMetadata?)modRegistry.Get(@"Platonymous.Toolkit"); + if (pyTk is not null && !pyTk.Manifest.Version.IsNewerThan("1.23.0")) + PyTkPatcher.PyTk = pyTk; + } + + /// <inheritdoc /> + public override void Apply(Harmony harmony, IMonitor monitor) + { + try + { + // get mod info + IModMetadata? pyTk = PyTkPatcher.PyTk; + if (pyTk is null) + return; + + // get patch method + const string patchMethodName = "PatchImage"; + MethodInfo? patch = AccessTools.Method(pyTk.Mod!.GetType(), patchMethodName); + if (patch is null) + { + monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); + monitor.Log($"Couldn't find patch method '{pyTk.Mod.GetType().FullName}.{patchMethodName}'."); + return; + } + + // apply patch + harmony = new($"{harmony.Id}.compatibility-patches.PyTK"); + harmony.Patch( + original: AccessTools.Method(typeof(AssetDataForImage), nameof(AssetDataForImage.PatchImage), new[] { typeof(Texture2D), typeof(Rectangle), typeof(Rectangle), typeof(PatchMode) }), + prefix: new HarmonyMethod(patch) + ); + } + catch (Exception ex) + { + monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); + monitor.Log(ex.GetLogSummary()); + } + } + } +} diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 78cdb315..53c37e97 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -24,7 +24,4 @@ <None Update="i18n\*.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> - - <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" /> - <Import Project="..\SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems" Label="Shared" /> </Project> diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 39d22b5f..15a1e0f3 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.14.7", + "Version": "3.15.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.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 8eaf2475..1a11742c 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.14.7", + "Version": "3.15.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.14.7" + "MinimumApiVersion": "3.15.0" } diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs index 6be97526..d14c116f 100644 --- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -29,11 +29,8 @@ namespace SMAPI.Tests.Core /// <summary>The random number generator with which to create sample values.</summary> private readonly Random Random = new(); - /// <summary>Sample user inputs for season names.</summary> - private static readonly IInterfaceProxyFactory[] ProxyFactories = { - new InterfaceProxyFactory(), - new OriginalInterfaceProxyFactory() - }; + /// <summary>The proxy factory to use in unit tests.</summary> + private static readonly IInterfaceProxyFactory[] ProxyFactories = { new InterfaceProxyFactory() }; /********* diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 67997b30..2c32a932 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -14,11 +14,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.5.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> - <PackageReference Include="Moq" Version="4.16.1" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> + <PackageReference Include="Moq" Version="4.18.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="NUnit" Version="3.13.3" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index ec27bf79..e021993f 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -9,8 +9,8 @@ <Import Project="..\..\build\common.targets" /> <ItemGroup> - <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> <PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index f1400e62..4c2569e1 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -15,14 +15,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Azure.Storage.Blobs" Version="12.10.0" /> - <PackageReference Include="Hangfire.AspNetCore" Version="1.7.27" /> + <PackageReference Include="Azure.Storage.Blobs" Version="12.12.0" /> + <PackageReference Include="Hangfire.AspNetCore" Version="1.7.29" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" /> - <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> - <PackageReference Include="Humanizer.Core" Version="2.13.14" /> - <PackageReference Include="JetBrains.Annotations" Version="2021.3.0" /> - <PackageReference Include="Markdig" Version="0.26.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> + <PackageReference Include="Humanizer.Core" Version="2.14.1" /> + <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> + <PackageReference Include="Markdig" Version="0.30.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.5" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.5" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 53c9db82..16a89647 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -170,6 +170,11 @@ /********* ** Broke in SMAPI 3.14.0 *********/ + "CFAutomate": { + "ID": "Platonymous.CFAutomate", + "~2.12.9 | Status": "AssumeBroken", + "~2.12.9 | StatusReasonDetails": "causes runtime errors in newer versions of Automate" + }, "Dynamic Game Assets": { "ID": "spacechase0.DynamicGameAssets", "~1.4.1 | Status": "AssumeBroken", diff --git a/src/SMAPI.sln b/src/SMAPI.sln index d9f60a5c..2e5ba0ea 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -103,14 +103,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3B5BF14D-F61 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiProvider", "SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj", "{239AEEAC-07D1-4A3F-AA99-8C74F5038F50}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 - SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 - SMAPI.Internal\SMAPI.Internal.projitems*{491e775b-ead0-44d4-b6ca-f1fc3e316d33}*SharedItemsImports = 5 SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{6c16e948-3e5c-47a7-bf4b-07a7469a87a5}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index c63324e3..db88563e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// <summary>SMAPI's current raw semantic version.</summary> - internal static string RawApiVersion = "3.14.7"; + internal static string RawApiVersion = "3.15.0"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 97729c95..3393b22f 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; @@ -29,86 +30,154 @@ namespace StardewModdingAPI.Framework.Content : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// <inheritdoc /> - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { - // get texture + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source data if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - Texture2D target = this.Data; + throw new ArgumentNullException(nameof(source), "Can't patch from null source data."); - // get areas - sourceArea ??= new Rectangle(0, 0, source.Width, source.Height); - targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + // get the pixels for the source area + Color[] sourceData; + { + int areaX = sourceArea.Value.X; + int areaY = sourceArea.Value.Y; + int areaWidth = sourceArea.Value.Width; + int areaHeight = sourceArea.Value.Height; - // validate + if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height) + sourceData = source.Data; + else + { + sourceData = new Color[areaWidth * areaHeight]; + int i = 0; + for (int y = areaY, maxY = areaY + areaHeight - 1; y <= maxY; y++) + { + for (int x = areaX, maxX = areaX + areaWidth - 1; x <= maxX; x++) + { + int targetIndex = (y * source.Width) + x; + sourceData[i++] = source.Data[targetIndex]; + } + } + } + } + + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// <inheritdoc /> + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height); + + // validate source texture + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); if (!source.Bounds.Contains(sourceArea.Value)) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (!target.Bounds.Contains(targetArea.Value)) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Size != targetArea.Value.Size) - throw new InvalidOperationException("The source and target areas must be the same size."); // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = GC.AllocateUninitializedArray<Color>(pixelCount); source.GetData(0, sourceArea, sourceData, 0, pixelCount); - // merge data in overlay mode + // apply + this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode); + } + + /// <inheritdoc /> + public bool ExtendImage(int minWidth, int minHeight) + { + if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) + return false; + + Texture2D original = this.Data; + Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); + this.ReplaceWith(texture); + this.PatchImage(original); + return true; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the bounds for an image patch.</summary> + /// <param name="sourceArea">The source area to set if needed.</param> + /// <param name="targetArea">The target area to set if needed.</param> + /// <param name="sourceWidth">The width of the full source image.</param> + /// <param name="sourceHeight">The height of the full source image.</param> + private void GetPatchBounds([NotNull] ref Rectangle? sourceArea, [NotNull] ref Rectangle? targetArea, int sourceWidth, int sourceHeight) + { + sourceArea ??= new Rectangle(0, 0, sourceWidth, sourceHeight); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, this.Data.Width), Math.Min(sourceArea.Value.Height, this.Data.Height)); + } + + /// <summary>Overwrite part of the image.</summary> + /// <param name="sourceData">The image data to patch into the content.</param> + /// <param name="sourceWidth">The pixel width of the source image.</param> + /// <param name="sourceHeight">The pixel height of the source image.</param> + /// <param name="sourceArea">The part of the <paramref name="sourceData"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="sourceData"/> texture.</param> + /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param> + /// <param name="patchMode">Indicates how an image should be patched.</param> + /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> + /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> + /// <exception cref="InvalidOperationException">The content being read isn't an image.</exception> + private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode) + { + // get texture + Texture2D target = this.Data; + int pixelCount = sourceArea.Width * sourceArea.Height; + + // validate + if (sourceArea.X < 0 || sourceArea.Y < 0 || sourceArea.Right > sourceWidth || sourceArea.Bottom > sourceHeight) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (!target.Bounds.Contains(targetArea)) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Size != targetArea.Size) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // merge data if (patchMode == PatchMode.Overlay) { // get target data - Color[] targetData = GC.AllocateUninitializedArray<Color>(pixelCount); - target.GetData(0, targetArea, targetData, 0, pixelCount); + Color[] mergedData = GC.AllocateUninitializedArray<Color>(pixelCount); + target.GetData(0, targetArea, mergedData, 0, pixelCount); // merge pixels - for (int i = 0; i < sourceData.Length; i++) + for (int i = 0; i < pixelCount; i++) { Color above = sourceData[i]; - Color below = targetData[i]; + Color below = mergedData[i]; // shortcut transparency if (above.A < MinOpacity) - { - sourceData[i] = below; continue; - } if (below.A < MinOpacity) - { - sourceData[i] = above; - continue; - } + mergedData[i] = above; // merge pixels - // This performs a conventional alpha blend for the pixels, which are already - // premultiplied by the content pipeline. The formula is derived from - // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. - // Note: don't use named arguments here since they're different between - // Linux/macOS and Windows. - float alphaBelow = 1 - (above.A / 255f); - sourceData[i] = new Color( - (int)(above.R + (below.R * alphaBelow)), // r - (int)(above.G + (below.G * alphaBelow)), // g - (int)(above.B + (below.B * alphaBelow)), // b - Math.Max(above.A, below.A) // a - ); + else + { + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + float alphaBelow = 1 - (above.A / 255f); + mergedData[i] = new Color( + r: (int)(above.R + (below.R * alphaBelow)), + g: (int)(above.G + (below.G * alphaBelow)), + b: (int)(above.B + (below.B * alphaBelow)), + alpha: Math.Max(above.A, below.A) + ); + } } - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - - /// <inheritdoc /> - public bool ExtendImage(int minWidth, int minHeight) - { - if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) - return false; - Texture2D original = this.Data; - Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); - this.ReplaceWith(texture); - this.PatchImage(original); - return true; + target.SetData(0, targetArea, mergedData, 0, pixelCount); + } + else + target.SetData(0, targetArea, sourceData, 0, pixelCount); } } } diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 736ee5da..bf42812b 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Framework.Content ** Fields *********/ /// <summary>The underlying |
