Skip to content

Commit e1f276e

Browse files
authored
Merge pull request #1827 from UnderminersTeam/2024-6-saveload
Add (most) save/load support for GameMaker 2024.6
2 parents 9b2eca6 + b4dd901 commit e1f276e

File tree

4 files changed

+440
-9
lines changed

4 files changed

+440
-9
lines changed

UndertaleModLib/Models/UndertaleRoom.cs

+143
Original file line numberDiff line numberDiff line change
@@ -2021,6 +2021,7 @@ public class LayerAssetsData : LayerData
20212021
public UndertalePointerList<SequenceInstance> Sequences { get; set; }
20222022
public UndertalePointerList<SpriteInstance> NineSlices { get; set; } // Removed in 2.3.2, before never used
20232023
public UndertalePointerList<ParticleSystemInstance> ParticleSystems { get; set; }
2024+
public UndertalePointerList<TextItemInstance> TextItems { get; set; }
20242025

20252026
/// <inheritdoc />
20262027
public void Serialize(UndertaleWriter writer)
@@ -2034,6 +2035,8 @@ public void Serialize(UndertaleWriter writer)
20342035
writer.WriteUndertaleObjectPointer(NineSlices);
20352036
if (writer.undertaleData.IsNonLTSVersionAtLeast(2023, 2))
20362037
writer.WriteUndertaleObjectPointer(ParticleSystems);
2038+
if (writer.undertaleData.IsVersionAtLeast(2024, 6))
2039+
writer.WriteUndertaleObjectPointer(TextItems);
20372040
}
20382041
writer.WriteUndertaleObject(LegacyTiles);
20392042
writer.WriteUndertaleObject(Sprites);
@@ -2044,12 +2047,18 @@ public void Serialize(UndertaleWriter writer)
20442047
writer.WriteUndertaleObject(NineSlices);
20452048
if (writer.undertaleData.IsNonLTSVersionAtLeast(2023, 2))
20462049
writer.WriteUndertaleObject(ParticleSystems);
2050+
if (writer.undertaleData.IsVersionAtLeast(2024, 6))
2051+
writer.WriteUndertaleObject(TextItems);
20472052
}
20482053
}
20492054

20502055
/// <inheritdoc />
20512056
public void Unserialize(UndertaleReader reader)
20522057
{
2058+
// Track first pointer target to detect additional data
2059+
long firstPointerTarget = reader.ReadUInt32();
2060+
reader.Position -= 4;
2061+
20532062
LegacyTiles = reader.ReadUndertaleObjectPointer<UndertalePointerList<Tile>>();
20542063
Sprites = reader.ReadUndertaleObjectPointer<UndertalePointerList<SpriteInstance>>();
20552064
if (reader.undertaleData.IsVersionAtLeast(2, 3))
@@ -2059,6 +2068,10 @@ public void Unserialize(UndertaleReader reader)
20592068
NineSlices = reader.ReadUndertaleObjectPointer<UndertalePointerList<SpriteInstance>>();
20602069
if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2))
20612070
ParticleSystems = reader.ReadUndertaleObjectPointer<UndertalePointerList<ParticleSystemInstance>>();
2071+
if (firstPointerTarget > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6))
2072+
reader.undertaleData.SetGMS2Version(2024, 6); // There's more data before legacy tiles, so must be 2024.6+
2073+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
2074+
TextItems = reader.ReadUndertaleObjectPointer<UndertalePointerList<TextItemInstance>>();
20622075
}
20632076
reader.ReadUndertaleObject(LegacyTiles);
20642077
reader.ReadUndertaleObject(Sprites);
@@ -2069,6 +2082,8 @@ public void Unserialize(UndertaleReader reader)
20692082
reader.ReadUndertaleObject(NineSlices);
20702083
if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2))
20712084
reader.ReadUndertaleObject(ParticleSystems);
2085+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
2086+
reader.ReadUndertaleObject(TextItems);
20722087
}
20732088
}
20742089

