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
[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)