Skip to content

Commit a27bb5c

Browse files
committed
Rewrite ImportGraphics(Advanced) collision masks
Fixes support for 2024.6+ and also adds support for no collision masks being generated for rectangle shapes (in semi-recent GameMaker versions).
1 parent 5e513b1 commit a27bb5c

File tree

2 files changed

+189
-78
lines changed

2 files changed

+189
-78
lines changed

UndertaleModTool/Scripts/Resource Repackers/ImportGraphics.csx

Lines changed: 92 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ bool importAsSprite = false;
2222
Regex sprFrameRegex = new(@"^(.+?)(?:_(\d+))$", RegexOptions.Compiled);
2323
string importFolder = CheckValidity();
2424

25+
bool noMasksForBasicRectangles = Data.IsVersionAtLeast(2022, 9); // TODO: figure out the exact version, but this is pretty close
26+
2527
try
2628
{
2729
string packDir = Path.Combine(ExePath, "Packager");
@@ -40,6 +42,9 @@ try
4042
int lastTextPage = Data.EmbeddedTextures.Count - 1;
4143
int lastTextPageItem = Data.TexturePageItems.Count - 1;
4244

45+
bool bboxMasks = Data.IsVersionAtLeast(2024, 6);
46+
Dictionary<UndertaleSprite, Node> maskNodes = new();
47+
4348
// Import everything into UTMT
4449
string prefix = outName.Replace(Path.GetExtension(outName), "");
4550
int atlasCount = 0;
@@ -135,8 +140,8 @@ try
135140
UndertaleString spriteUTString = Data.Strings.MakeString(spriteName);
136141
UndertaleSprite newSprite = new();
137142
newSprite.Name = spriteUTString;
138-
newSprite.Width = (uint)n.Bounds.Width;
139-
newSprite.Height = (uint)n.Bounds.Height;
143+
newSprite.Width = (uint)n.Texture.BoundingWidth;
144+
newSprite.Height = (uint)n.Texture.BoundingHeight;
140145
newSprite.MarginLeft = n.Texture.TargetX;
141146
newSprite.MarginRight = n.Texture.TargetX + n.Bounds.Width - 1;
142147
newSprite.MarginTop = n.Texture.TargetY;
@@ -149,37 +154,16 @@ try
149154
newSprite.Textures.Add(null);
150155
}
151156

152-
// FIXME: this needs support for 2024.6+ collision masks, which only use bounding box
153-
// (should use newSprite.CalculateMaskDimensions(Data) as well as newSprite.NewMaskEntry(Data))
154-
newSprite.CollisionMasks.Add(newSprite.NewMaskEntry());
155-
156-
int width = ((n.Bounds.Width + 7) / 8) * 8;
157-
BitArray maskingBitArray = new BitArray(width * n.Bounds.Height);
158-
for (int y = 0; y < n.Bounds.Height; y++)
159-
{
160-
for (int x = 0; x < n.Bounds.Width; x++)
161-
{
162-
IMagickColor<byte> pixelColor = atlasPixels.GetPixel(x + n.Bounds.X, y + n.Bounds.Y).ToColor();
163-
maskingBitArray[y * width + x] = (pixelColor.A > 0);
164-
}
165-
}
166-
BitArray tempBitArray = new BitArray(width * n.Bounds.Height);
167-
for (int i = 0; i < maskingBitArray.Length; i += 8)
157+
// Only generate collision masks for sprites that need them (in newer GameMaker versions)
158+
if (!noMasksForBasicRectangles ||
159+
newSprite.SepMasks is not (UndertaleSprite.SepMaskType.AxisAlignedRect or UndertaleSprite.SepMaskType.RotatedRect))
168160
{
169-
for (int j = 0; j < 8; j++)
170-
{
171-
tempBitArray[j + i] = maskingBitArray[-(j - 7) + i];
172-
}
161+
// Generate mask later (when the current atlas is about to be unloaded)
162+
maskNodes.Add(newSprite, n);
173163
}
174164

