Merge pull request #870 from atravita-mods/arraypooling
Use array pooling and various optimizations to reduce allocations
This commit is contained in:
commit
2e918823ba
|
@ -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;
|
||||
{
|
||||
int areaX = sourceArea.Value.X;
|
||||
int areaY = sourceArea.Value.Y;
|
||||
int areaWidth = sourceArea.Value.Width;
|
||||
int areaHeight = sourceArea.Value.Height;
|
||||
// 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;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// apply
|
||||
this.PatchImageImpl(sourceData, source.Width, source.Height, sourceArea.Value, targetArea.Value, patchMode);
|
||||
// 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++)
|
||||
{
|
||||
int sourceIndex = (y * source.Width) + areaX;
|
||||
int targetIndex = (y - areaY) * areaWidth;
|
||||
Array.Copy(source.Data, sourceIndex, sourceData, targetIndex, areaWidth);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
texture.SetData(data, 0, count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Color>.Shared.Return(data);
|
||||
}
|
||||
|
||||
if (changed)
|
||||
texture.SetData(data);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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}]";
|
||||
|
|
Loading…
Reference in New Issue