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; }