#!/usr/bin/env python3 """GML Bytecode Disassembler for Avatar: The Last Airbender""" import struct, sys from dataclasses import dataclass OPCODES = { 0x00: ("PUSH_0", False), 0x01: ("PUSH_1", False), 0x02: ("PUSH_2", False), 0x03: ("PUSH_3", False), 0x04: ("PUSH_4", False), 0x05: ("PUSH_5", False), 0x0D: ("RETURN", False), 0x13: ("END_BLOCK", False), 0x14: ("NOT", False), 0x17: ("NEG", False), 0x19: ("DUP", False), 0x1F: ("POP", False), 0x23: ("ADD", False), 0x24: ("SUB", False), 0x25: ("MUL", False), 0x26: ("DIV", False), 0x2C: ("EQ", False), 0x2D: ("NE", False), 0x2E: ("LT", False), 0x2F: ("GT", False), 0x34: ("AND", False), 0x35: ("OR", False), 0x28: ("GET_VAR", True), 0x33: ("SET_VAR", True), 0x1A: ("PUSH_STR", True), 0x29: ("GET_ARG", True), 0x2A: ("GET_LOCAL", True), 0x2B: ("SET_LOCAL", True), 0x32: ("INIT_VAR", True), 0x1B: ("JUMP", True), 0x20: ("JMP_FALSE", True), 0x21: ("JMP_TRUE", True), 0x31: ("CALL", True), } @dataclass class Function: index: int; name: str; params: int; locals: int; bytecode: bytes class GMLDisassembler: def __init__(self, data): self.data, self.strings, self.functions = data, [], [] self._parse() def _parse(self): self.string_table_size = struct.unpack(">I", self.data[20:24])[0] st_end = 24 + self.string_table_size cur = b"" for b in self.data[24:st_end]: if b == 0: if cur: self.strings.append(cur.decode("ascii", errors="replace")) cur = b"" else: cur += bytes([b]) pos_list = [i for i in range(st_end, len(self.data)-4) if self.data[i:i+4]==b"cnuf"] for i, pos in enumerate(pos_list): nxt = pos_list[i+1] if i+1I", self.data[pos+4:pos+8])[0] p = struct.unpack(">I", self.data[pos+12:pos+16])[0] l = struct.unpack(">I", self.data[pos+16:pos+20])[0] nm = self.strings[idx] if idx=4 and struct.unpack(">I",f.bytecode[0:4])[0]>=0x100 else 0) while p < len(bc)-3: op = struct.unpack(">I", bc[p:p+4])[0] if op in OPCODES: nm, has = OPCODES[op] if has and p+7I", bc[p+4:p+8])[0]; p += 8 if nm in ("GET_VAR","SET_VAR","PUSH_STR","CALL"): out.append(f" {nm} {self.get_str(arg)}") elif "JUMP" in nm or "JMP" in nm: out.append(f" {nm} @{arg}") else: out.append(f" {nm} {arg}") else: p += 4; out.append(f" {nm}") elif op < 0x100: out.append(f" OP_{op:02X}"); p += 4 else: p += 4 out.append("}"); return "\n".join(out) def disasm_all(self): return f"// GML: {len(self.functions)} funcs, {len(self.strings)} strings\n\n" + "\n\n".join(self.disasm(f) for f in self.functions) if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: gml_disasm.py "); sys.exit(1) with open(sys.argv[1], "rb") as f: d = f.read() print(GMLDisassembler(d).disasm_all())