175-
int numBytes = maskingBitArray.Length / 8;
176-
byte[] bytes = new byte[numBytes];
177-
tempBitArray.CopyTo(bytes, 0);
178-
for (int i = 0; i < bytes.Length; i++)
179-
newSprite.CollisionMasks[0].Data[i] = bytes[i];
180165
newSprite.Textures.Add(texentry);
181166
Data.Sprites.Add(newSprite);
182-
183167
continue;
184168
}
185169

@@ -194,17 +178,89 @@ try
194178

195179
sprite.Textures[frame] = texentry;
196180

197-
int MarginLeft = n.Texture.TargetX;
198-
int MarginRight = n.Texture.TargetX + n.Bounds.Width - 1;
199-
int MarginTop = n.Texture.TargetY;
200-
int MarginBottom = n.Texture.TargetY + n.Bounds.Height - 1;
201-
if (MarginLeft < sprite.MarginLeft) sprite.MarginLeft = MarginLeft;
202-
if (MarginTop < sprite.MarginTop) sprite.MarginTop = MarginTop;
203-
if (MarginRight > sprite.MarginRight) sprite.MarginRight = MarginRight;
204-
if (MarginBottom > sprite.MarginBottom) sprite.MarginBottom = MarginBottom;
181+
// Grow bounding box depending on how much is trimmed
182+
bool grewBoundingBox = false;
183+
int marginLeft = n.Texture.TargetX;
184+
int marginRight = n.Texture.TargetX + n.Bounds.Width - 1;
185+
int marginTop = n.Texture.TargetY;
186+
int marginBottom = n.Texture.TargetY + n.Bounds.Height - 1;
187+
if (marginLeft < sprite.MarginLeft)
188+
{
189+
sprite.MarginLeft = marginLeft;
190+
grewBoundingBox = true;
191+
}
192+
if (marginTop < sprite.MarginTop)
193+
{
194+
sprite.MarginTop = marginTop;
195+
grewBoundingBox = true;
196+
}
197+
if (marginRight > sprite.MarginRight)
198+
{
199+
sprite.MarginRight = marginRight;
200+
grewBoundingBox = true;
201+
}
202+
if (marginBottom > sprite.MarginBottom)
203+
{
204+
sprite.MarginBottom = marginBottom;
205+
grewBoundingBox = true;
206+
}
207+
208+
// Only generate collision masks for sprites that need them (in newer GameMaker versions)
209+
if (!noMasksForBasicRectangles ||
210+
sprite.SepMasks is not (UndertaleSprite.SepMaskType.AxisAlignedRect or UndertaleSprite.SepMaskType.RotatedRect) ||
211+
sprite.CollisionMasks.Count > 0)
212+
{
213+
if ((bboxMasks && grewBoundingBox) || (sprite.SepMasks is UndertaleSprite.SepMaskType.Precise && sprite.CollisionMasks.Count == 0))
214+
{
215+
// Use this node for the sprite's collision mask if the bounding box grew (or if no collision mask exists for a precise sprite)
216+
maskNodes[sprite] = n;
217+
}
218+
}
219+
}
220+
}
221+
}
222+
223+
// Update masks for when bounding box masks are enabled
224+
foreach ((UndertaleSprite maskSpr, Node maskNode) in maskNodes)
225+
{
226+
// Generate collision mask using either bounding box or sprite dimensions
227+
maskSpr.CollisionMasks.Clear();
228+
maskSpr.CollisionMasks.Add(maskSpr.NewMaskEntry(Data));
229+
(int maskWidth, int maskHeight) = maskSpr.CalculateMaskDimensions(Data);
230+
int maskStride = ((maskWidth + 7) / 8) * 8;
231+
232+
BitArray maskingBitArray = new BitArray(maskStride * maskHeight);
233+
for (int y = 0; y < maskHeight && y < maskNode.Bounds.Height; y++)
234+
{
235+
for (int x = 0; x < maskWidth && x < maskNode.Bounds.Width; x++)
236+
{
237+
IMagickColor<byte> pixelColor = atlasPixels.GetPixel(x + maskNode.Bounds.X, y + maskNode.Bounds.Y).ToColor();
238+
if (bboxMasks)
239+
{
240+
maskingBitArray[(y * maskStride) + x] = (pixelColor.A > 0);
241+
}
242+
else
243+
{
244+
maskingBitArray[((y + maskNode.Texture.TargetY) * maskStride) + x + maskNode.Texture.TargetX] = (pixelColor.A > 0);
245+
}
205246
}
206247
}
248+
BitArray tempBitArray = new BitArray(maskingBitArray.Length);
249+
for (int i = 0; i < maskingBitArray.Length; i += 8)
250+
{
251+
for (int j = 0; j < 8; j++)
252+
{
253+
tempBitArray[j + i] = maskingBitArray[-(j - 7) + i];
254+
}
255+
}
256+
257+
int numBytes = maskingBitArray.Length / 8;
258+
byte[] bytes = new byte[numBytes];
259+
tempBitArray.CopyTo(bytes, 0);
260+
for (int i = 0; i < bytes.Length; i++)
261+
maskSpr.CollisionMasks[0].Data[i] = bytes[i];
207262
}
263+
maskNodes.Clear();
208264

