summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj6
-rw-r--r--src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/ModEntry.cs7
-rw-r--r--src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs79
-rw-r--r--src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj3
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Tests/Core/InterfaceProxyTests.cs7
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj10
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj4
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj14
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json5
-rw-r--r--src/SMAPI.sln4
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs173
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs33
-rw-r--r--src/SMAPI/Framework/Content/RawTextureData.cs10
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs16
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs168
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs19
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs34
-rw-r--r--src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs118
-rw-r--r--src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs57
-rw-r--r--src/SMAPI/Framework/SCore.cs29
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs25
-rw-r--r--src/SMAPI/IAssetDataForImage.cs10
-rw-r--r--src/SMAPI/IContentHelper.cs2
-rw-r--r--src/SMAPI/IContentPack.cs2
-rw-r--r--src/SMAPI/IModContentHelper.cs2
-rw-r--r--src/SMAPI/IRawTextureData.cs17
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs26
-rw-r--r--src/SMAPI/Properties/AssemblyInfo.cs1
-rw-r--r--src/SMAPI/SMAPI.config.json18
-rw-r--r--src/SMAPI/SMAPI.csproj7
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 asset cache.</summary>
- private readonly IDictionary<string, object> Cache;
+ private readonly Dictionary<string, object> Cache;
/*********
@@ -29,7 +29,7 @@ namespace StardewModdingAPI.Framework.Content
}
/// <summary>The current cache keys.</summary>
- public IEnumerable<string> Keys => this.Cache.Keys;
+ public Dictionary<string, object>.KeyCollection Keys => this.Cache.Keys;
/*********
@@ -89,33 +89,36 @@ namespace StardewModdingAPI.Framework.Content
/// <returns>Returns the removed key (if any).</returns>
public bool Remove(string key, bool dispose)
{
- // get entry
- if (!this.Cache.TryGetValue(key, out object? value))
+ // remove and get entry
+ if (!this.Cache.Remove(key, out object? value))
return false;
// dispose & remove entry
if (dispose && value is IDisposable disposable)
disposable.Dispose();
- return this.Cache.Remove(key);
+ return true;
}
- /// <summary>Purge matched assets from the cache.</summary>
+ /// <summary>Purge assets matching <paramref name="predicate"/> from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
- /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns the removed keys (if any).</returns>
+ /// <param name="dispose">Whether to dispose invalidated assets. This should only be <see langword="true"/> when they're being invalidated as part of a <see cref="IDisposable.Dispose"/>, to avoid crashing the game.</param>
+ /// <returns>Returns any removed keys.</returns>
public IEnumerable<string> Remove(Func<string, object, bool> predicate, bool dispose)
{
- List<string> removed = new List<string>();
- foreach (string key in this.Cache.Keys.ToArray())
+ List<string> removed = new();
+ foreach ((string key, object value) in this.Cache)
{
- if (predicate(key, this.Cache[key]))
- {
- this.Remove(key, dispose);
+ if (predicate(key, value))
removed.Add(key);
- }
}
- return removed;
+
+ foreach (string key in removed)
+ this.Remove(key, dispose);
+
+ return removed.Count == 0
+ ? Enumerable.Empty<string>() // let GC collect the list in gen0 instead of potentially living longer
+ : removed;
}
}
}
diff --git a/src/SMAPI/Framework/Content/RawTextureData.cs b/src/SMAPI/Framework/Content/RawTextureData.cs
new file mode 100644
index 00000000..4a0835b0
--- /dev/null
+++ b/src/SMAPI/Framework/Content/RawTextureData.cs
@@ -0,0 +1,10 @@
+using Microsoft.Xna.Framework;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>The raw data for an image read from the filesystem.</summary>
+ /// <param name="Width">The image width.</param>
+ /// <param name="Height">The image height.</param>
+ /// <param name="Data">The loaded image data.</param>
+ internal record RawTextureData(int Width, int Height, Color[] Data) : IRawTextureData;
+}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index cfeb35c8..3e09ac62 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -32,6 +32,9 @@ namespace StardewModdingAPI.Framework
/// <summary>An asset key prefix for assets from SMAPI mod folders.</summary>
private readonly string ManagedPrefix = "SMAPI";
+ /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
+ private readonly bool UseRawImageLoading;
+
/// <summary>Get a file lookup for the given directory.</summary>
private readonly Func<string, IFileLookup> GetFileLookup;
@@ -130,7 +133,8 @@ namespace StardewModdingAPI.Framework
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
+ /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations, bool useRawImageLoading)
{
this.GetFileLookup = getFileLookup;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
@@ -141,6 +145,7 @@ 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",
@@ -219,7 +224,8 @@ namespace StardewModdingAPI.Framework
reflection: this.Reflection,
jsonHelper: this.JsonHelper,
onDisposing: this.OnDisposing,
- fileLookup: this.GetFileLookup(rootDirectory)
+ fileLookup: this.GetFileLookup(rootDirectory),
+ useRawImageLoading: this.UseRawImageLoading
);
this.ContentManagers.Add(manager);
return manager;
@@ -465,7 +471,7 @@ namespace StardewModdingAPI.Framework
assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value),
ignoreWorld: Context.IsWorldFullyUnloaded,
out IDictionary<IAssetName, bool> propagated,
- out bool updatedNpcWarps
+ out bool updatedWarpRoutes
);
// log summary
@@ -481,8 +487,8 @@ namespace StardewModdingAPI.Framework
? $"Propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."
: "Propagated 0 core assets."
);
- if (updatedNpcWarps)
- report.AppendLine("Updated NPC pathfinding cache.");
+ if (updatedWarpRoutes)
+ report.AppendLine("Updated NPC warp route cache.");
}
this.Monitor.Log(report.ToString().TrimEnd());
}
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 4390d472..446f4a67 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -9,6 +9,7 @@ using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Deprecations;
+using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
@@ -93,6 +94,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <inheritdoc />
public override T LoadExact<T>(IAssetName assetName, bool useCache)
{
+ if (typeof(IRawTextureData).IsAssignableFrom(typeof(T)))
+ throw new SContentLoadException(ContentLoadErrorType.Other, $"Can't load {nameof(IRawTextureData)} assets from the game content pipeline. This asset type is only available for mod files.");
+
// raise first-load callback
if (GameContentManager.IsFirstLoad)
{
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 1b94b8c6..8c5d0f84 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -7,6 +9,8 @@ using BmFont;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
+using SkiaSharp;
+using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialization;
@@ -20,11 +24,14 @@ using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
{
/// <summary>A content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
- internal class ModContentManager : BaseContentManager
+ internal sealed class ModContentManager : BaseContentManager
{
/*********
** Fields
*********/
+ /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
+ private readonly bool UseRawImageLoading;
+
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
@@ -38,7 +45,17 @@ namespace StardewModdingAPI.Framework.ContentManagers
private readonly IFileLookup FileLookup;
/// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary>
- private static readonly HashSet<string> LocalTilesheetExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".xnb" };
+ private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" };
+
+ /// <summary>A lookup of image file paths to whether they have PyTK scaling information.</summary>
+ private static readonly Dictionary<string, bool> IsPyTkScaled = new(StringComparer.OrdinalIgnoreCase);
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether to enable legacy compatibility mode for PyTK scale-up textures.</summary>
+ internal static bool EnablePyTkLegacyMode;
/*********
@@ -57,13 +74,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="fileLookup">A lookup for files within the <paramref name="rootDirectory"/>.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup)
+ /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFileLookup fileLookup, bool useRawImageLoading)
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
{
this.GameContentManager = gameContentManager;
this.FileLookup = fileLookup;
this.JsonHelper = jsonHelper;
this.ModName = modName;
+ this.UseRawImageLoading = useRawImageLoading;
this.TryLocalizeKeys = false;
}
@@ -119,8 +138,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
_ => this.HandleUnknownFileType<T>(assetName, file)
};
}
- catch (Exception ex) when (ex is not SContentLoadException)
+ catch (Exception ex)
{
+ if (ex is SContentLoadException)
+ throw;
+
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
}
@@ -130,6 +152,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <inheritdoc />
+ [Obsolete($"Temporary {nameof(ModContentManager)}s are unsupported")]
public override LocalizedContentManager CreateTemporary()
{
throw new NotSupportedException("Can't create a temporary mod content manager.");
@@ -155,11 +178,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T LoadFont<T>(IAssetName assetName, FileInfo file)
{
- // validate
- if (!typeof(T).IsAssignableFrom(typeof(XmlSource)))
- throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'.");
+ this.AssertValidType<T>(assetName, file, typeof(XmlSource));
- // load
string source = File.ReadAllText(file.FullName);
return (T)(object)new XmlSource(source);
}
@@ -182,15 +202,89 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T LoadImageFile<T>(IAssetName assetName, FileInfo file)
{
- // validate
- if (typeof(T) != typeof(Texture2D))
- throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
+ this.AssertValidType<T>(assetName, file, typeof(Texture2D), typeof(IRawTextureData));
+ bool expectsRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData));
+ bool asRawData = expectsRawData || this.UseRawImageLoading;
+
+ // disable raw data if PyTK will rescale the image (until it supports raw data)
+ if (asRawData && !expectsRawData)
+ {
+ if (ModContentManager.EnablePyTkLegacyMode)
+ {
+ if (!ModContentManager.IsPyTkScaled.TryGetValue(file.FullName, out bool isScaled))
+ {
+ string? dirPath = file.DirectoryName;
+ string fileName = $"{Path.GetFileNameWithoutExtension(file.Name)}.pytk.json";
+
+ string path = dirPath is not null
+ ? Path.Combine(dirPath, fileName)
+ : fileName;
+
+ ModContentManager.IsPyTkScaled[file.FullName] = isScaled = File.Exists(path);
+ }
+
+ asRawData = !isScaled;
+ if (!asRawData)
+ this.Monitor.LogOnce("Enabled compatibility mode for PyTK scaled textures. This won't cause any issues, but may impact performance.", LogLevel.Warn);
+ }
+ else
+ asRawData = true;
+ }
// load
- using FileStream stream = File.OpenRead(file.FullName);
- Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
- return (T)(object)texture;
+ if (asRawData)
+ {
+ IRawTextureData raw = this.LoadRawImageData(file, expectsRawData);
+
+ if (expectsRawData)
+ return (T)raw;
+ else
+ {
+ Texture2D texture = new(Game1.graphics.GraphicsDevice, raw.Width, raw.Height);
+ texture.SetData(raw.Data);
+ return (T)(object)texture;
+ }
+ }
+ else
+ {
+ using FileStream stream = File.OpenRead(file.FullName);
+ Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
+ texture = this.PremultiplyTransparency(texture);
+ return (T)(object)texture;
+ }
+ }
+
+ /// <summary>Load the raw image data from a file on disk.</summary>
+ /// <param name="file">The file whose data to load.</param>
+ /// <param name="forRawData">Whether the data is being loaded for an <see cref="IRawTextureData"/> (true) or <see cref="Texture2D"/> (false) instance.</param>
+ /// <remarks>This is separate to let framework mods intercept the data before it's loaded, if needed.</remarks>
+ [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")]
+ [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "The 'forRawData' parameter is only added for mods which may intercept this method.")]
+ private IRawTextureData LoadRawImageData(FileInfo file, bool forRawData)
+ {
+ // load raw data
+ int width;
+ int height;
+ SKPMColor[] rawPixels;
+ {
+ using FileStream stream = File.OpenRead(file.FullName);
+ using SKBitmap bitmap = SKBitmap.Decode(stream);
+ rawPixels = SKPMColor.PreMultiply(bitmap.Pixels);
+ width = bitmap.Width;
+ height = bitmap.Height;
+ }
+
+ // convert to XNA pixel format
+ var pixels = GC.AllocateUninitializedArray<Color>(rawPixels.Length);
+ for (int i = 0; i < pixels.Length; i++)
+ {
+ SKPMColor pixel = rawPixels[i];
+ pixels[i] = pixel.Alpha == 0
+ ? Color.Transparent
+ : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha);
+ }
+
+ return new RawTextureData(width, height, pixels);
}
/// <summary>Load an unpacked image file (<c>.tbin</c> or <c>.tmx</c>).</summary>
@@ -199,11 +293,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T LoadMapFile<T>(IAssetName assetName, FileInfo file)
{
- // validate
- if (typeof(T) != typeof(Map))
- throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
+ this.AssertValidType<T>(assetName, file, typeof(Map));
- // load
FormatManager formatManager = FormatManager.Instance;
Map map = formatManager.LoadMap(file.FullName);
map.assetPath = assetName.Name;
@@ -216,6 +307,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="assetName">The asset name relative to the loader root directory.</param>
private T LoadXnbFile<T>(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.");
+
// the underlying content manager adds a .xnb extension implicitly, so
// we need to strip it here to avoid trying to load a '.xnb.xnb' file.
IAssetName loadName = assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)
@@ -242,11 +336,24 @@ namespace StardewModdingAPI.Framework.ContentManagers
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
}
+ /// <summary>Assert that the asset type is compatible with one of the allowed types.</summary>
+ /// <typeparam name="TAsset">The actual asset type.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file being loaded.</param>
+ /// <param name="validTypes">The allowed asset types.</param>
+ /// <exception cref="SContentLoadException">The <typeparamref name="TAsset"/> is not compatible with any of the <paramref name="validTypes"/>.</exception>
+ private void AssertValidType<TAsset>(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))}'.");
+ }
+
/// <summary>Get an error which indicates that an asset couldn't be loaded.</summary>
/// <param name="errorType">Why loading an asset through the content pipeline failed.</param>
/// <param name="assetName">The asset name that failed to load.</param>
/// <param name="reasonPhrase">The reason the file couldn't be loaded.</param>
/// <param name="exception">The underlying exception, if applicable.</param>
+ [DebuggerStepThrough, DebuggerHidden]
private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
{
return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
@@ -261,7 +368,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
FileInfo file = this.FileLookup.GetFile(path);
// try with default image extensions
- if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension))
+ if (!file.Exists && typeof(Texture2D).IsAssignableFrom(typeof(T)) && !ModContentManager.LocalTilesheetExtensions.Contains(file.Extension, StringComparer.OrdinalIgnoreCase))
{
foreach (string extension in ModContentManager.LocalTilesheetExtensions)
{
@@ -284,7 +391,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
private Texture2D PremultiplyTransparency(Texture2D texture)
{
// premultiply pixels
- Color[] data = new Color[texture.Width * texture.Height];
+ Color[] data = GC.AllocateUninitializedArray<Color>(texture.Width * texture.Height);
texture.GetData(data);
bool changed = false;
for (int i = 0; i < data.Length; i++)
@@ -324,7 +431,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// reverse incorrect eager tilesheet path prefixing
if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder))
- imageSource = imageSource.Substring(relativeMapFolder.Length + 1);
+ imageSource = imageSource[(relativeMapFolder.Length + 1)..];
// validate tilesheet path
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
@@ -345,8 +452,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
tilesheet.ImageSource = assetName.Name;
}
}
- catch (Exception ex) when (ex is not SContentLoadException)
+ catch (Exception ex)
{
+ if (ex is SContentLoadException)
+ throw;
+
throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex);
}
}
@@ -361,7 +471,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <remarks>See remarks on <see cref="FixTilesheetPaths"/>.</remarks>
private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error)
{
- assetName = null;
error = null;
// nothing to do
@@ -376,7 +485,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// opened in Tiled, while still mapping it to the vanilla 'Maps/spring_town' asset at runtime.
{
string filename = Path.GetFileName(relativePath);
- if (filename.StartsWith("."))
+ if (filename.StartsWith('.'))
relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.'));
}
@@ -391,7 +500,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// get from game assets
- IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false);
+ AssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false);
try
{
this.GameContentManager.LoadLocalized<Texture2D>(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
@@ -412,6 +521,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// not found
+ assetName = null;
error = "The tilesheet couldn't be found relative to either map file or the game's content folder.";
return false;
}
@@ -422,11 +532,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
// get file path
string path = Path.Combine(this.GameContentManager.FullRootDirectory, key);
- if (!path.EndsWith(".xnb"))
+ if (!path.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase))
path += ".xnb";
// get file
- return new FileInfo(path).Exists;
+ return File.Exists(path);
}
/// <summary>Get the asset key for a tilesheet in the game's <c>Maps</c> content folder.</summary>
@@ -442,7 +552,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// remove file extension from unpacked file
if (key.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
- key = key.Substring(0, key.Length - 4);
+ key = key[..^4];
return key;
}
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index ed5b6959..d811ed5c 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -31,8 +31,8 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
private const char IgnoreChar = InterceptingTextWriter.IgnoreChar;
- /// <summary>Get a named monitor instance.</summary>
- private readonly Func<string, Monitor> GetMonitorImpl;
+ /// <summary>Create a monitor instance given the ID and name.</summary>
+ private readonly Func<string, string, Monitor> GetMonitorImpl;
/// <summary>Regex patterns which match console non-error messages to suppress from the console and log.</summary>
private readonly Regex[] SuppressConsolePatterns =
@@ -88,23 +88,23 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="logPath">The log file path to write.</param>
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
- /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param>
+ /// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param>
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
/// <param name="getScreenIdForLog">Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</param>
- public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog)
+ public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, HashSet<string> verboseLogging, bool isDeveloperMode, Func<int?> getScreenIdForLog)
{
// init log file
this.LogFile = new LogFileManager(logPath);
// init monitor
- this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
+ this.GetMonitorImpl = (id, name) => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog)
{
WriteToConsole = writeToConsole,
ShowTraceInConsole = isDeveloperMode,
ShowFullStampInConsole = isDeveloperMode
};
- this.Monitor = this.GetMonitor("SMAPI");
- this.MonitorForGame = this.GetMonitor("game");
+ this.Monitor = this.GetMonitor("SMAPI", "SMAPI");
+ this.MonitorForGame = this.GetMonitor("game", "game");
// redirect direct console output
this.ConsoleInterceptor = new InterceptingTextWriter(
@@ -124,10 +124,11 @@ namespace StardewModdingAPI.Framework.Logging
}
/// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
+ /// <param name="id">The unique ID for the mod context.</param>
/// <param name="name">The name of the module which will log messages with this instance.</param>
- public Monitor GetMonitor(string name)
+ public Monitor GetMonitor(string id, string name)
{
- return this.GetMonitorImpl(name);
+ return this.GetMonitorImpl(id, name);
}
/// <summary>Set the title of the SMAPI console window.</summary>
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index 1c7cd3ed..baef6144 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -20,10 +20,9 @@ namespace StardewModdingAPI.Framework.Models
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
- [nameof(VerboseLogging)] = false,
[nameof(LogNetworkTraffic)] = false,
[nameof(RewriteMods)] = true,
- [nameof(UsePintail)] = true,
+ [nameof(UseRawImageLoading)] = true,
[nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux
};
@@ -57,14 +56,15 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>The base URL for SMAPI's web API, used to perform update checks.</summary>
public string WebApiBaseUrl { get; }
- /// <summary>Whether SMAPI should log more information about the game context.</summary>
- public bool VerboseLogging { get; }
+ /// <summary>The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</summary>
+ /// <remarks>The possible values are "*" (everything is verbose), "SMAPI", (SMAPI itself), or mod IDs.</remarks>
+ public HashSet<string> VerboseLogging { get; }
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
public bool RewriteMods { get; }
- /// <summary>Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</summary>
- public bool UsePintail { get; }
+ /// <summary>Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</summary>
+ public bool UseRawImageLoading { get; }
/// <summary>Whether to make SMAPI file APIs case-insensitive, even on Linux.</summary>
public bool UseCaseInsensitivePaths { get; }
@@ -76,7 +76,7 @@ namespace StardewModdingAPI.Framework.Models
public ColorSchemeConfig ConsoleColors { get; }
/// <summary>The mod IDs SMAPI should ignore when performing update checks or validating update keys.</summary>
- public string[] SuppressUpdateChecks { get; }
+ public HashSet<string> SuppressUpdateChecks { get; }
/********
@@ -89,14 +89,14 @@ namespace StardewModdingAPI.Framework.Models
/// <param name="useBetaChannel">Whether to show beta versions as valid updates.</param>
/// <param name="gitHubProjectName">SMAPI's GitHub project name, used to perform update checks.</param>
/// <param name="webApiBaseUrl">The base URL for SMAPI's web API, used to perform update checks.</param>
- /// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param>
+ /// <param name="verboseLogging">The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting.</param>
/// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
- /// <param name="usePintail">Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</param>
+ /// <param name="useRawImageLoading">Whether to use raw image data when possible, instead of initializing an XNA Texture2D instance through the GPU.</param>
/// <param name="useCaseInsensitivePaths">>Whether to make SMAPI file APIs case-insensitive, even on Linux.</param>
/// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param>
/// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param>
/// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param>
- public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
@@ -104,13 +104,13 @@ namespace StardewModdingAPI.Framework.Models
this.UseBetaChannel = useBetaChannel ?? (bool)SConfig.DefaultValues[nameof(this.UseBetaChannel)];
this.GitHubProjectName = gitHubProjectName;
this.WebApiBaseUrl = webApiBaseUrl;
- this.VerboseLogging = verboseLogging ?? (bool)SConfig.DefaultValues[nameof(this.VerboseLogging)];
+ this.VerboseLogging = new HashSet<string>(verboseLogging ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)];
- this.UsePintail = usePintail ?? (bool)SConfig.DefaultValues[nameof(this.UsePintail)];
+ 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;
- this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>();
+ this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Override the value of <see cref="DeveloperMode"/>.</summary>
@@ -132,9 +132,11 @@ namespace StardewModdingAPI.Framework.Models
custom[name] = value;
}
- HashSet<string> curSuppressUpdateChecks = new(this.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
- if (SConfig.DefaultSuppressUpdateChecks.Count != curSuppressUpdateChecks.Count || SConfig.DefaultSuppressUpdateChecks.Any(p => !curSuppressUpdateChecks.Contains(p)))
- custom[nameof(this.SuppressUpdateChecks)] = "[" + string.Join(", ", this.SuppressUpdateChecks) + "]";
+ if (!this.SuppressUpdateChecks.SetEquals(SConfig.DefaultSuppressUpdateChecks))
+ custom[nameof(this.SuppressUpdateChecks)] = $"[{string.Join(", ", this.SuppressUpdateChecks)}]";
+
+ if (this.VerboseLogging.Any())
+ custom[nameof(this.VerboseLogging)] = $"[{string.Join(", ", this.VerboseLogging)}]";
return custom;
}
diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs
deleted file mode 100644
index 9576f768..00000000
--- a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-using System;
-using System.Linq;
-using System.Reflection;
-using System.Reflection.Emit;
-
-namespace StardewModdingAPI.Framework.Reflection
-{
- /// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary>
- internal class OriginalInterfaceProxyBuilder
- {
- /*********
- ** Fields
- *********/
- /// <summary>The target class type.</summary>
- private readonly Type TargetType;
-
- /// <summary>The generated proxy type.</summary>
- private readonly Type ProxyType;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="name">The type name to generate.</param>
- /// <param name="moduleBuilder">The CLR module in which to create proxy classes.</param>
- /// <param name="interfaceType">The interface type to implement.</param>
- /// <param name="targetType">The target type.</param>
- public OriginalInterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType)
- {
- // validate
- if (name == null)
- throw new ArgumentNullException(nameof(name));
- if (targetType == null)
- throw new ArgumentNullException(nameof(targetType));
-
- // define proxy type
- TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class);
- proxyBuilder.AddInterfaceImplementation(interfaceType);
-
- // create field to store target instance
- FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);
-
- // create constructor which accepts target instance and sets field
- {
- ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType });
- ILGenerator il = constructor.GetILGenerator();
-
- il.Emit(OpCodes.Ldarg_0); // this
- // ReSharper disable once AssignNullToNotNullAttribute -- never null
- il.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)!); // call base constructor
- il.Emit(OpCodes.Ldarg_0); // this
- il.Emit(OpCodes.Ldarg_1); // load argument
- il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument
- il.Emit(OpCodes.Ret);
- }
-
- // proxy methods
- foreach (MethodInfo proxyMethod in interfaceType.GetMethods())
- {
- var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray());
- if (targetMethod == null)
- throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API.");
-
- this.ProxyMethod(proxyBuilder, targetMethod, targetField);
- }
-
- // save info
- this.TargetType = targetType;
- this.ProxyType = proxyBuilder.CreateType()!;
- }
-
- /// <summary>Create an instance of the proxy for a target instance.</summary>
- /// <param name="targetInstance">The target instance.</param>
- public object CreateInstance(object targetInstance)
- {
- ConstructorInfo? constructor = this.ProxyType.GetConstructor(new[] { this.TargetType });
- if (constructor == null)
- throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen
- return constructor.Invoke(new[] { targetInstance });
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>Define a method which proxies access to a method on the target.</summary>
- /// <param name="proxyBuilder">The proxy type being generated.</param>
- /// <param name="target">The target method.</param>
- /// <param name="instanceField">The proxy field containing the API instance.</param>
- private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField)
- {
- Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray();
-
- // create method
- MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual);
- methodBuilder.SetParameters(argTypes);
- methodBuilder.SetReturnType(target.ReturnType);
-
- // create method body
- {
- ILGenerator il = methodBuilder.GetILGenerator();
-
- // load target instance
- il.Emit(OpCodes.Ldarg_0);
- il.Emit(OpCodes.Ldfld, instanceField);
-
- // invoke target method on instance
- for (int i = 0; i < argTypes.Length; i++)
- il.Emit(OpCodes.Ldarg, i + 1);
- il.Emit(OpCodes.Call, target);
-
- // return result
- il.Emit(OpCodes.Ret);
- }
- }
- }
-}
diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs
deleted file mode 100644
index d6966978..00000000
--- a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Reflection.Emit;
-
-namespace StardewModdingAPI.Framework.Reflection
-{
- /// <inheritdoc />
- internal class OriginalInterfaceProxyFactory : IInterfaceProxyFactory
- {
- /*********
- ** Fields
- *********/
- /// <summary>The CLR module in which to create proxy classes.</summary>
- private readonly ModuleBuilder ModuleBuilder;
-
- /// <summary>The generated proxy types.</summary>
- private readonly IDictionary<string, OriginalInterfaceProxyBuilder> Builders = new Dictionary<string, OriginalInterfaceProxyBuilder>();
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- public OriginalInterfaceProxyFactory()
- {
- AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
- this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
- }
-
- /// <inheritdoc />
- public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
- where TInterface : class
- {
- lock (this.Builders)
- {
- // validate
- if (instance == null)
- throw new InvalidOperationException("Can't proxy access to a null API.");
- if (!typeof(TInterface).IsInterface)
- throw new InvalidOperationException("The proxy type must be an interface, not a class.");
-
- // get proxy type
- Type targetType = instance.GetType();
- string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>";
- if (!this.Builders.TryGetValue(proxyTypeName, out OriginalInterfaceProxyBuilder? builder))
- {
- builder = new OriginalInterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType);
- this.Builders[proxyTypeName] = builder;
- }
-
- // create instance
- return (TInterface)builder.CreateInstance(instance);
- }
- }
- }
-}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 731731d4..385a94ea 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -194,7 +194,7 @@ namespace StardewModdingAPI.Framework
if (developerMode.HasValue)
this.Settings.OverrideDeveloperMode(developerMode.Value);
- this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog);
+ this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, verboseLogging: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog);
this.CommandManager = new CommandManager(this.Monitor);
this.EventManager = new EventManager(this.ModRegistry);
SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
@@ -1231,7 +1231,7 @@ namespace StardewModdingAPI.Framework
}
// make sure it's a content pack for the requesting mod
- if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest.ContentPackFor?.UniqueID, mod.Manifest.UniqueID))
+ if (!onBehalfOf.IsContentPack || !string.Equals(onBehalfOf.Manifest.ContentPackFor?.UniqueID, mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase))
{
mod.LogAsModOnce($"{errorPrefix}: that isn't a content pack for this mod.", LogLevel.Warn);
return null;
@@ -1301,7 +1301,8 @@ namespace StardewModdingAPI.Framework
onAssetLoaded: this.OnAssetLoaded,
onAssetsInvalidated: this.OnAssetsInvalidated,
getFileLookup: this.GetFileLookup,
- requestAssetOperations: this.RequestAssetOperations
+ requestAssetOperations: this.RequestAssetOperations,
+ useRawImageLoading: this.Settings.UseRawImageLoading
);
if (this.ContentCore.Language != this.Translator.LocaleEnum)
this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language);
@@ -1507,7 +1508,7 @@ namespace StardewModdingAPI.Framework
{
try
{
- HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
+ HashSet<string> suppressUpdateChecks = this.Settings.SuppressUpdateChecks;
// prepare search model
List<ModSearchEntryModel> searchMods = new List<ModSearchEntryModel>();
@@ -1608,10 +1609,8 @@ namespace StardewModdingAPI.Framework
using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods))
{
// init
- HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
- IInterfaceProxyFactory proxyFactory = this.Settings.UsePintail
- ? new InterfaceProxyFactory()
- : new OriginalInterfaceProxyFactory();
+ HashSet<string> suppressUpdateChecks = this.Settings.SuppressUpdateChecks;
+ IInterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
// load mods
foreach (IModMetadata mod in mods)
@@ -1637,6 +1636,14 @@ namespace StardewModdingAPI.Framework
// initialize translations
this.ReloadTranslations(loaded);
+ // set temporary PyTK compatibility mode
+ // 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.
+ {
+ IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit");
+ ModContentManager.EnablePyTkLegacyMode = pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.23.1");
+ }
+
// initialize loaded non-content-pack mods
this.Monitor.Log("Launching mods...", LogLevel.Debug);
#pragma warning disable CS0612, CS0618 // deprecated code
@@ -1827,7 +1834,7 @@ namespace StardewModdingAPI.Framework
// load as content pack
if (mod.IsContentPack)
{
- IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
+ IMonitor monitor = this.LogManager.GetMonitor(manifest.UniqueID, mod.DisplayName);
IFileLookup fileLookup = this.GetFileLookup(mod.DirectoryPath);
GameContentHelper gameContentHelper = new(this.ContentCore, mod, mod.DisplayName, monitor, this.Reflection);
IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), this.Reflection);
@@ -1902,7 +1909,7 @@ namespace StardewModdingAPI.Framework
}
// init mod helpers
- IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
+ IMonitor monitor = this.LogManager.GetMonitor(manifest.UniqueID, mod.DisplayName);
TranslationHelper translationHelper = new(mod, contentCore.GetLocale(), contentCore.Language);
IModHelper modHelper;
{
@@ -1965,7 +1972,7 @@ namespace StardewModdingAPI.Framework
);
// create mod helpers
- IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name);
+ IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.UniqueID, packManifest.Name);
GameContentHelper gameContentHelper = new(contentCore, fakeMod, packManifest.Name, packMonitor, this.Reflection);
IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, fakeMod, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), this.Reflection);
TranslationHelper packTranslationHelper = new(fakeMod, contentCore.GetLocale(), contentCore.Language);
diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
index b5fc1f57..1fcda077 100644
--- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
+++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
@@ -27,15 +27,6 @@ namespace MonoMod.Utils
private static readonly object[] _NoArgs = Array.Empty<object>();
private static readonly object?[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null };
- private static readonly Type? t_RuntimeModule =
- typeof(Module).Assembly
- .GetType("System.Reflection.RuntimeModule");
-
- private static readonly PropertyInfo? p_RuntimeModule_RuntimeType =
- typeof(Module).Assembly
- .GetType("System.Reflection.RuntimeModule")
- ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
-
private static readonly Type? t_RuntimeType =
typeof(Type).Assembly
.GetType("System.RuntimeType");
@@ -109,22 +100,6 @@ namespace MonoMod.Utils
}
}
- public static Type? GetModuleType(this Module? module)
- {
- // Sadly we can't blindly resolve type 0x02000001 as the runtime throws ArgumentException.
-
- if (module == null || t_RuntimeModule == null || !t_RuntimeModule.IsInstanceOfType(module))
- return null;
-
- // .NET
- if (p_RuntimeModule_RuntimeType != null)
- return (Type?)p_RuntimeModule_RuntimeType.GetValue(module, _NoArgs);
-
- // The hotfix doesn't apply to Mono anyway, thus that's not copied over.
-
- return null;
- }
-
public static Type? GetRealDeclaringType(this MemberInfo member)
{
return member.DeclaringType ?? member.Module.GetModuleType();
diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs
index 6f8a4719..3e5b833d 100644
--- a/src/SMAPI/IAssetDataForImage.cs
+++ b/src/SMAPI/IAssetDataForImage.cs
@@ -18,6 +18,16 @@ namespace StardewModdingAPI
/// <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>
+ void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace);
+
+ /// <summary>Overwrite part of the image.</summary>
+ /// <param name="source">The image to patch into the content.</param>
+ /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> 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>
void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace);
/// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs
index 2cd0c1fc..7637edf0 100644
--- a/src/SMAPI/IContentHelper.cs
+++ b/src/SMAPI/IContentHelper.cs
@@ -35,7 +35,7 @@ namespace StardewModdingAPI
** Public methods
*********/
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
- /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam>
+ /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/> (for mod content only), and data structures; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs
index 1215fe0b..73b1a860 100644
--- a/src/SMAPI/IContentPack.cs
+++ b/src/SMAPI/IContentPack.cs
@@ -48,7 +48,7 @@ namespace StardewModdingAPI
where TModel : class;
/// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
- /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
+ /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/>, and data structures; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="key">The relative file path within the content pack (case-insensitive).</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
diff --git a/src/SMAPI/IModContentHelper.cs b/src/SMAPI/IModContentHelper.cs
index f1f6ce94..1e2d82a8 100644
--- a/src/SMAPI/IModContentHelper.cs
+++ b/src/SMAPI/IModContentHelper.cs
@@ -12,7 +12,7 @@ namespace StardewModdingAPI
** Public methods
*********/
/// <summary>Load content from the mod folder and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
- /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam>
+ /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, <see cref="IRawTextureData"/>, and data structures; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="relativePath">The local path to a content file relative to the mod folder.</param>
/// <exception cref="ArgumentException">The <paramref name="relativePath"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
diff --git a/src/SMAPI/IRawTextureData.cs b/src/SMAPI/IRawTextureData.cs
new file mode 100644
index 00000000..a4da52f3
--- /dev/null
+++ b/src/SMAPI/IRawTextureData.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace StardewModdingAPI
+{
+ /// <summary>The raw data for an image read from the filesystem.</summary>
+ public interface IRawTextureData
+ {
+ /// <summary>The image width.</summary>
+ int Width { get; }
+
+ /// <summary>The image height.</summary>
+ int Height { get; }
+
+ /// <summary>The loaded image data.</summary>
+ Color[] Data { get; }
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 8ed6b591..b783b2b9 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -85,8 +85,8 @@ namespace StardewModdingAPI.Metadata
/// <param name="assets">The asset keys and types to reload.</param>
/// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
/// <param name="propagatedAssets">A lookup of asset names to whether they've been propagated.</param>
- /// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param>
- public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> propagatedAssets, out bool updatedNpcWarps)
+ /// <param name="changedWarpRoutes">Whether the NPC pathfinding warp route cache was reloaded.</param>
+ public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> propagatedAssets, out bool changedWarpRoutes)
{
// get base name lookup
propagatedAssets = assets
@@ -107,7 +107,7 @@ namespace StardewModdingAPI.Metadata
});
// reload assets
- updatedNpcWarps = false;
+ changedWarpRoutes = false;
foreach (var bucket in buckets)
{
switch (bucket.Key)
@@ -126,10 +126,10 @@ namespace StardewModdingAPI.Metadata
foreach (var entry in bucket)
{
bool changed = false;
- bool curChangedMapWarps = false;
+ bool curChangedMapRoutes = false;
try
{
- changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapWarps);
+ changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapRoutes);
}
catch (Exception ex)
{
@@ -137,14 +137,14 @@ namespace StardewModdingAPI.Metadata
}
propagatedAssets[entry.Key] = changed;
- updatedNpcWarps = updatedNpcWarps || curChangedMapWarps;
+ changedWarpRoutes = changedWarpRoutes || curChangedMapRoutes;
}
break;
}
}
- // reload NPC pathfinding cache if any map changed
- if (updatedNpcWarps)
+ // reload NPC pathfinding cache if any map routes changed
+ if (changedWarpRoutes)
NPC.populateRoutesFromLocationToLocationList();
}
@@ -156,14 +156,14 @@ namespace StardewModdingAPI.Metadata
/// <param name="assetName">The asset name to reload.</param>
/// <param name="type">The asset type to reload.</param>
/// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
- /// <param name="changedWarps">Whether any map warps were changed as part of this propagation.</param>
+ /// <param name="changedWarpRoutes">Whether the locations reachable by warps from this location changed as part of this propagation.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")]
- private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps)
+ private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarpRoutes)
{
var content = this.MainContentManager;
string key = assetName.BaseName;
- changedWarps = false;
+ changedWarpRoutes = false;
/****
** Special case: current map tilesheet
@@ -197,7 +197,7 @@ namespace StardewModdingAPI.Metadata
static ISet<string> GetWarpSet(GameLocation location)
{
return new HashSet<string>(
- location.warps.Select(p => $"{p.X} {p.Y} {p.TargetName} {p.TargetX} {p.TargetY}")
+ location.warps.Select(p => p.TargetName)
);
}
@@ -205,7 +205,7 @@ namespace StardewModdingAPI.Metadata
this.UpdateMap(info);
var newWarps = GetWarpSet(location);
- changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
+ changedWarpRoutes = changedWarpRoutes || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
anyChanged = true;
}
}
diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs
index daa58943..afff16b8 100644
--- a/src/SMAPI/Properties/AssemblyInfo.cs
+++ b/src/SMAPI/Properties/AssemblyInfo.cs
@@ -3,3 +3,4 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SMAPI.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing
[assembly: InternalsVisibleTo("ContentPatcher")]
+[assembly: InternalsVisibleTo("ErrorHandler")]
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 8324f45b..97e8e00c 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -16,9 +16,17 @@ copy all the settings, or you may cause bugs due to overridden changes in future
*/
{
/**
- * Whether SMAPI should log more information about the game context.
+ * The logs for which to enable verbose logging, which may show a lot more information to
+ * simplify troubleshooting.
+ *
+ * The possible values are:
+ * - "*" for everything (not recommended);
+ * - "SMAPI" for messages from SMAPI itself;
+ * - mod IDs from their manifest.json files.
+ *
+ * For example: [ "SMAPI", "Pathoschild.ContentPatcher" ]
*/
- "VerboseLogging": false,
+ "VerboseLogging": [],
/**
* Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new
@@ -53,6 +61,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"UsePintail": true,
/**
+ * 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
* part of their normal functionality, so these warnings are meaningless without further
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index a0ca54cc..3abefeab 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -23,9 +23,9 @@
<ItemGroup>
<PackageReference Include="LargeAddressAware" Version="1.0.5" />
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
- <PackageReference Include="MonoMod.Common" Version="21.6.21.1" />
- <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
- <PackageReference Include="Pintail" Version="2.1.0" />
+ <PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+ <PackageReference Include="Pintail" Version="2.2.0" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
@@ -41,6 +41,7 @@
<Reference Include="GalaxyCSharp" HintPath="$(GamePath)\GalaxyCSharp.dll" Private="False" />
<Reference Include="Lidgren.Network" HintPath="$(GamePath)\Lidgren.Network.dll" Private="False" />
<Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" />
+ <Reference Include="SkiaSharp" HintPath="$(GamePath)\SkiaSharp.dll" Private="False" />
<Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" />
</ItemGroup>