From 0260a3809fd8d28f8b4408fb85223e0a4a1b4975 Mon Sep 17 00:00:00 2001
From: setupwitch <165732903+setupwitch@users.noreply.github.com>
Date: Sun, 21 Jun 2026 09:06:30 -0400
Subject: [PATCH 1/2] Port over DogScepter's collision mask detection
---
UndertaleModLib/Models/UndertaleSprite.cs | 144 ++++++++++++++++++++--
1 file changed, 135 insertions(+), 9 deletions(-)
diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs
index e6a7a5229..3e453cc1b 100644
--- a/UndertaleModLib/Models/UndertaleSprite.cs
+++ b/UndertaleModLib/Models/UndertaleSprite.cs
@@ -81,8 +81,7 @@ public class UndertaleSprite : UndertaleNamedResource, IProjectAsset, PrePaddedO
/// The separation mask type this sprite has.
///
public SepMaskType SepMasks { get; set; }
-
-
+
///
/// The x-coordinate of the origin of the sprite.
///
@@ -175,7 +174,7 @@ public int OriginYWrapper
///
public event PropertyChangedEventHandler PropertyChanged;
-
+
///
public override string ToString()
{
@@ -271,7 +270,7 @@ public MaskEntry NewMaskEntry(UndertaleData data = null)
}
uint len = (uint)((width + 7) / 8 * height);
- return new MaskEntry(new byte[len], width, height);
+ return new MaskEntry(new byte[len], width, height, ColMaskType.Precise);
}
///
@@ -306,7 +305,127 @@ public enum SepMaskType : uint
Precise = 1,
RotatedRect = 2
}
+
+ ///
+ /// The collision type of the sprite, chooses how the collision mask is generated.
+ ///
+ public enum ColMaskType
+ {
+ ///
+ /// Precise for the first frame.
+ ///
+ Precise,
+
+ ///
+ /// A rectangle.
+ ///
+ Rectangle,
+
+ ///
+ /// A circular shape.
+ ///
+ Ellipse,
+
+ ///
+ /// A diamond shape.
+ ///
+ Diamond,
+
+ ///
+ /// A rectangle that supports rotation.
+ ///
+ RotatedRectangle,
+
+ ///
+ /// Collision for a Spine sprite.
+ ///
+ Spine,
+ }
+
+ // this is referenced from dogscepter
+ public ColMaskType ResolveMaskType(byte[] maskData, UndertaleSprite sprite, UndertaleData data)
+ {
+ // automatically assume spine
+ if (sprite.SSpriteType == SpriteType.Spine)
+ {
+ return ColMaskType.Spine;
+ }
+ if (sprite.SepMasks != SepMaskType.Precise)
+ {
+ return sprite.SepMasks switch
+ {
+ SepMaskType.AxisAlignedRect => ColMaskType.Rectangle,
+ SepMaskType.RotatedRect => ColMaskType.RotatedRectangle
+ };
+ }
+
+ if (sprite.SepMasks == SepMaskType.Precise)
+ {
+ float centerX = (sprite.MarginLeft + sprite.MarginRight) / 2;
+ float centerY = (sprite.MarginTop + sprite.MarginBottom) / 2;
+ float radiusX = centerX - sprite.MarginLeft + 0.5f;
+ float radiusY = centerY - sprite.MarginTop + 0.5f;
+
+ if (radiusX <= 0 || radiusY <= 0)
+ {
+ return ColMaskType.Precise;
+ }
+
+ int boundLeft = Math.Clamp(sprite.MarginLeft, 0, (int)sprite.Width);
+ int boundRight = Math.Clamp(sprite.MarginRight, 0, (int)sprite.Width - 1);
+ int boundTop = Math.Clamp(sprite.MarginTop, 0, (int)sprite.Height);
+ int boundBottom = Math.Clamp(sprite.MarginBottom, 0, (int)sprite.Height - 1);
+
+ (int maskWidth, int maskHeight) = sprite.CalculateMaskDimensions(data);
+ int stride = (maskWidth + 7) / 8 * 8;
+
+ bool isDiamond = true, isEllipse = true, foundAny = false;
+
+ for (int sprY = boundTop; sprY <= boundBottom; sprY++)
+ {
+ for (int sprX = boundLeft; sprX <= boundRight; sprX++)
+ {
+ int maskX = sprX;
+ int maskY = sprY;
+
+ // this is to compensate for the collision mask changes
+ if (data.IsVersionAtLeast(2024, 6))
+ {
+ maskX -= sprite.MarginLeft;
+ maskY -= sprite.MarginTop;
+ }
+
+ if (maskX < 0 || maskX >= maskWidth || maskY < 0 || maskY >= maskHeight)
+ {
+ continue;
+ }
+
+ int bitIndex = maskX + (maskY * stride);
+ byte b = maskData[bitIndex / 8];
+ // taken from GetReverse() in dogscepter
+ bool bitSet = (b & (1 << (7 - bitIndex % 8))) != 0;
+
+ if (bitSet)
+ {
+ float normalX = (sprX - centerX) / radiusX;
+ float normalY = (sprY - centerY) / radiusY;
+
+ bool inDiamond = Math.Abs(normalX) + Math.Abs(normalY) <= 1f;
+ bool inEllipse = Math.Pow(normalX, 2.0d) + Math.Pow(normalY, 2d) <= 1d;
+
+ isDiamond &= inDiamond;
+ isEllipse &= inEllipse;
+ }
+ }
+ }
+
+ if (isDiamond) return ColMaskType.Diamond;
+ if (isEllipse) return ColMaskType.Ellipse;
+ }
+ return ColMaskType.Precise; // just default to precise
+ }
+
[PropertyChanged.AddINotifyPropertyChangedInterface]
public class TextureEntry : UndertaleObject, IStaticChildObjectsSize, IDisposable
{
@@ -340,6 +459,11 @@ public void Dispose()
public class MaskEntry : IDisposable
{
public byte[] Data { get; set; }
+
+ ///
+ /// The type of collision mask used, UTMT only.
+ ///
+ public ColMaskType AssumedColMaskType { get; set; }
///
/// Width of this sprite mask. UTMT only.
@@ -353,14 +477,15 @@ public class MaskEntry : IDisposable
public MaskEntry()
{
}
-
- public MaskEntry(byte[] data, int width, int height)
+
+ public MaskEntry(byte[] data, int width, int height, ColMaskType assumedColMasktype)
{
this.Data = data;
this.Width = width;
this.Height = height;
+ this.AssumedColMaskType = assumedColMasktype;
}
-
+
///
public void Dispose()
{
@@ -958,7 +1083,8 @@ private void ReadMaskData(UndertaleReader reader)
uint total = 0;
for (uint i = 0; i < maskCount; i++)
{
- newMasks.Add(new MaskEntry(reader.ReadBytes((int)len), width, height));
+ byte[] maskData = reader.ReadBytes((int)len);
+ newMasks.Add(new MaskEntry(maskData, width, height, ResolveMaskType(maskData, this, reader.undertaleData)));
total += len;
}
@@ -974,7 +1100,7 @@ private void ReadMaskData(UndertaleReader reader)
{
throw new IOException("Mask data size incorrect");
}
-
+
// Assign masks to sprite
CollisionMasks = new(newMasks);
}
From 74d009ea1d0f4bfa27f0f2203441c3cf9e2356e8 Mon Sep 17 00:00:00 2001
From: setupwitch <165732903+setupwitch@users.noreply.github.com>
Date: Sun, 21 Jun 2026 09:40:44 -0400
Subject: [PATCH 2/2] Clean up ResolveColMaskType and stop processing masks at
load time
---
UndertaleModLib/Models/UndertaleSprite.cs | 52 +++++++++++++----------
1 file changed, 29 insertions(+), 23 deletions(-)
diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs
index 3e453cc1b..de84ca063 100644
--- a/UndertaleModLib/Models/UndertaleSprite.cs
+++ b/UndertaleModLib/Models/UndertaleSprite.cs
@@ -270,7 +270,7 @@ public MaskEntry NewMaskEntry(UndertaleData data = null)
}
uint len = (uint)((width + 7) / 8 * height);
- return new MaskEntry(new byte[len], width, height, ColMaskType.Precise);
+ return new MaskEntry(new byte[len], width, height);
}
///
@@ -343,41 +343,41 @@ public enum ColMaskType
}
// this is referenced from dogscepter
- public ColMaskType ResolveMaskType(byte[] maskData, UndertaleSprite sprite, UndertaleData data)
+ private ColMaskType ResolveColMaskType(MaskEntry maskEntry, UndertaleData data)
{
// automatically assume spine
- if (sprite.SSpriteType == SpriteType.Spine)
+ if (SSpriteType == SpriteType.Spine)
{
return ColMaskType.Spine;
}
- if (sprite.SepMasks != SepMaskType.Precise)
+ if (SepMasks != SepMaskType.Precise)
{
- return sprite.SepMasks switch
+ return SepMasks switch
{
SepMaskType.AxisAlignedRect => ColMaskType.Rectangle,
SepMaskType.RotatedRect => ColMaskType.RotatedRectangle
};
}
- if (sprite.SepMasks == SepMaskType.Precise)
+ if (SepMasks == SepMaskType.Precise)
{
- float centerX = (sprite.MarginLeft + sprite.MarginRight) / 2;
- float centerY = (sprite.MarginTop + sprite.MarginBottom) / 2;
- float radiusX = centerX - sprite.MarginLeft + 0.5f;
- float radiusY = centerY - sprite.MarginTop + 0.5f;
+ float centerX = (MarginLeft + MarginRight) / 2;
+ float centerY = (MarginTop + MarginBottom) / 2;
+ float radiusX = centerX - MarginLeft + 0.5f;
+ float radiusY = centerY - MarginTop + 0.5f;
if (radiusX <= 0 || radiusY <= 0)
{
return ColMaskType.Precise;
}
- int boundLeft = Math.Clamp(sprite.MarginLeft, 0, (int)sprite.Width);
- int boundRight = Math.Clamp(sprite.MarginRight, 0, (int)sprite.Width - 1);
- int boundTop = Math.Clamp(sprite.MarginTop, 0, (int)sprite.Height);
- int boundBottom = Math.Clamp(sprite.MarginBottom, 0, (int)sprite.Height - 1);
+ int boundLeft = Math.Clamp(MarginLeft, 0, (int)Width);
+ int boundRight = Math.Clamp(MarginRight, 0, (int)Width - 1);
+ int boundTop = Math.Clamp(MarginTop, 0, (int)Height);
+ int boundBottom = Math.Clamp(MarginBottom, 0, (int)Height - 1);
- (int maskWidth, int maskHeight) = sprite.CalculateMaskDimensions(data);
+ (int maskWidth, int maskHeight) = CalculateMaskDimensions(data);
int stride = (maskWidth + 7) / 8 * 8;
bool isDiamond = true, isEllipse = true, foundAny = false;
@@ -392,8 +392,8 @@ public ColMaskType ResolveMaskType(byte[] maskData, UndertaleSprite sprite, Unde
// this is to compensate for the collision mask changes
if (data.IsVersionAtLeast(2024, 6))
{
- maskX -= sprite.MarginLeft;
- maskY -= sprite.MarginTop;
+ maskX -= MarginLeft;
+ maskY -= MarginTop;
}
if (maskX < 0 || maskX >= maskWidth || maskY < 0 || maskY >= maskHeight)
@@ -402,7 +402,7 @@ public ColMaskType ResolveMaskType(byte[] maskData, UndertaleSprite sprite, Unde
}
int bitIndex = maskX + (maskY * stride);
- byte b = maskData[bitIndex / 8];
+ byte b = maskEntry.Data[bitIndex / 8];
// taken from GetReverse() in dogscepter
bool bitSet = (b & (1 << (7 - bitIndex % 8))) != 0;
@@ -425,6 +425,14 @@ public ColMaskType ResolveMaskType(byte[] maskData, UndertaleSprite sprite, Unde
}
return ColMaskType.Precise; // just default to precise
}
+
+ public void ResolveCollisionMaskTypes(UndertaleData data)
+ {
+ foreach (MaskEntry entry in CollisionMasks)
+ {
+ entry.AssumedColMaskType = ResolveColMaskType(entry, data);
+ }
+ }
[PropertyChanged.AddINotifyPropertyChangedInterface]
public class TextureEntry : UndertaleObject, IStaticChildObjectsSize, IDisposable
@@ -463,8 +471,7 @@ public class MaskEntry : IDisposable
///
/// The type of collision mask used, UTMT only.
///
- public ColMaskType AssumedColMaskType { get; set; }
-
+ public ColMaskType? AssumedColMaskType { get; set; }
///
/// Width of this sprite mask. UTMT only.
///
@@ -478,12 +485,11 @@ public MaskEntry()
{
}
- public MaskEntry(byte[] data, int width, int height, ColMaskType assumedColMasktype)
+ public MaskEntry(byte[] data, int width, int height)
{
this.Data = data;
this.Width = width;
this.Height = height;
- this.AssumedColMaskType = assumedColMasktype;
}
///
@@ -1084,7 +1090,7 @@ private void ReadMaskData(UndertaleReader reader)
for (uint i = 0; i < maskCount; i++)
{
byte[] maskData = reader.ReadBytes((int)len);
- newMasks.Add(new MaskEntry(maskData, width, height, ResolveMaskType(maskData, this, reader.undertaleData)));
+ newMasks.Add(new MaskEntry(maskData, width, height));
total += len;
}