209265
// Increment atlas
210266
atlasCount++;

UndertaleModTool/Scripts/Resource Repackers/ImportGraphicsAdvanced.csx

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ string importFolder = CheckValidity();
5757
string packDir = Path.Combine(ExePath, "Packager");
5858
Directory.CreateDirectory(packDir);
5959

60+
bool noMasksForBasicRectangles = Data.IsVersionAtLeast(2022, 9); // TODO: figure out the exact version, but this is pretty close
61+
6062
try
6163
{
6264
string sourcePath = importFolder;
@@ -71,6 +73,9 @@ try
7173
int lastTextPage = Data.EmbeddedTextures.Count - 1;
7274
int lastTextPageItem = Data.TexturePageItems.Count - 1;
7375

76+
bool bboxMasks = Data.IsVersionAtLeast(2024, 6);
77+
Dictionary<UndertaleSprite, Node> maskNodes = new();
78+
7479
// Import everything into UTMT
7580
string prefix = outName.Replace(Path.GetExtension(outName), "");
7681
int atlasCount = 0;
@@ -178,8 +183,8 @@ try
178183
UndertaleString spriteUTString = Data.Strings.MakeString(spriteName);
179184
UndertaleSprite newSprite = new UndertaleSprite();
180185
newSprite.Name = spriteUTString;
181-
newSprite.Width = (uint)n.Bounds.Width;
182-
newSprite.Height = (uint)n.Bounds.Height;
186+
newSprite.Width = (uint)n.Texture.BoundingWidth;
187+
newSprite.Height = (uint)n.Texture.BoundingHeight;
183188
newSprite.MarginLeft = n.Texture.TargetX;
184189
newSprite.MarginRight = n.Texture.TargetX + n.Bounds.Width - 1;
185190
newSprite.MarginTop = n.Texture.TargetY;
@@ -232,31 +237,15 @@ try
232237
for (int i = 0; i < frame; i++)
233238
newSprite.Textures.Add(null);
234239
}
235-
newSprite.CollisionMasks.Add(newSprite.NewMaskEntry());
236240