@@ -2082,13 +2097,18 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
20822097
uint sequencesPtr = 0;
20832098
uint nineSlicesPtr = 0;
20842099
uint partSystemsPtr = 0;
2100+
uint textItemsPtr = 0;
20852101
if (reader.undertaleData.IsVersionAtLeast(2, 3))
20862102
{
20872103
sequencesPtr = reader.ReadUInt32();
20882104
if (!reader.undertaleData.IsVersionAtLeast(2, 3, 2))
20892105
nineSlicesPtr = reader.ReadUInt32();
20902106
if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2))
20912107
partSystemsPtr = reader.ReadUInt32();
2108+
if (legacyTilesPtr > reader.AbsPosition && !reader.undertaleData.IsVersionAtLeast(2024, 6))
2109+
reader.undertaleData.SetGMS2Version(2024, 6); // There's more data before legacy tiles, so must be 2024.6+
2110+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
2111+
textItemsPtr = reader.ReadUInt32();
20922112
}
20932113

20942114
reader.AbsPosition = legacyTilesPtr;
@@ -2109,6 +2129,11 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
21092129
reader.AbsPosition = partSystemsPtr;
21102130
count += 1 + UndertalePointerList<ParticleSystemInstance>.UnserializeChildObjectCount(reader);
21112131
}
2132+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
2133+
{
2134+
reader.AbsPosition = textItemsPtr;
2135+
count += 1 + UndertalePointerList<TextItemInstance>.UnserializeChildObjectCount(reader);
2136+
}
21122137
}
21132138

21142139
return count;
@@ -2363,6 +2388,9 @@ public void Unserialize(UndertaleReader reader)
23632388
Rotation = reader.ReadSingle();
23642389
}
23652390

2391+
/// <summary>
2392+
/// Generates a random name for this instance, as a utility for room editing.
2393+
/// </summary>
23662394
//TODO: rework this method slightly.
23672395
public static UndertaleString GenerateRandomName(UndertaleData data)
23682396
{
@@ -2525,6 +2553,9 @@ public void Unserialize(UndertaleReader reader)
25252553
Rotation = reader.ReadSingle();
25262554
}
25272555

2556+
/// <summary>
2557+
/// Generates a random name for this instance, as a utility for room editing.
2558+
/// </summary>
25282559
public static UndertaleString GenerateRandomName(UndertaleData data)
25292560
{
25302561
return data.Strings.MakeString("particle_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8"));
@@ -2545,6 +2576,118 @@ public void Dispose()
25452576
Name = null;
25462577
}
25472578
}
2579+
2580+
public class TextItemInstance : UndertaleObject, INotifyPropertyChanged, IStaticChildObjCount, IStaticChildObjectsSize, IDisposable
2581+
{
2582+
/// <inheritdoc cref="IStaticChildObjCount.ChildObjectCount" />
2583+
public static readonly uint ChildObjectCount = 1;
2584+
2585+
/// <inheritdoc cref="IStaticChildObjectsSize.ChildObjectsSize" />
2586+
public static readonly uint ChildObjectsSize = 68;
2587+
2588+
public event PropertyChangedEventHandler PropertyChanged;
2589+
protected void OnPropertyChanged([CallerMemberName] string name = null)
2590+
{
2591+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
2592+
}
2593+
2594+
private UndertaleResourceById<UndertaleFont, UndertaleChunkFONT> _font = new();
2595+
2596+
// TODO: document these fields; some are self-explanatory but unsure on the behavior of all of them
2597+
public UndertaleString Name { get; set; }
2598+
public int X { get; set; }
2599+
public int Y { get; set; }
2600+
public UndertaleFont Font
2601+
{
2602+
get => _font.Resource;
2603+
set
2604+
{
2605+
_font.Resource = value;
2606+
OnPropertyChanged();
2607+
}
2608+
}
2609+
public float ScaleX { get; set; }
2610+
public float ScaleY { get; set; }
2611+
public float Rotation { get; set; }
2612+
public uint Color { get; set; }
2613+
public float OriginX { get; set; }
2614+
public float OriginY { get; set; }
2615+
public UndertaleString Text { get; set; }
2616+
public int Alignment { get; set; }
2617+
public float CharSpacing { get; set; }
2618+
public float LineSpacing { get; set; }
2619+
public float FrameWidth { get; set; }
2620+
public float FrameHeight { get; set; }
2621+
public bool Wrap { get; set; }
2622+
2623+
/// <inheritdoc />
2624+
public void Serialize(UndertaleWriter writer)
2625+
{
2626+
writer.WriteUndertaleString(Name);
2627+
writer.Write(X);
2628+
writer.Write(Y);
2629+
writer.WriteUndertaleObject(_font);
2630+
writer.Write(ScaleX);
2631+
writer.Write(ScaleY);
2632+
writer.Write(Rotation);
2633+
writer.Write(Color);
2634+
writer.Write(OriginX);
2635+
writer.Write(OriginY);
2636+
writer.WriteUndertaleString(Text);
2637+
writer.Write(Alignment);
2638+
writer.Write(CharSpacing);
2639+
writer.Write(LineSpacing);
2640+
writer.Write(FrameWidth);
2641+
writer.Write(FrameHeight);
2642+
writer.Write(Wrap);
2643+
}
2644+
2645+
/// <inheritdoc />
2646+
public void Unserialize(UndertaleReader reader)
2647+
{
2648+
Name = reader.ReadUndertaleString();
2649+
X = reader.ReadInt32();
2650+
Y = reader.ReadInt32();
2651+
_font = reader.ReadUndertaleObject<UndertaleResourceById<UndertaleFont, UndertaleChunkFONT>>();
2652+
ScaleX = reader.ReadSingle();
2653+
ScaleY = reader.ReadSingle();
2654+
Rotation = reader.ReadSingle();
2655+
Color = reader.ReadUInt32();
2656+
OriginX = reader.ReadSingle();
2657+
OriginY = reader.ReadSingle();
2658+
Text = reader.ReadUndertaleString();
2659+
Alignment = reader.ReadInt32();
2660+
CharSpacing = reader.ReadSingle();
2661+
LineSpacing = reader.ReadSingle();
2662+
FrameWidth = reader.ReadSingle();
2663+
FrameHeight = reader.ReadSingle();
2664+
Wrap = reader.ReadBoolean();
2665+
}
2666+
2667+
/// <summary>
2668+
/// Generates a random name for this instance, as a utility for room editing.
2669+
/// </summary>
2670+
public static UndertaleString GenerateRandomName(UndertaleData data)
2671+
{
2672+
return data.Strings.MakeString("textitem_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8"));
2673+
}
2674+
2675+
/// <inheritdoc />
2676+
public override string ToString()
2677+
{
2678+
return $"Text item {Name?.Content} with text \"{Text?.Content ?? "?"}\"";
2679+
}
2680+
2681+
/// <inheritdoc/>
2682+
public void Dispose()
2683+
{
2684+
GC.SuppressFinalize(this);
2685+
2686+
_font.Dispose();
2687+
Name = null;
2688+
Text = null;
2689+
}
2690+
}
25482691
}
25492692

