tweak new code
This commit is contained in:
parent
40d5cd7c05
commit
2e0bc5ddfe
|
@ -33,71 +33,59 @@ namespace StardewModdingAPI.Framework.Content
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void PatchImage(IRawTextureData 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)
|
||||||
{
|
{
|
||||||
// nullcheck
|
|
||||||
if (source == null)
|
if (source == null)
|
||||||
throw new ArgumentNullException(nameof(source), "Can't patch from null source data.");
|
throw new ArgumentNullException(nameof(source), "Can't patch from null source data.");
|
||||||
|
|
||||||
|
// get normalized bounds
|
||||||
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||||
|
|
||||||
// check to see if the Data is sufficiently long.
|
|
||||||
// while SMAPI's impl is going to be, it's not necessarily the case for mod impl.
|
|
||||||
if (source.Data.Length < (sourceArea.Value.Bottom - 1) * source.Width + sourceArea.Value.Right)
|
if (source.Data.Length < (sourceArea.Value.Bottom - 1) * source.Width + sourceArea.Value.Right)
|
||||||
throw new ArgumentException("Source data insufficiently long for this operation.");
|
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;
|
||||||
|
|
||||||
// get the pixels for the source area
|
// shortcut: if the area width matches the source image, we can apply the image as-is without needing
|
||||||
Color[] sourceData;
|
// to copy the pixels into a smaller subset. It's fine if the source is taller than the area, since we'll
|
||||||
|
// just ignore the extra data at the end of the pixel array.
|
||||||
|
if (areaWidth == source.Width)
|
||||||
{
|
{
|
||||||
int areaX = sourceArea.Value.X;
|
this.PatchImageImpl(source.Data, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY);
|
||||||
int areaY = sourceArea.Value.Y;
|
return;
|
||||||
int areaWidth = sourceArea.Value.Width;
|
}
|
||||||
int areaHeight = sourceArea.Value.Height;
|
|
||||||
|
|
||||||
if (areaWidth == source.Width)
|
// else copy the pixels within the smaller area & apply that
|
||||||
|
int pixelCount = areaWidth * areaHeight;
|
||||||
|
Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++)
|
||||||
{
|
{
|
||||||
// It's actually fine if the source is taller than the sourceArea
|
int sourceIndex = (y * source.Width) + areaX;
|
||||||
// the "extra" bits on the end of the array can just be ignored.
|
int targetIndex = (y - areaY) * areaWidth;
|
||||||
sourceData = source.Data;
|
Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth);
|
||||||
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode, areaY);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
int pixelCount = areaWidth * areaHeight;
|
|
||||||
sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// slower copying, line by line
|
|
||||||
for (int y = areaY, maxY = areaY + areaHeight; y < maxY; y++)
|
|
||||||
{
|
|
||||||
int sourceIndex = (y * source.Width) + areaX;
|
|
||||||
int targetIndex = (y - areaY) * areaWidth;
|
|
||||||
Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply
|
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
|
||||||
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
|
}
|
||||||
}
|
finally
|
||||||
finally
|
{
|
||||||
{
|
ArrayPool<Color>.Shared.Return(sourceData);
|
||||||
ArrayPool<Color>.Shared.Return(sourceData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
|
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
|
||||||
{
|
{
|
||||||
// nullcheck
|
|
||||||
if (source == null)
|
if (source == null)
|
||||||
throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
|
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);
|
this.GetPatchBounds(ref sourceArea, ref targetArea, source.Width, source.Height);
|
||||||
|
|
||||||
// validate source bounds
|
|
||||||
if (!source.Bounds.Contains(sourceArea.Value))
|
if (!source.Bounds.Contains(sourceArea.Value))
|
||||||
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
|
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;
|
int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
|
||||||
Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
Color[] sourceData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||||
try
|
try
|
||||||
|
@ -164,94 +152,91 @@ namespace StardewModdingAPI.Framework.Content
|
||||||
if (sourceArea.Size != targetArea.Size)
|
if (sourceArea.Size != targetArea.Size)
|
||||||
throw new InvalidOperationException("The source and target areas must be the same size.");
|
throw new InvalidOperationException("The source and target areas must be the same size.");
|
||||||
|
|
||||||
|
// shortcut: replace the entire area
|
||||||
if (patchMode == PatchMode.Replace)
|
if (patchMode == PatchMode.Replace)
|
||||||
target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// merge data
|
target.SetData(0, targetArea, sourceData, startRow * sourceArea.Width, pixelCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Content packs have a habit of using large amounts of blank space.
|
// skip transparent pixels at the start & end (e.g. large spritesheet with a few sprites replaced)
|
||||||
// Adjusting bounds to ignore transparent pixels at the start and end.
|
int startIndex = -1;
|
||||||
|
int endIndex = -1;
|
||||||
int startIndex = -1;
|
{
|
||||||
for (int i = startRow * sourceArea.Width; i < pixelCount; i++)
|
for (int i = startRow * sourceArea.Width; i < pixelCount; i++)
|
||||||
{
|
{
|
||||||
if (sourceData[i].A >= MinOpacity)
|
if (sourceData[i].A >= AssetDataForImage.MinOpacity)
|
||||||
{
|
{
|
||||||
startIndex = i;
|
startIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startIndex == -1)
|
if (startIndex == -1)
|
||||||
return; // apparently a completely blank texture?
|
return; // blank texture
|
||||||
|
|
||||||
int endIndex = -1;
|
|
||||||
for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--)
|
for (int i = startRow * sourceArea.Width + pixelCount - 1; i >= startIndex; i--)
|
||||||
{
|
{
|
||||||
if (sourceData[i].A >= MinOpacity)
|
if (sourceData[i].A >= AssetDataForImage.MinOpacity)
|
||||||
{
|
{
|
||||||
endIndex = i;
|
endIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endIndex == -1)
|
if (endIndex == -1)
|
||||||
return; // should never happen
|
return; // ???
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate new Y bounds
|
// update target rectangle
|
||||||
int topoffset = startIndex / sourceArea.Width;
|
int sourceOffset;
|
||||||
int bottomoffset = endIndex / sourceArea.Width;
|
{
|
||||||
|
int topOffset = startIndex / sourceArea.Width;
|
||||||
|
int bottomOffset = endIndex / sourceArea.Width;
|
||||||
|
|
||||||
// Update target rectangle
|
targetArea = new(targetArea.X, targetArea.Y + topOffset, targetArea.Width, bottomOffset - topOffset + 1);
|
||||||
targetArea = new(targetArea.X, targetArea.Y + topoffset, targetArea.Width, bottomoffset - topoffset + 1);
|
|
||||||
pixelCount = targetArea.Width * targetArea.Height;
|
pixelCount = targetArea.Width * targetArea.Height;
|
||||||
|
sourceOffset = topOffset * sourceArea.Width;
|
||||||
|
}
|
||||||
|
|
||||||
int sourceoffset = topoffset * sourceArea.Width;
|
// apply
|
||||||
|
Color[] mergedData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
target.GetData(0, targetArea, mergedData, 0, pixelCount);
|
||||||
|
|
||||||
// get target data
|
for (int i = startIndex; i <= endIndex; i++)
|
||||||
Color[] mergedData = ArrayPool<Color>.Shared.Rent(pixelCount);
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
target.GetData(0, targetArea, mergedData, 0, pixelCount);
|
int targetIndex = i - sourceOffset;
|
||||||
|
|
||||||
|
Color above = sourceData[i];
|
||||||
|
Color below = mergedData[targetIndex];
|
||||||
|
|
||||||
|
// shortcut transparency
|
||||||
|
if (above.A < AssetDataForImage.MinOpacity)
|
||||||
|
continue;
|
||||||
|
if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue)
|
||||||
|
mergedData[targetIndex] = above;
|
||||||
|
|
||||||
// merge pixels
|
// merge pixels
|
||||||
for (int i = startIndex; i <= endIndex; i++)
|
else
|
||||||
{
|
{
|
||||||
int targetIndex = i - sourceoffset;
|
// This performs a conventional alpha blend for the pixels, which are already
|
||||||
|
// premultiplied by the content pipeline. The formula is derived from
|
||||||
// ref locals here? Not sure.
|
// https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/.
|
||||||
Color above = sourceData[i];
|
float alphaBelow = 1 - (above.A / 255f);
|
||||||
Color below = mergedData[targetIndex];
|
mergedData[targetIndex] = new Color(
|
||||||
|
r: (int)(above.R + (below.R * alphaBelow)),
|
||||||
// shortcut transparency
|
g: (int)(above.G + (below.G * alphaBelow)),
|
||||||
if (above.A < MinOpacity)
|
b: (int)(above.B + (below.B * alphaBelow)),
|
||||||
continue;
|
alpha: Math.Max(above.A, below.A)
|
||||||
if (below.A < MinOpacity || above.A == byte.MaxValue)
|
);
|
||||||
mergedData[targetIndex] = above;
|
|
||||||
|
|
||||||
// merge pixels
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This performs a conventional alpha blend for the pixels, which are already
|
|
||||||
// premultiplied by the content pipeline. The formula is derived from
|
|
||||||
// https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/.
|
|
||||||
float alphaBelow = 1 - (above.A / 255f);
|
|
||||||
mergedData[targetIndex] = new Color(
|
|
||||||
r: (int)(above.R + (below.R * alphaBelow)),
|
|
||||||
g: (int)(above.G + (below.G * alphaBelow)),
|
|
||||||
b: (int)(above.B + (below.B * alphaBelow)),
|
|
||||||
alpha: Math.Max(above.A, below.A)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
target.SetData(0, targetArea, mergedData, 0, pixelCount);
|
target.SetData(0, targetArea, mergedData, 0, pixelCount);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ArrayPool<Color>.Shared.Return(mergedData);
|
ArrayPool<Color>.Shared.Return(mergedData);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,7 +241,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
{
|
{
|
||||||
using FileStream stream = File.OpenRead(file.FullName);
|
using FileStream stream = File.OpenRead(file.FullName);
|
||||||
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
|
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
|
||||||
texture = this.PremultiplyTransparency(texture);
|
this.PremultiplyTransparency(texture);
|
||||||
return (T)(object)texture;
|
return (T)(object)texture;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -345,17 +345,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
this.ThrowLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(TAsset)}'; must be type '{string.Join("' or '", validTypes.Select(p => p.FullName))}'.");
|
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>Throws 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="errorType">Why loading an asset through the content pipeline failed.</param>
|
||||||
/// <param name="assetName">The asset name that failed to load.</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="reasonPhrase">The reason the file couldn't be loaded.</param>
|
||||||
/// <param name="exception">The underlying exception, if applicable.</param>
|
/// <param name="exception">The underlying exception, if applicable.</param>
|
||||||
|
/// <exception cref="SContentLoadException" />
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
[DebuggerStepThrough, DebuggerHidden]
|
[DebuggerStepThrough, DebuggerHidden]
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
#if NET6_0_OR_GREATER
|
|
||||||
[StackTraceHidden]
|
|
||||||
#endif
|
|
||||||
private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
|
private void ThrowLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
|
||||||
{
|
{
|
||||||
throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
throw new SContentLoadException(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
||||||
|
@ -390,9 +388,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
/// <param name="texture">The texture to premultiply.</param>
|
/// <param name="texture">The texture to premultiply.</param>
|
||||||
/// <returns>Returns a premultiplied texture.</returns>
|
/// <returns>Returns a premultiplied texture.</returns>
|
||||||
/// <remarks>Based on <a href="https://gamedev.stackexchange.com/a/26037">code by David Gouveia</a>.</remarks>
|
/// <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
|
|
||||||
int count = texture.Width * texture.Height;
|
int count = texture.Width * texture.Height;
|
||||||
Color[] data = ArrayPool<Color>.Shared.Rent(count);
|
Color[] data = ArrayPool<Color>.Shared.Rent(count);
|
||||||
try
|
try
|
||||||
|
@ -412,8 +409,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
texture.SetData(data, 0, count);
|
texture.SetData(data, 0, count);
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -25,15 +25,13 @@ namespace StardewModdingAPI.Framework
|
||||||
private readonly LogFileManager LogFile;
|
private readonly LogFileManager LogFile;
|
||||||
|
|
||||||
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
|
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
|
||||||
private static readonly int MaxLevelLength = (from level in Enum.GetValues<LogLevel>() select level.ToString().Length).Max();
|
private static readonly int MaxLevelLength = Enum.GetValues<LogLevel>().Max(level => level.ToString().Length);
|
||||||
|
|
||||||
/// <summary>A mapping of console log levels to their string form.</summary>
|
/// <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(k => k, v => v.ToString().ToUpper().PadRight(MaxLevelLength));
|
private static readonly Dictionary<ConsoleLogLevel, string> LogStrings = Enum.GetValues<ConsoleLogLevel>().ToDictionary(level => level, level => level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength));
|
||||||
|
|
||||||
private readonly record struct LogOnceCacheEntry(string message, LogLevel level);
|
|
||||||
|
|
||||||
/// <summary>A cache of messages that should only be logged once.</summary>
|
/// <summary>A cache of messages that should only be logged once.</summary>
|
||||||
private readonly HashSet<LogOnceCacheEntry> 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>
|
/// <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;
|
private readonly Func<int?> GetScreenIdForLog;
|
||||||
|
@ -89,7 +87,7 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void LogOnce(string message, LogLevel level = LogLevel.Trace)
|
public void LogOnce(string message, LogLevel level = LogLevel.Trace)
|
||||||
{
|
{
|
||||||
if (this.LogOnceCache.Add(new LogOnceCacheEntry(message, level)))
|
if (this.LogOnceCache.Add(new LogOnceCacheKey(message, level)))
|
||||||
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
|
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +150,7 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="level">The log level.</param>
|
/// <param name="level">The log level.</param>
|
||||||
private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
|
private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
|
||||||
{
|
{
|
||||||
string levelStr = LogStrings[level];
|
string levelStr = Monitor.LogStrings[level];
|
||||||
int? playerIndex = this.GetScreenIdForLog();
|
int? playerIndex = this.GetScreenIdForLog();
|
||||||
|
|
||||||
return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]";
|
return $"[{DateTime.Now:HH:mm:ss} {levelStr}{(playerIndex != null ? $" screen_{playerIndex}" : "")} {source}]";
|
||||||
|
|
Loading…
Reference in New Issue