-
Notifications
You must be signed in to change notification settings - Fork 239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add (most) save/load support for GameMaker 2024.6 #1827
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2021,6 +2021,7 @@ public class LayerAssetsData : LayerData | |
public UndertalePointerList<SequenceInstance> Sequences { get; set; } | ||
public UndertalePointerList<SpriteInstance> NineSlices { get; set; } // Removed in 2.3.2, before never used | ||
public UndertalePointerList<ParticleSystemInstance> ParticleSystems { get; set; } | ||
public UndertalePointerList<TextItemInstance> TextItems { get; set; } | ||
|
||
/// <inheritdoc /> | ||
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); | ||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void Unserialize(UndertaleReader reader) | ||
{ | ||
// Track first pointer target to detect additional data | ||
long firstPointerTarget = reader.ReadUInt32(); | ||
reader.Position -= 4; | ||
|
||
LegacyTiles = reader.ReadUndertaleObjectPointer<UndertalePointerList<Tile>>(); | ||
Sprites = reader.ReadUndertaleObjectPointer<UndertalePointerList<SpriteInstance>>(); | ||
if (reader.undertaleData.IsVersionAtLeast(2, 3)) | ||
|
@@ -2059,6 +2068,10 @@ public void Unserialize(UndertaleReader reader) | |
NineSlices = reader.ReadUndertaleObjectPointer<UndertalePointerList<SpriteInstance>>(); | ||
if (reader.undertaleData.IsNonLTSVersionAtLeast(2023, 2)) | ||
ParticleSystems = reader.ReadUndertaleObjectPointer<UndertalePointerList<ParticleSystemInstance>>(); | ||
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<UndertalePointerList<TextItemInstance>>(); | ||
} | ||
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,13 +2097,18 @@ 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(); | ||
if (!reader.undertaleData.IsVersionAtLeast(2, 3, 2)) | ||
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<ParticleSystemInstance>.UnserializeChildObjectCount(reader); | ||
} | ||
if (reader.undertaleData.IsVersionAtLeast(2024, 6)) | ||
{ | ||
reader.AbsPosition = textItemsPtr; | ||
count += 1 + UndertalePointerList<TextItemInstance>.UnserializeChildObjectCount(reader); | ||
} | ||
} | ||
|
||
return count; | ||
|
@@ -2363,6 +2388,9 @@ public void Unserialize(UndertaleReader reader) | |
Rotation = reader.ReadSingle(); | ||
} | ||
|
||
/// <summary> | ||
/// Generates a random name for this instance, as a utility for room editing. | ||
/// </summary> | ||
//TODO: rework this method slightly. | ||
public static UndertaleString GenerateRandomName(UndertaleData data) | ||
{ | ||
|
@@ -2525,6 +2553,9 @@ public void Unserialize(UndertaleReader reader) | |
Rotation = reader.ReadSingle(); | ||
} | ||
|
||
/// <summary> | ||
/// Generates a random name for this instance, as a utility for room editing. | ||
/// </summary> | ||
public static UndertaleString GenerateRandomName(UndertaleData data) | ||
{ | ||
return data.Strings.MakeString("particle_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8")); | ||
|
@@ -2545,6 +2576,118 @@ public void Dispose() | |
Name = null; | ||
} | ||
} | ||
|
||
public class TextItemInstance : UndertaleObject, INotifyPropertyChanged, IStaticChildObjCount, IStaticChildObjectsSize, IDisposable | ||
{ | ||
/// <inheritdoc cref="IStaticChildObjCount.ChildObjectCount" /> | ||
public static readonly uint ChildObjectCount = 1; | ||
|
||
/// <inheritdoc cref="IStaticChildObjectsSize.ChildObjectsSize" /> | ||
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<UndertaleFont, UndertaleChunkFONT> _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; } | ||
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; } | ||
|
||
/// <inheritdoc /> | ||
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); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void Unserialize(UndertaleReader reader) | ||
{ | ||
Name = reader.ReadUndertaleString(); | ||
X = reader.ReadInt32(); | ||
Y = reader.ReadInt32(); | ||
_font = reader.ReadUndertaleObject<UndertaleResourceById<UndertaleFont, UndertaleChunkFONT>>(); | ||
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(); | ||
} | ||
|
||
/// <summary> | ||
/// Generates a random name for this instance, as a utility for room editing. | ||
/// </summary> | ||
public static UndertaleString GenerateRandomName(UndertaleData data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docstring |
||
{ | ||
return data.Strings.MakeString("textitem_" + ((uint)Random.Shared.Next(-Int32.MaxValue, Int32.MaxValue)).ToString("X8")); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override string ToString() | ||
{ | ||
return $"Text item {Name?.Content} with text \"{Text?.Content ?? "?"}\""; | ||
} | ||
|
||
/// <inheritdoc/> | ||
public void Dispose() | ||
{ | ||
GC.SuppressFinalize(this); | ||
|
||
_font.Dispose(); | ||
Name = null; | ||
Text = null; | ||
} | ||
} | ||
} | ||
|
||
public enum AnimationSpeedType : uint | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(); | ||
Comment on lines
+700
to
+703
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Were these fields always there? why did we never read them before? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are required in 2024.6 and above to determine the size of a sprite's collision mask data, which now depends on the size of its bounding box (which is what these values are). |
||
|
||
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<TextureEntry>.UnserializeChildObjectCount(reader); | ||
SkipMaskData(reader, width, height); | ||
SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop); | ||
break; | ||
|
||
case SpriteType.SWF: | ||
|
@@ -796,37 +800,89 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) | |
{ | ||
reader.Position -= 4; | ||
count += 1 + UndertaleSimpleList<TextureEntry>.UnserializeChildObjectCount(reader); | ||
SkipMaskData(reader, width, height); | ||
SkipMaskData(reader, width, height, marginRight, marginLeft, marginBottom, marginTop); | ||
} | ||
|
||
return count; | ||
} | ||
|
||
/// <summary> | ||
/// Returns the width and height of the collision mask for this sprite, which changes depending on GameMaker version. | ||
/// </summary> | ||
public (uint Width, uint Height) CalculateMaskDimensions(UndertaleData data) | ||
{ | ||
if (data.IsVersionAtLeast(2024, 6)) | ||
{ | ||
return CalculateBboxMaskDimensions(MarginRight, MarginLeft, MarginBottom, MarginTop); | ||
} | ||
return CalculateFullMaskDimensions(Width, Height); | ||
} | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public static (uint Width, uint Height) CalculateBboxMaskDimensions(int marginRight, int marginLeft, int marginBottom, int marginTop) | ||
{ | ||
return ((uint)(marginRight - marginLeft + 1), (uint)(marginBottom - marginTop + 1)); | ||
} | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
/// <remarks> | ||
/// This simply returns the width and height supplied, but is intended for clarity in the code. | ||
/// </remarks> | ||
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<MaskEntry> 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++) | ||
{ | ||
newMasks.Add(new MaskEntry(reader.ReadBytes((int)len))); | ||
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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docstrings. Please add at least a TODO if you're not implementing them in this PR (altho would be prferred if you added them)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ended up just adding a TODO because I don't know all the possible values here yet.