237-
int width = ((n.Bounds.Width + 7) / 8) * 8;
238-
BitArray maskingBitArray = new BitArray(width * n.Bounds.Height);
239-
for (int y = 0; y < n.Bounds.Height; y++)
240-
{
241-
for (int x = 0; x < n.Bounds.Width; x++)
242-
{
243-
IMagickColor<byte> pixelColor = atlasPixels.GetPixel(x + n.Bounds.X, y + n.Bounds.Y).ToColor();
244-
maskingBitArray[y * width + x] = (pixelColor.A > 0);
245-
}
246-
}
247-
BitArray tempBitArray = new BitArray(width * n.Bounds.Height);
248-
for (int i = 0; i < maskingBitArray.Length; i += 8)
241+
// Only generate collision masks for sprites that need them (in newer GameMaker versions)
242+
if (!noMasksForBasicRectangles ||
243+
newSprite.SepMasks is not (UndertaleSprite.SepMaskType.AxisAlignedRect or UndertaleSprite.SepMaskType.RotatedRect))
249244
{
250-
for (int j = 0; j < 8; j++)
251-
{
252-
tempBitArray[j + i] = maskingBitArray[-(j - 7) + i];
253-
}
245+
// Generate mask later (when the current atlas is about to be unloaded)
246+
maskNodes.Add(newSprite, n);
254247
}
255-
int numBytes = maskingBitArray.Length / 8;
256-
byte[] bytes = new byte[numBytes];
257-
tempBitArray.CopyTo(bytes, 0);
258-
for (int i = 0; i < bytes.Length; i++)
259-
newSprite.CollisionMasks[0].Data[i] = bytes[i];
248+
260249
newSprite.Textures.Add(texentry);
261250
Data.Sprites.Add(newSprite);
262251
continue;
@@ -314,17 +303,90 @@ try
314303
break;
315304
}
316305

