diff --git a/UndertaleModCli/Program.UMTLibInherited.cs b/UndertaleModCli/Program.UMTLibInherited.cs index b556878aa..7c2aa61ec 100644 --- a/UndertaleModCli/Program.UMTLibInherited.cs +++ b/UndertaleModCli/Program.UMTLibInherited.cs @@ -417,6 +417,9 @@ public string GetDecompiledText(string codeName, GlobalDecompileContext context /// public string GetDecompiledText(UndertaleCode code, GlobalDecompileContext context = null) { + if (code.ParentEntry is not null) + return $"// This code entry is a reference to an anonymous function within \"{code.ParentEntry.Name.Content}\", decompile that instead."; + GlobalDecompileContext decompileContext = context is null ? new(Data, false) : context; try { @@ -437,6 +440,9 @@ public string GetDisassemblyText(string codeName) /// public string GetDisassemblyText(UndertaleCode code) { + if (code.ParentEntry is not null) + return $"; This code entry is a reference to an anonymous function within \"{code.ParentEntry.Name.Content}\", disassemble that instead."; + try { return code != null ? code.Disassemble(Data.Variables, Data.CodeLocals.For(code)) : ""; @@ -639,6 +645,8 @@ public void ReplaceTextInGML(string codeName, string keyword, string replacement public void ReplaceTextInGML(UndertaleCode code, string keyword, string replacement, bool caseSensitive = false, bool isRegex = false, GlobalDecompileContext context = null) { if (code == null) throw new ArgumentNullException(nameof(code)); + if (code.ParentEntry is not null) + return; EnsureDataLoaded(); @@ -748,6 +756,9 @@ void ImportCode(string codeName, string gmlCode, bool isGML = true, bool doParse code.Name = Data.Strings.MakeString(codeName); Data.Code.Add(code); } + else if (code.ParentEntry is not null) + return; + if (Data?.GeneralInfo.BytecodeVersion > 14 && Data.CodeLocals.ByName(codeName) == null) { UndertaleCodeLocals locals = new UndertaleCodeLocals(); @@ -1155,6 +1166,9 @@ public string GetGUIDFromCodeName(string codeName) void SafeImport(string codeName, string gmlCode, bool isGML, bool destroyASM = true, bool checkDecompiler = false, bool throwOnError = false) { UndertaleCode code = Data.Code.ByName(codeName); + if (code?.ParentEntry is not null) + return; + try { if (isGML) diff --git a/UndertaleModLib/Models/UndertaleAnimationCurve.cs b/UndertaleModLib/Models/UndertaleAnimationCurve.cs index 2669e9674..35ee81f81 100644 --- a/UndertaleModLib/Models/UndertaleAnimationCurve.cs +++ b/UndertaleModLib/Models/UndertaleAnimationCurve.cs @@ -65,6 +65,25 @@ public void Unserialize(UndertaleReader reader, bool includeName) Channels = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + return UnserializeChildObjectCount(reader, true); + } + + /// + /// Where to deserialize from. + /// Whether to include in the deserialization. + public static uint UnserializeChildObjectCount(UndertaleReader reader, bool includeName) + { + if (!includeName) + reader.Position += 4; // "GraphType" + else + reader.Position += 4 + 4; // + "Name" + + return 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + /// public override string ToString() { @@ -117,6 +136,21 @@ public void Unserialize(UndertaleReader reader) Points = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 12; + + // "Points" + uint count = reader.ReadUInt32(); + if (reader.undertaleData.IsVersionAtLeast(2, 3, 1)) + reader.Position += 24 * count; + else + reader.Position += 12 * count; + + return 1 + count; + } + /// public void Dispose() { @@ -159,24 +193,6 @@ public void Unserialize(UndertaleReader reader) X = reader.ReadSingle(); Value = reader.ReadSingle(); - if (!reader.undertaleData.IsVersionAtLeast(2, 3, 1)) - { - if (reader.ReadUInt32() != 0) // in 2.3 a int with the value of 0 would be set here, - { // it cannot be version 2.3 if this value isn't 0 - reader.undertaleData.SetGMS2Version(2, 3, 1); - reader.Position -= 4; - } - else - { - // At all points (besides the first one) - // if BezierX0 equals to 0 (the above check) - // then BezierY0 equals to 0 as well (the below check) - if (reader.ReadUInt32() == 0) - reader.undertaleData.SetGMS2Version(2, 3, 1); - reader.Position -= 8; - } - } - if (reader.undertaleData.IsVersionAtLeast(2, 3, 1)) { BezierX0 = reader.ReadSingle(); diff --git a/UndertaleModLib/Models/UndertaleCode.cs b/UndertaleModLib/Models/UndertaleCode.cs index 2c266d5ba..a73eb8ffe 100644 --- a/UndertaleModLib/Models/UndertaleCode.cs +++ b/UndertaleModLib/Models/UndertaleCode.cs @@ -109,7 +109,41 @@ Opcode.PushBltn or Opcode.PushI _ => throw new IOException("Unknown opcode " + op.ToString().ToUpper()), }; } - + private static byte ConvertInstructionKind(byte kind) + { + kind = kind switch + { + 0x03 => 0x07, + 0x04 => 0x08, + 0x05 => 0x09, + 0x06 => 0x0A, + 0x07 => 0x0B, + 0x08 => 0x0C, + 0x09 => 0x0D, + 0x0A => 0x0E, + 0x0B => 0x0F, + 0x0C => 0x10, + 0x0D => 0x11, + 0x0E => 0x12, + 0x0F => 0x13, + 0x10 => 0x14, + 0x11 or 0x12 or 0x13 or 0x14 or 0x16 => 0x15, + 0xDA => 0xD9, + 0x41 => 0x45, + 0x82 => 0x86, + 0xB7 => 0xB6, + 0xB8 => 0xB7, + 0xB9 => 0xB8, + 0x9D => 0x9C, + 0x9E => 0x9D, + 0x9F => 0x9E, + 0xBB => 0xBA, + 0xBC => 0xBB, + _ => kind + }; + + return kind; + } public enum DataType : byte { @@ -537,98 +571,13 @@ public void Serialize(UndertaleWriter writer) /// public void Unserialize(UndertaleReader reader) { - uint instructionStartAddress = reader.Position; + long instructionStartAddress = reader.Position; reader.Position += 3; // skip for now, we'll read them later byte kind = reader.ReadByte(); if (reader.Bytecode14OrLower) { // Convert opcode to our enum - switch (kind) - { - case 0x03: - kind = 0x07; - break; - case 0x04: - kind = 0x08; - break; - case 0x05: - kind = 0x09; - break; - case 0x06: - kind = 0x0A; - break; - case 0x07: - kind = 0x0B; - break; - case 0x08: - kind = 0x0C; - break; - case 0x09: - kind = 0x0D; - break; - case 0x0A: - kind = 0x0E; - break; - case 0x0B: - kind = 0x0F; - break; - case 0x0C: - kind = 0x10; - break; - case 0x0D: - kind = 0x11; - break; - case 0x0E: - kind = 0x12; - break; - case 0x0F: - kind = 0x13; - break; - case 0x10: - kind = 0x14; - break; - case 0x11: - case 0x12: - case 0x13: - case 0x14: - // case 0x15: - case 0x16: - kind = 0x15; - break; - case 0xDA: - kind = 0xD9; - break; - case 0x41: - kind = 0x45; - break; - case 0x82: - kind = 0x86; - break; - case 0xB7: - kind = 0xB6; - break; - case 0xB8: - kind = 0xB7; - break; - case 0xB9: - kind = 0xB8; - break; - case 0x9D: - kind = 0x9C; - break; - case 0x9E: - kind = 0x9D; - break; - case 0x9F: - kind = 0x9E; - break; - case 0xBB: - kind = 0xBA; - break; - case 0xBC: - kind = 0xBB; - break; - } + kind = ConvertInstructionKind(kind); } Kind = (Opcode)kind; reader.Position = instructionStartAddress; @@ -657,7 +606,7 @@ public void Unserialize(UndertaleReader reader) if (reader.Bytecode14OrLower && Kind == Opcode.Cmp) ComparisonKind = (ComparisonType)(reader.ReadByte() - 0x10); else - reader.ReadByte(); + reader.Position++; if (Kind == Opcode.And || Kind == Opcode.Or) { @@ -674,7 +623,7 @@ public void Unserialize(UndertaleReader reader) JumpOffset = reader.ReadInt24(); if (JumpOffset == -1048576) // magic? encoded in little endian as 00 00 F0, which is like below JumpOffsetPopenvExitMagic = true; - reader.ReadByte(); + reader.Position++; break; } @@ -696,7 +645,7 @@ public void Unserialize(UndertaleReader reader) } //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.ReadByte(); + reader.Position++; } break; @@ -707,7 +656,7 @@ public void Unserialize(UndertaleReader reader) Type1 = (DataType)(TypePair & 0xf); Type2 = (DataType)(TypePair >> 4); //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.ReadByte(); + reader.Position++; if (Type1 == DataType.Int16) { // Special scenario - the swap instruction @@ -749,7 +698,7 @@ public void Unserialize(UndertaleReader reader) } } //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.ReadByte(); + reader.Position++; switch (Type1) { case DataType.Double: @@ -786,7 +735,7 @@ public void Unserialize(UndertaleReader reader) ArgumentsCount = reader.ReadUInt16(); Type1 = (DataType)reader.ReadByte(); //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.ReadByte(); + reader.Position++; Function = reader.ReadUndertaleObject>(); } break; @@ -803,6 +752,77 @@ public void Unserialize(UndertaleReader reader) throw new IOException("Unknown opcode " + Kind.ToString().ToUpper()); } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + long instructionStartAddress = reader.Position; + reader.Position += 3; // skip for now, we'll read them later + byte kind = reader.ReadByte(); + if (reader.Bytecode14OrLower) + { + // Convert opcode to our enum + kind = ConvertInstructionKind(kind); + } + Opcode Kind = (Opcode)kind; + reader.Position = instructionStartAddress; + switch (GetInstructionType(Kind)) + { + case InstructionType.SingleTypeInstruction: + case InstructionType.DoubleTypeInstruction: + case InstructionType.ComparisonInstruction: + case InstructionType.GotoInstruction: + case InstructionType.BreakInstruction: + reader.Position += 4; + break; + + case InstructionType.PopInstruction: + reader.Position += 2; // "TypeInst" + int type1 = reader.ReadByte() & 0xf; + if (type1 != 0x0f) + { + reader.Position += 1 + 4; + return 1; // "Destination" + } + else + reader.Position++; + break; + + case InstructionType.PushInstruction: + { + reader.Position += 2; + DataType Type1 = (DataType)reader.ReadByte(); + reader.Position++; + switch (Type1) + { + case DataType.Double: + case DataType.Int64: + reader.Position += 8; + break; + + case DataType.Float: + case DataType.Int32: + case DataType.Boolean: + reader.Position += 4; + break; + + case DataType.Variable: + case DataType.String: + reader.Position += 4; + return 1; + } + } + break; + + case InstructionType.CallInstruction: + reader.Position += 8; + return 1; // "Function" + + default: + throw new IOException("Unknown opcode " + Kind.ToString().ToUpper()); + } + + return 0; + } /// public override string ToString() @@ -993,6 +1013,8 @@ public class UndertaleCode : UndertaleNamedResource, UndertaleObjectWithBlobs, I public uint Length { get; set; } + public static int CurrCodeIndex { get; set; } + /// /// The amount of local variables this code entry has.
@@ -1076,7 +1098,6 @@ public void Serialize(UndertaleWriter writer) writer.Write(BytecodeRelativeAddress); writer.Write(Offset); } - } /// @@ -1091,11 +1112,14 @@ public void Unserialize(UndertaleReader reader) else if (reader.Bytecode14OrLower) { Instructions.Clear(); - uint here = reader.Position; - uint stop = here + Length; - while (reader.Position < stop) + if (reader.InstructionArraysLengths is not null) + Instructions.Capacity = reader.InstructionArraysLengths[CurrCodeIndex]; + + long here = reader.AbsPosition; + long stop = here + Length; + while (reader.AbsPosition < stop) { - uint a = (reader.Position - here) / 4; + uint a = (uint)(reader.AbsPosition - here) / 4; UndertaleInstruction instr = reader.ReadUndertaleObject(); instr.Address = a; Instructions.Add(instr); @@ -1112,27 +1136,105 @@ public void Unserialize(UndertaleReader reader) WeirdLocalFlag = true; } int BytecodeRelativeAddress = reader.ReadInt32(); - _bytecodeAbsoluteAddress = (uint)((int)reader.Position - 4 + BytecodeRelativeAddress); - uint here = reader.Position; - reader.Position = _bytecodeAbsoluteAddress; + _bytecodeAbsoluteAddress = (uint)((int)reader.AbsPosition - 4 + BytecodeRelativeAddress); + if (Length > 0 && reader.undertaleData.IsVersionAtLeast(2, 3) && reader.GetOffsetMap().TryGetValue(_bytecodeAbsoluteAddress, out var i)) { ParentEntry = (i as UndertaleInstruction).Entry; ParentEntry.ChildEntries.Add(this); + + Offset = reader.ReadUInt32(); + return; } + + long here = reader.AbsPosition; + reader.AbsPosition = _bytecodeAbsoluteAddress; + Instructions.Clear(); - while (reader.Position < _bytecodeAbsoluteAddress + Length) + if (reader.InstructionArraysLengths is not null) + Instructions.Capacity = reader.InstructionArraysLengths[CurrCodeIndex]; + while (reader.AbsPosition < _bytecodeAbsoluteAddress + Length) { - uint a = (reader.Position - _bytecodeAbsoluteAddress) / 4; + uint a = (uint)(reader.AbsPosition - _bytecodeAbsoluteAddress) / 4; UndertaleInstruction instr = reader.ReadUndertaleObject(); instr.Address = a; Instructions.Add(instr); } if (ParentEntry == null && Instructions.Count != 0) Instructions[0].Entry = this; - reader.Position = here; + + reader.AbsPosition = here; Offset = reader.ReadUInt32(); } + + if (reader.InstructionArraysLengths is not null) + CurrCodeIndex++; + } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 4; // "Name" + uint length = reader.ReadUInt32(); + + if (reader.Bytecode14OrLower) + { + long here = reader.Position; + long stop = here + length; + + // Get instructions count + uint instrCount = 0; + uint instrSubCount = 0; + while (reader.Position < stop) + { + instrCount++; + instrSubCount += UndertaleInstruction.UnserializeChildObjectCount(reader); + } + + reader.InstructionArraysLengths[CurrCodeIndex] = (int)instrCount; + + count += instrCount + instrSubCount; + } + else + { + reader.Position += 4; + + int bytecodeRelativeAddress = reader.ReadInt32(); + uint bytecodeAbsoluteAddress = (uint)((int)reader.Position - 4 + bytecodeRelativeAddress); + + if (length == 0 || reader.GMS2BytecodeAddresses.Contains(bytecodeAbsoluteAddress)) + { + reader.Position += 4; // "Offset" + return count; + } + + reader.GMS2BytecodeAddresses.Add(bytecodeAbsoluteAddress); + + long here = reader.Position; + reader.Position = bytecodeAbsoluteAddress; + + // Get instructions counts + uint instrCount = 0; + uint instrSubCount = 0; + while (reader.Position < bytecodeAbsoluteAddress + length) + { + instrCount++; + instrSubCount += UndertaleInstruction.UnserializeChildObjectCount(reader); + } + + reader.InstructionArraysLengths[CurrCodeIndex] = (int)instrCount; + + reader.Position = here; + reader.Position += 4; // "Offset" + + count += instrCount + instrSubCount; + } + + CurrCodeIndex++; + + return count; } public void UpdateAddresses() @@ -1195,6 +1297,9 @@ public IList FindReferencedLocalVars() /// The instructions to append. public void Append(IList instructions) { + if (ParentEntry is not null) + return; + Instructions.AddRange(instructions); UpdateAddresses(); } @@ -1205,6 +1310,9 @@ public void Append(IList instructions) /// The new instructions for this code entry. public void Replace(IList instructions) { + if (ParentEntry is not null) + return; + Instructions.Clear(); Append(instructions); } @@ -1217,6 +1325,9 @@ public void Replace(IList instructions) /// if the GML code does not compile or if there's an error writing the code to the profile entry. public void AppendGML(string gmlCode, UndertaleData data) { + if (ParentEntry is not null) + return; + CompileContext context = Compiler.Compiler.CompileGMLText(gmlCode, data, this); if (!context.SuccessfulCompile || context.HasError) { @@ -1252,6 +1363,9 @@ public void AppendGML(string gmlCode, UndertaleData data) /// If the GML code does not compile or if there's an error writing the code to the profile entry. public void ReplaceGML(string gmlCode, UndertaleData data) { + if (ParentEntry is not null) + return; + CompileContext context = Compiler.Compiler.CompileGMLText(gmlCode, data, this); if (!context.SuccessfulCompile || context.HasError) { diff --git a/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs b/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs index dc9d58bf3..8a91a1907 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs @@ -44,7 +44,7 @@ public void Unserialize(UndertaleReader reader) /// public void UnserializePadding(UndertaleReader reader) { - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); } diff --git a/UndertaleModLib/Models/UndertaleEmbeddedImage.cs b/UndertaleModLib/Models/UndertaleEmbeddedImage.cs index c61c94b00..6bc0745a3 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedImage.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedImage.cs @@ -22,8 +22,11 @@ namespace UndertaleModLib.Models; /// 32-bit pointer to something relating to a texture page entry? /// /// . -public class UndertaleEmbeddedImage : UndertaleNamedResource, IDisposable +public class UndertaleEmbeddedImage : UndertaleNamedResource, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 8; + /// /// The name of the . /// diff --git a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs index ee898ca6f..c7808905a 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs @@ -17,8 +17,15 @@ namespace UndertaleModLib.Models; /// An embedded texture entry in the data file. ///
[PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleEmbeddedTexture : UndertaleNamedResource, IDisposable +public class UndertaleEmbeddedTexture : UndertaleNamedResource, IDisposable, + IStaticChildObjCount, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 4; // minimal size + /// /// The name of the embedded texture entry. /// @@ -179,7 +186,7 @@ public void UnserializeBlob(UndertaleReader reader) if (_textureData == null || TextureExternal) return; - while (reader.Position % 0x80 != 0) + while (reader.AbsPosition % 0x80 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); @@ -427,11 +434,11 @@ public void Unserialize(UndertaleReader reader) /// /// Unserializes the texture from any type of reader (can be from any source). /// - public void Unserialize(FileBinaryReader reader, bool gm2022_5) + public void Unserialize(IBinaryReader reader, bool gm2022_5) { sharedStream ??= new(); - uint startAddress = reader.Position; + long startAddress = reader.Position; byte[] header = reader.ReadBytes(8); if (!header.SequenceEqual(PNGHeader)) @@ -484,13 +491,13 @@ public void Unserialize(FileBinaryReader reader, bool gm2022_5) { // PNG is big endian and BinaryRead can't handle that (damn) uint len = (uint)reader.ReadByte() << 24 | (uint)reader.ReadByte() << 16 | (uint)reader.ReadByte() << 8 | (uint)reader.ReadByte(); - string type = Encoding.UTF8.GetString(reader.ReadBytes(4)); + uint type = reader.ReadUInt32(); reader.Position += len + 4; - if (type == "IEND") + if (type == 0x444e4549) // 0x444e4549 -> "IEND" break; } - uint length = reader.Position - startAddress; + long length = reader.Position - startAddress; reader.Position = startAddress; TextureBlob = reader.ReadBytes((int)length); } diff --git a/UndertaleModLib/Models/UndertaleExtension.cs b/UndertaleModLib/Models/UndertaleExtension.cs index 35cf1f4ea..875c27b2a 100644 --- a/UndertaleModLib/Models/UndertaleExtension.cs +++ b/UndertaleModLib/Models/UndertaleExtension.cs @@ -53,8 +53,11 @@ public enum UndertaleExtensionVarType : uint /// A class representing an argument for s. /// [PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleExtensionFunctionArg : UndertaleObject +public class UndertaleExtensionFunctionArg : UndertaleObject, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectsSize = 4; + /// /// The variable type of this argument. /// @@ -140,6 +143,14 @@ public void Unserialize(UndertaleReader reader) Arguments = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 20; + + return 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + /// public override string ToString() { @@ -186,6 +197,18 @@ public void Unserialize(UndertaleReader reader) Functions = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 16; + + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + return count; + } + /// public override string ToString() { @@ -218,8 +241,11 @@ public void Dispose() [PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleExtensionOption : UndertaleObject, IDisposable +public class UndertaleExtensionOption : UndertaleObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 12; + public enum OptionKind : uint { Boolean = 0, @@ -344,4 +370,26 @@ public void Unserialize(UndertaleReader reader) Files = reader.ReadUndertaleObject>(); } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 12; + if (reader.undertaleData.IsVersionAtLeast(2022, 6)) + { + uint filesPtr = reader.ReadUInt32(); + uint optionsPtr = reader.ReadUInt32(); + + reader.AbsPosition = filesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + reader.AbsPosition = optionsPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + } + else + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + return count; + } } \ No newline at end of file diff --git a/UndertaleModLib/Models/UndertaleFeatureFlags.cs b/UndertaleModLib/Models/UndertaleFeatureFlags.cs index 1ea1c20f1..533eb87aa 100644 --- a/UndertaleModLib/Models/UndertaleFeatureFlags.cs +++ b/UndertaleModLib/Models/UndertaleFeatureFlags.cs @@ -26,6 +26,12 @@ public void Unserialize(UndertaleReader reader) List.Unserialize(reader); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + return UndertaleSimpleListString.UnserializeChildObjectCount(reader); + } + /// public void Dispose() { diff --git a/UndertaleModLib/Models/UndertaleFilterEffect.cs b/UndertaleModLib/Models/UndertaleFilterEffect.cs index 4d358072a..afdf48511 100644 --- a/UndertaleModLib/Models/UndertaleFilterEffect.cs +++ b/UndertaleModLib/Models/UndertaleFilterEffect.cs @@ -6,8 +6,11 @@ namespace UndertaleModLib.Models; /// A filter effect as it's used in a GameMaker data file. These are GameMaker: Studio 2.3.6+ only. /// [PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleFilterEffect : UndertaleNamedResource, IDisposable +public class UndertaleFilterEffect : UndertaleNamedResource, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 8; + /// /// The name of the . /// diff --git a/UndertaleModLib/Models/UndertaleFont.cs b/UndertaleModLib/Models/UndertaleFont.cs index 07e695d22..499ba5cdb 100644 --- a/UndertaleModLib/Models/UndertaleFont.cs +++ b/UndertaleModLib/Models/UndertaleFont.cs @@ -163,11 +163,22 @@ public void Unserialize(UndertaleReader reader) Kerning = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 14; + + return 1 + UndertaleSimpleListShort.UnserializeChildObjectCount(reader); + } + /// /// A class representing kerning for a glyph. /// - public class GlyphKerning : UndertaleObject + public class GlyphKerning : UndertaleObject, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectsSize = 4; + /// /// TODO: unknown? /// @@ -266,6 +277,20 @@ public void Unserialize(UndertaleReader reader) Glyphs = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + int skipSize = 40; + if (reader.undertaleData.GeneralInfo?.BytecodeVersion >= 17) + skipSize += 4; // AscenderOffset + if (reader.undertaleData.IsVersionAtLeast(2022, 2)) + skipSize += 4; // Ascender + + reader.Position += skipSize; + + return 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + } + /// public override string ToString() { diff --git a/UndertaleModLib/Models/UndertaleFunction.cs b/UndertaleModLib/Models/UndertaleFunction.cs index cd0767d10..50cb49dc7 100644 --- a/UndertaleModLib/Models/UndertaleFunction.cs +++ b/UndertaleModLib/Models/UndertaleFunction.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using static UndertaleModLib.Models.UndertaleGeneralInfo; @@ -9,8 +10,11 @@ namespace UndertaleModLib.Models; /// A function entry as it's used in a GameMaker data file. /// [PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleFunction : UndertaleNamedResource, UndertaleInstruction.ReferencedObject, IDisposable +public class UndertaleFunction : UndertaleNamedResource, UndertaleInstruction.ReferencedObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 12; + public FunctionClassification Classification { get; set; } /// @@ -94,7 +98,7 @@ public void Dispose() public class UndertaleCodeLocals : UndertaleNamedResource, IDisposable { public UndertaleString Name { get; set; } - public ObservableCollection Locals { get; } = new ObservableCollection(); + public ObservableCollection Locals { get; private set; } = new ObservableCollection(); /// public void Serialize(UndertaleWriter writer) @@ -112,22 +116,34 @@ public void Unserialize(UndertaleReader reader) { uint count = reader.ReadUInt32(); Name = reader.ReadUndertaleString(); - Locals.Clear(); + List newLocals = new((int)count); for (uint i = 0; i < count; i++) - { - Locals.Add(reader.ReadUndertaleObject()); - } + newLocals.Add(reader.ReadUndertaleObject()); + Locals = new(newLocals); Util.DebugUtil.Assert(Locals.Count == count); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = reader.ReadUInt32(); + + reader.Position += 4 + count * LocalVar.ChildObjectsSize; + + return count; + } + public bool HasLocal(string varName) { return Locals.Any(local=>local.Name.Content == varName); } // TODO: INotifyPropertyChanged - public class LocalVar : UndertaleObject, IDisposable + public class LocalVar : UndertaleObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 8; + public uint Index { get; set; } public UndertaleString Name { get; set; } diff --git a/UndertaleModLib/Models/UndertaleGameObject.cs b/UndertaleModLib/Models/UndertaleGameObject.cs index ebf431eee..65ee498fa 100644 --- a/UndertaleModLib/Models/UndertaleGameObject.cs +++ b/UndertaleModLib/Models/UndertaleGameObject.cs @@ -35,6 +35,8 @@ public class UndertaleGameObject : UndertaleNamedResource, INotifyPropertyChange public UndertaleResourceById _parentId = new(); public UndertaleResourceById _textureMaskId = new(); + public static readonly int EventTypeCount = Enum.GetValues(typeof(EventType)).Length; + /// /// The name of the game object. /// @@ -158,8 +160,9 @@ protected void OnPropertyChanged([CallerMemberName] string name = null) /// public UndertaleGameObject() { - for (int i = 0; i < Enum.GetValues(typeof(EventType)).Length; i++) - Events.Add(new UndertalePointerList()); + Events.SetCapacity(EventTypeCount); + for (int i = 0; i < EventTypeCount; i++) + Events.InternalAdd(new UndertalePointerList()); } /// @@ -240,6 +243,7 @@ public void Unserialize(UndertaleReader reader) Awake = reader.ReadBoolean(); Kinematic = reader.ReadBoolean(); // Needs to be done manually because count is separated + PhysicsVertices.Capacity = physicsShapeVertexCount; for (int i = 0; i < physicsShapeVertexCount; i++) { UndertalePhysicsVertex v = new UndertalePhysicsVertex(); @@ -249,8 +253,27 @@ public void Unserialize(UndertaleReader reader) Events = reader.ReadUndertaleObject>>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + if (reader.undertaleData.IsVersionAtLeast(2022, 5)) + reader.Position += 64 + 4; // + "Managed" + else + reader.Position += 64; + + int physicsShapeVertexCount = reader.ReadInt32(); + reader.Position += 12 + (uint)physicsShapeVertexCount * UndertalePhysicsVertex.ChildObjectsSize; + + count += 2 + 1 + UndertalePointerList>.UnserializeChildObjectCount(reader); + + return count; + } + #region EventHandlerFor() overloads - //TODO: what do all these eventhandlers do? can't find any references right now. + // TODO: Add documentation for these methods. + // These methods are used by scripts for getting a code entry for a certain event of the game object. public UndertaleCode EventHandlerFor(EventType type, uint subtype, IList strg, IList codelist, IList localslist) { @@ -384,7 +407,7 @@ public void Dispose() { foreach (var subEv in ev) subEv?.Dispose(); - } + } Name = null; Events = new(); } @@ -458,6 +481,14 @@ public void Unserialize(UndertaleReader reader) Actions = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 4; // "EventSubtype" + + return 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + } + /// public void Dispose() { @@ -472,8 +503,14 @@ public void Dispose() /// /// An action in an event. /// - public class EventAction : UndertaleObject, INotifyPropertyChanged, IDisposable + public class EventAction : UndertaleObject, INotifyPropertyChanged, IDisposable, + IStaticChildObjectsSize, IStaticChildObjCount { + /// + public static readonly uint ChildObjectCount = 1; + /// + public static readonly uint ChildObjectsSize = 56; + // All the unknown values seem to be provided for compatibility only - in older versions of GM:S they stored the drag and drop blocks, // but newer versions compile them down to GML bytecode anyway // Possible meaning of values: https://github.com/WarlockD/GMdsam/blob/26aefe3e90a7a7a1891cb83f468079546f32b4b7/GMdsam/GameMaker/ChunkTypes.cs#L466 @@ -554,8 +591,11 @@ public void Dispose() /// Class representing a physics vertex used for a of type . /// [PropertyChanged.AddINotifyPropertyChangedInterface] - public class UndertalePhysicsVertex : UndertaleObject + public class UndertalePhysicsVertex : UndertaleObject, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectsSize = 8; + /// /// The x position of the vertex. /// diff --git a/UndertaleModLib/Models/UndertaleGeneralInfo.cs b/UndertaleModLib/Models/UndertaleGeneralInfo.cs index 1826857b5..fc2fee293 100644 --- a/UndertaleModLib/Models/UndertaleGeneralInfo.cs +++ b/UndertaleModLib/Models/UndertaleGeneralInfo.cs @@ -309,6 +309,35 @@ public enum FunctionClassification : ulong /// public bool InfoTimestampOffset { get; set; } = true; + public static (uint, uint, uint, uint) TestForCommonGMSVersions(UndertaleReader reader, + (uint, uint, uint, uint) readVersion) + { + (uint Major, uint Minor, uint Release, uint Build) detectedVer = readVersion; + + // Some GMS2+ version detection. The rest is spread around, mostly in UndertaleChunks.cs + if (reader.AllChunkNames.Contains("FEAT")) // 2022.8 + detectedVer = (2022, 8, 0, 0); + else if (reader.AllChunkNames.Contains("FEDS")) // 2.3.6 + detectedVer = (2, 3, 6, 0); + else if (reader.AllChunkNames.Contains("SEQN")) // 2.3 + detectedVer = (2, 3, 0, 0); + else if (reader.AllChunkNames.Contains("TGIN")) // 2.2.1 + detectedVer = (2, 2, 1, 0); + + if (detectedVer.Major > 2 || (detectedVer.Major == 2 && detectedVer.Minor >= 3)) + { + CompileContext.GMS2_3 = true; + DecompileContext.GMS2_3 = true; + } + else + { + CompileContext.GMS2_3 = false; + DecompileContext.GMS2_3 = false; + } + + return detectedVer; + } + /// /// If or has an invalid length. public void Serialize(UndertaleWriter writer) @@ -412,33 +441,25 @@ public void Unserialize(UndertaleReader reader) Release = reader.ReadUInt32(); Build = reader.ReadUInt32(); - // Some GMS2+ version detection. The rest is spread around, mostly in UndertaleChunks.cs - if (reader.AllChunkNames.Contains("FEAT")) // 2022.8 - { - Major = 2022; Minor = 8; Release = 0; Build = 0; - } - else if (reader.AllChunkNames.Contains("FEDS")) // 2.3.6 - { - Major = 2; Minor = 3; Release = 6; Build = 0; - } - else if (reader.AllChunkNames.Contains("SEQN")) // 2.3 - { - Major = 2; Minor = 3; Release = 0; Build = 0; - } - else if (reader.AllChunkNames.Contains("TGIN")) // 2.2.1 - { - Major = 2; Minor = 2; Release = 1; Build = 0; - } - if (Major > 2 || (Major == 2 && Minor >= 3)) - { - CompileContext.GMS2_3 = true; - DecompileContext.GMS2_3 = true; - } - else + var detectedVer = TestForCommonGMSVersions(reader, (Major, Minor, Release, Build)); + (Major, Minor, Release, Build) = detectedVer; + + if (reader.undertaleData.GeneralInfo is not null) { - CompileContext.GMS2_3 = false; - DecompileContext.GMS2_3 = false; + var prevGenInfo = reader.undertaleData.GeneralInfo; + // If previous version is greater than current + if (prevGenInfo.Major > Major + || prevGenInfo.Major == Major && prevGenInfo.Minor > Minor + || prevGenInfo.Major == Major && prevGenInfo.Minor == Minor && prevGenInfo.Release > Release + || prevGenInfo.Major == Major && prevGenInfo.Minor == Minor && prevGenInfo.Release == Release && prevGenInfo.Build > Build) + { + Major = prevGenInfo.Major; + Minor = prevGenInfo.Minor; + Release = prevGenInfo.Release; + Build = prevGenInfo.Build; + } } + DefaultWindowWidth = reader.ReadUInt32(); DefaultWindowHeight = reader.ReadUInt32(); Info = (InfoFlags)reader.ReadUInt32(); @@ -495,6 +516,19 @@ public void Unserialize(UndertaleReader reader) reader.Bytecode14OrLower = BytecodeVersion <= 14; } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position++; // "IsDebuggerDisabled" + byte bytecodeVer = reader.ReadByte(); + bool readDebugPort = bytecodeVer >= 14; + + reader.Position += (uint)(122 + (readDebugPort ? 4 : 0)); + + // "RoomOrder" + return 1 + UndertaleSimpleResourcesList.UnserializeChildObjectCount(reader); + } + /// /// Generates "info number" used for GMS2 UIDs. /// @@ -704,8 +738,9 @@ public enum OptionsFlags : ulong /// A class for game constants. /// [PropertyChanged.AddINotifyPropertyChangedInterface] - public class Constant : UndertaleObject, IDisposable + public class Constant : UndertaleObject, IStaticChildObjectsSize, IDisposable { + public static readonly uint ChildObjectsSize = 8; /// /// The name of the constant. /// @@ -867,6 +902,22 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + bool newFormat = reader.ReadInt32() == int.MinValue; + reader.Position -= 4; + + reader.Position += newFormat ? 60u : 140u; + count += 3; // images + + // "Constants" + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + + return count; + } + /// public void Dispose() { diff --git a/UndertaleModLib/Models/UndertalePath.cs b/UndertaleModLib/Models/UndertalePath.cs index 8a921f52e..98c63ba7e 100644 --- a/UndertaleModLib/Models/UndertalePath.cs +++ b/UndertaleModLib/Models/UndertalePath.cs @@ -39,8 +39,11 @@ public class UndertalePath : UndertaleNamedResource, IDisposable /// A point in a . /// [PropertyChanged.AddINotifyPropertyChangedInterface] - public class PathPoint : UndertaleObject + public class PathPoint : UndertaleObject, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectsSize = 12; + /// /// The X position of the . /// @@ -93,6 +96,14 @@ public void Unserialize(UndertaleReader reader) Points = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 16; + + return 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + /// public override string ToString() { diff --git a/UndertaleModLib/Models/UndertaleRoom.cs b/UndertaleModLib/Models/UndertaleRoom.cs index 166da84ff..d398a13ac 100644 --- a/UndertaleModLib/Models/UndertaleRoom.cs +++ b/UndertaleModLib/Models/UndertaleRoom.cs @@ -148,7 +148,7 @@ public enum RoomEntryFlags : uint /// /// The list of game objects this room uses. /// - public UndertalePointerListLenCheck GameObjects { get; private set; } = new UndertalePointerListLenCheck(); + public UndertalePointerList GameObjects { get; private set; } = new UndertalePointerList(); /// /// The list of tiles this room uses. @@ -165,6 +165,8 @@ public enum RoomEntryFlags : uint /// public UndertaleSimpleList> Sequences { get; private set; } = new UndertaleSimpleList>(); + public static bool CheckedForGMS2_2_2_302; + /// /// Calls for in order to update the room background color.
/// Only used for GameMaker: Studio 2 rooms. @@ -238,8 +240,8 @@ public Layer BGColorLayer return _layers?.Where(l => l.LayerType is LayerType.Background && l.BackgroundData.Sprite is null && l.BackgroundData.Color != 0) - .OrderBy(l => l.LayerDepth) - .FirstOrDefault(); + .OrderBy(l => l.LayerDepth) + .FirstOrDefault(); } } @@ -255,14 +257,60 @@ protected void OnPropertyChanged([CallerMemberName] string name = null) ///
public UndertaleRoom() { + Backgrounds.SetCapacity(8); + Views.SetCapacity(8); for (int i = 0; i < 8; i++) - Backgrounds.Add(new Background()); + Backgrounds.InternalAdd(new Background()); for (int i = 0; i < 8; i++) - Views.Add(new View()); + Views.InternalAdd(new View()); if (Flags.HasFlag(RoomEntryFlags.EnableViews)) Views[0].Enabled = true; } + private static void CheckForGMS2_2_2_302(UndertaleReader reader) + { + if (reader.undertaleData.IsVersionAtLeast(2, 2, 2, 302)) + { + CheckedForGMS2_2_2_302 = true; + + uint newSize = GameObject.ChildObjectsSize + 8; + reader.SetStaticChildObjectsSize(typeof(GameObject), newSize); + + return; + } + + long returnTo = reader.Position; + reader.Position -= 4; + + uint gameObjPtr = reader.ReadUInt32(); + uint tilePtr = reader.ReadUInt32(); + + reader.AbsPosition = gameObjPtr; // "GameObjects" + uint objCount = reader.ReadUInt32(); + if (objCount > 0) + { + uint firstPtr = reader.ReadUInt32(); + uint secondPtr; + if (objCount == 1) + secondPtr = tilePtr; + else + secondPtr = reader.ReadUInt32(); + + if (secondPtr - firstPtr == 48) + { + reader.undertaleData.SetGMS2Version(2, 2, 2, 302); + + //"GameObject.ImageSpeed" + "...ImageIndex" + uint newSize = GameObject.ChildObjectsSize + 8; + reader.SetStaticChildObjectsSize(typeof(GameObject), newSize); + } + } + + reader.Position = returnTo; + + CheckedForGMS2_2_2_302 = true; + } + /// public void Serialize(UndertaleWriter writer) { @@ -338,9 +386,12 @@ public void Unserialize(UndertaleReader reader) Flags = (RoomEntryFlags)reader.ReadUInt32(); Backgrounds = reader.ReadUndertaleObjectPointer>(); Views = reader.ReadUndertaleObjectPointer>(); - GameObjects = reader.ReadUndertaleObjectPointer>(); - uint tilePtr = reader.ReadUInt32(); - Tiles = reader.GetUndertaleObjectAtAddress>(tilePtr); + GameObjects = reader.ReadUndertaleObjectPointer>(); + + if (!CheckedForGMS2_2_2_302) + CheckForGMS2_2_2_302(reader); + + Tiles = reader.ReadUndertaleObjectPointer>(); World = reader.ReadBoolean(); Top = reader.ReadUInt32(); Left = reader.ReadUInt32(); @@ -359,7 +410,7 @@ public void Unserialize(UndertaleReader reader) } reader.ReadUndertaleObject(Backgrounds); reader.ReadUndertaleObject(Views); - reader.ReadUndertaleObject(GameObjects, tilePtr); + reader.ReadUndertaleObject(GameObjects); reader.ReadUndertaleObject(Tiles); if (reader.undertaleData.IsGameMaker2()) { @@ -373,8 +424,9 @@ public void Unserialize(UndertaleReader reader) layer.InstancesData.Instances.Clear(); foreach (var id in layer.InstancesData.InstanceIds) { - if (GameObjects.ByInstanceID(id) != null) - layer.InstancesData.Instances.Add(GameObjects.ByInstanceID(id)); + GameObject gameObj = GameObjects.ByInstanceID(id); + if (gameObj is not null) + layer.InstancesData.Instances.Add(gameObj); else { /* Attempt to resolve null objects. @@ -395,6 +447,57 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 40; + count += 1; // "_creationCodeId" + + uint backgroundPtr = reader.ReadUInt32(); + uint viewsPtr = reader.ReadUInt32(); + uint gameObjsPtr = reader.ReadUInt32(); + if (!CheckedForGMS2_2_2_302) + CheckForGMS2_2_2_302(reader); + uint tilesPtr = reader.ReadUInt32(); + uint layersPtr = 0; + uint sequencesPtr = 0; + + reader.Position += 32; + + if (reader.undertaleData.IsGameMaker2()) + { + layersPtr = reader.ReadUInt32(); + if (reader.undertaleData.IsVersionAtLeast(2, 3)) + sequencesPtr = reader.ReadUInt32(); + } + + reader.AbsPosition = backgroundPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + reader.AbsPosition = viewsPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + reader.AbsPosition = gameObjsPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + reader.AbsPosition = tilesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + if (reader.undertaleData.IsGameMaker2()) + { + reader.AbsPosition = layersPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + if (reader.undertaleData.IsVersionAtLeast(2, 3)) + { + reader.AbsPosition = sequencesPtr; + count += 1 + UndertaleSimpleList> + .UnserializeChildObjectCount(reader); + } + } + + return count; + } + /// /// Initialize the room by setting every or /// (depending on the GameMaker version), and optionally calculate the room grid size. @@ -527,8 +630,15 @@ public interface IRoomObject /// /// A background with properties as it's used in a room. /// - public class Background : UndertaleObject, INotifyPropertyChanged, IDisposable + public class Background : UndertaleObject, INotifyPropertyChanged, IDisposable, + IStaticChildObjCount, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 40; + private UndertaleRoom _parentRoom; /// @@ -696,8 +806,15 @@ public void Dispose() /// /// A view with properties as it's used in a room. /// - public class View : UndertaleObject, INotifyPropertyChanged, IDisposable + public class View : UndertaleObject, INotifyPropertyChanged, IDisposable, + IStaticChildObjCount, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 56; + /// /// Whether this view is enabled. /// @@ -823,8 +940,15 @@ public void Dispose() /// /// A game object with properties as it's used in a room. /// - public class GameObject : UndertaleObjectLenCheck, IRoomObject, INotifyPropertyChanged, IDisposable + public class GameObject : UndertaleObject, IRoomObject, INotifyPropertyChanged, IDisposable, + IStaticChildObjCount, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectCount = 2; + + /// + public static readonly uint ChildObjectsSize = 36; + private UndertaleResourceById _objectDefinition = new(); private UndertaleResourceById _creationCode = new(); private UndertaleResourceById _preCreateCode = new(); @@ -984,12 +1108,6 @@ public void Serialize(UndertaleWriter writer) /// public void Unserialize(UndertaleReader reader) - { - Unserialize(reader, -1); - } - - /// - public void Unserialize(UndertaleReader reader, int length) { X = reader.ReadInt32(); Y = reader.ReadInt32(); @@ -998,10 +1116,8 @@ public void Unserialize(UndertaleReader reader, int length) _creationCode = reader.ReadUndertaleObject>(); ScaleX = reader.ReadSingle(); ScaleY = reader.ReadSingle(); - if (length == 48) + if (reader.undertaleData.IsVersionAtLeast(2, 2, 2, 302)) { - if (!reader.undertaleData.IsVersionAtLeast(2, 2, 2, 302)) - reader.undertaleData.SetGMS2Version(2, 2, 2, 302); ImageSpeed = reader.ReadSingle(); ImageIndex = reader.ReadInt32(); } @@ -1030,8 +1146,15 @@ public void Dispose() /// /// A tile with properties as it's used in a room. /// - public class Tile : UndertaleObject, IRoomObject, INotifyPropertyChanged, IDisposable + public class Tile : UndertaleObject, IRoomObject, INotifyPropertyChanged, IDisposable, + IStaticChildObjCount, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 48; + /// /// Whether this tile is from an asset layer.
/// for GameMaker Studio: 2 games, otherwise . @@ -1365,21 +1488,48 @@ public void Unserialize(UndertaleReader reader) EffectProperties = reader.ReadUndertaleObject>(); } - switch (LayerType) + Data = LayerType switch { - case LayerType.Instances: Data = reader.ReadUndertaleObject(); break; - case LayerType.Tiles: Data = reader.ReadUndertaleObject(); break; - case LayerType.Background: Data = reader.ReadUndertaleObject(); break; - case LayerType.Assets: Data = reader.ReadUndertaleObject(); break; - case LayerType.Effect: - // Because effect data is empty in 2022.1+, it would erroneously read the next object. - Data = - reader.undertaleData.IsVersionAtLeast(2022, 1) - ? new LayerEffectData() { EffectType = EffectType, Properties = EffectProperties } - : reader.ReadUndertaleObject(); - break; - default: throw new Exception("Unsupported layer type " + LayerType); + LayerType.Instances => reader.ReadUndertaleObject(), + LayerType.Tiles => reader.ReadUndertaleObject(), + LayerType.Background => reader.ReadUndertaleObject(), + LayerType.Assets => reader.ReadUndertaleObject(), + LayerType.Effect => // Because effect data is empty in 2022.1+, it would erroneously read the next object. + reader.undertaleData.IsVersionAtLeast(2022, 1) + ? new LayerEffectData() { EffectType = EffectType, Properties = EffectProperties } + : reader.ReadUndertaleObject(), + _ => throw new Exception("Unsupported layer type " + LayerType) + }; + } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 8; + LayerType layerType = (LayerType)reader.ReadUInt32(); + reader.Position += 24; + + // Effect properties + if (reader.undertaleData.IsVersionAtLeast(2022, 1)) + { + reader.Position += 8; + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); } + + count += layerType switch + { + LayerType.Instances => 1 + LayerInstancesData.UnserializeChildObjectCount(reader), + LayerType.Tiles => 1 + LayerTilesData.UnserializeChildObjectCount(reader), + LayerType.Background => 1 + LayerBackgroundData.UnserializeChildObjectCount(reader), + LayerType.Assets => 1 + LayerAssetsData.UnserializeChildObjectCount(reader), + LayerType.Effect => reader.undertaleData.IsVersionAtLeast(2022, 1) + ? 0 : 1 + LayerEffectData.UnserializeChildObjectCount(reader), + _ => 0 + }; + + return count; } /// @@ -1414,14 +1564,23 @@ public void Serialize(UndertaleWriter writer) /// public void Unserialize(UndertaleReader reader) { - uint InstanceCount = reader.ReadUInt32(); - InstanceIds = new uint[InstanceCount]; + uint instanceCount = reader.ReadUInt32(); + InstanceIds = new uint[instanceCount]; Instances.Clear(); - for (uint i = 0; i < InstanceCount; i++) + for (uint i = 0; i < instanceCount; i++) InstanceIds[i] = reader.ReadUInt32(); // UndertaleRoom.Unserialize resolves these IDs to objects later } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint instanceCount = reader.ReadUInt32(); + reader.Position += instanceCount * 4; + + return 0; + } + /// public void Dispose() { @@ -1520,6 +1679,20 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 4; // _background + + uint tilesX = reader.ReadUInt32(); + uint tilesY = reader.ReadUInt32(); + reader.Position += tilesX * tilesY * 4; + + return count; + } + /// public void Dispose() { @@ -1531,8 +1704,14 @@ public void Dispose() } } - public class LayerBackgroundData : LayerData, INotifyPropertyChanged + public class LayerBackgroundData : LayerData, IStaticChildObjCount, IStaticChildObjectsSize, INotifyPropertyChanged { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 40; + private Layer _parentLayer; private UndertaleResourceById _sprite = new(); // Apparently there's a mode where it's a background reference, but probably not necessary @@ -1608,6 +1787,14 @@ public void Unserialize(UndertaleReader reader) AnimationSpeedType = (AnimationSpeedType)reader.ReadUInt32(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += ChildObjectsSize; + + return ChildObjectCount; + } + /// public void Dispose() { @@ -1668,6 +1855,40 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + uint legacyTilesPtr = reader.ReadUInt32(); + uint spritesPtr = reader.ReadUInt32(); + uint sequencesPtr = 0; + uint nineSlicesPtr = 0; + if (reader.undertaleData.IsVersionAtLeast(2, 3)) + { + sequencesPtr = reader.ReadUInt32(); + if (!reader.undertaleData.IsVersionAtLeast(2, 3, 2)) + nineSlicesPtr = reader.ReadUInt32(); + } + + reader.AbsPosition = legacyTilesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + reader.AbsPosition = spritesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + if (reader.undertaleData.IsVersionAtLeast(2, 3)) + { + reader.AbsPosition = sequencesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + if (!reader.undertaleData.IsVersionAtLeast(2, 3, 2)) + { + reader.AbsPosition = nineSlicesPtr; + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + } + } + + return count; + } + /// public void Dispose() { @@ -1725,6 +1946,17 @@ public void Unserialize(UndertaleReader reader) Properties = reader.ReadUndertaleObject>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + if (reader.undertaleData.IsVersionAtLeast(2022, 1)) + return 0; + + reader.Position += 4; // "EffectType" + + return 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + /// public void Dispose() { @@ -1743,8 +1975,11 @@ public void Dispose() } [PropertyChanged.AddINotifyPropertyChangedInterface] - public class EffectProperty : UndertaleObject, IDisposable + public class EffectProperty : UndertaleObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 12; + public enum PropertyType { Real = 0, @@ -1782,8 +2017,14 @@ public void Dispose() } } - public class SpriteInstance : UndertaleObject, INotifyPropertyChanged, IDisposable + public class SpriteInstance : UndertaleObject, INotifyPropertyChanged, IStaticChildObjCount, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 44; + private UndertaleResourceById _sprite = new(); public UndertaleString Name { get; set; } @@ -1884,8 +2125,14 @@ public void Dispose() } } - public class SequenceInstance : UndertaleObject, INotifyPropertyChanged, IDisposable + public class SequenceInstance : UndertaleObject, INotifyPropertyChanged, IStaticChildObjCount, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectCount = 1; + + /// + public static readonly uint ChildObjectsSize = 44; + private UndertaleResourceById _sequence = new(); public UndertaleString Name { get; set; } diff --git a/UndertaleModLib/Models/UndertaleScript.cs b/UndertaleModLib/Models/UndertaleScript.cs index f25c7c92d..3edfe4a1a 100644 --- a/UndertaleModLib/Models/UndertaleScript.cs +++ b/UndertaleModLib/Models/UndertaleScript.cs @@ -6,8 +6,11 @@ namespace UndertaleModLib.Models; /// /// A script entry in a data file. /// -public class UndertaleScript : UndertaleNamedResource, INotifyPropertyChanged, IDisposable +public class UndertaleScript : UndertaleNamedResource, INotifyPropertyChanged, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 8; + /// /// The name of the script entry. /// diff --git a/UndertaleModLib/Models/UndertaleSequence.cs b/UndertaleModLib/Models/UndertaleSequence.cs index 995998527..57a7197df 100644 --- a/UndertaleModLib/Models/UndertaleSequence.cs +++ b/UndertaleModLib/Models/UndertaleSequence.cs @@ -80,6 +80,25 @@ public void Unserialize(UndertaleReader reader) Moments = reader.ReadUndertaleObject>>(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 32; + + count += 1 + UndertaleSimpleList>.UnserializeChildObjectCount(reader); + + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + + int funcIDsCount = reader.ReadInt32(); + reader.Position += (uint)funcIDsCount * 8; + + count += 1 + UndertaleSimpleList>.UnserializeChildObjectCount(reader); + + return count; + } + /// public void Dispose() { @@ -132,6 +151,38 @@ public void Unserialize(UndertaleReader reader) Channels[channel] = data; } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 16; + int chCount = reader.ReadInt32(); + + Type t = typeof(T); + if (t.IsAssignableTo(typeof(IStaticChildObjectsSize))) + { + uint subSize = reader.GetStaticChildObjectsSize(t); + uint subCount = 0; + + if (t.IsAssignableTo(typeof(IStaticChildObjCount))) + subCount = reader.GetStaticChildCount(t); + + reader.Position += (uint)(chCount * 4 + chCount * subSize); + + return (uint)chCount * subCount; + } + + var unserializeFunc = reader.GetUnserializeCountFunc(t); + for (int i = 0; i < chCount; i++) + { + reader.Position += 4; // channel ID + count += unserializeFunc(reader); + } + + return count; + } } public class BroadcastMessage : UndertaleObject @@ -150,6 +201,12 @@ public void Unserialize(UndertaleReader reader) Messages = new UndertaleSimpleListString(); Messages.Unserialize(reader); } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + return UndertaleSimpleListString.UnserializeChildObjectCount(reader); + } } public class Moment : UndertaleObject @@ -172,6 +229,16 @@ public void Unserialize(UndertaleReader reader) if (InternalCount > 0) Event = reader.ReadUndertaleString(); } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + int internalCount = reader.ReadInt32(); + if (internalCount > 0) + reader.Position += 4; // "Event" + + return 0; + } } public class Track : UndertaleObject @@ -183,7 +250,7 @@ public class Track : UndertaleObject public bool IsCreationTrack { get; set; } public List Tags { get; set; } public List Tracks { get; set; } // Sub-tracks - public TrackKeyframes Keyframes { get; set; } + public ITrackKeyframes Keyframes { get; set; } public List OwnedResources { get; set; } public UndertaleString GMAnimCurveString; @@ -265,10 +332,16 @@ public void Unserialize(UndertaleReader reader) UndertaleString ForceReadString() { UndertaleString res = reader.ReadUndertaleString(); - uint returnTo = reader.Position; + if (res.Content is not null) + return res; + + reader.SwitchReaderType(false); + long returnTo = reader.Position; reader.Position = reader.GetOffsetMapRev()[res]; reader.ReadUndertaleObject(); reader.Position = returnTo; + reader.SwitchReaderType(true); + return res; } @@ -282,11 +355,11 @@ UndertaleString ForceReadString() int ownedResCount = reader.ReadInt32(); int trackCount = reader.ReadInt32(); - Tags = new List(); + Tags = new List(tagCount); for (int i = 0; i < tagCount; i++) Tags.Add(reader.ReadInt32()); - OwnedResources = new List(); + OwnedResources = new List(ownedResCount); for (int i = 0; i < ownedResCount; i++) { GMAnimCurveString = ForceReadString(); @@ -297,7 +370,7 @@ UndertaleString ForceReadString() OwnedResources.Add(res); } - Tracks = new List(); + Tracks = new List(trackCount); for (int i = 0; i < trackCount; i++) Tracks.Add(reader.ReadUndertaleObject()); @@ -347,25 +420,130 @@ UndertaleString ForceReadString() throw new NotImplementedException("GMClipMaskTrack not implemented, report this"); } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + string ForceReadString() + { + uint strPtr = reader.ReadUInt32(); + + reader.SwitchReaderType(false); + long returnTo = reader.Position; + reader.Position = strPtr - 4; + string res = reader.ReadGMString(); + reader.Position = returnTo; + reader.SwitchReaderType(true); + + return res; + } + + string modelName = ForceReadString(); + reader.Position += 16; + + int tagCount = reader.ReadInt32(); + int ownedResCount = reader.ReadInt32(); + int trackCount = reader.ReadInt32(); + + reader.Position += (uint)tagCount * 4; // "Tags" + + for (int i = 0; i < ownedResCount; i++) + { + reader.Position += 4; // "GMAnimCurveString" + count += UndertaleAnimationCurve.UnserializeChildObjectCount(reader); + } + + // "Tracks" + for (int i = 0; i < trackCount; i++) + count += 1 + UnserializeChildObjectCount(reader); + + switch (modelName) + { + case "GMAudioTrack": + count += 1 + AudioKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMInstanceTrack": + count += 1 + InstanceKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMGraphicTrack": + count += 1 + GraphicKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMSequenceTrack": + count += 1 + SequenceKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMSpriteFramesTrack": + count += 1 + SpriteFramesKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMAssetTrack": // TODO? + throw new NotImplementedException("GMAssetTrack not implemented, report this"); + case "GMBoolTrack": + count += 1 + BoolKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMStringTrack": + count += 1 + StringKeyframes.UnserializeChildObjectCount(reader); + break; + // TODO? + //case "GMIntTrack": + // count += 1 + IntKeyframes.UnserializeChildObjectCount(reader); + // break; + case "GMRealTrack": + case "GMColourTrack": + count += 1 + RealKeyframes.UnserializeChildObjectCount(reader); + break; + case "GMTextTrack": // Introduced in GM 2022.2 + count += 1 + TextKeyframes.UnserializeChildObjectCount(reader); + break; + + case "GMParticleTrack": + throw new NotImplementedException("GMParticleTrack not implemented, report this"); + case "GMGroupTrack": + throw new NotImplementedException("GMGroupTrack not implemented, report this"); + case "GMClipMaskTrack": + throw new NotImplementedException("GMClipMaskTrack not implemented, report this"); + } + + return count; + } } /// Here begins all of the keyframe data classes. Some generics used to shorten sections, but some verbosity maintained - public class TrackKeyframes : UndertaleObject + public interface ITrackKeyframes : UndertaleObject { + } + public class TrackKeyframes : ITrackKeyframes where T : UndertaleObject, new() + { + public UndertaleSimpleList> List; + /// public virtual void Serialize(UndertaleWriter writer) { while (writer.Position % 4 != 0) writer.Write((byte)0); + + List.Serialize(writer); } /// public virtual void Unserialize(UndertaleReader reader) { - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); + + List = new UndertaleSimpleList>(); + List.Unserialize(reader); + } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + while (reader.AbsPosition % 4 != 0) + reader.Position++; + + return UndertaleSimpleList>.UnserializeChildObjectCount(reader); } } @@ -385,9 +563,40 @@ public virtual void Unserialize(UndertaleReader reader) Resource = new T(); Resource.Unserialize(reader); } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + // At this moment, T could be only "UndertaleResourceById<>". + // If that changes, you should replace the contents with the following: + // return reader.GetChildObjectCount(); + + reader.Position += 4; + return 0; + } + } + + public class SimpleIntData : IStaticChildObjectsSize, UndertaleObject + { + /// + public static readonly uint ChildObjectsSize = 4; + + public int Value { get; set; } + + /// + public void Serialize(UndertaleWriter writer) + { + writer.Write(Value); + } + + /// + public void Unserialize(UndertaleReader reader) + { + Value = reader.ReadInt32(); + } } - public class AudioKeyframes : TrackKeyframes + public class AudioKeyframes : TrackKeyframes { public class Data : ResourceData> { @@ -409,197 +618,64 @@ public override void Unserialize(UndertaleReader reader) throw new IOException("Expected 0 in Audio keyframe"); Mode = reader.ReadInt32(); } - } - public UndertaleSimpleList> List; + /// + public static new uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = ResourceData>.UnserializeChildObjectCount(reader); - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } + reader.Position += 8; - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); + return count; + } } } - public class InstanceKeyframes : TrackKeyframes + public class InstanceKeyframes : TrackKeyframes { public class Data : ResourceData> { } - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } - - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); - } } - public class GraphicKeyframes : TrackKeyframes + public class GraphicKeyframes : TrackKeyframes { public class Data : ResourceData> { } - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } - - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); - } } - public class SequenceKeyframes : TrackKeyframes + public class SequenceKeyframes : TrackKeyframes { public class Data : ResourceData> { } - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } - - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); - } - } - - public class SpriteFramesData : UndertaleObject - { - public int Value { get; set; } - - /// - public void Serialize(UndertaleWriter writer) - { - writer.Write(Value); - } - - /// - public void Unserialize(UndertaleReader reader) - { - Value = reader.ReadInt32(); - } - } - - public class SpriteFramesKeyframes : TrackKeyframes - { - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } - - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); - } } - public class BoolData : UndertaleObject + public class SpriteFramesKeyframes : TrackKeyframes { - public int Value { get; set; } - - /// - public void Serialize(UndertaleWriter writer) - { - writer.Write(Value); - } - - /// - public void Unserialize(UndertaleReader reader) - { - Value = reader.ReadInt32(); - } + public class Data : SimpleIntData { } } - public class BoolKeyframes : TrackKeyframes + public class BoolKeyframes : TrackKeyframes { - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } - - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); - } + public class Data : SimpleIntData { } } - public class StringData : UndertaleObject + public class StringKeyframes : TrackKeyframes { - public UndertaleString Value { get; set; } - - /// - public void Serialize(UndertaleWriter writer) + public class Data : UndertaleObject, IStaticChildObjectsSize { - writer.WriteUndertaleString(Value); - } + /// + public static readonly uint ChildObjectsSize = 4; - /// - public void Unserialize(UndertaleReader reader) - { - Value = reader.ReadUndertaleString(); - } - } + public UndertaleString Value { get; set; } - public class StringKeyframes : TrackKeyframes - { - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) - { - base.Serialize(writer); - List.Serialize(writer); - } + /// + public void Serialize(UndertaleWriter writer) + { + writer.WriteUndertaleString(Value); + } - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); - List = new UndertaleSimpleList>(); - List.Unserialize(reader); + /// + public void Unserialize(UndertaleReader reader) + { + Value = reader.ReadUndertaleString(); + } } } @@ -644,6 +720,24 @@ public virtual void Unserialize(UndertaleReader reader) AssetAnimCurve.Unserialize(reader); } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + // "IsCurveEmbedded" + if (reader.ReadBoolean()) + { + reader.Position += 4; + + count += UndertaleAnimationCurve.UnserializeChildObjectCount(reader, false); + } + else + reader.Position += 4; + + return count; + } } public class IntData : CurveData @@ -663,17 +757,25 @@ public override void Unserialize(UndertaleReader reader) Value = reader.ReadInt32(); base.Unserialize(reader); } + + /// + public static new uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 4; // "Value" + + return CurveData.UnserializeChildObjectCount(reader); + } } - public class IntKeyframes : TrackKeyframes + public class IntKeyframes : TrackKeyframes { - public UndertaleSimpleList> List; public int Interpolation; /// public override void Serialize(UndertaleWriter writer) { - base.Serialize(writer); + while (writer.Position % 4 != 0) + writer.Write((byte)0); writer.Write(Interpolation); @@ -683,13 +785,26 @@ public override void Serialize(UndertaleWriter writer) /// public override void Unserialize(UndertaleReader reader) { - base.Unserialize(reader); + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); Interpolation = reader.ReadInt32(); List = new UndertaleSimpleList>(); List.Unserialize(reader); } + + /// + public static new uint UnserializeChildObjectCount(UndertaleReader reader) + { + while (reader.AbsPosition % 4 != 0) + reader.Position++; + + reader.Position += 4; // "Interpolation" + + return UndertaleSimpleList>.UnserializeChildObjectCount(reader); + } } public class RealData : CurveData @@ -709,17 +824,25 @@ public override void Unserialize(UndertaleReader reader) Value = reader.ReadSingle(); base.Unserialize(reader); } + + /// + public static new uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 4; // "Value" + + return CurveData.UnserializeChildObjectCount(reader); + } } - public class RealKeyframes : TrackKeyframes + public class RealKeyframes : TrackKeyframes { - public UndertaleSimpleList> List; public int Interpolation; /// public override void Serialize(UndertaleWriter writer) { - base.Serialize(writer); + while (writer.Position % 4 != 0) + writer.Write((byte)0); writer.Write(Interpolation); @@ -729,73 +852,70 @@ public override void Serialize(UndertaleWriter writer) /// public override void Unserialize(UndertaleReader reader) { - base.Unserialize(reader); + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); Interpolation = reader.ReadInt32(); List = new UndertaleSimpleList>(); List.Unserialize(reader); } - } - - // Source - https://github.com/YoYoGames/GameMaker-HTML5/blob/develop/scripts/yySequence.js#L2227 - // ("yyTextTrackKey") - public class TextData : UndertaleObject - { - private int _alignment; - public UndertaleString Text { get; set; } - public bool Wrap { get; set; } - public int AlignmentV - { - get => (_alignment >> 8) & 0xff; - set => _alignment = (_alignment & 0xff) | (value & 0xff) << 8; - } - public int AlignmentH + /// + public static new uint UnserializeChildObjectCount(UndertaleReader reader) { - get => _alignment & 0xff; - set => _alignment = (_alignment & ~0xff) | (value & 0xff); - } - public int FontIndex { get; set; } + while (reader.AbsPosition % 4 != 0) + reader.Position++; - /// - public void Serialize(UndertaleWriter writer) - { - writer.WriteUndertaleString(Text); - writer.Write(Wrap); - writer.Write(_alignment); - writer.Write(FontIndex); - } + reader.Position += 4; // "Interpolation" - /// - public void Unserialize(UndertaleReader reader) - { - Text = reader.ReadUndertaleString(); - Wrap = reader.ReadBoolean(); - _alignment = reader.ReadInt32(); - FontIndex = reader.ReadInt32(); + return UndertaleSimpleList>.UnserializeChildObjectCount(reader); } } - public class TextKeyframes : TrackKeyframes + + public class TextKeyframes : TrackKeyframes { - - public UndertaleSimpleList> List; - - /// - public override void Serialize(UndertaleWriter writer) + // Source - https://github.com/YoYoGames/GameMaker-HTML5/blob/develop/scripts/yySequence.js#L2227 + // ("yyTextTrackKey") + public class Data : UndertaleObject, IStaticChildObjectsSize { - base.Serialize(writer); + /// + public static readonly uint ChildObjectsSize = 16; - List.Serialize(writer); - } + private int _alignment; - /// - public override void Unserialize(UndertaleReader reader) - { - base.Unserialize(reader); + public UndertaleString Text { get; set; } + public bool Wrap { get; set; } + public int AlignmentV + { + get => (_alignment >> 8) & 0xff; + set => _alignment = (_alignment & 0xff) | (value & 0xff) << 8; + } + public int AlignmentH + { + get => _alignment & 0xff; + set => _alignment = (_alignment & ~0xff) | (value & 0xff); + } + public int FontIndex { get; set; } - List = new UndertaleSimpleList>(); - List.Unserialize(reader); + /// + public void Serialize(UndertaleWriter writer) + { + writer.WriteUndertaleString(Text); + writer.Write(Wrap); + writer.Write(_alignment); + writer.Write(FontIndex); + } + + /// + public void Unserialize(UndertaleReader reader) + { + Text = reader.ReadUndertaleString(); + Wrap = reader.ReadBoolean(); + _alignment = reader.ReadInt32(); + FontIndex = reader.ReadInt32(); + } } } } diff --git a/UndertaleModLib/Models/UndertaleShader.cs b/UndertaleModLib/Models/UndertaleShader.cs index 263752b84..425c2c9d5 100644 --- a/UndertaleModLib/Models/UndertaleShader.cs +++ b/UndertaleModLib/Models/UndertaleShader.cs @@ -12,8 +12,11 @@ public class UndertaleShader : UndertaleNamedResource, IDisposable /// The vertex shader attributes a shader can have. ///
[PropertyChanged.AddINotifyPropertyChangedInterface] - public class VertexShaderAttribute : UndertaleObject, IDisposable + public class VertexShaderAttribute : UndertaleObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 4; + /// /// The name of the vertex shader attribute. /// @@ -198,7 +201,7 @@ private static void WritePadding(UndertaleWriter writer, int amount) private static void ReadPadding(UndertaleReader reader, int amount) { - while ((reader.Position & amount) != 0) + while ((reader.AbsPosition & amount) != 0) { if (reader.ReadByte() != 0) throw new UndertaleSerializationException("Failed to read shader padding: should be some zero bytes"); @@ -332,7 +335,7 @@ public void Unserialize(UndertaleReader reader) next = HLSL11_PixelData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); HLSL11_VertexData.ReadData(reader, length); } if (!HLSL11_PixelData.IsNull) @@ -345,7 +348,7 @@ public void Unserialize(UndertaleReader reader) next = PSSL_VertexData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); HLSL11_PixelData.ReadData(reader, length); } @@ -359,7 +362,7 @@ public void Unserialize(UndertaleReader reader) next = PSSL_PixelData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); PSSL_VertexData.ReadData(reader, length); } if (!PSSL_PixelData.IsNull) @@ -372,7 +375,7 @@ public void Unserialize(UndertaleReader reader) next = Cg_PSVita_VertexData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); PSSL_PixelData.ReadData(reader, length); } @@ -386,7 +389,7 @@ public void Unserialize(UndertaleReader reader) next = Cg_PSVita_PixelData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); Cg_PSVita_VertexData.ReadData(reader, length); } if (!Cg_PSVita_PixelData.IsNull) @@ -399,7 +402,7 @@ public void Unserialize(UndertaleReader reader) next = Cg_PS3_VertexData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); Cg_PSVita_PixelData.ReadData(reader, length); } @@ -415,7 +418,7 @@ public void Unserialize(UndertaleReader reader) next = Cg_PS3_PixelData._Position; else next = EntryEnd; - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); Cg_PS3_VertexData.ReadData(reader, length); } if (!Cg_PS3_PixelData.IsNull) @@ -424,12 +427,23 @@ public void Unserialize(UndertaleReader reader) // Calculate length of data uint next = EntryEnd; // final possible data, nothing else to check for - int length = (int)(next - reader.Position); + int length = (int)(next - reader.AbsPosition); Cg_PS3_PixelData.ReadData(reader, length); } } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 40; + + // Since shaders are stored in a pointer list, and there are no + // more child objects that are in the pool, then there is no + // need to unserializing remaining elements + return 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + /// /// Possible shader types a shader can have. /// @@ -499,7 +513,7 @@ public void Serialize(UndertaleWriter writer, bool writeLength = true) public void Unserialize(UndertaleReader reader, bool readLength = true) { - _PointerLocation = reader.Position; + _PointerLocation = (uint)reader.AbsPosition; _Position = reader.ReadUInt32(); if (readLength) _Length = reader.ReadUInt32(); diff --git a/UndertaleModLib/Models/UndertaleSound.cs b/UndertaleModLib/Models/UndertaleSound.cs index acd70ebb7..d5cd2bf58 100644 --- a/UndertaleModLib/Models/UndertaleSound.cs +++ b/UndertaleModLib/Models/UndertaleSound.cs @@ -161,6 +161,39 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 4; + AudioEntryFlags flags = (AudioEntryFlags)reader.ReadUInt32(); + reader.Position += 20; + + int audioGroupID; + + if (flags.HasFlag(AudioEntryFlags.Regular) && reader.undertaleData.GeneralInfo?.BytecodeVersion >= 14) + { + audioGroupID = reader.ReadInt32(); + count++; + } + else + { + audioGroupID = reader.undertaleData.GetBuiltinSoundGroupID(); + reader.Position += 4; // "Preload" + } + + if (audioGroupID == reader.undertaleData.GetBuiltinSoundGroupID()) + { + reader.Position += 4; // "_audioFile" + count++; + } + else + reader.Position += 4; // "_audioFile.CachedId" + + return count; + } + /// public override string ToString() { @@ -184,8 +217,11 @@ public void Dispose() /// Audio group entry in a data file. ///
[PropertyChanged.AddINotifyPropertyChangedInterface] -public class UndertaleAudioGroup : UndertaleNamedResource, IDisposable +public class UndertaleAudioGroup : UndertaleNamedResource, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 4; + /// /// The name of the audio group. /// diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs index e89bbcc66..1ceddbe8f 100644 --- a/UndertaleModLib/Models/UndertaleSprite.cs +++ b/UndertaleModLib/Models/UndertaleSprite.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; @@ -53,6 +54,15 @@ public void Unserialize(UndertaleReader reader) TexBlob = reader.ReadBytes(reader.ReadInt32()); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 8; // Size + reader.Position += (uint)reader.ReadInt32(); // "TexBlob" + + return 0; + } + /// public override string ToString() { @@ -188,7 +198,7 @@ public int OriginYWrapper /// /// The collision masks of the sprite. /// - public ObservableCollection CollisionMasks { get; } = new ObservableCollection(); + public ObservableCollection CollisionMasks { get; private set; } = new ObservableCollection(); // Special sprite types (always used in GMS2) public uint SVersion { get; set; } = 1; @@ -286,8 +296,11 @@ public enum SepMaskType : uint } [PropertyChanged.AddINotifyPropertyChangedInterface] - public class TextureEntry : UndertaleObject, IDisposable + public class TextureEntry : UndertaleObject, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 4; + public UndertaleTexturePageItem Texture { get; set; } /// @@ -578,7 +591,8 @@ public void Unserialize(UndertaleReader reader) SpineCacheVersion = reader.ReadInt32(); Util.DebugUtil.Assert(SpineCacheVersion == 1, "Invalid Spine cache format version number, expected 1, got " + SpineCacheVersion); } - Util.DebugUtil.Assert(SpineVersion == 3 || SpineVersion == 2 || SpineVersion == 1, "Invalid Spine format version number, expected 3, 2 or 1, got " + SpineVersion); + Util.DebugUtil.Assert(SpineVersion <= 3 && SpineVersion >= 1, + "Invalid Spine format version number, expected 3, 2 or 1, got " + SpineVersion); int jsonLength = reader.ReadInt32(); int atlasLength = reader.ReadInt32(); int textures = reader.ReadInt32(); // count in v2(and newer) and size in bytes in v1. @@ -600,7 +614,7 @@ public void Unserialize(UndertaleReader reader) atlas.PageWidth = atlasWidth; atlas.PageHeight = atlasHeight; atlas.TexBlob = reader.ReadBytes(textures); - SpineTextures.Add(atlas); + SpineTextures.InternalAdd(atlas); break; } case 2: @@ -609,11 +623,13 @@ public void Unserialize(UndertaleReader reader) SpineJSON = Encoding.UTF8.GetString(DecodeSpineBlob(reader.ReadBytes(jsonLength))); SpineAtlas = Encoding.UTF8.GetString(DecodeSpineBlob(reader.ReadBytes(atlasLength))); + SpineTextures.SetCapacity(textures); + // the length is stored before json and atlases so we can't use ReadUndertaleObjectList // same goes for serialization. for (int t = 0; t < textures; t++) { - SpineTextures.Add(reader.ReadUndertaleObject()); + SpineTextures.InternalAdd(reader.ReadUndertaleObject()); } break; @@ -626,7 +642,7 @@ public void Unserialize(UndertaleReader reader) if (sequenceOffset != 0) { if (reader.ReadInt32() != 1) - throw new IOException("Expected 1"); + throw new UndertaleSerializationException("Sequence data unserialization error - expected 1"); V2Sequence = reader.ReadUndertaleObject(); } @@ -643,25 +659,160 @@ public void Unserialize(UndertaleReader reader) } } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Align(4); + + uint count = 0; + + reader.Position += 4; // "Name" + uint width = reader.ReadUInt32(); + uint height = reader.ReadUInt32(); + + reader.Position += 44; + + if (reader.ReadInt32() == -1) + { + int sequenceOffset = 0; + int nineSliceOffset = 0; + + uint sVersion = reader.ReadUInt32(); + SpriteType sSpriteType = (SpriteType)reader.ReadUInt32(); + if (reader.undertaleData.IsGameMaker2()) + { + reader.Position += 8; // playback speed values + + if (sVersion >= 2) + { + sequenceOffset = reader.ReadInt32(); + if (sVersion >= 3) + { + if (!reader.undertaleData.IsVersionAtLeast(2, 3, 2)) + reader.undertaleData.SetGMS2Version(2, 3, 2); + nineSliceOffset = reader.ReadInt32(); + } + } + } + + switch (sSpriteType) + { + case SpriteType.Normal: + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + SkipMaskData(reader, width, height); + break; + + case SpriteType.SWF: + int swfVersion = reader.ReadInt32(); + if (swfVersion == 8) + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + + // "YYSWF" classes are not in the pool + return count; + + case SpriteType.Spine: + { + reader.Align(4); + + int spineVersion = reader.ReadInt32(); + if (spineVersion >= 3) + reader.Position += 4; // "SpineCacheVersion" + Util.DebugUtil.Assert(spineVersion <= 3 && spineVersion >= 1, + "Invalid Spine format version number, expected 3, 2 or 1, got " + spineVersion); + + int jsonLength = reader.ReadInt32(); + int atlasLength = reader.ReadInt32(); + int textures = reader.ReadInt32(); + + switch (spineVersion) + { + case 1: + reader.Position += 8 + jsonLength + atlasLength + textures; + break; + + case 2: + case 3: + { + reader.Position += jsonLength + atlasLength; + + // TODO: make this return count instead if spine sprite + // couldn't have sequence or nine slices data. + for (int i = 0; i < textures; i++) + UndertaleSpineTextureEntry.UnserializeChildObjectCount(reader); + + count += (uint)textures; + } + break; + } + } + break; + } + + if (sequenceOffset != 0) + { + if (reader.ReadInt32() != 1) + throw new UndertaleSerializationException($"Sequence data count unserialization error - expected 1"); + count += 1 + UndertaleSequence.UnserializeChildObjectCount(reader); + } + + if (nineSliceOffset != 0) + { + reader.Position += NineSlice.ChildObjectsSize; + count++; + } + } + else + { + reader.Position -= 4; + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + SkipMaskData(reader, width, height); + } + + return count; + } + private void ReadMaskData(UndertaleReader reader) { - uint MaskCount = reader.ReadUInt32(); + uint maskCount = reader.ReadUInt32(); uint len = (Width + 7) / 8 * Height; - CollisionMasks.Clear(); + List newMasks = new((int)maskCount); uint total = 0; - for (uint i = 0; i < MaskCount; i++) + for (uint i = 0; i < maskCount; i++) { - CollisionMasks.Add(new MaskEntry(reader.ReadBytes((int)len))); + newMasks.Add(new MaskEntry(reader.ReadBytes((int)len))); total += len; } + CollisionMasks = new(newMasks); + while (total % 4 != 0) { if (reader.ReadByte() != 0) throw new IOException("Mask padding"); total++; } - Util.DebugUtil.Assert(total == CalculateMaskDataSize(Width, Height, MaskCount)); + Util.DebugUtil.Assert(total == CalculateMaskDataSize(Width, Height, maskCount)); + } + private static void SkipMaskData(UndertaleReader reader, uint width, uint height) + { + uint maskCount = reader.ReadUInt32(); + uint len = (width + 7) / 8 * height; + + uint total = 0; + for (uint i = 0; i < maskCount; i++) + { + reader.Position += len; // "new MaskEntry()" + total += len; + } + + // Skip padding + int skipSize = 0; + while (total % 4 != 0) + { + skipSize++; + total++; + } + reader.Position += skipSize; } public uint CalculateMaskDataSize(uint width, uint height, uint maskcount) @@ -681,12 +832,16 @@ public void SerializePrePadding(UndertaleWriter writer) /// public void UnserializePrePadding(UndertaleReader reader) { + // If you are modifying this, you must also modify "UnserializeChildObjectCount()" reader.Align(4); } [PropertyChanged.AddINotifyPropertyChangedInterface] - public class NineSlice : UndertaleObject + public class NineSlice : UndertaleObject, IStaticChildObjectsSize { + /// + public static readonly uint ChildObjectsSize = 40; + public int Left { get; set; } public int Top { get; set; } public int Right { get; set; } diff --git a/UndertaleModLib/Models/UndertaleTags.cs b/UndertaleModLib/Models/UndertaleTags.cs index cf5ce41ff..f80ab656b 100644 --- a/UndertaleModLib/Models/UndertaleTags.cs +++ b/UndertaleModLib/Models/UndertaleTags.cs @@ -60,6 +60,17 @@ public void Unserialize(UndertaleReader reader) AssetTags[t.ID] = t.Tags; } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + count += UndertaleSimpleListString.UnserializeChildObjectCount(reader); + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + return count; + } + /// public void Dispose() { @@ -88,6 +99,13 @@ public void Unserialize(UndertaleReader reader) Tags = reader.ReadUndertaleObject(); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + reader.Position += 4; + return 1 + UndertaleSimpleListString.UnserializeChildObjectCount(reader); + } + /// public void Dispose() { diff --git a/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs b/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs index a986357fc..47bf0bf7b 100644 --- a/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs +++ b/UndertaleModLib/Models/UndertaleTextureGroupInfo.cs @@ -171,6 +171,45 @@ public void Unserialize(UndertaleReader reader) reader.ReadUndertaleObject(Tilesets); } + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 4; // "Name" + + if (reader.undertaleData.IsVersionAtLeast(2022, 9)) + reader.Position += 12; + + uint texPagesPtr = reader.ReadUInt32(); + uint spritesPtr = reader.ReadUInt32(); + uint spineSpritesPtr = reader.ReadUInt32(); + uint fontsPtr = reader.ReadUInt32(); + uint tilesetsPtr = reader.ReadUInt32(); + + reader.AbsPosition = texPagesPtr; + count += 1 + UndertaleSimpleResourcesList + .UnserializeChildObjectCount(reader); + + reader.AbsPosition = spritesPtr; + count += 1 + UndertaleSimpleResourcesList + .UnserializeChildObjectCount(reader); + + reader.AbsPosition = spineSpritesPtr; + count += 1 + UndertaleSimpleResourcesList + .UnserializeChildObjectCount(reader); + + reader.AbsPosition = fontsPtr; + count += 1 + UndertaleSimpleResourcesList + .UnserializeChildObjectCount(reader); + + reader.AbsPosition = tilesetsPtr; + count += 1 + UndertaleSimpleResourcesList + .UnserializeChildObjectCount(reader); + + return count; + } + /// public override string ToString() { diff --git a/UndertaleModLib/Models/UndertaleTexturePageItem.cs b/UndertaleModLib/Models/UndertaleTexturePageItem.cs index 9ef917b3a..612041e1e 100644 --- a/UndertaleModLib/Models/UndertaleTexturePageItem.cs +++ b/UndertaleModLib/Models/UndertaleTexturePageItem.cs @@ -14,8 +14,11 @@ namespace UndertaleModLib.Models; /// anything outside of that is just transparent.
/// , , and are part of the texture page which /// are drawn over , , , . -public class UndertaleTexturePageItem : UndertaleNamedResource, INotifyPropertyChanged, IDisposable +public class UndertaleTexturePageItem : UndertaleNamedResource, INotifyPropertyChanged, IStaticChildObjectsSize, IDisposable { + /// + public static readonly uint ChildObjectsSize = 22; + /// /// The name of the texture page item. /// diff --git a/UndertaleModLib/Models/UndertaleTimeline.cs b/UndertaleModLib/Models/UndertaleTimeline.cs index 4f9032ac8..c649903a9 100644 --- a/UndertaleModLib/Models/UndertaleTimeline.cs +++ b/UndertaleModLib/Models/UndertaleTimeline.cs @@ -136,7 +136,7 @@ public void Unserialize(UndertaleReader reader) // Read the actions for each moment for (int i = 0; i < momentCount; i++) { - if (reader.Position != unnecessaryPointers[i]) + if (reader.AbsPosition != unnecessaryPointers[i]) throw new UndertaleSerializationException("Invalid action list pointer"); // Read action list and assign time point (put into list) @@ -144,4 +144,21 @@ public void Unserialize(UndertaleReader reader) Moments.Add(new UndertaleTimelineMoment(timePoints[i], timeEvent)); } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = 0; + + reader.Position += 4; // "Name" + + int momentCount = reader.ReadInt32(); + + reader.Position += (uint)momentCount * 8; + + for (int i = 0; i < momentCount; i++) + count += 1 + UndertalePointerList.UnserializeChildObjectCount(reader); + + return count; + } } \ No newline at end of file diff --git a/UndertaleModLib/UndertaleBaseTypes.cs b/UndertaleModLib/UndertaleBaseTypes.cs index efb4828b4..44d5604a9 100644 --- a/UndertaleModLib/UndertaleBaseTypes.cs +++ b/UndertaleModLib/UndertaleBaseTypes.cs @@ -20,16 +20,18 @@ public interface UndertaleObject ///
/// Where to deserialize from. void Unserialize(UndertaleReader reader); - } - public interface UndertaleObjectLenCheck : UndertaleObject - { - void Unserialize(UndertaleReader reader, int length); - } - - public interface UndertaleObjectEndPos : UndertaleObject - { - void Unserialize(UndertaleReader reader, uint endPosition); + /* + * As for C# 10, it's impossible to inherit static methods from an interface :( + * (so this method is for inheriting XML commentary only) + */ + /// + /// Deserializes the total child object count of this object from specified . + /// + /// Where to deserialize from. + /// The object count. + static uint UnserializeChildObjectCount(UndertaleReader reader) => 0; + } public interface UndertaleObjectWithBlobs @@ -107,4 +109,21 @@ public interface ISearchable /// is the empty string (""); otherwise, false. bool SearchMatches(string filter); } + + public interface IStaticChildObjCount + { + /// + /// The total child object count of the current object type. + /// Used for the object count unserialization. + /// + public static readonly uint ChildObjectCount = 0; + } + public interface IStaticChildObjectsSize + { + /// + /// The summary child objects size of the current object type. + /// Used for the object count unserialization. + /// + public static readonly uint ChildObjectsSize = 0; + } } diff --git a/UndertaleModLib/UndertaleChunkTypes.cs b/UndertaleModLib/UndertaleChunkTypes.cs index 0e5baad92..38216d2e9 100644 --- a/UndertaleModLib/UndertaleChunkTypes.cs +++ b/UndertaleModLib/UndertaleChunkTypes.cs @@ -17,6 +17,7 @@ public abstract class UndertaleChunk internal abstract void SerializeChunk(UndertaleWriter writer); internal abstract void UnserializeChunk(UndertaleReader reader); + internal abstract uint UnserializeObjectCount(UndertaleReader reader); public void Serialize(UndertaleWriter writer) { @@ -79,11 +80,13 @@ public static UndertaleChunk Unserialize(UndertaleReader reader) } UndertaleChunk chunk = (UndertaleChunk)Activator.CreateInstance(type); - Util.DebugUtil.Assert(chunk.Name == name); + Util.DebugUtil.Assert(chunk.Name == name, + $"Chunk name mismatch: expected \"{name}\", got \"{chunk.Name}\"."); chunk.Length = length; reader.SubmitMessage("Reading chunk " + chunk.Name); var lenReader = reader.EnsureLengthFromHere(chunk.Length); + reader.CopyChunkToBuffer(length); chunk.UnserializeChunk(reader); if (name != "FORM" && name != reader.LastChunkName) @@ -96,12 +99,12 @@ public static UndertaleChunk Unserialize(UndertaleReader reader) { int e = reader.undertaleData.PaddingAlignException; uint pad = (e == -1 ? 16 : (uint)e); - while (reader.Position % pad != 0) + while (reader.AbsPosition % pad != 0) { if (reader.ReadByte() != 0) { reader.Position -= 1; - if (reader.Position % 4 == 0) + if (reader.AbsPosition % 4 == 0) reader.undertaleData.PaddingAlignException = 4; else reader.undertaleData.PaddingAlignException = 1; @@ -111,6 +114,7 @@ public static UndertaleChunk Unserialize(UndertaleReader reader) } } + reader.SwitchReaderType(false); lenReader.ToHere(); return chunk; @@ -121,7 +125,44 @@ public static UndertaleChunk Unserialize(UndertaleReader reader) } catch (Exception e) { - throw new UndertaleSerializationException(e.Message + "\nat " + reader.Position.ToString("X8") + " while reading chunk " + name, e); + throw new UndertaleSerializationException(e.Message + "\nat " + reader.AbsPosition.ToString("X8") + " while reading chunk " + name, e); + } + } + public static uint CountChunkChildObjects(UndertaleReader reader) + { + string name = "(unknown)"; + try + { + name = reader.ReadChars(4); + uint length = reader.ReadUInt32(); + + Type type = Type.GetType(typeof(UndertaleChunk).FullName + name); + if (type == null) + throw new IOException("Unknown chunk " + name + "!!!"); + + UndertaleChunk chunk = (UndertaleChunk)Activator.CreateInstance(type); + Util.DebugUtil.Assert(chunk.Name == name, + $"Chunk name mismatch: expected \"{name}\", got \"{chunk.Name}\"."); + chunk.Length = length; + + long chunkStart = reader.Position; + + reader.SubmitMessage("Counting objects of chunk " + chunk.Name); + reader.CopyChunkToBuffer(length); + uint count = chunk.UnserializeObjectCount(reader); + + reader.SwitchReaderType(false); + reader.Position = chunkStart + chunk.Length; + + return count; + } + catch (UndertaleSerializationException e) + { + throw new UndertaleSerializationException(e.Message + " in chunk " + name, e); + } + catch (Exception e) + { + throw new UndertaleSerializationException(e.Message + "\nat " + reader.AbsPosition.ToString("X8") + " while counting objects of chunk " + name, e); } } } @@ -156,6 +197,15 @@ internal override void UnserializeChunk(UndertaleReader reader) Object = reader.ReadUndertaleObject(); } + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + uint count = 1; + + count += reader.GetChildObjectCount(); + + return count; + } + public UndertaleObject GetObject() => Object; public override string ToString() @@ -179,6 +229,11 @@ internal override void UnserializeChunk(UndertaleReader reader) List.Unserialize(reader); } + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + return UndertalePointerList.UnserializeChildObjectCount(reader); + } + public IList GetList() => List; public void GenerateIndexDict() { @@ -225,19 +280,35 @@ internal override void SerializeChunk(UndertaleWriter writer) internal override void UnserializeChunk(UndertaleReader reader) { uint count = reader.ReadUInt32(); + List.SetCapacity(count); + for (int i = 0; i < count; i++) Align &= (reader.ReadUInt32() % Alignment == 0); for (int i = 0; i < count; i++) { if (Align) { - while (reader.Position % Alignment != 0) + while (reader.AbsPosition % Alignment != 0) if (reader.ReadByte() != 0) throw new IOException("AlignUpdatedListChunk padding error"); } - List.Add(reader.ReadUndertaleObject()); + List.InternalAdd(reader.ReadUndertaleObject()); } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + uint count = reader.ReadUInt32(); + if (count == 0) + return 0; + + Type t = typeof(T); + if (t != typeof(UndertaleBackground) && t != typeof(UndertaleString)) + throw new InvalidOperationException( + "\"UndertaleAlignUpdatedListChunk\" supports the count unserialization only for backgrounds and strings."); + + return count; + } } public abstract class UndertaleSimpleListChunk : UndertaleChunk, IUndertaleSimpleListChunk where T : UndertaleObject, new() @@ -254,6 +325,11 @@ internal override void UnserializeChunk(UndertaleReader reader) List.Unserialize(reader); } + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + return reader.ReadUInt32(); + } + public IList GetList() => List; } @@ -266,5 +342,7 @@ internal override void SerializeChunk(UndertaleWriter writer) internal override void UnserializeChunk(UndertaleReader reader) { } + + internal override uint UnserializeObjectCount(UndertaleReader reader) => 0; } } diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs index d6059efeb..c92ead56e 100644 --- a/UndertaleModLib/UndertaleChunks.cs +++ b/UndertaleModLib/UndertaleChunks.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using UndertaleModLib.Models; @@ -62,9 +63,10 @@ internal override void SerializeChunk(UndertaleWriter writer) internal override void UnserializeChunk(UndertaleReader reader) { - Chunks.Clear(); + if (Chunks.Count != 1 || Chunks.Keys.First() != "GEN8") + Chunks.Clear(); ChunksTypeDict.Clear(); - uint startPos = reader.Position; + long startPos = reader.Position; // First, find the last chunk in the file because of padding changes // (also, calculate all present chunks while we're at it) @@ -87,17 +89,77 @@ internal override void UnserializeChunk(UndertaleReader reader) if (chunk != null) { if (Chunks.ContainsKey(chunk.Name)) - throw new IOException("Duplicate chunk " + chunk.Name); + { + if (Chunks.Count == 1 && chunk.Name == "GEN8") + Chunks.Clear(); + else + throw new IOException("Duplicate chunk " + chunk.Name); + } + Chunks.Add(chunk.Name, chunk); ChunksTypeDict.Add(chunk.GetType(), chunk); } } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + uint totalCount = 0; + + long startPos = reader.Position; + reader.AllChunkNames = new List(); + while (reader.Position < reader.Length) + { + string chunkName = reader.ReadChars(4); + reader.AllChunkNames.Add(chunkName); + uint length = reader.ReadUInt32(); + reader.Position += length; + } + reader.Position = startPos; + + if (reader.AllChunkNames[0] == "GEN8") + { + UndertaleChunkGEN8 gen8Chunk = new(); + gen8Chunk.UnserializeGeneralData(reader); + Chunks.Add("GEN8", gen8Chunk); + + reader.Position = startPos; + } + + while (reader.Position < startPos + Length) + totalCount += reader.CountChunkChildObjects(); + + return totalCount; + } } public class UndertaleChunkGEN8 : UndertaleSingleChunk { public override string Name => "GEN8"; + + public void UnserializeGeneralData(UndertaleReader reader) + { + Object = new UndertaleGeneralInfo(); + + reader.Position += 8; // Chunk name + length + + reader.Position++; // "IsDebuggerDisabled" + Object.BytecodeVersion = reader.ReadByte(); + reader.undertaleData.UnsupportedBytecodeVersion + = Object.BytecodeVersion < 13 || Object.BytecodeVersion > 17; + reader.Bytecode14OrLower = Object.BytecodeVersion <= 14; + + reader.Position += 42; + + Object.Major = reader.ReadUInt32(); + Object.Minor = reader.ReadUInt32(); + Object.Release = reader.ReadUInt32(); + Object.Build = reader.ReadUInt32(); + + var readVer = (Object.Major, Object.Minor, Object.Release, Object.Build); + var detectedVer = UndertaleGeneralInfo.TestForCommonGMSVersions(reader, readVer); + (Object.Major, Object.Minor, Object.Release, Object.Build) = detectedVer; + } } public class UndertaleChunkOPTN : UndertaleSingleChunk @@ -108,6 +170,11 @@ public class UndertaleChunkOPTN : UndertaleSingleChunk public class UndertaleChunkLANG : UndertaleSingleChunk { public override string Name => "LANG"; + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + return 1; + } } public class UndertaleChunkEXTN : UndertaleListChunk @@ -115,77 +182,88 @@ public class UndertaleChunkEXTN : UndertaleListChunk public override string Name => "EXTN"; public List productIdData = new List(); - internal override void UnserializeChunk(UndertaleReader reader) + private static bool checkedFor2022_6; + private void CheckFor2022_6(UndertaleReader reader) { - if (reader.undertaleData.IsVersionAtLeast(2, 3) && !reader.undertaleData.IsVersionAtLeast(2022, 6)) + if (!reader.undertaleData.IsVersionAtLeast(2, 3) || reader.undertaleData.IsVersionAtLeast(2022, 6)) + { + checkedFor2022_6 = true; + return; + } + + bool definitely2022_6 = true; + long returnPosition = reader.AbsPosition; + + int extCount = reader.ReadInt32(); + if (extCount > 0) { - // Check for 2022.6, if possible - bool definitely2022_6 = true; - uint returnPosition = reader.Position; + uint firstExtPtr = reader.ReadUInt32(); + uint firstExtEndPtr = (extCount >= 2) ? reader.ReadUInt32() /* second ptr */ : (uint)(returnPosition + this.Length); - int extCount = reader.ReadInt32(); - if (extCount > 0) + reader.AbsPosition = firstExtPtr + 12; + uint newPointer1 = reader.ReadUInt32(); + uint newPointer2 = reader.ReadUInt32(); + + if (newPointer1 != reader.AbsPosition) + definitely2022_6 = false; // first pointer mismatch + else if (newPointer2 <= reader.AbsPosition || newPointer2 >= (returnPosition + this.Length)) + definitely2022_6 = false; // second pointer out of bounds + else { - uint firstExtPtr = reader.ReadUInt32(); - uint firstExtEndPtr = (extCount >= 2) ? reader.ReadUInt32() /* second ptr */ : (returnPosition + this.Length); - - reader.Position = firstExtPtr + 12; - uint newPointer1 = reader.ReadUInt32(); - uint newPointer2 = reader.ReadUInt32(); - - if (newPointer1 != reader.Position) - definitely2022_6 = false; // first pointer mismatch - else if (newPointer2 <= reader.Position || newPointer2 >= (returnPosition + this.Length)) - definitely2022_6 = false; // second pointer out of bounds - else + // Check ending position + reader.AbsPosition = newPointer2; + uint optionCount = reader.ReadUInt32(); + if (optionCount > 0) { - // Check ending position - reader.Position = newPointer2; - uint optionCount = reader.ReadUInt32(); - if (optionCount > 0) + long newOffsetCheck = reader.AbsPosition + (4 * (optionCount - 1)); + if (newOffsetCheck >= (returnPosition + this.Length)) { - long newOffsetCheck = reader.Position + (4 * (optionCount - 1)); + // Option count would place us out of bounds + definitely2022_6 = false; + } + else + { + reader.Position += (4 * (optionCount - 1)); + newOffsetCheck = reader.ReadUInt32() + 12; // jump past last option if (newOffsetCheck >= (returnPosition + this.Length)) { - // Option count would place us out of bounds + // Pointer list element would place us out of bounds definitely2022_6 = false; } else { - reader.Position += (4 * (optionCount - 1)); - newOffsetCheck = reader.ReadUInt32() + 12; // jump past last option - if (newOffsetCheck >= (returnPosition + this.Length)) - { - // Pointer list element would place us out of bounds - definitely2022_6 = false; - } - else - { - reader.Position = (uint)newOffsetCheck; - } + reader.AbsPosition = (uint)newOffsetCheck; } } - if (definitely2022_6) + } + if (definitely2022_6) + { + if (extCount == 1) { - if (extCount == 1) - { - reader.Position += 16; // skip GUID data (only one of them) - if (reader.Position % 16 != 0) - reader.Position += 16 - (reader.Position % 16); // align to chunk end - } - if (reader.Position != firstExtEndPtr) - definitely2022_6 = false; + reader.Position += 16; // skip GUID data (only one of them) + if (reader.AbsPosition % 16 != 0) + reader.Position += 16 - (reader.AbsPosition % 16); // align to chunk end } + if (reader.AbsPosition != firstExtEndPtr) + definitely2022_6 = false; } } - else - definitely2022_6 = false; + } + else + definitely2022_6 = false; - reader.Position = returnPosition; + reader.AbsPosition = returnPosition; - if (definitely2022_6) - reader.undertaleData.SetGMS2Version(2022, 6); - } + if (definitely2022_6) + reader.undertaleData.SetGMS2Version(2022, 6); + + checkedFor2022_6 = true; + } + + internal override void UnserializeChunk(UndertaleReader reader) + { + if (!checkedFor2022_6) + CheckFor2022_6(reader); base.UnserializeChunk(reader); @@ -218,6 +296,15 @@ internal override void SerializeChunk(UndertaleWriter writer) writer.Write(data); } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_6 = false; + + CheckFor2022_6(reader); + + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkSOND : UndertaleListChunk @@ -285,9 +372,9 @@ internal override void UnserializeChunk(UndertaleReader reader) { reader.Position -= 4; int chunkLength = reader.ReadInt32(); - uint chunkEnd = reader.Position + (uint)chunkLength; + long chunkEnd = reader.AbsPosition + chunkLength; - uint beginPosition = reader.Position; + long beginPosition = reader.Position; // Figure out where the starts/ends of each shader object are int count = reader.ReadInt32(); @@ -296,7 +383,7 @@ internal override void UnserializeChunk(UndertaleReader reader) { objectLocations[i] = (uint)reader.ReadInt32(); } - objectLocations[count] = chunkEnd; + objectLocations[count] = (uint)chunkEnd; Dictionary objPool = reader.GetOffsetMap(); Dictionary objPoolRev = reader.GetOffsetMapRev(); @@ -320,6 +407,65 @@ public class UndertaleChunkFONT : UndertaleListChunk public override string Name => "FONT"; public byte[] Padding; + private static bool checkedFor2022_2; + private void CheckForGM2022_2(UndertaleReader reader) + { + /* This code performs four checks to identify GM2022.2. + * First, as you've seen, is the bytecode version. + * Second, we assume it is. If there are no Glyphs, we are vindicated by the impossibility of null values there. + * Third, we check that the Glyph Length is less than the chunk length. If it's going outside the chunk, that means + * that the length was misinterpreted. + * Fourth, in case of a terrible fluke causing this to appear valid erroneously, we verify that each pointer leads into the next. + * And if someone builds their game so the first pointer is absolutely valid length data and the next font is valid glyph data- + * screw it, call Jacky720 when someone constructs that and you want to mod it. + * Maybe try..catch on the whole shebang? + */ + if (reader.undertaleData.GeneralInfo?.BytecodeVersion < 17 || reader.undertaleData.IsVersionAtLeast(2022, 2)) + { + checkedFor2022_2 = true; + return; + } + + long positionToReturn = reader.Position; + bool GMS2022_2 = false; + + if (reader.ReadUInt32() > 0) // Font count + { + uint firstFontPointer = reader.ReadUInt32(); + reader.AbsPosition = firstFontPointer + 48; // There are 48 bytes of existing metadata. + uint glyphsLength = reader.ReadUInt32(); + GMS2022_2 = true; + if ((glyphsLength * 4) > this.Length) + { + GMS2022_2 = false; + } + else if (glyphsLength != 0) + { + List glyphPointers = new List((int)glyphsLength); + for (uint i = 0; i < glyphsLength; i++) + glyphPointers.Add(reader.ReadUInt32()); + foreach (uint pointer in glyphPointers) + { + if (reader.AbsPosition != pointer) + { + GMS2022_2 = false; + break; + } + + reader.Position += 14; + ushort kerningLength = reader.ReadUInt16(); + reader.Position += (uint)4 * kerningLength; // combining read/write would apparently break + } + } + + } + if (GMS2022_2) + reader.undertaleData.SetGMS2Version(2022, 2); + reader.Position = positionToReturn; + + checkedFor2022_2 = true; + } + internal override void SerializeChunk(UndertaleWriter writer) { base.SerializeChunk(writer); @@ -336,59 +482,22 @@ internal override void SerializeChunk(UndertaleWriter writer) internal override void UnserializeChunk(UndertaleReader reader) { - if (!reader.undertaleData.IsVersionAtLeast(2022, 2) && reader.undertaleData.GeneralInfo?.BytecodeVersion >= 17) - { - /* This code performs four checks to identify GM2022.2. - * First, as you've seen, is the bytecode version. - * Second, we assume it is. If there are no Glyphs, we are vindicated by the impossibility of null values there. - * Third, we check that the Glyph Length is less than the chunk length. If it's going outside the chunk, that means - * that the length was misinterpreted. - * Fourth, in case of a terrible fluke causing this to appear valid erroneously, we verify that each pointer leads into the next. - * And if someone builds their game so the first pointer is absolutely valid length data and the next font is valid glyph data- - * screw it, call Jacky720 when someone constructs that and you want to mod it. - * Maybe try..catch on the whole shebang? - */ - uint positionToReturn = reader.Position; - bool GMS2022_2 = false; - if (reader.ReadUInt32() > 0) // Font count - { - uint firstFontPointer = reader.ReadUInt32(); - reader.Position = firstFontPointer + 48; // There are 48 bytes of existing metadata. - uint glyphsLength = reader.ReadUInt32(); - GMS2022_2 = true; - if ((glyphsLength * 4) > this.Length) - { - GMS2022_2 = false; - } - else if (glyphsLength != 0) - { - List glyphPointers = new List(); - for (uint i = 0; i < glyphsLength; i++) - glyphPointers.Add(reader.ReadUInt32()); - foreach (uint pointer in glyphPointers) - { - if (reader.Position != pointer) - { - GMS2022_2 = false; - break; - } - - reader.Position += 14; - ushort kerningLength = reader.ReadUInt16(); - reader.Position += (uint) 4 * kerningLength; // combining read/write would apparently break - } - } - - } - if (GMS2022_2) - reader.undertaleData.SetGMS2Version(2022, 2); - reader.Position = positionToReturn; - } + if (!checkedFor2022_2) + CheckForGM2022_2(reader); base.UnserializeChunk(reader); Padding = reader.ReadBytes(512); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_2 = false; + + CheckForGM2022_2(reader); + + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkTMLN : UndertaleListChunk @@ -400,47 +509,71 @@ public class UndertaleChunkOBJT : UndertaleListChunk { public override string Name => "OBJT"; - internal override void SerializeChunk(UndertaleWriter writer) - { - base.SerializeChunk(writer); - } + private static bool checkedFor2022_5; - internal override void UnserializeChunk(UndertaleReader reader) + // Simple chunk parser to check for 2022.5, assumes old format until shown otherwise + private void CheckFor2022_5(UndertaleReader reader) { - // Simple chunk parser to check for 2022.5, assumes old format until shown otherwise - if (!reader.undertaleData.IsVersionAtLeast(2022, 5) && reader.undertaleData.IsVersionAtLeast(2, 3)) + if (!reader.undertaleData.IsVersionAtLeast(2, 3) || reader.undertaleData.IsVersionAtLeast(2022, 5)) + { + checkedFor2022_5 = true; + return; + } + + long positionToReturn = reader.Position; + bool GM2022_5 = false; + + if (reader.ReadUInt32() > 0) // Object count { - uint positionToReturn = reader.Position; - bool GM2022_5 = false; - if (reader.ReadUInt32() > 0) // Object count + uint firstObjectPointer = reader.ReadUInt32(); + reader.AbsPosition = firstObjectPointer + 64; + uint vertexCount = reader.ReadUInt32(); + + // If any of these checks fail, it's 2022.5 + GM2022_5 = true; + // Bounds check on vertex data + if (reader.Position + 12 + vertexCount * 8 < positionToReturn + this.Length) { - uint firstObjectPointer = reader.ReadUInt32(); - reader.Position = firstObjectPointer + 64; - uint vertexCount = reader.ReadUInt32(); - - // If any of these checks fail, it's 2022.5 - GM2022_5 = true; - // Bounds check on vertex data - if (reader.Position + 12 + vertexCount * 8 < positionToReturn + this.Length) + reader.Position += 12 + vertexCount * 8; + // A pointer list of events + if (reader.ReadUInt32() == UndertaleGameObject.EventTypeCount) { - reader.Position += (uint)(12 + vertexCount * 8); - // 15 events as a pointer list - if (reader.ReadUInt32() == 15) - { - uint subEventPointer = reader.ReadUInt32(); - // Should start right after the list - if (reader.Position + 56 == subEventPointer) - GM2022_5 = false; - } + uint subEventPointer = reader.ReadUInt32(); + // Should start right after the list + if (reader.AbsPosition + 56 == subEventPointer) + GM2022_5 = false; } } - if (GM2022_5) - reader.undertaleData.SetGMS2Version(2022, 5); - reader.Position = positionToReturn; } + if (GM2022_5) + reader.undertaleData.SetGMS2Version(2022, 5); + + reader.Position = positionToReturn; + + checkedFor2022_5 = true; + } + + internal override void SerializeChunk(UndertaleWriter writer) + { + base.SerializeChunk(writer); + } + + internal override void UnserializeChunk(UndertaleReader reader) + { + if (!checkedFor2022_5) + CheckFor2022_5(reader); base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_5 = false; + + CheckFor2022_5(reader); + + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkROOM : UndertaleListChunk @@ -449,17 +582,41 @@ public class UndertaleChunkROOM : UndertaleListChunk internal override void UnserializeChunk(UndertaleReader reader) { - CheckForEffectData(reader); + if (!checkedFor2022_1) + CheckForEffectData(reader); base.UnserializeChunk(reader); } + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_1 = false; + UndertaleRoom.CheckedForGMS2_2_2_302 = false; + + CheckForEffectData(reader); + + if (reader.undertaleData.GeneralInfo?.BytecodeVersion >= 16) + { + // "GameObject._preCreateCode" + + Type gameObjType = typeof(GameObject); + + uint newValue = GameObject.ChildObjectCount + 1; + reader.SetStaticChildCount(gameObjType, newValue); + newValue = GameObject.ChildObjectsSize + 4; + reader.SetStaticChildObjectsSize(gameObjType, newValue); + } + + return base.UnserializeObjectCount(reader); + } + + private static bool checkedFor2022_1; private void CheckForEffectData(UndertaleReader reader) { // Do a length check on room layers to see if this is 2022.1 or higher - if (!reader.undertaleData.IsVersionAtLeast(2022, 1) && reader.undertaleData.IsVersionAtLeast(2, 3)) + if (reader.undertaleData.IsVersionAtLeast(2, 3) && !reader.undertaleData.IsVersionAtLeast(2022, 1)) { - uint returnTo = reader.Position; + long returnTo = reader.Position; // Iterate over all rooms until a length check is performed int roomCount = reader.ReadInt32(); @@ -469,17 +626,17 @@ private void CheckForEffectData(UndertaleReader reader) // Advance to room data we're interested in (and grab pointer for next room) reader.Position = returnTo + 4 + (4 * roomIndex); uint roomPtr = (uint)reader.ReadInt32(); - reader.Position = roomPtr + (22 * 4); + reader.AbsPosition = roomPtr + (22 * 4); // Get the pointer for this room's layer list, as well as pointer to sequence list uint layerListPtr = (uint)reader.ReadInt32(); int seqnPtr = reader.ReadInt32(); - reader.Position = layerListPtr; + reader.AbsPosition = layerListPtr; int layerCount = reader.ReadInt32(); if (layerCount >= 1) { // Get pointer into the individual layer data (plus 8 bytes) for the first layer in the room - uint jumpOffset = (uint)(reader.ReadInt32() + 8); + int jumpOffset = reader.ReadInt32() + 8; // Find the offset for the end of this layer int nextOffset; @@ -489,25 +646,25 @@ private void CheckForEffectData(UndertaleReader reader) nextOffset = reader.ReadInt32(); // (pointer to next element in the layer list) // Actually perform the length checks, depending on layer data - reader.Position = jumpOffset; + reader.AbsPosition = jumpOffset; switch ((LayerType)reader.ReadInt32()) { case LayerType.Background: - if (nextOffset - reader.Position > 16 * 4) + if (nextOffset - reader.AbsPosition > 16 * 4) reader.undertaleData.SetGMS2Version(2022, 1); finished = true; break; case LayerType.Instances: reader.Position += 6 * 4; int instanceCount = reader.ReadInt32(); - if (nextOffset - reader.Position != (instanceCount * 4)) + if (nextOffset - reader.AbsPosition != (instanceCount * 4)) reader.undertaleData.SetGMS2Version(2022, 1); finished = true; break; case LayerType.Assets: reader.Position += 6 * 4; int tileOffset = reader.ReadInt32(); - if (tileOffset != reader.Position + 8) + if (tileOffset != reader.AbsPosition + 8) reader.undertaleData.SetGMS2Version(2022, 1); finished = true; break; @@ -515,14 +672,14 @@ private void CheckForEffectData(UndertaleReader reader) reader.Position += 7 * 4; int tileMapWidth = reader.ReadInt32(); int tileMapHeight = reader.ReadInt32(); - if (nextOffset - reader.Position != (tileMapWidth * tileMapHeight * 4)) + if (nextOffset - reader.AbsPosition != (tileMapWidth * tileMapHeight * 4)) reader.undertaleData.SetGMS2Version(2022, 1); finished = true; break; case LayerType.Effect: reader.Position += 7 * 4; int propertyCount = reader.ReadInt32(); - if (nextOffset - reader.Position != (propertyCount * 3 * 4)) + if (nextOffset - reader.AbsPosition != (propertyCount * 3 * 4)) reader.undertaleData.SetGMS2Version(2022, 1); finished = true; break; @@ -532,6 +689,8 @@ private void CheckForEffectData(UndertaleReader reader) reader.Position = returnTo; } + + checkedFor2022_1 = true; } } @@ -590,7 +749,32 @@ internal override void UnserializeChunk(UndertaleReader reader) List = null; return; } + + UndertaleCode.CurrCodeIndex = 0; base.UnserializeChunk(reader); + + reader.InstructionArraysLengths = null; + } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (Length == 0) + return 0; + + if (reader.undertaleData.UnsupportedBytecodeVersion) + return reader.ReadUInt32(); + + int codeCount = (int)reader.ReadUInt32(); + reader.Position -= 4; + + reader.GMS2BytecodeAddresses = new(codeCount); + reader.InstructionArraysLengths = new int[codeCount]; + UndertaleCode.CurrCodeIndex = 0; + + uint count = base.UnserializeObjectCount(reader); + reader.GMS2BytecodeAddresses.Clear(); + + return count; } } @@ -641,7 +825,7 @@ internal override void UnserializeChunk(UndertaleReader reader) if (reader.undertaleData.UnsupportedBytecodeVersion) return; - uint startPosition = reader.Position; + long startPosition = reader.Position; uint varLength; if (!reader.Bytecode14OrLower) { @@ -654,9 +838,27 @@ internal override void UnserializeChunk(UndertaleReader reader) else varLength = 12; List.Clear(); + List.Capacity = (int)(Length / varLength); while (reader.Position + varLength <= startPosition + Length) List.Add(reader.ReadUndertaleObject()); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (Length == 0) + return 0; + + if (reader.undertaleData.UnsupportedBytecodeVersion) + return 0; + + if (!reader.Bytecode14OrLower) + { + reader.Position += 12; + return (Length - 12) / 20; + } + else + return Length / 12; + } } public class UndertaleChunkFUNC : UndertaleChunk @@ -698,8 +900,9 @@ internal override void UnserializeChunk(UndertaleReader reader) return; if (reader.Bytecode14OrLower) { - uint startPosition = reader.Position; + long startPosition = reader.Position; Functions.Clear(); + Functions.SetCapacity(Length / 12); while (reader.Position + 12 <= startPosition + Length) Functions.Add(reader.ReadUndertaleObject()); } @@ -709,6 +912,24 @@ internal override void UnserializeChunk(UndertaleReader reader) CodeLocals = reader.ReadUndertaleObject>(); } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (Length == 0 && reader.undertaleData.GeneralInfo?.BytecodeVersion > 14) + return 0; + + uint count = 0; + + if (!reader.Bytecode14OrLower) + { + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); + } + else + count = Length / 12; + + return count; + } } public class UndertaleChunkSTRG : UndertaleAlignUpdatedListChunk @@ -729,61 +950,25 @@ internal override void UnserializeChunk(UndertaleReader reader) base.UnserializeChunk(reader); // padding - while (reader.Position % 0x80 != 0) + while (reader.AbsPosition % 0x80 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error in STRG"); } + + // There's no need to check padding in "UnserializeObjectCount()" } public class UndertaleChunkTXTR : UndertaleListChunk { public override string Name => "TXTR"; - internal override void SerializeChunk(UndertaleWriter writer) - { - base.SerializeChunk(writer); - - // texture blobs - if (List.Count > 0) - { - // Compressed size can't be bigger than maximum decompressed size - int maxSize = List.Select(x => x.TextureData.TextureBlob?.Length ?? 0).Max(); - UndertaleEmbeddedTexture.TexData.InitSharedStream(maxSize); - - bool anythingUsesQoi = false; - foreach (var tex in List) - { - if (tex.TextureExternal && !tex.TextureLoaded) - continue; // don't accidentally load everything... - if (tex.TextureData.FormatQOI) - { - anythingUsesQoi = true; - break; - } - } - if (anythingUsesQoi) - { - // Calculate maximum size of QOI converter buffer - maxSize = List.Select(x => x.TextureData.Width * x.TextureData.Height).Max() - * QoiConverter.MaxChunkSize + QoiConverter.HeaderSize + (writer.undertaleData.IsVersionAtLeast(2022, 3) ? 0 : 4); - QoiConverter.InitSharedBuffer(maxSize); - } - } - foreach (UndertaleEmbeddedTexture obj in List) - obj.SerializeBlob(writer); - - // padding - // TODO: Maybe the padding is more global and every chunk is padded to 4 byte boundaries? - while (writer.Position % 4 != 0) - writer.Write((byte)0); - } - - internal override void UnserializeChunk(UndertaleReader reader) + private static bool checkedFor2022_3; + private void CheckFor2022_3And5(UndertaleReader reader) { // Detect GM2022.3 - if (!reader.undertaleData.IsVersionAtLeast(2022, 3) && reader.undertaleData.IsVersionAtLeast(2, 3)) + if (reader.undertaleData.IsVersionAtLeast(2, 3) && !reader.undertaleData.IsVersionAtLeast(2022, 3)) { - uint positionToReturn = reader.Position; + long positionToReturn = reader.Position; // Check for 2022.3 format uint texCount = reader.ReadUInt32(); @@ -809,8 +994,8 @@ internal override void UnserializeChunk(UndertaleReader reader) { // Go to each texture, and then to each texture's data reader.Position = positionToReturn + 4 + (i * 4); - reader.Position = reader.ReadUInt32() + 12; // go to texture, at an offset - reader.Position = reader.ReadUInt32(); // go to texture data + reader.AbsPosition = reader.ReadUInt32() + 12; // go to texture, at an offset + reader.AbsPosition = reader.ReadUInt32(); // go to texture data byte[] header = reader.ReadBytes(4); if (header.SequenceEqual(UndertaleEmbeddedTexture.TexData.QOIAndBZip2Header)) { @@ -825,7 +1010,7 @@ internal override void UnserializeChunk(UndertaleReader reader) is2022_5 = true; else { - reader.ReadByte(); + reader.Position++; if (reader.ReadUInt24() != 0x594131) // digits of pi... (block header) is2022_5 = true; else if (reader.ReadUInt24() != 0x595326) @@ -844,20 +1029,92 @@ internal override void UnserializeChunk(UndertaleReader reader) reader.Position = positionToReturn; } - base.UnserializeChunk(reader); + checkedFor2022_3 = true; + } + + internal override void SerializeChunk(UndertaleWriter writer) + { + base.SerializeChunk(writer); // texture blobs + if (List.Count > 0) + { + // Compressed size can't be bigger than maximum decompressed size + int maxSize = List.Select(x => x.TextureData.TextureBlob?.Length ?? 0).Max(); + UndertaleEmbeddedTexture.TexData.InitSharedStream(maxSize); + + bool anythingUsesQoi = false; + foreach (var tex in List) + { + if (tex.TextureExternal && !tex.TextureLoaded) + continue; // don't accidentally load everything... + if (tex.TextureData.FormatQOI) + { + anythingUsesQoi = true; + break; + } + } + if (anythingUsesQoi) + { + // Calculate maximum size of QOI converter buffer + maxSize = List.Select(x => x.TextureData.Width * x.TextureData.Height).Max() + * QoiConverter.MaxChunkSize + QoiConverter.HeaderSize + (writer.undertaleData.IsVersionAtLeast(2022, 3) ? 0 : 4); + QoiConverter.InitSharedBuffer(maxSize); + } + } foreach (UndertaleEmbeddedTexture obj in List) + obj.SerializeBlob(writer); + + // padding + // TODO: Maybe the padding is more global and every chunk is padded to 4 byte boundaries? + while (writer.Position % 4 != 0) + writer.Write((byte)0); + } + + internal override void UnserializeChunk(UndertaleReader reader) + { + if (!checkedFor2022_3) + CheckFor2022_3And5(reader); + + base.UnserializeChunk(reader); + reader.SwitchReaderType(false); + + // texture blobs + for (int index = 0; index < List.Count; index++) { + UndertaleEmbeddedTexture obj = List[index]; + obj.UnserializeBlob(reader); - obj.Name = new UndertaleString("Texture " + List.IndexOf(obj).ToString()); + obj.Name = new UndertaleString("Texture " + index.ToString()); } // padding + // (not "AbsPosition" because of "reader.SwitchReaderType(false)") while (reader.Position % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_3 = false; + + CheckFor2022_3And5(reader); + + uint txtrSize = UndertaleEmbeddedTexture.ChildObjectsSize; + if (reader.undertaleData.IsVersionAtLeast(2, 3)) + txtrSize += 4; // "GeneratedMips" + if (reader.undertaleData.IsVersionAtLeast(2022, 3)) + txtrSize += 4; // "TextureBlockSize" + if (reader.undertaleData.IsVersionAtLeast(2022, 9)) + txtrSize += 12; + + if (txtrSize != UndertaleEmbeddedTexture.ChildObjectsSize) + reader.SetStaticChildObjectsSize(typeof(UndertaleEmbeddedTexture), txtrSize); + + // Texture blobs are already included in the count + return base.UnserializeObjectCount(reader); + } } public class UndertaleChunkAUDO : UndertaleListChunk @@ -878,6 +1135,13 @@ internal override void UnserializeChunk(UndertaleReader reader) List[index].Name = new UndertaleString("EmbeddedSound " + index.ToString()); } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + // Though "UndertaleEmbeddedAudio" has dynamic child objects size, + // there's still no need to unserialize the count for each object. + return reader.ReadUInt32(); + } } // GMS2 only @@ -901,6 +1165,17 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new Exception("Expected EMBI version 1"); base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + if (reader.ReadUInt32() != 1) + throw new Exception("Expected EMBI version 1"); + + return base.UnserializeObjectCount(reader); + } } // GMS2.2.1+ only @@ -908,11 +1183,44 @@ public class UndertaleChunkTGIN : UndertaleListChunk { public override string Name => "TGIN"; + private static bool checkedFor2022_9; + private void CheckFor2022_9(UndertaleReader reader) + { + if (!reader.undertaleData.IsVersionAtLeast(2, 3) || reader.undertaleData.IsVersionAtLeast(2022, 9)) + { + checkedFor2022_9 = true; + return; + } + + // Check for 2022.9 + long returnPosition = reader.AbsPosition; + + uint tginCount = reader.ReadUInt32(); + if (tginCount > 0) + { + uint tginPtr = reader.ReadUInt32(); + uint secondTginPtr = (tginCount >= 2) ? reader.ReadUInt32() : (uint)(returnPosition + this.Length); + reader.AbsPosition = tginPtr + 4; + + // Check to see if the pointer located at this address points within this object + // If not, then we know we're using a new format! + uint ptr = reader.ReadUInt32(); + if (ptr < tginPtr || ptr >= secondTginPtr) + reader.undertaleData.SetGMS2Version(2022, 9); + } + + reader.AbsPosition = returnPosition; + + checkedFor2022_9 = true; + } + internal override void SerializeChunk(UndertaleWriter writer) { if (!writer.undertaleData.IsGameMaker2()) throw new InvalidOperationException(); + writer.Write((uint)1); // Version + base.SerializeChunk(writer); } @@ -920,31 +1228,30 @@ internal override void UnserializeChunk(UndertaleReader reader) { if (!reader.undertaleData.IsGameMaker2()) throw new InvalidOperationException(); + if (reader.ReadUInt32() != 1) throw new IOException("Expected TGIN version 1"); - if (reader.undertaleData.IsVersionAtLeast(2, 3) && !reader.undertaleData.IsVersionAtLeast(2022, 9)) - { - // Check for 2022.9 - uint returnPosition = reader.Position; - uint tginCount = reader.ReadUInt32(); - if (tginCount > 0) - { - uint tginPtr = reader.ReadUInt32(); - uint secondTginPtr = (tginCount >= 2) ? reader.ReadUInt32() : (returnPosition + this.Length); - reader.Position = tginPtr + 4; - - // Check to see if the pointer located at this address points within this object - // If not, then we know we're using a new format! - uint ptr = reader.ReadUInt32(); - if (ptr < tginPtr || ptr >= secondTginPtr) - reader.undertaleData.SetGMS2Version(2022, 9); - } + if (!checkedFor2022_9) + CheckFor2022_9(reader); - reader.Position = returnPosition; - } base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedFor2022_9 = false; + + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + if (reader.ReadUInt32() != 1) + throw new IOException("Expected TGIN version 1"); + + CheckFor2022_9(reader); + + return base.UnserializeObjectCount(reader); + } } // GMS2.3+ only @@ -952,6 +1259,45 @@ public class UndertaleChunkACRV : UndertaleListChunk { public override string Name => "ACRV"; + private static bool checkedForGMS2_3_1; + private void CheckForGMS2_3_1(UndertaleReader reader) + { + if (reader.undertaleData.IsVersionAtLeast(2, 3, 1)) + { + checkedForGMS2_3_1 = true; + return; + } + + long returnTo = reader.Position; + + uint count = reader.ReadUInt32(); + if (count == 0) + { + reader.Position = returnTo; + checkedForGMS2_3_1 = true; + return; + } + + reader.AbsPosition = reader.ReadUInt32(); // go to the first "Point" + reader.Position += 8; + + if (reader.ReadUInt32() != 0) // in 2.3 a int with the value of 0 would be set here, + { // it cannot be version 2.3 if this value isn't 0 + reader.undertaleData.SetGMS2Version(2, 3, 1); + reader.Position -= 4; + } + else + { + if (reader.ReadUInt32() == 0) // At all points (besides the first one) + reader.undertaleData.SetGMS2Version(2, 3, 1); // if BezierX0 equals to 0 (the above check) + reader.Position -= 8; // then BezierY0 equals to 0 as well (the current check) + } + + reader.Position = returnTo; + + checkedForGMS2_3_1 = true; + } + internal override void SerializeChunk(UndertaleWriter writer) { if (!writer.undertaleData.IsGameMaker2()) @@ -971,15 +1317,35 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new InvalidOperationException(); // Padding - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); if (reader.ReadUInt32() != 1) throw new IOException("Expected ACRV version 1"); + if (!checkedForGMS2_3_1) + CheckForGMS2_3_1(reader); + base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + checkedForGMS2_3_1 = false; + + // Padding + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); + + if (reader.ReadUInt32() != 1) + throw new IOException("Expected ACRV version 1"); + + CheckForGMS2_3_1(reader); + + return base.UnserializeObjectCount(reader); + } } // GMS2.3+ only @@ -1010,7 +1376,7 @@ internal override void UnserializeChunk(UndertaleReader reader) return; // Padding - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); @@ -1020,6 +1386,27 @@ internal override void UnserializeChunk(UndertaleReader reader) base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + // Apparently SEQN can be empty + if (Length == 0) + return 0; + + // Padding + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); + + uint version = reader.ReadUInt32(); + if (version != 1) + throw new IOException("Expected SEQN version 1, got " + version.ToString()); + + return base.UnserializeObjectCount(reader); + } } // GMS2.3+ only @@ -1046,7 +1433,7 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new InvalidOperationException(); // Padding - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); @@ -1055,6 +1442,22 @@ internal override void UnserializeChunk(UndertaleReader reader) base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + // Padding + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); + + if (reader.ReadUInt32() != 1) + throw new IOException("Expected TAGS version 1"); + + return base.UnserializeObjectCount(reader); + } } // GMS2.3.6+ only @@ -1081,7 +1484,7 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new InvalidOperationException(); // Padding - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); @@ -1090,6 +1493,23 @@ internal override void UnserializeChunk(UndertaleReader reader) base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + // Padding + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); + + uint version = reader.ReadUInt32(); + if (version != 1) + throw new IOException("Expected FEDS version 1, got " + version.ToString()); + + return base.UnserializeObjectCount(reader); + } } // GMS2022.8+ only @@ -1114,11 +1534,24 @@ internal override void UnserializeChunk(UndertaleReader reader) throw new InvalidOperationException(); // Padding - while (reader.Position % 4 != 0) + while (reader.AbsPosition % 4 != 0) if (reader.ReadByte() != 0) throw new IOException("Padding error!"); base.UnserializeChunk(reader); } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + if (!reader.undertaleData.IsGameMaker2()) + throw new InvalidOperationException(); + + // Padding + while (reader.AbsPosition % 4 != 0) + if (reader.ReadByte() != 0) + throw new IOException("Padding error!"); + + return base.UnserializeObjectCount(reader); + } } } diff --git a/UndertaleModLib/UndertaleData.cs b/UndertaleModLib/UndertaleData.cs index 73253bf9c..1289bf296 100644 --- a/UndertaleModLib/UndertaleData.cs +++ b/UndertaleModLib/UndertaleData.cs @@ -460,6 +460,12 @@ public void SetGMS2Version(uint major, uint minor = 0, uint release = 0, uint bu /// Whether the version of the data file is the same or higher than a specified version. public bool IsVersionAtLeast(uint major, uint minor = 0, uint release = 0, uint build = 0) { + if (GeneralInfo is null) + { + Debug.WriteLine("\"UndertaleData.IsVersionAtLeast()\" error - \"GeneralInfo\" is null."); + return false; + } + if (GeneralInfo.Major != major) return (GeneralInfo.Major > major); diff --git a/UndertaleModLib/UndertaleDebugChunks.cs b/UndertaleModLib/UndertaleDebugChunks.cs index 42aca4fd9..96b003b40 100644 --- a/UndertaleModLib/UndertaleDebugChunks.cs +++ b/UndertaleModLib/UndertaleDebugChunks.cs @@ -32,7 +32,7 @@ internal override void SerializeChunk(UndertaleWriter writer) internal override void UnserializeChunk(UndertaleReader reader) { Chunks.Clear(); - uint startPos = reader.Position; + long startPos = reader.Position; while (reader.Position < startPos + Length) { UndertaleChunk chunk = reader.ReadUndertaleChunk(); @@ -44,6 +44,11 @@ internal override void UnserializeChunk(UndertaleReader reader) } } } + + internal override uint UnserializeObjectCount(UndertaleReader reader) + { + throw new NotImplementedException(); + } } public class UndertaleDebugChunkSCPT : UndertaleListChunk diff --git a/UndertaleModLib/UndertaleIO.cs b/UndertaleModLib/UndertaleIO.cs index 2f44a0ecb..f195191ac 100644 --- a/UndertaleModLib/UndertaleIO.cs +++ b/UndertaleModLib/UndertaleIO.cs @@ -1,9 +1,11 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Ports; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using UndertaleModLib.Compiler; @@ -24,8 +26,11 @@ public interface UndertaleResourceRef : UndertaleObject int SerializeById(UndertaleWriter writer); } - public class UndertaleResourceById : UndertaleResourceRef, IDisposable where T : UndertaleResource, new() where ChunkT : UndertaleListChunk + public class UndertaleResourceById : UndertaleResourceRef, IStaticChildObjectsSize, IDisposable where T : UndertaleResource, new() where ChunkT : UndertaleListChunk { + /// + public static readonly uint ChildObjectsSize = 4; + public int CachedId { get; set; } = -1; public T Resource { get; set; } @@ -140,7 +145,7 @@ public void Unserialize(UndertaleReader reader) } } - public class UndertaleReader : Util.FileBinaryReader + public class UndertaleReader : AdaptiveBinaryReader { /// /// function to delegate warning messages to @@ -177,6 +182,8 @@ public UndertaleReader(Stream input, FilePath = fs.Name; Directory = Path.GetDirectoryName(FilePath); } + + FillUnserializeCountDictionaries(); } // TODO: This would be more useful if it reported location like the exceptions did @@ -205,6 +212,10 @@ public UndertaleChunk ReadUndertaleChunk() { return UndertaleChunk.Unserialize(this); } + public uint CountChunkChildObjects() + { + return UndertaleChunk.CountChunkChildObjects(this); + } private List resUpdate = new List(); internal UndertaleData undertaleData; @@ -224,6 +235,28 @@ public UndertaleData ReadUndertaleData() DebugUtil.Assert(data.FORM.Name == name); data.FORM.Length = length; + long startPos = Position; + uint poolSize = 0; + if (!ProcessCountExc()) // process an exception from "FillUnserializeCountDictionaries()" + { + try + { + poolSize = data.FORM.UnserializeObjectCount(this); + } + catch (Exception e) + { + countUnserializeExc = e; + Debug.WriteLine(e); + + SwitchReaderType(false); + } + } + utListPtrsPool = null; + + InitializePools(poolSize); + + Position = startPos; + var lenReader = EnsureLengthFromHere(data.FORM.Length); data.FORM.UnserializeChunk(this); lenReader.ToHere(); @@ -233,9 +266,15 @@ public UndertaleData ReadUndertaleData() res.PostUnserialize(this); resUpdate.Clear(); - data.BuiltinList = new BuiltinList(data); - Decompiler.AssetTypeResolver.InitializeTypes(data); - UndertaleEmbeddedTexture.FindAllTextureInfo(data); + // Skip if it's "audiogroup*.dat" file + if (!FilePath.EndsWith(".dat")) + { + data.BuiltinList = new BuiltinList(data); + Decompiler.AssetTypeResolver.InitializeTypes(data); + UndertaleEmbeddedTexture.FindAllTextureInfo(data); + } + + ProcessCountExc(poolSize); return data; } @@ -255,10 +294,199 @@ public override bool ReadBoolean() throw new IOException("Invalid boolean value: " + a); } - private Dictionary objectPool = new Dictionary(); - private Dictionary objectPoolRev = new Dictionary(); + private Dictionary objectPool; + private Dictionary objectPoolRev; private HashSet unreadObjects = new HashSet(); + private Exception countUnserializeExc = null; + private readonly Dictionary> unserializeFuncDict = new(); + private readonly Dictionary staticObjCountDict = new(); + private readonly Dictionary staticObjSizeDict = new(); + public HashSet GMS2BytecodeAddresses; + public int[] InstructionArraysLengths; + + private readonly BindingFlags publicStaticFlags + = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; + private readonly Type[] readerArgType = { typeof(UndertaleReader) }; + private readonly Type delegateType = typeof(Func); + private readonly Func blankCountFunc = new(_ => { return 0; }); + + public ArrayPool utListPtrsPool = ArrayPool.Create(100000, 17); + + private bool ProcessCountExc(uint poolSize = 0) + { + if (countUnserializeExc is not null) + { + try + { + string fileDir = Path.GetDirectoryName(Environment.ProcessPath); + File.WriteAllText(Path.Combine(fileDir, "unserializeCountError.txt"), + countUnserializeExc.ToString() + "\n" + + countUnserializeExc.Message + "\n" + + countUnserializeExc.StackTrace); + + SubmitWarning("Warning - there was an error while trying to unserialize total object count.\n" + + "The error log is saved to \"unserializeCountError.txt\"." + + "Please report that error to UndertaleModTool GitHub."); + } + catch { } + + countUnserializeExc = null; + + return true; + } + + if (poolSize != 0 && poolSize != objectPool.Count) + { + SubmitWarning("Warning - the estimated object pool size differs from the actual size.\n" + + "Please report this on UndertaleModTool GitHub."); + } + + return false; + } + private void FillUnserializeCountDictionaries() + { + try + { + Assembly currAssem = Assembly.GetExecutingAssembly(); + Type[] allTypes = currAssem.GetTypes(); + + Type utObjectType = typeof(UndertaleObject); + Type staticObjCountType = typeof(IStaticChildObjCount); + Type staticObjSizeType = typeof(IStaticChildObjectsSize); + + allTypes = allTypes.Where(t => t.IsAssignableTo(utObjectType)).ToArray(); + foreach (Type t in allTypes) + { + // It's not possible to call a static method of generic classes without present type argument. + if (t.ContainsGenericParameters) + continue; + + MethodInfo mi = t.GetMethod("UnserializeChildObjectCount", publicStaticFlags, readerArgType); + if (mi is null) + continue; + + var func = Delegate.CreateDelegate(delegateType, mi) as Func; + if (func is null) + { + Debug.WriteLine($"Can't create a delegate from MethodInfo of type \"{t.FullName}\""); + continue; + } + + unserializeFuncDict[t] = func; + } + + for (int i = 0; i < allTypes.Length; i++) + { + Type t = allTypes[i]; + FieldInfo fi; + object res; + + // It's not supported to get a static field from generic classes without present type argument. + if (t.ContainsGenericParameters) + continue; + + if (t.IsAssignableTo(staticObjCountType)) + { + fi = t.GetField("ChildObjectCount", publicStaticFlags); + if (fi is null) + { + Debug.WriteLine($"Can't get \"ChildObjectCount\" field of \"{t.FullName}\""); + continue; + } + + res = fi.GetValue(null); + if (res is null) + { + Debug.WriteLine($"Can't get value of \"ChildObjectCount\" of \"{t.FullName}\""); + continue; + } + + staticObjCountDict[t] = (uint)res; + } + + if (t.IsAssignableTo(staticObjSizeType)) + { + fi = t.GetField("ChildObjectsSize", publicStaticFlags); + if (fi is null) + { + Debug.WriteLine($"Can't get \"ChildObjectsSize\" field of \"{t.FullName}\""); + continue; + } + + res = fi.GetValue(null); + if (res is null) + { + Debug.WriteLine($"Can't get value of \"ChildObjectsSize\" of \"{t.FullName}\""); + continue; + } + + staticObjSizeDict[t] = (uint)res; + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + countUnserializeExc = e; + } + } + public Func GetUnserializeCountFunc(Type objType) + { + if (!unserializeFuncDict.TryGetValue(objType, out var res)) + { + MethodInfo mi = objType.GetMethod("UnserializeChildObjectCount", publicStaticFlags, readerArgType); + if (mi is null) + { + Debug.WriteLine($"\"UndertaleReader.unserializeFuncDict\" doesn't contain a method for \"{objType.FullName}\"."); + return blankCountFunc; + } + + //Debug.WriteLine($"Adding a generic class method for \"{objType.FullName}\" to \"UndertaleReader.unserializeFuncDict\"."); + + var func = Delegate.CreateDelegate(delegateType, mi) as Func; + if (func is null) + { + Debug.WriteLine($"Can't create a delegate from MethodInfo of type \"{objType.FullName}\""); + return blankCountFunc; + } + + unserializeFuncDict[objType] = func; + + res = func; + } + + return res; + } + public uint GetStaticChildCount(Type objType) + { + if (!staticObjCountDict.TryGetValue(objType, out uint res)) + { + Debug.WriteLine($"\"UndertaleReader.staticObjCountDict\" doesn't contain type \"{objType.FullName}\"."); + return 0; + } + + return res; + } + public uint GetStaticChildObjectsSize(Type objType) + { + if (!staticObjSizeDict.TryGetValue(objType, out uint res)) + { + Debug.WriteLine($"\"UndertaleReader.staticObjSizeDict\" doesn't contain type \"{objType.FullName}\"."); + return 0; + } + + return res; + } + public void SetStaticChildCount(Type objType, uint count) + { + staticObjCountDict[objType] = count; + } + public void SetStaticChildObjectsSize(Type objType, uint size) + { + staticObjSizeDict[objType] = size; + } + public Dictionary GetOffsetMap() { return objectPool; @@ -269,6 +497,48 @@ public Dictionary GetOffsetMapRev() return objectPoolRev; } + public void InitializePools(uint objCount = 0) + { + if (objCount == 0) + { + objectPool = new(); + objectPoolRev = new(); + } + else + { + int objCountInt = (int)objCount; + objectPool = new(objCountInt); + objectPoolRev = new(objCountInt); + } + } + + public uint GetChildObjectCount(Type t) + { + if (!unserializeFuncDict.TryGetValue(t, out var func)) + { + if (staticObjSizeDict.TryGetValue(t, out uint size)) + { + Position += size; + + staticObjCountDict.TryGetValue(t, out uint subCount); + + return subCount; + } + + throw new UndertaleSerializationException( + $"\"UndertaleReader.unserializeFuncDict\" doesn't contain a method for \"{t.FullName}\"."); + } + + return func(this); + } + public uint GetChildObjectCount() where T : UndertaleObject + { + Type t = typeof(T); + + return GetChildObjectCount(t); + } + + public T GetUndertaleObjectAtAddress(uint address) where T : UndertaleObject, new() { if (address == 0) @@ -296,12 +566,12 @@ public uint GetAddressForUndertaleObject(UndertaleObject obj) try { var expectedAddress = GetAddressForUndertaleObject(obj); - if (expectedAddress != Position) + if (expectedAddress != AbsPosition) { - SubmitWarning("Reading misaligned at " + Position.ToString("X8") + ", realigning back to " + expectedAddress.ToString("X8") + "\nHIGH RISK OF DATA LOSS! The file is probably corrupted, or uses unsupported features\nProceed at your own risk"); - Position = expectedAddress; + SubmitWarning("Reading misaligned at " + AbsPosition.ToString("X8") + ", realigning back to " + expectedAddress.ToString("X8") + "\nHIGH RISK OF DATA LOSS! The file is probably corrupted, or uses unsupported features\nProceed at your own risk"); + AbsPosition = expectedAddress; } - unreadObjects.Remove(Position); + unreadObjects.Remove((uint)AbsPosition); obj.Unserialize(this); } catch (Exception e) @@ -310,47 +580,9 @@ public uint GetAddressForUndertaleObject(UndertaleObject obj) } } - public void ReadUndertaleObject(T obj, uint endPosition) where T : UndertaleObjectEndPos, new() - { - try - { - var expectedAddress = GetAddressForUndertaleObject(obj); - if (expectedAddress != Position) - { - SubmitWarning("Reading misaligned at " + Position.ToString("X8") + ", realigning back to " + expectedAddress.ToString("X8") + "\nHIGH RISK OF DATA LOSS! The file is probably corrupted, or uses unsupported features\nProceed at your own risk"); - Position = expectedAddress; - } - unreadObjects.Remove(Position); - obj.Unserialize(this, endPosition); - } - catch (Exception e) - { - throw new UndertaleSerializationException(e.Message + "\nat " + Position.ToString("X8") + " while reading object " + typeof(T).FullName, e); - } - } - - public void ReadUndertaleObject(T obj, int length) where T : UndertaleObjectLenCheck, new() - { - try - { - var expectedAddress = GetAddressForUndertaleObject(obj); - if (expectedAddress != Position) - { - SubmitWarning("Reading misaligned at " + Position.ToString("X8") + ", realigning back to " + expectedAddress.ToString("X8") + "\nHIGH RISK OF DATA LOSS! The file is probably corrupted, or uses unsupported features\nProceed at your own risk"); - Position = expectedAddress; - } - unreadObjects.Remove(Position); - obj.Unserialize(this, length); - } - catch (Exception e) - { - throw new UndertaleSerializationException(e.Message + "\nat " + Position.ToString("X8") + " while reading object " + typeof(T).FullName, e); - } - } - public T ReadUndertaleObject() where T : UndertaleObject, new() { - T obj = GetUndertaleObjectAtAddress(Position); + T obj = GetUndertaleObjectAtAddress((uint)AbsPosition); ReadUndertaleObject(obj); return obj; } @@ -400,7 +632,7 @@ public void ToHere() int diff = (int)expectedLength - (int)length; Console.WriteLine("WARNING: File specified length " + expectedLength + ", but read only " + length + " (" + diff + " padding?)"); if (diff > 0) - reader.Position = reader.Position + (uint)diff; + reader.Position += (uint)diff; else throw new IOException("Read underflow"); } @@ -409,7 +641,7 @@ public void ToHere() public void Align(int alignment, byte paddingbyte = 0x00) { - while ((Position & (alignment - 1)) != paddingbyte) + while ((AbsPosition & (alignment - 1)) != paddingbyte) { DebugUtil.Assert(ReadByte() == paddingbyte, "Invalid alignment padding"); } @@ -421,7 +653,7 @@ public EnsureLengthOperation EnsureLengthFromHere(uint expectedLength) } } - public class UndertaleWriter : Util.FileBinaryWriter + public class UndertaleWriter : FileBinaryWriter { internal UndertaleData undertaleData; diff --git a/UndertaleModLib/UndertaleLists.cs b/UndertaleModLib/UndertaleLists.cs index 63d46ce2f..aa3cb2449 100644 --- a/UndertaleModLib/UndertaleLists.cs +++ b/UndertaleModLib/UndertaleLists.cs @@ -3,14 +3,59 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; +using System.Reflection; using UndertaleModLib.Models; namespace UndertaleModLib { - public class UndertaleSimpleList : ObservableCollection, UndertaleObject where T : UndertaleObject, new() + public abstract class UndertaleListBase : ObservableCollection, UndertaleObject { + private readonly List internalList; + + public UndertaleListBase() + { + try + { + FieldInfo itemsField = typeof(Collection) + .GetField("items", BindingFlags.NonPublic | BindingFlags.Instance); + internalList = (List)itemsField.GetValue(this); + + } + catch (Exception e) + { + throw new UndertaleSerializationException($"{e.Message}\nwhile trying to initialize \"UndertalePointerList<{typeof(T).FullName}>\"."); + } + } + + /// + public abstract void Serialize(UndertaleWriter writer); + /// - public void Serialize(UndertaleWriter writer) + public abstract void Unserialize(UndertaleReader reader); + + public void SetCapacity(int capacity) + { + try + { + internalList.Capacity = capacity; + } + catch (Exception e) + { + throw new UndertaleSerializationException($"{e.Message}\nwhile trying to \"SetCapacity()\" of \"UndertalePointerList<{typeof(T).FullName}>\"."); + } + } + public void SetCapacity(uint capacity) => SetCapacity((int)capacity); + + public void InternalAdd(T item) + { + internalList.Add(item); + } + } + + public class UndertaleSimpleList : UndertaleListBase where T : UndertaleObject, new() + { + /// + public override void Serialize(UndertaleWriter writer) { writer.Write((uint)Count); for (int i = 0; i < Count; i++) @@ -27,15 +72,16 @@ public void Serialize(UndertaleWriter writer) } /// - public void Unserialize(UndertaleReader reader) + public override void Unserialize(UndertaleReader reader) { uint count = reader.ReadUInt32(); Clear(); + SetCapacity(count); for (uint i = 0; i < count; i++) { try { - Add(reader.ReadUndertaleObject()); + InternalAdd(reader.ReadUndertaleObject()); } catch (UndertaleSerializationException e) { @@ -43,12 +89,59 @@ public void Unserialize(UndertaleReader reader) } } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = reader.ReadUInt32(); + if (count == 0) + return 0; + + uint totalCount = 0; + + Type t = typeof(T); + if (t.IsAssignableTo(typeof(UndertaleResourceRef))) + { + // UndertaleResourceById = 4 bytes + reader.Position += count * 4; + + return count; + } + + if (t.IsAssignableTo(typeof(IStaticChildObjectsSize))) + { + uint subSize = reader.GetStaticChildObjectsSize(t); + uint subCount = 0; + + if (t.IsAssignableTo(typeof(IStaticChildObjCount))) + subCount = reader.GetStaticChildCount(t); + + reader.Position += count * subSize; + + return count + count * subCount; + } + + var unserializeFunc = reader.GetUnserializeCountFunc(t); + for (uint i = 0; i < count; i++) + { + try + { + totalCount += 1 + unserializeFunc(reader); + } + catch (UndertaleSerializationException e) + { + throw new UndertaleSerializationException(e.Message + "\nwhile reading child object count of item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); + } + } + + return totalCount; + } } - public class UndertaleSimpleListString : ObservableCollection, UndertaleObject + public class UndertaleSimpleListString : UndertaleListBase { /// - public void Serialize(UndertaleWriter writer) + public override void Serialize(UndertaleWriter writer) { writer.Write((uint)Count); for (int i = 0; i < Count; i++) @@ -65,15 +158,16 @@ public void Serialize(UndertaleWriter writer) } /// - public void Unserialize(UndertaleReader reader) + public override void Unserialize(UndertaleReader reader) { uint count = reader.ReadUInt32(); Clear(); + SetCapacity(count); for (uint i = 0; i < count; i++) { try { - Add(reader.ReadUndertaleString()); + InternalAdd(reader.ReadUndertaleString()); } catch (UndertaleSerializationException e) { @@ -81,23 +175,33 @@ public void Unserialize(UndertaleReader reader) } } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + uint count = reader.ReadUInt32(); + reader.Position += count * 4; + return 0; + } } - public class UndertaleSimpleListShort : ObservableCollection, UndertaleObject where T : UndertaleObject, new() + public class UndertaleSimpleListShort : UndertaleListBase where T : UndertaleObject, new() { - public UndertaleSimpleListShort() + private void EnsureShortCount() { - base.CollectionChanged += EnsureShortCount; + if (Count > Int16.MaxValue) + throw new InvalidOperationException("Count of short SimpleList exceeds maximum number allowed."); } - private void EnsureShortCount(object sender, NotifyCollectionChangedEventArgs e) + /// + public new void Add(T item) { - if (e.NewItems != null && e.NewItems.Count > Int16.MaxValue) - throw new InvalidOperationException("Count of short SimpleList exceeds maximum number allowed."); + base.Add(item); + EnsureShortCount(); } /// - public void Serialize(UndertaleWriter writer) + public override void Serialize(UndertaleWriter writer) { writer.Write((ushort)Count); for (int i = 0; i < Count; i++) @@ -114,15 +218,16 @@ public void Serialize(UndertaleWriter writer) } /// - public void Unserialize(UndertaleReader reader) + public override void Unserialize(UndertaleReader reader) { ushort count = reader.ReadUInt16(); Clear(); + SetCapacity(count); for (ushort i = 0; i < count; i++) { try { - Add(reader.ReadUndertaleObject()); + InternalAdd(reader.ReadUndertaleObject()); } catch (UndertaleSerializationException e) { @@ -130,12 +235,51 @@ public void Unserialize(UndertaleReader reader) } } } + + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) + { + ushort count = reader.ReadUInt16(); + if (count == 0) + return 0; + + uint totalCount = 0; + + Type t = typeof(T); + if (t.IsAssignableTo(typeof(IStaticChildObjectsSize))) + { + uint subSize = reader.GetStaticChildObjectsSize(t); + uint subCount = 0; + + if (t.IsAssignableTo(typeof(IStaticChildObjCount))) + subCount = reader.GetStaticChildCount(t); + + reader.Position += count * subSize; + + return count + count * subCount; + } + + var unserializeFunc = reader.GetUnserializeCountFunc(t); + for (uint i = 0; i < count; i++) + { + try + { + totalCount += 1 + unserializeFunc(reader); + } + catch (UndertaleSerializationException e) + { + throw new UndertaleSerializationException(e.Message + "\nwhile reading child object count of item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); + } + } + + return totalCount; + } } - public class UndertalePointerList : ObservableCollection, UndertaleObject where T : UndertaleObject, new() + public class UndertalePointerList : UndertaleListBase where T : UndertaleObject, new() { /// - public void Serialize(UndertaleWriter writer) + public override void Serialize(UndertaleWriter writer) { writer.Write((uint)Count); foreach (T obj in this) @@ -155,13 +299,15 @@ public void Serialize(UndertaleWriter writer) { try { - (this[i] as PrePaddedObject)?.SerializePrePadding(writer); + T obj = this[i]; - writer.WriteUndertaleObject(this[i]); + (obj as PrePaddedObject)?.SerializePrePadding(writer); + + writer.WriteUndertaleObject(obj); // The last object does NOT get padding (TODO: at least in AUDO) if (i != Count - 1) - (this[i] as PaddedObject)?.SerializePadding(writer); + (obj as PaddedObject)?.SerializePadding(writer); } catch (UndertaleSerializationException e) { @@ -171,40 +317,49 @@ public void Serialize(UndertaleWriter writer) } /// - public void Unserialize(UndertaleReader reader) + public override void Unserialize(UndertaleReader reader) { uint count = reader.ReadUInt32(); Clear(); + SetCapacity(count); for (uint i = 0; i < count; i++) { try { - Add(reader.ReadUndertaleObjectPointer()); + InternalAdd(reader.ReadUndertaleObjectPointer()); } catch (UndertaleSerializationException e) { throw new UndertaleSerializationException(e.Message + "\nwhile reading pointer to item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); } } - if (Count > 0 && reader.Position != reader.GetAddressForUndertaleObject(this[0])) + if (Count > 0) { - int skip = (int)reader.GetAddressForUndertaleObject(this[0]) - (int)reader.Position; - if (skip > 0) + uint pos = reader.GetAddressForUndertaleObject(this[0]); + if (reader.AbsPosition != pos) { - //Console.WriteLine("Skip " + skip + " bytes of blobs"); - reader.Position = reader.Position + (uint)skip; + long skip = pos - reader.AbsPosition; + if (skip > 0) + { + //Console.WriteLine("Skip " + skip + " bytes of blobs"); + reader.AbsPosition += skip; + } + else + throw new IOException("First list item starts inside the pointer list?!?!"); } - else - throw new IOException("First list item starts inside the pointer list?!?!"); } for (uint i = 0; i < count; i++) { try { - (this[(int)i] as PrePaddedObject)?.UnserializePrePadding(reader); - reader.ReadUndertaleObject(this[(int)i]); + T obj = this[(int)i]; + + (obj as PrePaddedObject)?.UnserializePrePadding(reader); + + reader.ReadUndertaleObject(obj); + if (i != count - 1) - (this[(int)i] as PaddedObject)?.UnserializePadding(reader); + (obj as PaddedObject)?.UnserializePadding(reader); } catch (UndertaleSerializationException e) { @@ -212,57 +367,62 @@ public void Unserialize(UndertaleReader reader) } } } - } - public class UndertalePointerListLenCheck : UndertalePointerList, UndertaleObjectEndPos where T : UndertaleObjectLenCheck, new() - { - /// - public void Unserialize(UndertaleReader reader, uint endPosition) + /// + public static uint UnserializeChildObjectCount(UndertaleReader reader) { uint count = reader.ReadUInt32(); - Clear(); - List pointers = new List(); - for (uint i = 0; i < count; i++) + if (count == 0) + return 0; + + uint totalCount = 0; + + Type t = typeof(T); + if (t.IsAssignableTo(typeof(IStaticChildObjectsSize))) { - try - { - uint ptr = reader.ReadUInt32(); - pointers.Add(ptr); - Add(reader.GetUndertaleObjectAtAddress(ptr)); - } - catch (UndertaleSerializationException e) - { - throw new UndertaleSerializationException(e.Message + "\nwhile reading pointer to item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); - } + uint subSize = reader.GetStaticChildObjectsSize(t); + uint subCount = 0; + + if (t.IsAssignableTo(typeof(IStaticChildObjCount))) + subCount = reader.GetStaticChildCount(t); + + reader.Position += count * 4 + count * subSize; + + return count + count * subCount; } - if (Count > 0 && reader.Position != reader.GetAddressForUndertaleObject(this[0])) + + uint[] pointers = reader.utListPtrsPool.Rent((int)count); + for (uint i = 0; i < count; i++) + pointers[i] = reader.ReadUInt32(); + + uint pos = pointers[0]; + if (reader.AbsPosition != pos) { - int skip = (int)reader.GetAddressForUndertaleObject(this[0]) - (int)reader.Position; + long skip = pos - reader.AbsPosition; if (skip > 0) - { - //Console.WriteLine("Skip " + skip + " bytes of blobs"); - reader.Position = reader.Position + (uint)skip; - } + reader.AbsPosition += skip; else throw new IOException("First list item starts inside the pointer list?!?!"); } + + var unserializeFunc = reader.GetUnserializeCountFunc(t); for (uint i = 0; i < count; i++) { try { - (this[(int)i] as PrePaddedObject)?.UnserializePrePadding(reader); - if ((i + 1) < count) - reader.ReadUndertaleObject(this[(int)i], (int)(pointers[(int)i + 1] - reader.Position)); - else - reader.ReadUndertaleObject(this[(int)i], (int)(endPosition - reader.Position)); - if (i != count - 1) - (this[(int)i] as PaddedObject)?.UnserializePadding(reader); + reader.AbsPosition = pointers[i]; + totalCount += 1 + unserializeFunc(reader); } catch (UndertaleSerializationException e) { - throw new UndertaleSerializationException(e.Message + "\nwhile reading item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); + reader.utListPtrsPool.Return(pointers); + throw new UndertaleSerializationException(e.Message + "\nwhile reading child object count of item " + (i + 1) + " of " + count + " in a list of " + typeof(T).FullName, e); } } + + reader.utListPtrsPool.Return(pointers); + + return totalCount; } } diff --git a/UndertaleModLib/Util/AdaptiveBinaryReader.cs b/UndertaleModLib/Util/AdaptiveBinaryReader.cs new file mode 100644 index 000000000..5522678f0 --- /dev/null +++ b/UndertaleModLib/Util/AdaptiveBinaryReader.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UndertaleModLib.Util +{ + public interface IBinaryReader : IDisposable + { + public abstract Stream Stream { get; set; } + public abstract long Length { get; } + public abstract long Position { get; set; } + + public abstract byte ReadByte(); + public virtual bool ReadBoolean() => false; + public abstract string ReadChars(int count); + public abstract byte[] ReadBytes(int count); + public abstract short ReadInt16(); + public abstract ushort ReadUInt16(); + public abstract int ReadInt24(); + public abstract uint ReadUInt24(); + public abstract int ReadInt32(); + public abstract uint ReadUInt32(); + public abstract float ReadSingle(); + public abstract double ReadDouble(); + public abstract long ReadInt64(); + public abstract ulong ReadUInt64(); + public abstract string ReadGMString(); + public abstract void SkipGMString(); + } + + public class AdaptiveBinaryReader : IBinaryReader + { + private readonly FileBinaryReader fileBinaryReader; + private readonly BufferBinaryReader bufferBinaryReader; + private IBinaryReader _currentReader; + private bool isUsingBufferReader = false; + private bool isCurrChunkTooLarge = false; + private IBinaryReader CurrentReader + { + get => _currentReader; + set + { + _currentReader = value; + isUsingBufferReader = value is BufferBinaryReader; + } + } + + private readonly Encoding encoding = new UTF8Encoding(false); + public Encoding Encoding { get => encoding; } + public Stream Stream { get; set; } + public long Length { get; private set; } + + // I've done some benchmarks, and they show that + // "if..else" is faster than using interfaces here. + // (at least in C# 10) + public long Position + { + get + { + if (isUsingBufferReader) + return bufferBinaryReader.Position; + else + return Stream.Position; + } + set + { + if (isUsingBufferReader) + bufferBinaryReader.Position = value; + else + fileBinaryReader.Position = value; + } + } + public long AbsPosition + { + get + { + if (isUsingBufferReader) + return bufferBinaryReader.ChunkStartPosition + bufferBinaryReader.Position - 8; + else + return Stream.Position; + } + set + { + if (isUsingBufferReader) + { +#if DEBUG + if (value > Length) + throw new IOException("Reading out of bounds."); +#endif + bufferBinaryReader.Position = value - bufferBinaryReader.ChunkStartPosition + 8; + } + else + fileBinaryReader.Position = value; + } + } + + public AdaptiveBinaryReader(Stream stream, Encoding encoding = null) + { + fileBinaryReader = new(stream, encoding); + bufferBinaryReader = new(stream, encoding); + CurrentReader = fileBinaryReader; + + Length = stream.Length; + Stream = stream; + + if (stream.Position != 0) + stream.Seek(0, SeekOrigin.Begin); + + if (encoding is not null) + this.encoding = encoding; + } + + public void CopyChunkToBuffer(uint length) + { + if (length <= 12 * 1024 * 1024) + { + isCurrChunkTooLarge = false; + CurrentReader = bufferBinaryReader; + bufferBinaryReader.CopyChunkToBuffer(length); + } + else + { + isCurrChunkTooLarge = true; + CurrentReader = fileBinaryReader; + } + } + + public void SwitchReaderType(bool isBufferBinaryReader) + { + if (!isBufferBinaryReader && CurrentReader == bufferBinaryReader) + { + fileBinaryReader.Position = AbsPosition; + CurrentReader = fileBinaryReader; + } + else if (isBufferBinaryReader && !isCurrChunkTooLarge + && CurrentReader == fileBinaryReader) + { + CurrentReader = bufferBinaryReader; + AbsPosition = fileBinaryReader.Position; + } + } + + public byte ReadByte() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadByte(); + else + return fileBinaryReader.ReadByte(); + } + public virtual bool ReadBoolean() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadBoolean(); + else + return fileBinaryReader.ReadBoolean(); + } + public string ReadChars(int count) + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadChars(count); + else + return fileBinaryReader.ReadChars(count); + } + public byte[] ReadBytes(int count) + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadBytes(count); + else + return fileBinaryReader.ReadBytes(count); + } + public short ReadInt16() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadInt16(); + else + return fileBinaryReader.ReadInt16(); + } + public ushort ReadUInt16() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadUInt16(); + else + return fileBinaryReader.ReadUInt16(); + } + public int ReadInt24() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadInt24(); + else + return fileBinaryReader.ReadInt24(); + } + public uint ReadUInt24() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadUInt24(); + else + return fileBinaryReader.ReadUInt24(); + } + public int ReadInt32() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadInt32(); + else + return fileBinaryReader.ReadInt32(); + } + public uint ReadUInt32() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadUInt32(); + else + return fileBinaryReader.ReadUInt32(); + } + public float ReadSingle() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadSingle(); + else + return fileBinaryReader.ReadSingle(); + } + public double ReadDouble() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadDouble(); + else + return fileBinaryReader.ReadDouble(); + } + public long ReadInt64() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadInt64(); + else + return fileBinaryReader.ReadInt64(); + } + public ulong ReadUInt64() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadUInt64(); + else + return fileBinaryReader.ReadUInt64(); + } + public string ReadGMString() + { + if (isUsingBufferReader) + return bufferBinaryReader.ReadGMString(); + else + return fileBinaryReader.ReadGMString(); + } + public void SkipGMString() + { + if (isUsingBufferReader) + bufferBinaryReader.SkipGMString(); + else + fileBinaryReader.SkipGMString(); + } + + public void Dispose() + { + if (Stream is not null) + { + Stream.Close(); + Stream.Dispose(); + } + bufferBinaryReader.Dispose(); + } + } +} diff --git a/UndertaleModLib/Util/BufferBinaryReader.cs b/UndertaleModLib/Util/BufferBinaryReader.cs index 7e7a04b70..e38d13b5f 100644 --- a/UndertaleModLib/Util/BufferBinaryReader.cs +++ b/UndertaleModLib/Util/BufferBinaryReader.cs @@ -1,48 +1,182 @@ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Text; namespace UndertaleModLib.Util { - // Reimplemented based on DogScepter's implementation - public class BufferBinaryReader + // Initial implementation was based on DogScepter's implementation + public class BufferBinaryReader : IBinaryReader { - private readonly byte[] buffer; - private Encoding encoding; - public Encoding Encoding { get => encoding; } + // A faster implementation of "MemoryStream" + private class ChunkBuffer + { + private readonly byte[] _buffer; + + private int _position, _length; + public int Position { get => _position; set => _position = value; } + public int Length { get => _length; } + public int Capacity { get; } + + public ChunkBuffer(int capacity) + { + _buffer = new byte[capacity]; + Capacity = capacity; + } + + public int Read(byte[] buffer, int count) + { + int n = _length - _position; + if (n > count) + n = count; + if (n <= 0) + { +#if DEBUG + throw new IOException("Reading out of bounds"); +#else + return 0; +#endif + } + + if (n <= 8) + { + int byteCount = n; + while (--byteCount >= 0) + buffer[byteCount] = _buffer[_position + byteCount]; + } + else + Buffer.BlockCopy(_buffer, _position, buffer, 0, n); + _position += n; + + return n; + } + public int Read(Span buffer) + { + int n = Math.Min(_length - _position, buffer.Length); + if (n <= 0) + { +#if DEBUG + throw new IOException("Reading out of bounds"); +#else + return 0; +#endif + } + + new Span(_buffer, _position, n).CopyTo(buffer); + + _position += n; + return n; + } + public byte ReadByte() + { + int currPos = _position; + int newPos = _position + 1; + if (newPos > _length) + { +#if DEBUG + throw new IOException("Reading out of bounds"); +#else + return 0; +#endif + } + + _position = newPos; + return _buffer[currPos]; + } + + public void Write(byte[] buffer, int count) + { + int i = _position + count; + if (i < 0) + throw new IOException("Writing out of the chunk buffer bounds."); + + // "MemoryStream" also extends the buffer if + // the length becomes greater than the capacity + _length = i; - public int Offset { get; set; } - public long Length { get; private set; } - public byte[] Buffer { get => buffer; } + if ((count <= 8) && (buffer != _buffer)) + { + int byteCount = count; + while (--byteCount >= 0) + { + _buffer[_position + byteCount] = buffer[byteCount]; + } + } + else + { + Buffer.BlockCopy(buffer, 0, _buffer, _position, count); + } - public uint Position + _position = i; + } + } + + + private readonly byte[] buffer = new byte[16]; + private readonly ChunkBuffer chunkBuffer; + private readonly byte[] chunkCopyBuffer = new byte[81920]; + + private readonly Encoding encoding = new UTF8Encoding(false); + public Stream Stream { get; set; } + + private readonly long _length; + public long Length { get => _length; } + + public long Position { - get => (uint)Offset; - set => Offset = (int)value; + get => chunkBuffer.Position; + set => chunkBuffer.Position = (int)value; } + public long ChunkStartPosition { get; set; } - public BufferBinaryReader(Stream stream) + public BufferBinaryReader(Stream stream, Encoding encoding = null) { - Length = stream.Length; - buffer = new byte[Length]; - Offset = 0; + _length = stream.Length; + Stream = stream; + + // Check data file length + if (Length >= 12 * 1024 * 1024) // 12 MB + chunkBuffer = new(12 * 1024 * 1024); + else + chunkBuffer = new((int)Length); if (stream.Position != 0) stream.Seek(0, SeekOrigin.Begin); - stream.Read(buffer, 0, (int)Length); - stream.Close(); - encoding = new UTF8Encoding(false); + if (encoding is not null) + this.encoding = encoding; + } + + public void CopyChunkToBuffer(uint length) + { + Stream.Position -= 8; // Chunk name + length + chunkBuffer.Position = 0; + + // Source - https://stackoverflow.com/a/13022108/12136394 + int read; + int remaining = (int)length + 8; + while (remaining > 0 && + (read = Stream.Read(chunkCopyBuffer, 0, Math.Min(chunkCopyBuffer.Length, remaining))) > 0) + { + chunkBuffer.Write(chunkCopyBuffer, read); + remaining -= read; + } + + Stream.Position -= length; + chunkBuffer.Position -= (int)length; + + ChunkStartPosition = Stream.Position; + } + private ReadOnlySpan ReadToBuffer(int count) + { + chunkBuffer.Read(buffer, count); + return buffer; } public byte ReadByte() { -#if DEBUG - if (Offset < 0 || Offset + 1 > Length) - throw new IOException("Reading out of bounds"); -#endif - return buffer[Offset++]; + return chunkBuffer.ReadByte(); } public virtual bool ReadBoolean() @@ -53,147 +187,132 @@ public virtual bool ReadBoolean() public string ReadChars(int count) { #if DEBUG - if (Offset < 0 || Offset + count > Length) + if (chunkBuffer.Position + count > _length) throw new IOException("Reading out of bounds"); #endif - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < count; i++) - sb.Append(Convert.ToChar(buffer[Offset++])); - return sb.ToString(); + if (count > 1024) + { + byte[] buf = new byte[count]; + chunkBuffer.Read(buf, count); + + return encoding.GetString(buf); + } + else + { + Span buf = stackalloc byte[count]; + if (count > 0) + chunkBuffer.Read(buf); + + return encoding.GetString(buf); + } } public byte[] ReadBytes(int count) { #if DEBUG - if (Offset < 0 || Offset + count > Length) + if (chunkBuffer.Position + count > _length) throw new IOException("Reading out of bounds"); #endif byte[] val = new byte[count]; - System.Buffer.BlockCopy(buffer, Offset, val, 0, count); - Offset += count; + if (count > 0) + chunkBuffer.Read(val, count); return val; } public short ReadInt16() { -#if DEBUG - if (Offset < 0 || Offset + 2 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (short)(buffer[Offset++] | buffer[Offset++] << 8); + return BinaryPrimitives.ReadInt16LittleEndian(ReadToBuffer(2)); } public ushort ReadUInt16() { -#if DEBUG - if (Offset < 0 || Offset + 2 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (ushort)(buffer[Offset++] | buffer[Offset++] << 8); + return BinaryPrimitives.ReadUInt16LittleEndian(ReadToBuffer(2)); } public int ReadInt24() { -#if DEBUG - if (Offset < 0 || Offset + 3 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (int)(buffer[Offset++] | buffer[Offset++] << 8 | (sbyte)buffer[Offset++] << 16); + ReadToBuffer(3); + return buffer[0] | buffer[1] << 8 | (sbyte)buffer[2] << 16; } public uint ReadUInt24() { -#if DEBUG - if (Offset < 0 || Offset + 3 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (uint)(buffer[Offset++] | buffer[Offset++] << 8 | buffer[Offset++] << 16); + ReadToBuffer(3); + return (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16); } public int ReadInt32() { -#if DEBUG - if (Offset < 0 || Offset + 4 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (int)(buffer[Offset++] | buffer[Offset++] << 8 | - buffer[Offset++] << 16 | (sbyte)buffer[Offset++] << 24); + return BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); } public uint ReadUInt32() { -#if DEBUG - if (Offset < 0 || Offset + 4 > Length) - throw new IOException("Reading out of bounds"); -#endif - return (uint)(buffer[Offset++] | buffer[Offset++] << 8 | - buffer[Offset++] << 16 | buffer[Offset++] << 24); + return BinaryPrimitives.ReadUInt32LittleEndian(ReadToBuffer(4)); } public float ReadSingle() { -#if DEBUG - if (Offset < 0 || Offset + 4 > Length) - throw new IOException("Reading out of bounds"); -#endif - float val = BitConverter.ToSingle(buffer, Offset); - Offset += 4; - return val; + return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4))); } public double ReadDouble() { -#if DEBUG - if (Offset < 0 || Offset + 8 > Length) - throw new IOException("Reading out of bounds"); -#endif - double val = BitConverter.ToDouble(buffer, Offset); - Offset += 8; - return val; + return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8))); } public long ReadInt64() { -#if DEBUG - if (Offset < 0 || Offset + 8 > Length) - throw new IOException("Reading out of bounds"); -#endif - long val = BitConverter.ToInt64(buffer, Offset); - Offset += 8; - return val; + return BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8)); } public ulong ReadUInt64() { -#if DEBUG - if (Offset < 0 || Offset + 8 > Length) - throw new IOException("Reading out of bounds"); -#endif - ulong val = BitConverter.ToUInt64(buffer, Offset); - Offset += 8; - return val; + return BinaryPrimitives.ReadUInt64LittleEndian(ReadToBuffer(8)); } public string ReadGMString() { #if DEBUG - if (Offset < 0 || Offset + 8 > Length) + if (chunkBuffer.Position + 5 > _length) throw new IOException("Reading out of bounds"); #endif - int length = (int)(buffer[Offset++] | buffer[Offset++] << 8 | buffer[Offset++] << 16 | buffer[Offset++] << 24); + int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); #if DEBUG - if (Offset + length + 1 >= Length) + if (chunkBuffer.Position + length + 1 >= _length) throw new IOException("Reading out of bounds"); #endif - string res = encoding.GetString(buffer, Offset, length); + string res; + if (length > 1024) + { + byte[] buf = new byte[length]; + chunkBuffer.Read(buf, length); + res = encoding.GetString(buf); + } + else + { + Span buf = stackalloc byte[length]; + if (buf.Length > 0) + chunkBuffer.Read(buf); + res = encoding.GetString(buf); + } + #if DEBUG - Offset += length; - if (buffer[Offset++] != 0) + if (ReadByte() != 0) throw new IOException("String not null terminated!"); #else - Offset += length + 1; + Position++; #endif return res; } + public void SkipGMString() + { + int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); + Position += (uint)length + 1; + } + + public void Dispose() + { + } } } \ No newline at end of file diff --git a/UndertaleModLib/Util/FileBinaryReader.cs b/UndertaleModLib/Util/FileBinaryReader.cs index a92074862..8cdc46d1e 100644 --- a/UndertaleModLib/Util/FileBinaryReader.cs +++ b/UndertaleModLib/Util/FileBinaryReader.cs @@ -7,33 +7,33 @@ namespace UndertaleModLib.Util { // Reimplemented based on DogScepter's implementation - public class FileBinaryReader : IDisposable + public class FileBinaryReader : IBinaryReader { private readonly byte[] buffer = new byte[16]; - private Encoding encoding = new UTF8Encoding(false); - public Encoding Encoding { get => encoding; } + private readonly Encoding encoding = new UTF8Encoding(false); public Stream Stream { get; set; } - public long Length { get; private set; } + private readonly long _length; + public long Length { get => _length; } - public uint Position + public long Position { - get => (uint)Stream.Position; + get => Stream.Position; set { +#if DEBUG if (value > Length) throw new IOException("Reading out of bounds."); - +#endif Stream.Position = value; } } public FileBinaryReader(Stream stream, Encoding encoding = null) { - Length = stream.Length; + _length = stream.Length; Stream = stream; - Position = 0; if (stream.Position != 0) stream.Seek(0, SeekOrigin.Begin); @@ -51,7 +51,7 @@ private ReadOnlySpan ReadToBuffer(int count) public byte ReadByte() { #if DEBUG - if (Position + 1 > Length) + if (Stream.Position + 1 > _length) throw new IOException("Reading out of bounds"); #endif return (byte)Stream.ReadByte(); @@ -65,7 +65,7 @@ public virtual bool ReadBoolean() public string ReadChars(int count) { #if DEBUG - if (Position + count > Length) + if (Stream.Position + count > _length) throw new IOException("Reading out of bounds"); #endif if (count > 1024) @@ -87,7 +87,7 @@ public string ReadChars(int count) public byte[] ReadBytes(int count) { #if DEBUG - if (Position + count > Length) + if (Stream.Position + count > _length) throw new IOException("Reading out of bounds"); #endif byte[] val = new byte[count]; @@ -98,7 +98,7 @@ public byte[] ReadBytes(int count) public short ReadInt16() { #if DEBUG - if (Position + 2 > Length) + if (Stream.Position + 2 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadInt16LittleEndian(ReadToBuffer(2)); @@ -107,7 +107,7 @@ public short ReadInt16() public ushort ReadUInt16() { #if DEBUG - if (Position + 2 > Length) + if (Stream.Position + 2 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadUInt16LittleEndian(ReadToBuffer(2)); @@ -116,7 +116,7 @@ public ushort ReadUInt16() public int ReadInt24() { #if DEBUG - if (Position + 3 > Length) + if (Stream.Position + 3 > _length) throw new IOException("Reading out of bounds"); #endif ReadToBuffer(3); @@ -126,7 +126,7 @@ public int ReadInt24() public uint ReadUInt24() { #if DEBUG - if (Position + 3 > Length) + if (Stream.Position + 3 > _length) throw new IOException("Reading out of bounds"); #endif ReadToBuffer(3); @@ -136,7 +136,7 @@ public uint ReadUInt24() public int ReadInt32() { #if DEBUG - if (Position + 4 > Length) + if (Stream.Position + 4 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); @@ -145,7 +145,7 @@ public int ReadInt32() public uint ReadUInt32() { #if DEBUG - if (Position + 4 > Length) + if (Stream.Position + 4 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadUInt32LittleEndian(ReadToBuffer(4)); @@ -154,7 +154,7 @@ public uint ReadUInt32() public float ReadSingle() { #if DEBUG - if (Position + 4 > Length) + if (Stream.Position + 4 > _length) throw new IOException("Reading out of bounds"); #endif return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4))); @@ -163,7 +163,7 @@ public float ReadSingle() public double ReadDouble() { #if DEBUG - if (Position + 8 > Length) + if (Stream.Position + 8 > _length) throw new IOException("Reading out of bounds"); #endif return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8))); @@ -172,7 +172,7 @@ public double ReadDouble() public long ReadInt64() { #if DEBUG - if (Position + 8 > Length) + if (Stream.Position + 8 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8)); @@ -181,7 +181,7 @@ public long ReadInt64() public ulong ReadUInt64() { #if DEBUG - if (Position + 8 > Length) + if (Stream.Position + 8 > _length) throw new IOException("Reading out of bounds"); #endif return BinaryPrimitives.ReadUInt64LittleEndian(ReadToBuffer(8)); @@ -190,12 +190,12 @@ public ulong ReadUInt64() public string ReadGMString() { #if DEBUG - if (Position + 8 > Length) + if (Stream.Position + 5 > _length) throw new IOException("Reading out of bounds"); #endif int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); #if DEBUG - if (Position + length + 1 >= Length) + if (Stream.Position + length + 1 >= _length) throw new IOException("Reading out of bounds"); #endif string res; @@ -220,10 +220,15 @@ public string ReadGMString() #endif return res; } + public void SkipGMString() + { + int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); + Position += (uint)length + 1; + } public void Dispose() { - if (Stream is not null) + if (Stream?.CanRead == true) { Stream.Close(); Stream.Dispose(); diff --git a/UndertaleModTool/ImportCodeSystem.cs b/UndertaleModTool/ImportCodeSystem.cs index 05f4888b7..ecc7153ae 100644 --- a/UndertaleModTool/ImportCodeSystem.cs +++ b/UndertaleModTool/ImportCodeSystem.cs @@ -107,6 +107,9 @@ public void ReplaceTextInGML(string codeName, string keyword, string replacement } public void ReplaceTextInGML(UndertaleCode code, string keyword, string replacement, bool caseSensitive = false, bool isRegex = false, GlobalDecompileContext context = null) { + if (code.ParentEntry is not null) + return; + EnsureDataLoaded(); string passBack = ""; @@ -201,6 +204,9 @@ void ImportCode(string codeName, string gmlCode, bool IsGML = true, bool doParse code.Name = Data.Strings.MakeString(codeName); Data.Code.Add(code); } + else if (code.ParentEntry is not null) + return; + if (Data?.GeneralInfo.BytecodeVersion > 14 && Data.CodeLocals.ByName(codeName) == null) { UndertaleCodeLocals locals = new UndertaleCodeLocals(); @@ -410,6 +416,9 @@ void ImportCode(string codeName, string gmlCode, bool IsGML = true, bool doParse void SafeImport(string codeName, string gmlCode, bool IsGML, bool destroyASM = true, bool CheckDecompiler = false, bool throwOnError = false) { UndertaleCode code = Data.Code.ByName(codeName); + if (code?.ParentEntry is not null) + return; + try { if (IsGML) diff --git a/UndertaleModTool/MainWindow.xaml.cs b/UndertaleModTool/MainWindow.xaml.cs index 7d9cabfb1..cd0e9f548 100644 --- a/UndertaleModTool/MainWindow.xaml.cs +++ b/UndertaleModTool/MainWindow.xaml.cs @@ -1017,6 +1017,11 @@ private async Task LoadFile(string filename, bool preventClose = false) data = UndertaleIO.Read(stream, warning => { this.ShowWarning(warning, "Loading warning"); + + if (warning.Contains("unserializeCountError.txt") + || warning.Contains("object pool size")) + return; + hadWarnings = true; }, message => { diff --git a/UndertaleModTool/ProfileSystem.cs b/UndertaleModTool/ProfileSystem.cs index 1d9ddfd46..7d3e5c038 100644 --- a/UndertaleModTool/ProfileSystem.cs +++ b/UndertaleModTool/ProfileSystem.cs @@ -23,6 +23,9 @@ public string GetDecompiledText(string codeName, GlobalDecompileContext context } public string GetDecompiledText(UndertaleCode code, GlobalDecompileContext context = null) { + if (code.ParentEntry is not null) + return $"// This code entry is a reference to an anonymous function within \"{code.ParentEntry.Name.Content}\", decompile that instead."; + GlobalDecompileContext DECOMPILE_CONTEXT = context is null ? new(Data, false) : context; try { @@ -36,6 +39,9 @@ public string GetDecompiledText(UndertaleCode code, GlobalDecompileContext conte public string GetDisassemblyText(UndertaleCode code) { + if (code.ParentEntry is not null) + return $"; This code entry is a reference to an anonymous function within \"{code.ParentEntry.Name.Content}\", disassemble that instead."; + try { return code != null ? code.Disassemble(Data.Variables, Data.CodeLocals.For(code)) : ""; diff --git a/UndertaleModTool/Scripts/Resource Unpackers/ExportASM.csx b/UndertaleModTool/Scripts/Resource Unpackers/ExportASM.csx index 84e4ba618..67f44170a 100644 --- a/UndertaleModTool/Scripts/Resource Unpackers/ExportASM.csx +++ b/UndertaleModTool/Scripts/Resource Unpackers/ExportASM.csx @@ -3,6 +3,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Linq; EnsureDataLoaded(); @@ -16,7 +17,10 @@ if (Directory.Exists(codeFolder)) Directory.CreateDirectory(codeFolder); -SetProgressBar(null, "Code Entries", 0, Data.Code.Count); +List toDump = Data.Code.Where(c => c.ParentEntry is null) + .ToList(); + +SetProgressBar(null, "Code Entries", 0, toDump.Count); StartProgressBarUpdater(); await DumpCode(); @@ -34,7 +38,7 @@ string GetFolder(string path) async Task DumpCode() { - await Task.Run(() => Parallel.ForEach(Data.Code, DumpCode)); + await Task.Run(() => Parallel.ForEach(toDump, DumpCode)); } void DumpCode(UndertaleCode code) diff --git a/UndertaleModTool/Scripts/Technical Scripts/ExportAllCode2_3.csx b/UndertaleModTool/Scripts/Technical Scripts/ExportAllCode2_3.csx index 54111c08b..55a4587a1 100644 --- a/UndertaleModTool/Scripts/Technical Scripts/ExportAllCode2_3.csx +++ b/UndertaleModTool/Scripts/Technical Scripts/ExportAllCode2_3.csx @@ -3,6 +3,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Linq; EnsureDataLoaded(); @@ -28,7 +29,10 @@ if (Directory.Exists(codeFolder)) Directory.CreateDirectory(codeFolder); -SetProgressBar(null, "Code Entries", 0, Data.Code.Count); +List toDump = Data.Code.Where(c => c.ParentEntry is null) + .ToList(); + +SetProgressBar(null, "Code Entries", 0, toDump.Count); StartProgressBarUpdater(); int failed = 0; @@ -48,16 +52,16 @@ void DumpCode() //Because 2.3 code names get way too long, we're gonna convert it to an index based system, starting with a lookup system string index_path = Path.Combine(codeFolder, "LookUpTable.txt"); string index_text = "This is zero indexed, index 0 starts at line 2."; - for (var i = 0; i < Data.Code.Count; i++) + for (var i = 0; i < toDump.Count; i++) { - UndertaleCode code = Data.Code[i]; + UndertaleCode code = toDump[i]; index_text += "\n"; index_text += code.Name.Content; } File.WriteAllText(index_path, index_text); - for (var i = 0; i < Data.Code.Count; i++) + for (var i = 0; i < toDump.Count; i++) { - UndertaleCode code = Data.Code[i]; + UndertaleCode code = toDump[i]; string path = Path.Combine(codeFolder, i.ToString() + ".gml"); try { diff --git a/UndertaleModTool/Scripts/Technical Scripts/ExportAllCodeSync.csx b/UndertaleModTool/Scripts/Technical Scripts/ExportAllCodeSync.csx index c7b6f5a86..db8edab4e 100644 --- a/UndertaleModTool/Scripts/Technical Scripts/ExportAllCodeSync.csx +++ b/UndertaleModTool/Scripts/Technical Scripts/ExportAllCodeSync.csx @@ -3,6 +3,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using System.Linq; EnsureDataLoaded(); @@ -16,7 +17,10 @@ if (Directory.Exists(codeFolder)) Directory.CreateDirectory(codeFolder); -SetProgressBar(null, "Code Entries", 0, Data.Code.Count); +List toDump = Data.Code.Where(c => c.ParentEntry is null) + .ToList(); + +SetProgressBar(null, "Code Entries", 0, toDump.Count); StartProgressBarUpdater(); int failed = 0; @@ -34,7 +38,7 @@ string GetFolder(string path) void DumpCode() { - foreach(UndertaleCode code in Data.Code) + foreach(UndertaleCode code in toDump) { string path = Path.Combine(codeFolder, code.Name.Content + ".gml"); if (code.ParentEntry == null)