25502693
public enum AnimationSpeedType : uint

UndertaleModLib/Models/UndertaleSound.cs

+12
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ public enum AudioEntryFlags : uint
144144
/// </summary>
145145
public int GroupID { get => _audioGroup.CachedId; set { _audioGroup.CachedId = value; OnPropertyChanged(); } }
146146

147+
/// <summary>
148+
/// The precomputed length of the sound's audio data.
149+
/// </summary>
150+
/// <remarks>Introduced in GameMaker 2024.6.</remarks>
151+
public float AudioLength { get; set; }
152+
147153
/// <inheritdoc />
148154
public event PropertyChangedEventHandler PropertyChanged;
149155

@@ -174,6 +180,9 @@ public void Serialize(UndertaleWriter writer)
174180
writer.WriteUndertaleObject(_audioFile);
175181
else
176182
writer.Write(_audioFile.CachedId);
183+
184+
if (writer.undertaleData.IsVersionAtLeast(2024, 6))
185+
writer.Write(AudioLength);
177186
}
178187

179188
/// <inheritdoc />
@@ -207,6 +216,9 @@ public void Unserialize(UndertaleReader reader)
207216
{
208217
_audioFile.CachedId = reader.ReadInt32();
209218
}
219+
220+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
221+
AudioLength = reader.ReadSingle();
210222
}
211223

212224
/// <inheritdoc cref="UndertaleObject.UnserializeChildObjectCount(UndertaleReader)"/>

UndertaleModLib/Models/UndertaleSprite.cs

+65-9
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,12 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
697697
reader.Position += 4; // "Name"
698698
uint width = reader.ReadUInt32();
699699
uint height = reader.ReadUInt32();
700+
int marginLeft = reader.ReadInt32();
701+
int marginRight = reader.ReadInt32();
702+
int marginBottom = reader.ReadInt32();
703+
int marginTop = reader.ReadInt32();
700704

701-
reader.Position += 44;
705+
reader.Position += 28;
702706