317-
int MarginLeft = n.Texture.TargetX;
318-
int MarginRight = n.Texture.TargetX + n.Bounds.Width - 1;
319-
int MarginTop = n.Texture.TargetY;
320-
int MarginBottom = n.Texture.TargetY + n.Bounds.Height - 1;
321-
if (MarginLeft < sprite.MarginLeft) sprite.MarginLeft = MarginLeft;
322-
if (MarginTop < sprite.MarginTop) sprite.MarginTop = MarginTop;
323-
if (MarginRight > sprite.MarginRight) sprite.MarginRight = MarginRight;
324-
if (MarginBottom > sprite.MarginBottom) sprite.MarginBottom = MarginBottom;
306+
// Grow bounding box depending on how much is trimmed
307+
bool grewBoundingBox = false;
308+
int marginLeft = n.Texture.TargetX;
309+
int marginRight = n.Texture.TargetX + n.Bounds.Width - 1;
310+
int marginTop = n.Texture.TargetY;
311+
int marginBottom = n.Texture.TargetY + n.Bounds.Height - 1;
312+
if (marginLeft < sprite.MarginLeft)
313+
{
314+
sprite.MarginLeft = marginLeft;
315+
grewBoundingBox = true;
316+
}
317+
if (marginTop < sprite.MarginTop)
318+
{
319+
sprite.MarginTop = marginTop;
320+
grewBoundingBox = true;
321+
}
322+
if (marginRight > sprite.MarginRight)
323+
{
324+
sprite.MarginRight = marginRight;
325+
grewBoundingBox = true;
326+
}
327+
if (marginBottom > sprite.MarginBottom)
328+
{
329+
sprite.MarginBottom = marginBottom;
330+
grewBoundingBox = true;
331+
}
332+
333+
// Only generate collision masks for sprites that need them (in newer GameMaker versions)
334+
if (!noMasksForBasicRectangles ||
335+
sprite.SepMasks is not (UndertaleSprite.SepMaskType.AxisAlignedRect or UndertaleSprite.SepMaskType.RotatedRect) ||
336+
sprite.CollisionMasks.Count > 0)
337+
{
338+
if ((bboxMasks && grewBoundingBox) || (sprite.SepMasks is UndertaleSprite.SepMaskType.Precise && sprite.CollisionMasks.Count == 0))
339+
{
340+
// Use this node for the sprite's collision mask if the bounding box grew (or if no collision mask exists for a precise sprite)
341+
maskNodes[sprite] = n;
342+
}
343+
}
344+
}
345+
}
346+
}
347+
348+
// Update masks for when bounding box masks are enabled
349+
foreach ((UndertaleSprite maskSpr, Node maskNode) in maskNodes)
350+
{
351+
// Generate collision mask using either bounding box or sprite dimensions
352+
maskSpr.CollisionMasks.Clear();
353+
maskSpr.CollisionMasks.Add(maskSpr.NewMaskEntry(Data));
354+
(int maskWidth, int maskHeight) = maskSpr.CalculateMaskDimensions(Data);
355+
int maskStride = ((maskWidth + 7) / 8) * 8;
356+
357+
BitArray maskingBitArray = new BitArray(maskStride * maskHeight);
358+
for (int y = 0; y < maskHeight && y < maskNode.Bounds.Height; y++)
359+
{
360+
for (int x = 0; x < maskWidth && x < maskNode.Bounds.Width; x++)
361+
{
362+
IMagickColor<byte> pixelColor = atlasPixels.GetPixel(x + maskNode.Bounds.X, y + maskNode.Bounds.Y).ToColor();
363+
if (bboxMasks)
364+
{
365+
maskingBitArray[(y * maskStride) + x] = (pixelColor.A > 0);
366+
}
367+
else
368+
{
369+
maskingBitArray[((y + maskNode.Texture.TargetY) * maskStride) + x + maskNode.Texture.TargetX] = (pixelColor.A > 0);
370+
}
325371
}
326372
}
373+
BitArray tempBitArray = new BitArray(maskingBitArray.Length);
374+
for (int i = 0; i < maskingBitArray.Length; i += 8)
375+
{
376+
for (int j = 0; j < 8; j++)
377+
{
378+
tempBitArray[j + i] = maskingBitArray[-(j - 7) + i];
379+
}
380+
}
381+
382+
int numBytes = maskingBitArray.Length / 8;
383+
byte[] bytes = new byte[numBytes];
384+
tempBitArray.CopyTo(bytes, 0);
385+
for (int i = 0; i < bytes.Length; i++)
386+
maskSpr.CollisionMasks[0].Data[i] = bytes[i];
327387
}
388+
maskNodes.Clear();
389+
328390
// Increment atlas
329391
atlasCount++;
330392
}
@@ -572,7 +634,7 @@ public class Packer
572634
throw new ScriptException(fi.FullName + " has 0 frames. Script has been stopped.");
573635
}
574636

575-
if (!isSprite && frames > 0)
637+
if (!isSprite && frames > 1)
576638
{
577639
throw new ScriptException(fi.FullName + " is not a sprite, but has more than 1 frame. Script has been stopped.");
578640
}
@@ -913,14 +975,8 @@ Pressing ""No"" will cause the program to ignore these images.");*/
913975
try
914976
{
915977
spriteName = stripped.Substring(0, lastUnderscore);
916-
// check if the frame number is a valid string or not
917-
918-
919-
920-
921-
922-
923978

979+
// Check if the frame number is a valid string or not
924980
Int32.Parse(stripped.Substring(lastUnderscore + 1));
925981
}
926982
catch
@@ -939,11 +995,10 @@ Pressing ""No"" will cause the program to ignore these images.");
939995
{
940996
continue;
941997
}
942-
// throw new ScriptException("Getting the sprite name of " + FileNameWithExtension + " failed.");
943998
}
944999

1000+
// If the sprite doesn't have an underscore, don't bother trying to parse it since it'll be single-frame anyways
9451001
int frame = 0;
946-
// if the sprite doesn't have an underscore, don't bother trying to parse it since it'll be single-frame anyways
9471002
if (spriteName != stripped)
9481003
{
9491004
Int32 validFrameNumber = 0;

0 commit comments

Comments
 (0)