summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.ModBuildConfig/build/smapi.targets20
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs4
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj2
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/Manifest.cs53
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs17
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj2
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs162
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs65
-rw-r--r--src/SMAPI/Framework/Logging/LogOnceCacheKey.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs2
-rw-r--r--src/SMAPI/Framework/Monitor.cs11
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs2
-rw-r--r--src/SMAPI/SMAPI.csproj2
13 files changed, 243 insertions, 109 deletions
diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets
index 12619439..b4fd312e 100644
--- a/src/SMAPI.ModBuildConfig/build/smapi.targets
+++ b/src/SMAPI.ModBuildConfig/build/smapi.targets
@@ -27,8 +27,12 @@
<EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging>
<BundleExtraAssemblies Condition="'$(BundleExtraAssemblies)' == ''"></BundleExtraAssemblies>
+ <!-- simplify conditions -->
+ <_BundleExtraAssembliesForGame>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|All\b', RegexOptions.IgnoreCase))</_BundleExtraAssembliesForGame>
+ <_BundleExtraAssembliesForAny>$([System.Text.RegularExpressions.Regex]::IsMatch('$(BundleExtraAssemblies)', '\bGame|System|ThirdParty|All\b', RegexOptions.IgnoreCase))</_BundleExtraAssembliesForAny>
+
<!-- coppy referenced DLLs into build output -->
- <CopyLocalLockFileAssemblies Condition="$(BundleExtraAssemblies.Contains('ThirdParty')) OR $(BundleExtraAssemblies.Contains('Game')) OR $(BundleExtraAssemblies.Contains('System')) OR $(BundleExtraAssemblies.Contains('All'))">true</CopyLocalLockFileAssemblies>
+ <CopyLocalLockFileAssemblies Condition="$(_BundleExtraAssembliesForAny)">true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'">
@@ -44,17 +48,17 @@
**********************************************-->
<ItemGroup>
<!-- game -->
- <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
- <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
- <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
- <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
+ <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(_BundleExtraAssembliesForGame)" />
+ <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(_BundleExtraAssembliesForGame)" />
+ <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(_BundleExtraAssembliesForGame)" />
+ <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(_BundleExtraAssembliesForGame)" />
<!-- SMAPI -->
- <Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
- <Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
+ <Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(_BundleExtraAssembliesForGame)" />
+ <Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(_BundleExtraAssembliesForGame)" />
<!-- Harmony -->
- <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" />
+ <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(_BundleExtraAssembliesForGame)" />
</ItemGroup>
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index a85ef109..d115810a 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -45,10 +45,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
".png",
".psd",
".tif",
+ ".xcf", // gimp files
// archives
".rar",
".zip",
+ ".7z",
+ ".tar",
+ ".tar.gz"
// backup files
".backup",
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
index 7b79105f..bd4f4e3d 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.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'" />
</ItemGroup>
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index da3ad608..8a449f0a 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters;
@@ -90,13 +91,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
[JsonConstructor]
public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys)
{
- this.UniqueID = this.NormalizeWhitespace(uniqueId);
- this.Name = this.NormalizeWhitespace(name);
- this.Author = this.NormalizeWhitespace(author);
- this.Description = this.NormalizeWhitespace(description);
+ this.UniqueID = this.NormalizeField(uniqueId);
+ this.Name = this.NormalizeField(name, replaceSquareBrackets: true);
+ this.Author = this.NormalizeField(author);
+ this.Description = this.NormalizeField(description);
this.Version = version;
this.MinimumApiVersion = minimumApiVersion;
- this.EntryDll = this.NormalizeWhitespace(entryDll);
+ this.EntryDll = this.NormalizeField(entryDll);
this.ContentPackFor = contentPackFor;
this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>();
this.UpdateKeys = updateKeys ?? Array.Empty<string>();
@@ -113,17 +114,47 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/*********
** Private methods
*********/
- /// <summary>Normalize whitespace in a raw string.</summary>
+ /// <summary>Normalize a manifest field to strip newlines, trim whitespace, and optionally strip square brackets.</summary>
/// <param name="input">The input to strip.</param>
+ /// <param name="replaceSquareBrackets">Whether to replace square brackets with round ones. This is used in the mod name to avoid breaking the log format.</param>
#if NET5_0_OR_GREATER
[return: NotNullIfNotNull("input")]
#endif
- private string? NormalizeWhitespace(string? input)
+ private string? NormalizeField(string? input, bool replaceSquareBrackets = false)
{
- return input
- ?.Trim()
- .Replace("\r", "")
- .Replace("\n", "");
+ input = input?.Trim();
+
+ if (!string.IsNullOrEmpty(input))
+ {
+ StringBuilder? builder = null;
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ switch (input[i])
+ {
+ case '\r':
+ case '\n':
+ builder ??= new StringBuilder(input);
+ builder[i] = ' ';
+ break;
+
+ case '[' when replaceSquareBrackets:
+ builder ??= new StringBuilder(input);
+ builder[i] = '(';
+ break;
+
+ case ']' when replaceSquareBrackets:
+ builder ??= new StringBuilder(input);
+ builder[i] = ')';
+ break;
+ }
+ }
+
+ if (builder != null)
+ input = builder.ToString();
+ }
+
+ return input;
}
}
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 401bba4f..71fb42c2 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -159,11 +159,20 @@ namespace StardewModdingAPI.Web.Controllers
continue;
}
+ // if there's only a prerelease version (e.g. from GitHub), don't override the main version
+ ISemanticVersion? curMain = data.Version;
+ ISemanticVersion? curPreview = data.PreviewVersion;
+ if (curPreview == null && curMain?.IsPrerelease() == true)
+ {
+ curPreview = curMain;
+ curMain = null;
+ }
+
// handle versions
- if (this.IsNewer(data.Version, main?.Version))
- main = new ModEntryVersionModel(data.Version, data.Url!);
- if (this.IsNewer(data.PreviewVersion, optional?.Version))
- optional = new ModEntryVersionModel(data.PreviewVersion, data.Url!);
+ if (this.IsNewer(curMain, main?.Version))
+ main = new ModEntryVersionModel(curMain, data.Url!);
+ if (this.IsNewer(curPreview, optional?.Version))
+ optional = new ModEntryVersionModel(curPreview, data.Url!);
}
// get unofficial version
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index d26cb6f8..81b187fe 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -25,7 +25,7 @@
<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.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 3393b22f..0380dd9e 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -32,59 +33,70 @@ namespace StardewModdingAPI.Framework.Content
/// <inheritdoc />
public void PatchImage(IRawTextureData source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
{
- this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
-
- // validate source data
if (source == null)
throw new ArgumentNullException(nameof(source), "Can't patch from null source data.");
- // get the pixels for the source area
- Color[] sourceData;
+ // get normalized bounds
+ this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
+ if (source.Data.Length < (sourceArea.Value.Bottom - 1) * source.Width + sourceArea.Value.Right)
+ throw new ArgumentException("Can't apply image patch because the source image is smaller than the source area.", nameof(source));
+ int areaX = sourceArea.Value.X;
+ int areaY = sourceArea.Value.Y;
+ int areaWidth = sourceArea.Value.Width;
+ int areaHeight = sourceArea.Value.Height;
+
+ // shortcut: if the area width matches the source image, we can apply the image as-is without needing
+ // to copy the pixels into a smaller subset. It's fine if the source is taller than the area, since we'll
+ // just ignore the extra data at the end of the pixel array.
+ if (areaWidth == source.Width)
+ {
+ this.PatchImageImpl(source.Data, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY);
+ return;
+ }
+
+ // else copy the pixels within the smaller area & apply that
+ int pixelCount = areaWidth * areaHeight;
+ Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
+ try
{
- int areaX = sourceArea.Value.X;
- int areaY = sourceArea.Value.Y;
- int areaWidth = sourceArea.Value.Width;
- int areaHeight = sourceArea.Value.Height;
-
- if (areaX == 0 && areaY == 0 && areaWidth == source.Width && areaHeight == source.Height)
- sourceData = source.Data;
- else
+ for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++)
{
- 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];
- }
- }
+ int sourceIndex = (y * source.Width) + areaX;
+ int targetIndex = (y - areaY) * areaWidth;
+ Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth);
}
- }
- // apply
- this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
+ this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
+ }
+ finally
+ {
+ ArrayPool<Color>.Shared.Return(sourceData);
+ }
}
/// <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.");
+
+ // get normalized bounds
+ this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
if (!source.Bounds.Contains(sourceArea.Value))
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
- // get source data
+ // get source data & apply
int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
- Color[] sourceData = GC.AllocateUninitializedArray<Color>(pixelCount);
- source.GetData(0, sourceArea, sourceData, 0, pixelCount);
-
- // apply
- this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
+ Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
+ try
+ {
+ source.GetData(0, sourceArea, sourceData, 0, pixelCount);
+ this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
+ }
+ finally
+ {
+ ArrayPool<Color>.Shared.Return(sourceData);
+ }
}
/// <inheritdoc />
@@ -117,15 +129,16 @@ namespace StardewModdingAPI.Framework.Content
/// <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="sourceWidth">The pixel width of the original source image.</param>
+ /// <param name="sourceHeight">The pixel height of the original 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>
+ /// <param name="startRow">The row to start on, for the sourceData.</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)
+ private void PatchImageImpl(Color[] sourceData, int sourceWidth, int sourceHeight, Rectangle sourceArea, Rectangle targetArea, PatchMode patchMode, int startRow = 0)
{
// get texture
Texture2D target = this.Data;
@@ -139,24 +152,69 @@ namespace StardewModdingAPI.Framework.Content
if (sourceArea.Size != targetArea.Size)
throw new InvalidOperationException("The source and target areas must be the same size.");
- // merge data
- if (patchMode == PatchMode.Overlay)
+ // shortcut: replace the entire area
+ if (patchMode == PatchMode.Replace)
+ {
+ target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount);
+ return;
+ }
+
+ // skip transparent pixels at the start & end (e.g. large spritesheet with a few sprites replaced)
+ int startIndex = -1;
+ int endIndex = -1;
+ {
+ for (int i = startRow * sourceArea.Width; i < pixelCount; i++)
+ {
+ if (sourceData[i].A >= AssetDataForImage.MinOpacity)
+ {
+ startIndex = i;
+ break;
+ }
+ }
+ if (startIndex == -1)
+ return; // blank texture
+
+ for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--)
+ {
+ if (sourceData[i].A >= AssetDataForImage.MinOpacity)
+ {
+ endIndex = i;
+ break;
+ }
+ }
+ if (endIndex == -1)
+ return; // ???
+ }
+
+ // update target rectangle
+ int sourceOffset;
+ {
+ int topOffset = startIndex / sourceArea.Width;
+ int bottomOffset = endIndex / sourceArea.Width;
+
+ targetArea = new(targetArea.X, targetArea.Y + topOffset, targetArea.Width, bottomOffset - topOffset + 1);
+ pixelCount = targetArea.Width * targetArea.Height;
+ sourceOffset = topOffset * sourceArea.Width;
+ }
+
+ // apply
+ Color[] mergedData = ArrayPool<Color>.Shared.Rent(pixelCount);
+ try
{
- // get target data
- Color[] mergedData = GC.AllocateUninitializedArray<Color>(pixelCount);
target.GetData(0, targetArea, mergedData, 0, pixelCount);
- // merge pixels
- for (int i = 0; i < pixelCount; i++)
+ for (int i = startIndex; i <= endIndex; i++)
{
+ int targetIndex = i - sourceOffset;
+
Color above = sourceData[i];
- Color below = mergedData[i];
+ Color below = mergedData[targetIndex];
// shortcut transparency
- if (above.A < MinOpacity)
+ if (above.A < AssetDataForImage.MinOpacity)
continue;
- if (below.A < MinOpacity)
- mergedData[i] = above;
+ if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue)
+ mergedData[targetIndex] = above;
// merge pixels
else
@@ -165,7 +223,7 @@ namespace StardewModdingAPI.Framework.Content
// premultiplied by the content pipeline. The formula is derived from
// https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/.
float alphaBelow = 1 - (above.A / 255f);
- mergedData[i] = new Color(
+ mergedData[targetIndex] = new Color(
r: (int)(above.R + (below.R * alphaBelow)),
g: (int)(above.G + (below.G * alphaBelow)),
b: (int)(above.B + (below.B * alphaBelow)),
@@ -176,8 +234,10 @@ namespace StardewModdingAPI.Framework.Content
target.SetData(0, targetArea, mergedData, 0, pixelCount);
}
- else
- target.SetData(0, targetArea, sourceData, 0, pixelCount);
+ finally
+ {
+ ArrayPool<Color>.Shared.Return(mergedData);
+ }
}
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index cc6f8372..72dcf6e1 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -1,9 +1,11 @@
using System;
+using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using BmFont;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
@@ -111,7 +113,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
{
if (contentManagerID != this.Name)
- throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager.");
+ this.ThrowLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager.");
assetName = relativePath;
}
}
@@ -123,7 +125,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// get file
FileInfo file = this.GetModFile<T>(assetName.Name);
if (!file.Exists)
- throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist.");
+ this.ThrowLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist.");
// load content
asset = file.Extension.ToLower() switch
@@ -141,7 +143,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (ex is SContentLoadException)
throw;
- throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
+ this.ThrowLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
+ return default;
}
// track & return asset
@@ -189,7 +192,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
private T LoadDataFile<T>(IAssetName assetName, FileInfo file)
{
if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset))
- throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
+ this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
return asset;
}
@@ -238,7 +241,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
using FileStream stream = File.OpenRead(file.FullName);
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
+ this.PremultiplyTransparency(texture);
return (T)(object)texture;
}
}
@@ -301,7 +304,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
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.");
+ this.ThrowLoadError(assetName, ContentLoadErrorType.Other, $"can't read XNB file as type {typeof(IRawTextureData)}; that type can only be read from a PNG file.");
// the underlying content manager adds a .xnb extension implicitly, so
// we need to strip it here to avoid trying to load a '.xnb.xnb' file.
@@ -326,7 +329,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file)
{
- throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
+ this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
+ return default;
}
/// <summary>Assert that the asset type is compatible with one of the allowed types.</summary>
@@ -338,18 +342,21 @@ namespace StardewModdingAPI.Framework.ContentManagers
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))}'.");
+ this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'.");
}
- /// <summary>Get an error which indicates that an asset couldn't be loaded.</summary>
+ /// <summary>Throw 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>
+ /// <exception cref="SContentLoadException" />
+ [DoesNotReturn]
[DebuggerStepThrough, DebuggerHidden]
- private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
{
- return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
+ throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
}
/// <summary>Get a file from the mod folder.</summary>
@@ -381,26 +388,32 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="texture">The texture to premultiply.</param>
/// <returns>Returns a premultiplied texture.</returns>
/// <remarks>Based on <a href="https://gamedev.stackexchange.com/a/26037">code by David Gouveia</a>.</remarks>
- private Texture2D PremultiplyTransparency(Texture2D texture)
+ private void PremultiplyTransparency(Texture2D texture)
{
- // premultiply pixels
- Color[] data = GC.AllocateUninitializedArray<Color>(texture.Width * texture.Height);
- texture.GetData(data);
- bool changed = false;
- for (int i = 0; i < data.Length; i++)
+ int count = texture.Width * texture.Height;
+ Color[] data = ArrayPool<Color>.Shared.Rent(count);
+ try
{
- Color pixel = data[i];
- if (pixel.A is (byte.MinValue or byte.MaxValue))
- continue; // no need to change fully transparent/opaque pixels
+ texture.GetData(data, 0, count);
- data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
- changed = true;
- }
+ bool changed = false;
+ for (int i = 0; i < count; i++)
+ {
+ ref Color pixel = ref data[i];
+ if (pixel.A is (byte.MinValue or byte.MaxValue))
+ continue; // no need to change fully transparent/opaque pixels
- if (changed)
- texture.SetData(data);
+ data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
+ changed = true;
+ }
- return texture;
+ if (changed)
+ texture.SetData(data, 0, count);
+ }
+ finally
+ {
+ ArrayPool<Color>.Shared.Return(data);
+ }
}
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
diff --git a/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs b/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs
new file mode 100644
index 00000000..4d31ffeb
--- /dev/null
+++ b/src/SMAPI/Framework/Logging/LogOnceCacheKey.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace StardewModdingAPI.Framework.Logging
+{
+ /// <summary>The cache key for the <see cref="Monitor.LogOnceCache"/>.</summary>
+ /// <param name="Message">The log message.</param>
+ /// <param name="Level">The log level.</param>
+ [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "This is only used as a lookup key.")]
+ internal readonly record struct LogOnceCacheKey(string Message, LogLevel Level);
+}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 01037870..ae08d972 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// </remarks>
public static Assembly? ResolveAssembly(string name)
{
- string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture)
+ string shortName = name.Split(',', 2).First(); // get simple name (without version and culture)
return AppDomain.CurrentDomain
.GetAssemblies()
.FirstOrDefault(p => p.GetName().Name == shortName);
diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index 6b53daff..4ed2c9bb 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -25,10 +25,13 @@ namespace StardewModdingAPI.Framework
private readonly LogFileManager LogFile;
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
- private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
+ private static readonly int MaxLevelLength = Enum.GetValues<LogLevel>().Max(level => level.ToString().Length);
+
+ /// <summary>The cached representation for each level when added to a log header.</summary>
+ private static readonly Dictionary<ConsoleLogLevel, string> LogStrings = Enum.GetValues<ConsoleLogLevel>().ToDictionary(level => level, level => level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength));
/// <summary>A cache of messages that should only be logged once.</summary>
- private readonly HashSet<string> LogOnceCache = new();
+ private readonly HashSet<LogOnceCacheKey> LogOnceCache = new();
/// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary>
private readonly Func<int?> GetScreenIdForLog;
@@ -84,7 +87,7 @@ namespace StardewModdingAPI.Framework
/// <inheritdoc />
public void LogOnce(string message, LogLevel level = LogLevel.Trace)
{
- if (this.LogOnceCache.Add($"{message}|{level}"))
+ if (this.LogOnceCache.Add(new LogOnceCacheKey(message, level)))
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
}
@@ -147,7 +150,7 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log level.</param>
private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
{
- string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
+ string levelStr = Monitor.LogStrings[level];
int? playerIndex = this.GetScreenIdForLog();
return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]";
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
index 0b13434a..b24f4178 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
@@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
if (this.LastValues.Count > 0)
{
- this.AddedImpl.AddRange(this.LastValues);
+ this.RemovedImpl.AddRange(this.LastValues);
this.LastValues.Clear();
}
return;
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index 36db0545..e5d8937c 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -26,7 +26,7 @@
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" />
<PackageReference Include="Pintail" Version="2.2.1" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />