From 374090add042265a50f7ff68ac65463eecd9614d Mon Sep 17 00:00:00 2001 From: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:54:10 -0400 Subject: [PATCH 1/2] Add most save/load support for GameMaker 2024.6 Note: This does not account for some of the variable changes that came in earlier releases, so games may still fail to load until changes are merged from the upcoming Underanalyzer branch. --- UndertaleModLib/Models/UndertaleRoom.cs | 133 +++++++++++++ UndertaleModLib/Models/UndertaleSound.cs | 12 ++ UndertaleModLib/Models/UndertaleSprite.cs | 60 +++++- UndertaleModLib/UndertaleChunks.cs | 225 ++++++++++++++++++++++ 4 files changed, 421 insertions(+), 9 deletions(-) diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs index 0bda6428b..44cc68d4f 100644 --- a/UndertaleModLib/Models/UndertaleRoom.cs +++ b/UndertaleModLib/Models/UndertaleRoom.cs @@ -2021,6 +2021,7 @@ public class LayerAssetsData : LayerData public UndertalePointerList Sequences { get; set; } public UndertalePointerList NineSlices { get; set; } // Removed in 2.3.2, before never used public UndertalePointerList ParticleSystems { get; set; } + public UndertalePointerList TextItems { get; set; } /// public void Serialize(UndertaleWriter writer) @@ -2034,6 +2035,8 @@ public void Serialize(UndertaleWriter writer) writer.WriteUndertaleObjectPointer(NineSlices); if (writer.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) writer.WriteUndertaleObjectPointer(ParticleSystems); + if (writer.undertaleData.IsVersionAtLeast(2024, 6)) + writer.WriteUndertaleObjectPointer(TextItems); } writer.WriteUndertaleObject(LegacyTiles); writer.WriteUndertaleObject(Sprites); @@ -2044,12 +2047,18 @@ public void Serialize(UndertaleWriter writer) writer.WriteUndertaleObject(NineSlices); if (writer.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) writer.WriteUndertaleObject(ParticleSystems); + if (writer.undertaleData.IsVersionAtLeast(2024, 6)) + writer.WriteUndertaleObject(TextItems); } } /// public void Unserialize(UndertaleReader reader) { + // Track first pointer target to detect additional data + long firstPointerTarget = reader.ReadUInt32(); + reader.Position -= 4; + LegacyTiles = reader.ReadUndertaleObjectPointer>(); Sprites = reader.ReadUndertaleObjectPointer>(); if (reader.undertaleData.IsVersionAtLeast(2, 3)) @@ -2059,6 +2068,10 @@ public void Unserialize(UndertaleReader reader) NineSlices = reader.ReadUndertaleObjectPointer>(); if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) ParticleSystems = reader.ReadUndertaleObjectPointer>(); + if (firstPointerTarget > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6)) + reader.undertaleData.SetGMS2Version(2024, 6); // there's more data before legacy tiles, so must be 2024.6+ + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + TextItems = reader.ReadUndertaleObjectPointer>(); } reader.ReadUndertaleObject(LegacyTiles); reader.ReadUndertaleObject(Sprites); @@ -2069,6 +2082,8 @@ public void Unserialize(UndertaleReader reader) reader.ReadUndertaleObject(NineSlices); if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) reader.ReadUndertaleObject(ParticleSystems); + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + reader.ReadUndertaleObject(TextItems); } } @@ -2082,6 +2097,7 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) uint sequencesPtr = 0; uint nineSlicesPtr = 0; uint partSystemsPtr = 0; + uint textItemsPtr = 0; if (reader.undertaleData.IsVersionAtLeast(2, 3)) { sequencesPtr = reader.ReadUInt32(); @@ -2089,6 +2105,10 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) nineSlicesPtr = reader.ReadUInt32(); if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) partSystemsPtr = reader.ReadUInt32(); + if (legacyTilesPtr > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6)) + reader.undertaleData.SetGMS2Version(2024, 6); // there's more data before legacy tiles, so must be 2024.6+ + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + textItemsPtr = reader.ReadUInt32(); } reader.AbsPosition = legacyTilesPtr; @@ -2109,6 +2129,11 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) reader.AbsPosition = partSystemsPtr; count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); } + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + { + reader.AbsPosition = textItemsPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + } } return count; @@ -2545,6 +2570,114 @@ public void Dispose() Name = null; } } + + public class TextItemInstance : UndertaleObject, INotifyPropertyChanged, IStaticChildObjCount, IStaticChildObjectsSize, IDisposable + { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 68; + + public event PropertyChangedEventHandler PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + private UndertaleResourceById _font = new(); + + public UndertaleString Name { get; set; } + public int X { get; set; } + public int Y { get; set; } + public UndertaleFont Font + { + get => _font.Resource; + set + { + _font.Resource = value; + OnPropertyChanged(); + } + } + public float ScaleX { get; set; } + public float ScaleY { get; set; } + public float Rotation { get; set; } + public uint Color { get; set; } + public float OriginX { get; set; } + public float OriginY { get; set; } + public UndertaleString Text { get; set; } + public int Alignment { get; set; } + public float CharSpacing { get; set; } + public float LineSpacing { get; set; } + public float FrameWidth { get; set; } + public float FrameHeight { get; set; } + public bool Wrap { get; set; } + + /// + public void Serialize(UndertaleWriter writer) + { + writer.WriteUndertaleString(Name); + writer.Write(X); + writer.Write(Y); + writer.WriteUndertaleObject(_font); + writer.Write(ScaleX); + writer.Write(ScaleY); + writer.Write(Rotation); + writer.Write(Color); + writer.Write(OriginX); + writer.Write(OriginY); + writer.WriteUndertaleString(Text); + writer.Write(Alignment); + writer.Write(CharSpacing); + writer.Write(LineSpacing); + writer.Write(FrameWidth); + writer.Write(FrameHeight); + writer.Write(Wrap); + } + + /// + public void Unserialize(UndertaleReader reader) + { + Name = reader.ReadUndertaleString(); + X = reader.ReadInt32(); + Y = reader.ReadInt32(); + _font = reader.ReadUndertaleObject>(); + ScaleX = reader.ReadSingle(); + ScaleY = reader.ReadSingle(); + Rotation = reader.ReadSingle(); + Color = reader.ReadUInt32(); + OriginX = reader.ReadSingle(); + OriginY = reader.ReadSingle(); + Text = reader.ReadUndertaleString(); + Alignment = reader.ReadInt32(); + CharSpacing = reader.ReadSingle(); + LineSpacing = reader.ReadSingle(); + FrameWidth = reader.ReadSingle(); + FrameHeight = reader.ReadSingle(); + Wrap = reader.ReadBoolean(); + } + + public static UndertaleString GenerateRandomName(UndertaleData data) + { + return data.Strings.MakeString("textitem_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8")); + } + + /// + public override string ToString() + { + return "Text item " + Name?.Content + " with text \"" + (Text?.Content ?? "?") + "\""; + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + _font.Dispose(); + Name = null; + Text = null; + } + } } public enum AnimationSpeedType : uint diff --git a/UndertaleModLib/Models/UndertaleSound.cs b/UndertaleModLib/Models/UndertaleSound.cs index 724cebd35..cdb2a834a 100644 --- a/UndertaleModLib/Models/UndertaleSound.cs +++ b/UndertaleModLib/Models/UndertaleSound.cs @@ -144,6 +144,12 @@ public enum AudioEntryFlags : uint /// public int GroupID { get => _audioGroup.CachedId; set { _audioGroup.CachedId = value; OnPropertyChanged(); } } + /// + /// The precomputed length of the sound's audio data. + /// + /// Introduced in GameMaker 2024.6. + public float AudioLength { get; set; } + /// public event PropertyChangedEventHandler PropertyChanged; @@ -174,6 +180,9 @@ public void Serialize(UndertaleWriter writer) writer.WriteUndertaleObject(_audioFile); else writer.Write(_audioFile.CachedId); + + if (writer.undertaleData.IsVersionAtLeast(2024, 6)) + writer.Write(AudioLength); } /// @@ -207,6 +216,9 @@ public void Unserialize(UndertaleReader reader) { _audioFile.CachedId = reader.ReadInt32(); } + + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + AudioLength = reader.ReadSingle(); } /// diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs index e227bd4c9..8d298c5c1 100644 --- a/UndertaleModLib/Models/UndertaleSprite.cs +++ b/UndertaleModLib/Models/UndertaleSprite.cs @@ -697,8 +697,12 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) reader.Position += 4; // "Name" uint width = reader.ReadUInt32(); uint height = reader.ReadUInt32(); + int marginLeft = reader.ReadInt32(); + int marginRight = reader.ReadInt32(); + int marginBottom = reader.ReadInt32(); + int marginTop = reader.ReadInt32(); - reader.Position += 44; + reader.Position += 28; if (reader.ReadInt32() == -1) { @@ -727,7 +731,7 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) { case SpriteType.Normal: count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); - SkipMaskData(reader, width, height); + SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop); break; case SpriteType.SWF: @@ -796,17 +800,40 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) { reader.Position -= 4; count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); - SkipMaskData(reader, width, height); + SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop); } return count; } + public (uint Width, uint Height) CalculateMaskDimensions(UndertaleData data) + { + if (data.IsVersionAtLeast(2024, 6)) + { + return CalculateBboxMaskDimensions(MarginRight, MarginLeft, MarginBottom, MarginTop); + } + return CalculateFullMaskDimensions(Width, Height); + } + + public static (uint Width, uint Height) CalculateBboxMaskDimensions(int marginRight, int marginLeft, int marginBottom, int marginTop) + { + return ((uint)(marginRight - marginLeft + 1), (uint)(marginBottom - marginTop + 1)); + } + + public static (uint Width, uint Height) CalculateFullMaskDimensions(uint width, uint height) + { + return (width, height); + } + private void ReadMaskData(UndertaleReader reader) { + // Initialize mask list uint maskCount = reader.ReadUInt32(); - uint len = (Width + 7) / 8 * Height; List newMasks = new((int)maskCount); + + // Read in mask data + (uint width, uint height) = CalculateMaskDimensions(reader.undertaleData); + uint len = (width + 7) / 8 * height; uint total = 0; for (uint i = 0; i < maskCount; i++) { @@ -814,19 +841,34 @@ private void ReadMaskData(UndertaleReader reader) total += len; } - CollisionMasks = new(newMasks); - - while (total % 4 != 0) + while ((total % 4) != 0) { if (reader.ReadByte() != 0) + { throw new IOException("Mask padding"); + } total++; } - Util.DebugUtil.Assert(total == CalculateMaskDataSize(Width, Height, maskCount)); + if (total != CalculateMaskDataSize(width, height, maskCount)) + { + throw new IOException("Mask data size incorrect"); + } + + // Assign masks to sprite + CollisionMasks = new(newMasks); } - private static void SkipMaskData(UndertaleReader reader, uint width, uint height) + + private static void SkipMaskData(UndertaleReader reader, uint width, uint height, int marginRight, int marginLeft, int marginBottom, int marginTop) { uint maskCount = reader.ReadUInt32(); + if (reader.undertaleData.IsVersionAtLeast(2024, 6)) + { + (width, height) = CalculateBboxMaskDimensions(marginRight, marginLeft, marginBottom, marginTop); + } + else + { + (width, height) = CalculateFullMaskDimensions(width, height); + } uint len = (width + 7) / 8 * height; uint total = 0; diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs index 9c3610227..cf54ca60a 100644 --- a/UndertaleModLib/UndertaleChunks.cs +++ b/UndertaleModLib/UndertaleChunks.cs @@ -359,6 +359,67 @@ internal override uint UnserializeObjectCount(UndertaleReader reader) public class UndertaleChunkSOND : UndertaleListChunk { public override string Name => "SOND"; + + private static bool checkedFor2024_6; + private void CheckForGM2024_6(UndertaleReader reader) + { + if (!reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2) || reader.undertaleData.IsVersionAtLeast(2024, 6)) + { + checkedFor2024_6 = true; + return; + } + + long returnTo = reader.Position; + + uint soundCount = reader.ReadUInt32(); + if (soundCount >= 2) + { + // If first sound's theoretical (old) end offset is below the start offset of + // the next sound by exactly 4 bytes, then this is 2024.6. + uint firstSoundPtr = reader.ReadUInt32(); + uint secondSoundPtr = reader.ReadUInt32(); + if ((firstSoundPtr + (4 * 9)) == (secondSoundPtr - 4)) + { + reader.undertaleData.SetGMS2Version(2024, 6); + } + } + else if (soundCount == 1) + { + // If there's a nonzero value where padding should be at the + // end of the sound, then this is 2024.6. + uint firstSoundPtr = reader.ReadUInt32(); + reader.AbsPosition = firstSoundPtr + (4 * 9); + if ((reader.AbsPosition % 16) != 4) + { + // If this occurs, then something weird has happened at the start of the chunk? + throw new IOException("Expected to be on specific alignment at this point"); + } + if (reader.ReadUInt32() != 0) + { + reader.undertaleData.SetGMS2Version(2024, 6); + } + } + + reader.Position = returnTo; + checkedFor2024_6 = true; + } + + internal override void UnserializeChunk(UndertaleReader reader) + { + if (!checkedFor2024_6) + CheckForGM2024_6(reader); + + base.UnserializeChunk(reader); + } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2024_6 = false; + + CheckForGM2024_6(reader); + + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkAGRP : UndertaleListChunk @@ -369,6 +430,170 @@ public class UndertaleChunkAGRP : UndertaleListChunk public class UndertaleChunkSPRT : UndertaleListChunk { public override string Name => "SPRT"; + + private static bool checkedFor2024_6; + private void CheckForGM2024_6(UndertaleReader reader) + { + if (!reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2) || reader.undertaleData.IsVersionAtLeast(2024, 6)) + { + checkedFor2024_6 = true; + return; + } + + long returnTo = reader.Position; + long chunkStartPos = reader.AbsPosition; + + // Calculate the expected end position of the first sprite where the bbox size differs from width/height + uint spriteCount = reader.ReadUInt32(); + for (int i = 0; i < spriteCount; i++) + { + // Go to sprite's start position + reader.Position = returnTo + 4 + (4 * i); + uint spritePtr = reader.ReadUInt32(); + uint nextSpritePtr = 0; + if ((i + 1) < spriteCount) + nextSpritePtr = reader.ReadUInt32(); + reader.AbsPosition = spritePtr + 4 /* skip name */; + + // Check if bbox size differs from width/height + uint width = reader.ReadUInt32(); + uint height = reader.ReadUInt32(); + int marginLeft = reader.ReadInt32(); + int marginRight = reader.ReadInt32(); + int marginBottom = reader.ReadInt32(); + int marginTop = reader.ReadInt32(); + (uint bboxWidth, uint bboxHeight) = UndertaleSprite.CalculateBboxMaskDimensions(marginRight, marginLeft, marginBottom, marginTop); + (uint normalWidth, uint normalHeight) = UndertaleSprite.CalculateFullMaskDimensions(width, height); + if (bboxWidth == normalWidth && bboxHeight == normalHeight) + { + // We can't determine anything from this sprite + continue; + } + + reader.Position += 28; + + if (reader.ReadInt32() == -1) + { + uint sVersion = reader.ReadUInt32(); + UndertaleSprite.SpriteType sSpriteType = (UndertaleSprite.SpriteType)reader.ReadUInt32(); + + if (sSpriteType != UndertaleSprite.SpriteType.Normal) + { + // We can't determine anything from this sprite + continue; + } + + reader.Position += 8; // playback speed values + + if (sVersion != 3) + { + throw new IOException("Expected sprite version 3"); + } + uint sequenceOffset = reader.ReadUInt32(); + uint nineSliceOffset = reader.ReadUInt32(); + + // Skip past texture pointers + uint textureCount = reader.ReadUInt32(); + reader.Position += textureCount * 4; + + // Calculate how much space the "full" and "bbox" mask data take up + uint maskCount = reader.ReadUInt32(); + if (maskCount == 0) + { + // We can't determine anything from this sprite + continue; + } + uint fullLength = (normalWidth + 7) / 8 * normalHeight; + fullLength *= maskCount; + if ((fullLength % 4) != 0) + fullLength += (4 - (fullLength % 4)); + uint bboxLength = (bboxWidth + 7) / 8 * bboxHeight; + bboxLength *= maskCount; + if ((bboxLength % 4) != 0) + bboxLength += (4 - (bboxLength % 4)); + + // Calculate expected end offset + long expectedEndOffset; + bool endOffsetLenient = false; + if (sequenceOffset != 0) + { + expectedEndOffset = sequenceOffset; + } + else if (nineSliceOffset != 0) + { + expectedEndOffset = nineSliceOffset; + } + else if (nextSpritePtr != 0) + { + expectedEndOffset = nextSpritePtr; + } + else + { + // Use chunk length, and be lenient with it (due to chunk padding) + endOffsetLenient = true; + expectedEndOffset = chunkStartPos + Length; + } + + // If the "full" mask data runs past the expected end offset, and the "bbox" mask data does not, then this is 2024.6. + // Otherwise, stop processing and assume this is not 2024.6. + long fullEndPos = (reader.AbsPosition + fullLength); + if (fullEndPos != expectedEndOffset) + { + if (endOffsetLenient && (fullEndPos % 16) != 0 && fullEndPos + (16 - (fullEndPos % 16)) == expectedEndOffset) + { + // "Full" mask data doesn't exactly line up, but works if rounded up to the next chunk padding + break; + } + + long bboxEndPos = (reader.AbsPosition + bboxLength); + if (bboxEndPos == expectedEndOffset) + { + // "Bbox" mask data is valid + reader.undertaleData.SetGMS2Version(2024, 6); + } + else if (endOffsetLenient && (bboxEndPos % 16) != 0 && bboxEndPos + (16 - (bboxEndPos % 16)) == expectedEndOffset) + { + // "Bbox" mask data doesn't exactly line up, but works if rounded up to the next chunk padding + reader.undertaleData.SetGMS2Version(2024, 6); + } + else + { + // Neither option seems to have worked... + throw new IOException("Failed to detect mask type in 2024.6 detection"); + } + } + else + { + // "Full" mask data is valid + break; + } + } + else + { + throw new IOException("Expected special sprite type"); + } + } + + reader.Position = returnTo; + checkedFor2024_6 = true; + } + + internal override void UnserializeChunk(UndertaleReader reader) + { + if (!checkedFor2024_6) + CheckForGM2024_6(reader); + + base.UnserializeChunk(reader); + } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2024_6 = false; + + CheckForGM2024_6(reader); + + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkBGND : UndertaleAlignUpdatedListChunk From b4dd901e08e41697edec2a99a1985b9046bcd0c6 Mon Sep 17 00:00:00 2001 From: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat, 13 Jul 2024 15:58:25 -0400 Subject: [PATCH 2/2] Address reviews --- UndertaleModLib/Models/UndertaleRoom.cs | 16 +- UndertaleModLib/Models/UndertaleSprite.cs | 14 ++ UndertaleModLib/UndertaleChunks.cs | 175 +++++++++++----------- 3 files changed, 112 insertions(+), 93 deletions(-) diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs index 44cc68d4f..5ebeb32bf 100644 --- a/UndertaleModLib/Models/UndertaleRoom.cs +++ b/UndertaleModLib/Models/UndertaleRoom.cs @@ -2069,7 +2069,7 @@ public void Unserialize(UndertaleReader reader) if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) ParticleSystems = reader.ReadUndertaleObjectPointer>(); if (firstPointerTarget > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6)) - reader.undertaleData.SetGMS2Version(2024, 6); // there's more data before legacy tiles, so must be 2024.6+ + reader.undertaleData.SetGMS2Version(2024, 6); // There's more data before legacy tiles, so must be 2024.6+ if (reader.undertaleData.IsVersionAtLeast(2024, 6)) TextItems = reader.ReadUndertaleObjectPointer>(); } @@ -2106,7 +2106,7 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) partSystemsPtr = reader.ReadUInt32(); if (legacyTilesPtr > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6)) - reader.undertaleData.SetGMS2Version(2024, 6); // there's more data before legacy tiles, so must be 2024.6+ + reader.undertaleData.SetGMS2Version(2024, 6); // There's more data before legacy tiles, so must be 2024.6+ if (reader.undertaleData.IsVersionAtLeast(2024, 6)) textItemsPtr = reader.ReadUInt32(); } @@ -2388,6 +2388,9 @@ public void Unserialize(UndertaleReader reader) Rotation = reader.ReadSingle(); } + /// + /// Generates a random name for this instance, as a utility for room editing. + /// //TODO: rework this method slightly. public static UndertaleString GenerateRandomName(UndertaleData data) { @@ -2550,6 +2553,9 @@ public void Unserialize(UndertaleReader reader) Rotation = reader.ReadSingle(); } + /// + /// Generates a random name for this instance, as a utility for room editing. + /// public static UndertaleString GenerateRandomName(UndertaleData data) { return data.Strings.MakeString("particle_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8")); @@ -2587,6 +2593,7 @@ protected void OnPropertyChanged([CallerMemberName] string name = null) private UndertaleResourceById _font = new(); + // TODO: document these fields; some are self-explanatory but unsure on the behavior of all of them public UndertaleString Name { get; set; } public int X { get; set; } public int Y { get; set; } @@ -2657,6 +2664,9 @@ public void Unserialize(UndertaleReader reader) Wrap = reader.ReadBoolean(); } + /// + /// Generates a random name for this instance, as a utility for room editing. + /// public static UndertaleString GenerateRandomName(UndertaleData data) { return data.Strings.MakeString("textitem_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8")); @@ -2665,7 +2675,7 @@ public static UndertaleString GenerateRandomName(UndertaleData data) /// public override string ToString() { - return "Text item " + Name?.Content + " with text \"" + (Text?.Content ?? "?") + "\""; + return $"Text item {Name?.Content} with text \"{Text?.Content ?? "?"}\""; } /// diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs index 8d298c5c1..a43b76416 100644 --- a/UndertaleModLib/Models/UndertaleSprite.cs +++ b/UndertaleModLib/Models/UndertaleSprite.cs @@ -806,6 +806,9 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) return count; } + /// + /// Returns the width and height of the collision mask for this sprite, which changes depending on GameMaker version. + /// public (uint Width, uint Height) CalculateMaskDimensions(UndertaleData data) { if (data.IsVersionAtLeast(2024, 6)) @@ -815,11 +818,22 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) return CalculateFullMaskDimensions(Width, Height); } + /// + /// Calculates the width and height of a collision mask from the given margin/bounding box. + /// This method is used to calculate collision mask dimensions in GameMaker 2024.6 and above. + /// public static (uint Width, uint Height) CalculateBboxMaskDimensions(int marginRight, int marginLeft, int marginBottom, int marginTop) { return ((uint)(marginRight - marginLeft + 1), (uint)(marginBottom - marginTop + 1)); } + /// + /// Calculates the width and height of a collision mask from a given sprite's full width and height. + /// This method is used to calculate collision mask dimensions prior to GameMaker 2024.6. + /// + /// + /// This simply returns the width and height supplied, but is intended for clarity in the code. + /// public static (uint Width, uint Height) CalculateFullMaskDimensions(uint width, uint height) { return (width, height); diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs index cf54ca60a..4bbfc116c 100644 --- a/UndertaleModLib/UndertaleChunks.cs +++ b/UndertaleModLib/UndertaleChunks.cs @@ -453,7 +453,7 @@ private void CheckForGM2024_6(UndertaleReader reader) uint nextSpritePtr = 0; if ((i + 1) < spriteCount) nextSpritePtr = reader.ReadUInt32(); - reader.AbsPosition = spritePtr + 4 /* skip name */; + reader.AbsPosition = spritePtr + 4; // Skip past "Name" // Check if bbox size differs from width/height uint width = reader.ReadUInt32(); @@ -472,106 +472,101 @@ private void CheckForGM2024_6(UndertaleReader reader) reader.Position += 28; - if (reader.ReadInt32() == -1) + if (reader.ReadInt32() != -1) { - uint sVersion = reader.ReadUInt32(); - UndertaleSprite.SpriteType sSpriteType = (UndertaleSprite.SpriteType)reader.ReadUInt32(); - - if (sSpriteType != UndertaleSprite.SpriteType.Normal) - { - // We can't determine anything from this sprite - continue; - } + throw new IOException("Expected special sprite type"); + } - reader.Position += 8; // playback speed values + uint sVersion = reader.ReadUInt32(); + UndertaleSprite.SpriteType sSpriteType = (UndertaleSprite.SpriteType)reader.ReadUInt32(); - if (sVersion != 3) - { - throw new IOException("Expected sprite version 3"); - } - uint sequenceOffset = reader.ReadUInt32(); - uint nineSliceOffset = reader.ReadUInt32(); + if (sSpriteType != UndertaleSprite.SpriteType.Normal) + { + // We can't determine anything from this sprite + continue; + } - // Skip past texture pointers - uint textureCount = reader.ReadUInt32(); - reader.Position += textureCount * 4; + reader.Position += 8; // Playback speed values - // Calculate how much space the "full" and "bbox" mask data take up - uint maskCount = reader.ReadUInt32(); - if (maskCount == 0) - { - // We can't determine anything from this sprite - continue; - } - uint fullLength = (normalWidth + 7) / 8 * normalHeight; - fullLength *= maskCount; - if ((fullLength % 4) != 0) - fullLength += (4 - (fullLength % 4)); - uint bboxLength = (bboxWidth + 7) / 8 * bboxHeight; - bboxLength *= maskCount; - if ((bboxLength % 4) != 0) - bboxLength += (4 - (bboxLength % 4)); - - // Calculate expected end offset - long expectedEndOffset; - bool endOffsetLenient = false; - if (sequenceOffset != 0) - { - expectedEndOffset = sequenceOffset; - } - else if (nineSliceOffset != 0) - { - expectedEndOffset = nineSliceOffset; - } - else if (nextSpritePtr != 0) - { - expectedEndOffset = nextSpritePtr; - } - else - { - // Use chunk length, and be lenient with it (due to chunk padding) - endOffsetLenient = true; - expectedEndOffset = chunkStartPos + Length; - } + if (sVersion != 3) + { + throw new IOException("Expected sprite version 3"); + } + uint sequenceOffset = reader.ReadUInt32(); + uint nineSliceOffset = reader.ReadUInt32(); - // If the "full" mask data runs past the expected end offset, and the "bbox" mask data does not, then this is 2024.6. - // Otherwise, stop processing and assume this is not 2024.6. - long fullEndPos = (reader.AbsPosition + fullLength); - if (fullEndPos != expectedEndOffset) - { - if (endOffsetLenient && (fullEndPos % 16) != 0 && fullEndPos + (16 - (fullEndPos % 16)) == expectedEndOffset) - { - // "Full" mask data doesn't exactly line up, but works if rounded up to the next chunk padding - break; - } + // Skip past texture pointers + uint textureCount = reader.ReadUInt32(); + reader.Position += textureCount * 4; - long bboxEndPos = (reader.AbsPosition + bboxLength); - if (bboxEndPos == expectedEndOffset) - { - // "Bbox" mask data is valid - reader.undertaleData.SetGMS2Version(2024, 6); - } - else if (endOffsetLenient && (bboxEndPos % 16) != 0 && bboxEndPos + (16 - (bboxEndPos % 16)) == expectedEndOffset) - { - // "Bbox" mask data doesn't exactly line up, but works if rounded up to the next chunk padding - reader.undertaleData.SetGMS2Version(2024, 6); - } - else - { - // Neither option seems to have worked... - throw new IOException("Failed to detect mask type in 2024.6 detection"); - } - } - else - { - // "Full" mask data is valid - break; - } + // Calculate how much space the "full" and "bbox" mask data take up + uint maskCount = reader.ReadUInt32(); + if (maskCount == 0) + { + // We can't determine anything from this sprite + continue; + } + uint fullLength = (normalWidth + 7) / 8 * normalHeight; + fullLength *= maskCount; + if ((fullLength % 4) != 0) + fullLength += (4 - (fullLength % 4)); + uint bboxLength = (bboxWidth + 7) / 8 * bboxHeight; + bboxLength *= maskCount; + if ((bboxLength % 4) != 0) + bboxLength += (4 - (bboxLength % 4)); + + // Calculate expected end offset + long expectedEndOffset; + bool endOffsetLenient = false; + if (sequenceOffset != 0) + { + expectedEndOffset = sequenceOffset; + } + else if (nineSliceOffset != 0) + { + expectedEndOffset = nineSliceOffset; + } + else if (nextSpritePtr != 0) + { + expectedEndOffset = nextSpritePtr; } else { - throw new IOException("Expected special sprite type"); + // Use chunk length, and be lenient with it (due to chunk padding) + endOffsetLenient = true; + expectedEndOffset = chunkStartPos + Length; } + + // If the "full" mask data runs past the expected end offset, and the "bbox" mask data does not, then this is 2024.6. + // Otherwise, stop processing and assume this is not 2024.6. + long fullEndPos = (reader.AbsPosition + fullLength); + if (fullEndPos == expectedEndOffset) + { + // "Full" mask data is valid + break; + } + if (endOffsetLenient && (fullEndPos % 16) != 0 && fullEndPos + (16 - (fullEndPos % 16)) == expectedEndOffset) + { + // "Full" mask data doesn't exactly line up, but works if rounded up to the next chunk padding + break; + } + + long bboxEndPos = (reader.AbsPosition + bboxLength); + if (bboxEndPos == expectedEndOffset) + { + // "Bbox" mask data is valid + reader.undertaleData.SetGMS2Version(2024, 6); + break; + } + if (endOffsetLenient && (bboxEndPos % 16) != 0 && bboxEndPos + (16 - (bboxEndPos % 16)) == expectedEndOffset) + { + // "Bbox" mask data doesn't exactly line up, but works if rounded up to the next chunk padding + reader.undertaleData.SetGMS2Version(2024, 6); + break; + } + + // Neither option seems to have worked... + throw new IOException("Failed to detect mask type in 2024.6 detection"); } reader.Position = returnTo;