703707
if (reader.ReadInt32() == -1)
704708
{
@@ -727,7 +731,7 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
727731
{
728732
case SpriteType.Normal:
729733
count += 1 + UndertaleSimpleList<TextureEntry>.UnserializeChildObjectCount(reader);
730-
SkipMaskData(reader, width, height);
734+
SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop);
731735
break;
732736

733737
case SpriteType.SWF:
@@ -796,37 +800,89 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader)
796800
{
797801
reader.Position -= 4;
798802
count += 1 + UndertaleSimpleList<TextureEntry>.UnserializeChildObjectCount(reader);
799-
SkipMaskData(reader, width, height);
803+
SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop);
800804
}
801805

802806
return count;
803807
}
804808

809+
/// <summary>
810+
/// Returns the width and height of the collision mask for this sprite, which changes depending on GameMaker version.
811+
/// </summary>
812+
public (uint Width, uint Height) CalculateMaskDimensions(UndertaleData data)
813+
{
814+
if (data.IsVersionAtLeast(2024, 6))
815+
{
816+
return CalculateBboxMaskDimensions(MarginRight, MarginLeft, MarginBottom, MarginTop);
817+
}
818+
return CalculateFullMaskDimensions(Width, Height);
819+
}
820+
821+
/// <summary>
822+
/// Calculates the width and height of a collision mask from the given margin/bounding box.
823+
/// This method is used to calculate collision mask dimensions in GameMaker 2024.6 and above.
824+
/// </summary>
825+
public static (uint Width, uint Height) CalculateBboxMaskDimensions(int marginRight, int marginLeft, int marginBottom, int marginTop)
826+
{
827+
return ((uint)(marginRight - marginLeft + 1), (uint)(marginBottom - marginTop + 1));
828+
}
829+
830+
/// <summary>
831+
/// Calculates the width and height of a collision mask from a given sprite's full width and height.
832+
/// This method is used to calculate collision mask dimensions prior to GameMaker 2024.6.
833+
/// </summary>
834+
/// <remarks>
835+
/// This simply returns the width and height supplied, but is intended for clarity in the code.
836+
/// </remarks>
837+
public static (uint Width, uint Height) CalculateFullMaskDimensions(uint width, uint height)
838+
{
839+
return (width, height);
840+
}
841+
805842
private void ReadMaskData(UndertaleReader reader)
806843
{
844+
// Initialize mask list
807845
uint maskCount = reader.ReadUInt32();
808-
uint len = (Width + 7) / 8 * Height;
809846
List<MaskEntry> newMasks = new((int)maskCount);
847+
848+
// Read in mask data
849+
(uint width, uint height) = CalculateMaskDimensions(reader.undertaleData);
850+
uint len = (width + 7) / 8 * height;
810851
uint total = 0;
811852
for (uint i = 0; i < maskCount; i++)
812853
{
813854
newMasks.Add(new MaskEntry(reader.ReadBytes((int)len)));
814855
total += len;
815856
}
816857

817-
CollisionMasks = new(newMasks);
818-
819-
while (total % 4 != 0)
858+
while ((total % 4) != 0)
820859
{
821860
if (reader.ReadByte() != 0)
861+
{
822862
throw new IOException("Mask padding");
863+
}
823864
total++;
824865
}
825-
Util.DebugUtil.Assert(total == CalculateMaskDataSize(Width, Height, maskCount));
866+
if (total != CalculateMaskDataSize(width, height, maskCount))
867+
{
868+
throw new IOException("Mask data size incorrect");
869+
}
870+
871+
// Assign masks to sprite
872+
CollisionMasks = new(newMasks);
826873
}
827-
private static void SkipMaskData(UndertaleReader reader, uint width, uint height)
874+
875+
private static void SkipMaskData(UndertaleReader reader, uint width, uint height, int marginRight, int marginLeft, int marginBottom, int marginTop)
828876
{
829877
uint maskCount = reader.ReadUInt32();
878+
if (reader.undertaleData.IsVersionAtLeast(2024, 6))
879+
{
880+
(width, height) = CalculateBboxMaskDimensions(marginRight, marginLeft, marginBottom, marginTop);
881+
}
882+
else
883+
{
884+
(width, height) = CalculateFullMaskDimensions(width, height);
885+
}
830886
uint len = (width + 7) / 8 * height;
831887

832888
uint total = 0;

0 commit comments

Comments
 (0)