diff --git a/UndertaleModLib/Models/UndertaleCode.cs b/UndertaleModLib/Models/UndertaleCode.cs index 1a6095407..086b5ad34 100644 --- a/UndertaleModLib/Models/UndertaleCode.cs +++ b/UndertaleModLib/Models/UndertaleCode.cs @@ -110,6 +110,10 @@ Opcode.PushBltn or Opcode.PushI _ => throw new IOException("Unknown opcode " + op.ToString().ToUpper(CultureInfo.InvariantCulture)), }; } + + /// + /// Converts from bytecode 14 instruction opcodes to modern opcodes. + /// private static byte ConvertInstructionKind(byte kind) { kind = kind switch @@ -382,22 +386,39 @@ public Reference GetReference(bool allowResolve = false) where T : class, /// public void Serialize(UndertaleWriter writer) { + // Flag tracking whether we're writing bytecode 14 (old) instructions + bool bytecode14 = writer.Bytecode14OrLower; + + // Switch on the basic format of instruction to encode switch (GetInstructionType(Kind)) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: { + // Write "extra" byte, used on some instructions writer.Write(Extra); - if (writer.Bytecode14OrLower && Kind == Opcode.Cmp) + + // Write comparison kind, if present + if (bytecode14 && Kind == Opcode.Cmp) + { + // Bytecode 14 encodes its comparison in the opcode itself, not here writer.Write((byte)0); + } else + { + // Bytecode 15 and above encode a comparison kind outside of the opcode writer.Write((byte)ComparisonKind); - byte TypePair = (byte)((byte)Type2 << 4 | (byte)Type1); - writer.Write(TypePair); + } + + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); - if (writer.Bytecode14OrLower) + // Write opcode + if (bytecode14) { + // Translate relevant opcodes to their old bytecode 14 equivalents byte k = Kind switch { Opcode.Conv => 0x03, @@ -415,7 +436,7 @@ public void Serialize(UndertaleWriter writer) Opcode.Shl => 0x0F, Opcode.Shr => 0x10, Opcode.Dup => 0x82, - Opcode.Cmp => (byte)(ComparisonKind + 0x10), + Opcode.Cmp => (byte)(ComparisonKind + 0x10), // Comparison kind is encoded into opcode Opcode.Ret => 0x9D, Opcode.Exit => 0x9E, Opcode.Popz => 0x9F, @@ -424,153 +445,188 @@ public void Serialize(UndertaleWriter writer) writer.Write(k); } else + { + // Write opcode verbatim on modern bytecode versions writer.Write((byte)Kind); - } + } + break; + } case InstructionType.GotoInstruction: { - // See unserialize - if (writer.Bytecode14OrLower) + // Write jump offset + if (bytecode14) + { + // Bytecode 14 writes the offset verbatim writer.WriteInt24(JumpOffset); + } else if (JumpOffsetPopenvExitMagic) { + // If popenv exit magic is used, write that specifically writer.WriteInt24(0xF00000); } else { - uint JumpOffsetFixed = (uint)JumpOffset; - JumpOffsetFixed &= ~0xFF800000; - writer.WriteInt24((int)JumpOffsetFixed); + // If not using popenv exit magic, encode jump offset as 23-bit signed integer + uint jumpOffsetFixed = (uint)JumpOffset; + jumpOffsetFixed &= ~0xFF800000; + writer.WriteInt24((int)jumpOffsetFixed); } - if (writer.Bytecode14OrLower) + // Write opcode + if (bytecode14) { - if (Kind == Opcode.B) - writer.Write((byte)0xB7); - else if (Kind == Opcode.Bt) - writer.Write((byte)0xB8); - else if (Kind == Opcode.Bf) - writer.Write((byte)0xB9); - else if (Kind == Opcode.PushEnv) - writer.Write((byte)0xBB); - else if (Kind == Opcode.PopEnv) - writer.Write((byte)0xBC); - else - writer.Write((byte)Kind); + // Translate relevant opcodes to their old bytecode 14 equivalents + byte k = Kind switch + { + Opcode.B => 0xB7, + Opcode.Bt => 0xB8, + Opcode.Bf => 0xB9, + Opcode.PushEnv => 0xBB, + Opcode.PopEnv => 0xBC, + _ => (byte)Kind + }; + writer.Write(k); } else + { + // Write opcode verbatim on modern bytecode versions writer.Write((byte)Kind); - } + } + break; + } case InstructionType.PopInstruction: { if (Type1 == DataType.Int16) { - // Special scenario - the swap instruction - // TODO: Figure out the proper syntax, see #129 + // Special scenario - the swap instruction (see #129) + // Write swap value writer.Write(SwapExtra); - byte TypePair = (byte)((byte)Type2 << 4 | (byte)Type1); - writer.Write(TypePair); - if (writer.Bytecode14OrLower && Kind == Opcode.Pop) + + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Pop) writer.Write((byte)0x41); else writer.Write((byte)Kind); } else { + // Write instance type writer.Write((short)TypeInst); - byte TypePair = (byte)((byte)Type2 << 4 | (byte)Type1); - writer.Write(TypePair); - if (writer.Bytecode14OrLower && Kind == Opcode.Pop) + + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Pop) writer.Write((byte)0x41); else writer.Write((byte)Kind); + + // Write actual variable being stored to writer.WriteUndertaleObject(Destination); } - } + break; + } case InstructionType.PushInstruction: { - if (Type1 == DataType.Int16) - { - //Debug.Assert(Value.GetType() == typeof(short)); - writer.Write((short)Value); - } - else if (Type1 == DataType.Variable) - { - writer.Write((short)TypeInst); - } - else + // Write 16-bit integer, instance type, or empty data + writer.Write(Type1 switch { - writer.Write((short)0); - } + DataType.Int16 => (short)Value, + DataType.Variable => (short)TypeInst, + _ => (short)0 + }); + + // Write type (no second type is used) writer.Write((byte)Type1); - if (writer.Bytecode14OrLower) + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14) writer.Write((byte)0xC0); else writer.Write((byte)Kind); + + // Write value being pushed switch (Type1) { case DataType.Double: - //Debug.Assert(Value.GetType() == typeof(double)); writer.Write((double)Value); break; case DataType.Float: - //Debug.Assert(Value.GetType() == typeof(float)); writer.Write((float)Value); break; case DataType.Int32: if (Value.GetType() == typeof(Reference)) { + // Write function reference, rather than integer writer.WriteUndertaleObject((Reference)Value); break; } - //Debug.Assert(Value.GetType() == typeof(int)); writer.Write((int)Value); break; case DataType.Int64: - //Debug.Assert(Value.GetType() == typeof(long)); writer.Write((long)Value); break; case DataType.Boolean: - //Debug.Assert(Value.GetType() == typeof(bool)); writer.Write((bool)Value ? 1 : 0); break; case DataType.Variable: - //Debug.Assert(Value.GetType() == typeof(Reference)); writer.WriteUndertaleObject((Reference)Value); break; case DataType.String: - //Debug.Assert(Value.GetType() == typeof(UndertaleResourceById)); writer.WriteUndertaleObject((UndertaleResourceById)Value); break; case DataType.Int16: + // Data is encoded in the first two bytes of the instruction (was already written above) break; } - } + break; + } case InstructionType.CallInstruction: { + // Write number of arguments being used in call writer.Write(ArgumentsCount); + + // Write type (no second type is used) writer.Write((byte)Type1); - if (writer.Bytecode14OrLower && Kind == Opcode.Call) + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Call) writer.Write((byte)0xDA); else writer.Write((byte)Kind); + + // Write reference to the function being called writer.WriteUndertaleObject(Function); - } + break; + } case InstructionType.BreakInstruction: { - //Debug.Assert(Value.GetType() == typeof(short)); + // Write type of break instruction (encoded in Value) writer.Write((short)Value); + + // Write type (no second type is used) writer.Write((byte)Type1); + + // Write opcode writer.Write((byte)Kind); + + // Write integer argument, or function, if either is present if (Type1 == DataType.Int32) { if (Function != null) @@ -578,146 +634,177 @@ public void Serialize(UndertaleWriter writer) else writer.Write(IntArgument); } - } + break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {Kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } } /// public void Unserialize(UndertaleReader reader) { - long instructionStartAddress = reader.Position; - reader.Position += 3; // skip for now, we'll read them later - byte kind = reader.ReadByte(); - if (reader.Bytecode14OrLower) + // Flag tracking whether we're parsing bytecode 14 (old) instructions + bool bytecode14 = reader.Bytecode14OrLower; + + // Read first word from instruction + uint firstWord = reader.ReadUInt32(); + + // Read opcode from most significant byte + byte kindByte = (byte)((firstWord & 0xFF000000) >> 24); + Opcode kind = (Opcode)kindByte; + if (bytecode14) { - // Convert opcode to our enum - kind = ConvertInstructionKind(kind); + // Convert opcode from old format to new format + kind = (Opcode)ConvertInstructionKind(kindByte); } - Kind = (Opcode)kind; - reader.Position = instructionStartAddress; - switch (GetInstructionType(Kind)) + + // Extract first three bytes from first word + byte b0 = (byte)(firstWord & 0x000000FF); + byte b1 = (byte)((firstWord & 0x0000FF00) >> 8); + byte b2 = (byte)((firstWord & 0x00FF0000) >> 16); + + // Parse instruction contents + InstructionType instructionType = GetInstructionType(kind); + switch (instructionType) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: { - Extra = reader.ReadByte(); -#if DEBUG - if (Extra != 0 && Kind != Opcode.Dup && Kind != Opcode.CallV) - throw new IOException("Invalid padding in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); -#endif - ComparisonKind = (ComparisonType)reader.ReadByte(); - //if (!bytecode14 && (Kind == Opcode.Cmp) != ((byte)ComparisonKind != 0)) - // throw new IOException("Got unexpected comparison type in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture) + " (should be only in CMP)"); - byte TypePair = reader.ReadByte(); - Type1 = (DataType)(TypePair & 0xf); - Type2 = (DataType)(TypePair >> 4); -#if DEBUG - if (GetInstructionType(Kind) == InstructionType.SingleTypeInstruction && Type2 != (byte)0) - throw new IOException("Second type should be 0 in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); -#endif - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - if (reader.Bytecode14OrLower && Kind == Opcode.Cmp) - ComparisonKind = (ComparisonType)(reader.ReadByte() - 0x10); - else - reader.Position++; + // Parse instruction components from bytes + byte extra = b0; + ComparisonType comparisonKind = (ComparisonType)b1; + DataType type1 = (DataType)(b2 & 0xf); + DataType type2 = (DataType)(b2 >> 4); + + // Ensure basic conditions hold + if (extra != 0 && kind != Opcode.Dup && kind != Opcode.CallV) + { + throw new IOException($"Invalid padding in {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); + } - if (Kind == Opcode.And || Kind == Opcode.Or) + if (instructionType == InstructionType.SingleTypeInstruction && type2 != 0) { - if (Type1 == DataType.Boolean && Type2 == DataType.Boolean) - reader.undertaleData.ShortCircuit = false; + throw new IOException($"Second type should be 0 in {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } - } + + + // In bytecode 14, the comparison kind is encoded in the opcode itself + if (bytecode14 && kind == Opcode.Cmp) + { + comparisonKind = (ComparisonType)(kindByte - 0x10); + } + + // Check for "and.b.b" or "or.b.b", which imply the code was compiled without short-circuiting + if ((kind is Opcode.And or Opcode.Or) && type1 == DataType.Boolean && type2 == DataType.Boolean) + { + reader.undertaleData.ShortCircuit = false; + } + + // Assign to instruction + Extra = extra; + ComparisonKind = comparisonKind; + Type1 = type1; + Type2 = type2; + break; + } case InstructionType.GotoInstruction: { - if (reader.Bytecode14OrLower) + if (bytecode14) { - JumpOffset = reader.ReadInt24(); - if (JumpOffset == -1048576) // magic? encoded in little endian as 00 00 F0, which is like below - JumpOffsetPopenvExitMagic = true; - reader.Position++; + // Bytecode 14 has slightly different parsing + int jumpOffset = b0 | (b1 << 8) | ((sbyte)b2 << 16); + JumpOffset = jumpOffset; + JumpOffsetPopenvExitMagic = (jumpOffset == -1048576); // encoded in little endian as 00 00 F0 (same as below) break; } - uint v = reader.ReadUInt24(); - - JumpOffsetPopenvExitMagic = (v & 0x800000) != 0; + // Parse normally + uint v = (uint)(b0 | (b1 << 8) | (b2 << 16)); + bool popenvExitMagic = (v & 0x800000) != 0; + if (popenvExitMagic && v != 0xF00000) + { + throw new Exception("Popenv magic doesn't work, call issue #90 again"); + } // The rest is int23 signed value, so make sure uint r = v & 0x003FFFFF; -#if DEBUG - if (JumpOffsetPopenvExitMagic && v != 0xF00000) - throw new Exception("Popenv magic doesn't work, call issue #90 again"); - else -#endif + if ((v & 0x00C00000) != 0) { - if ((v & 0x00C00000) != 0) - r |= 0xFFC00000; - JumpOffset = (int)r; + r |= 0xFFC00000; } - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - } + // Assign to instruction + JumpOffset = (int)r; + JumpOffsetPopenvExitMagic = popenvExitMagic; + break; + } case InstructionType.PopInstruction: { - TypeInst = (InstanceType)reader.ReadInt16(); - byte TypePair = reader.ReadByte(); - Type1 = (DataType)(TypePair & 0xf); - Type2 = (DataType)(TypePair >> 4); - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - if (Type1 == DataType.Int16) + // Parse instruction components from bytes + InstanceType typeInst = (InstanceType)(b0 | (b1 << 8)); + DataType type1 = (DataType)(b2 & 0xf); + DataType type2 = (DataType)(b2 >> 4); + + if (type1 == DataType.Int16) { - // Special scenario - the swap instruction - // TODO: Figure out the proper syntax, see #129 - SwapExtra = (ushort)TypeInst; - TypeInst = 0; + // Special scenario - the swap instruction (see #129) + SwapExtra = (ushort)typeInst; + typeInst = 0; } else { + // Destination is an actual variable Destination = reader.ReadUndertaleObject>(); } - } + + // Assign remaining values to instruction + TypeInst = typeInst; + Type1 = type1; + Type2 = type2; + break; + } case InstructionType.PushInstruction: { - short val = reader.ReadInt16(); - Type1 = (DataType)reader.ReadByte(); - if (reader.Bytecode14OrLower) + // Parse instruction components from bytes + short val = (short)(b0 | (b1 << 8)); + DataType type1 = (DataType)b2; + + // Modify opcode of instruction, if in bytecode 14 + if (bytecode14) { - if (Type1 == DataType.Variable) + if (type1 == DataType.Variable) { switch (val) { case -5: - Kind = Opcode.PushGlb; + kind = Opcode.PushGlb; break; case -6: // builtin - Kind = Opcode.PushBltn; + kind = Opcode.PushBltn; break; case -7: - Kind = Opcode.PushLoc; + kind = Opcode.PushLoc; break; } } - else if (Type1 == DataType.Int16) + else if (type1 == DataType.Int16) { - Kind = Opcode.PushI; + kind = Opcode.PushI; } } - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - switch (Type1) + + // Parse data being pushed + switch (type1) { case DataType.Double: Value = reader.ReadDouble(); @@ -732,7 +819,7 @@ public void Unserialize(UndertaleReader reader) Value = reader.ReadInt64(); break; case DataType.Boolean: - Value = (reader.ReadUInt32() == 1); // TODO: double check + Value = reader.ReadBoolean(); break; case DataType.Variable: TypeInst = (InstanceType)val; @@ -745,122 +832,152 @@ public void Unserialize(UndertaleReader reader) Value = val; break; } - } + + // Assign remaining values to instruction + Type1 = type1; + break; + } case InstructionType.CallInstruction: { - ArgumentsCount = reader.ReadUInt16(); - Type1 = (DataType)reader.ReadByte(); - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; + // Parse instruction components from bytes + ArgumentsCount = (ushort)(b0 | (b1 << 8)); + Type1 = (DataType)b2; + + // Parse function being called Function = reader.ReadUndertaleObject>(); - } + break; + } case InstructionType.BreakInstruction: { - Value = reader.ReadInt16(); - Type1 = (DataType)reader.ReadByte(); - if (reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - if (Type1 == DataType.Int32) + // Parse instruction components from bytes + short value = (short)(b0 | (b1 << 8)); + DataType type1 = (DataType)b2; + + // Parse integer argument, if provided + if (type1 == DataType.Int32) { IntArgument = reader.ReadInt32(); + + // Existence of this argument implies GameMaker 2023.8 or above if (!reader.undertaleData.IsVersionAtLeast(2023, 8)) + { reader.undertaleData.SetGMS2Version(2023, 8); + } } - if (reader.undertaleData.IsVersionAtLeast(2, 3)) + + // If this is a chknullish instruction (ID -10), then this implies GameMaker 2.3.7 or above + if (value == -10 && reader.undertaleData.IsVersionAtLeast(2, 3)) { - if ((short)Value == -10) // chknullish instruction, added in 2.3.7 + if (!reader.undertaleData.IsVersionAtLeast(2, 3, 7)) { - if (!reader.undertaleData.IsVersionAtLeast(2, 3, 7)) - reader.undertaleData.SetGMS2Version(2, 3, 7); + reader.undertaleData.SetGMS2Version(2, 3, 7); } } - } + + // Assign remaining values to instruction + Value = value; + Type1 = type1; + break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {Kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } + + // Assign final opcode to instruction + Kind = kind; } + /// 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) + // Flag tracking whether we're parsing bytecode 14 (old) instructions + bool bytecode14 = reader.Bytecode14OrLower; + + // Read first word from instruction + uint firstWord = reader.ReadUInt32(); + + // Read opcode from most significant byte + Opcode kind = (Opcode)((firstWord & 0xFF000000) >> 24); + if (bytecode14) { - // Convert opcode to our enum - kind = ConvertInstructionKind(kind); + // Convert opcode from old format to new format + kind = (Opcode)ConvertInstructionKind((byte)kind); } - Opcode Kind = (Opcode)kind; - reader.Position = instructionStartAddress; - switch (GetInstructionType(Kind)) + + // Extract third byte from first word + byte b2 = (byte)((firstWord & 0x00FF0000) >> 16); + + // Parse instruction contents + InstructionType instructionType = GetInstructionType(kind); + switch (instructionType) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: case InstructionType.GotoInstruction: - reader.Position += 4; + // No special handling required break; case InstructionType.PopInstruction: - reader.Position += 2; // "TypeInst" - int type1 = reader.ReadByte() & 0xf; - if (type1 != 0x0f) + { + // Skip destination of pop instruction, if present + DataType type1 = (DataType)(b2 & 0xf); + if (type1 != DataType.Int16) { - reader.Position += 1 + 4; + reader.Position += 4; return 1; // "Destination" } - else - reader.Position++; break; + } case InstructionType.PushInstruction: + { + // Skip value being pushed, if present + DataType type1 = (DataType)(b2 & 0xf); + switch (type1) { - reader.Position += 2; - DataType Type1 = (DataType)reader.ReadByte(); - reader.Position++; - switch (Type1) - { - case DataType.Double: - case DataType.Int64: - reader.Position += 8; - break; + 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.Float: + case DataType.Int32: + case DataType.Boolean: + reader.Position += 4; + break; - case DataType.Variable: - case DataType.String: - reader.Position += 4; - return 1; - } + case DataType.Variable: + case DataType.String: + reader.Position += 4; + return 1; } break; + } case InstructionType.CallInstruction: - reader.Position += 8; + reader.Position += 4; return 1; // "Function" case InstructionType.BreakInstruction: + { + // Skip past integer argument, if present + DataType type1 = (DataType)(b2 & 0xf); + if (type1 == DataType.Int32) { - reader.Position += 2; - DataType Type1 = (DataType)reader.ReadByte(); - if (Type1 == DataType.Int32) - reader.Position += 5; - else - reader.Position += 1; - break; + reader.Position += 4; } + break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } return 0; @@ -981,8 +1098,7 @@ public void ToString(StringBuilder stringBuilder, UndertaleCode code, List sbh.Append(stringBuilder, ' '); if (Type1 == DataType.Int16) { - // Special scenario - the swap instruction - // TODO: Figure out the proper syntax, see #129 + // Special scenario - the swap instruction (see #129) sbh.Append(stringBuilder, SwapExtra); sbh.Append(stringBuilder, " ;;; this is a weird swap instruction, see #129"); }