summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs173
-rw-r--r--src/SMAPI/Framework/Content/RawTextureData.cs10
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs23
4 files changed, 152 insertions, 58 deletions
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/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/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 055dcc5f..eb4f4555 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -8,6 +8,7 @@ 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;
@@ -188,12 +189,17 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="file">The file to load.</param>
private T LoadImageFile<T>(IAssetName assetName, FileInfo file)
{
- // validate
+ // validate type
+ bool asRawData = false;
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)}'.");
+ {
+ asRawData = typeof(T) == typeof(IRawTextureData);
+ if (!asRawData)
+ throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}' or '{typeof(IRawTextureData)}'.");
+ }
// load
- if (this.UseExperimentalImageLoading)
+ if (asRawData || this.UseExperimentalImageLoading)
{
// load raw data
using FileStream stream = File.OpenRead(file.FullName);
@@ -211,9 +217,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// create texture
- Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height);
- texture.SetData(pixels);
- return (T)(object)texture;
+ if (asRawData)
+ return (T)(object)new RawTextureData(bitmap.Width, bitmap.Height, pixels);
+ else
+ {
+ Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height);
+ texture.SetData(pixels);
+ return (T)(object)texture;
+ }
}
else
{