XPlor/tools/gml_disasm.py

69 lines
3.5 KiB
Python
Raw Normal View History

#!/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+1<len(pos_list) else len(self.data)
idx = struct.unpack(">I", 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<len(self.strings) else f"func_{idx}"
self.functions.append(Function(idx, nm, p, l, self.data[pos+24:nxt]))
def get_str(self, i): return self.strings[i] if i<len(self.strings) else f"str_{i}"
def disasm(self, f):
out = [f"// {f.name} (params={f.params}, locals={f.locals})", f"function {f.name}() {{"]
bc, p = f.bytecode, (4 if len(f.bytecode)>=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+7<len(bc):
arg = struct.unpack(">I", 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 <file.gml>"); sys.exit(1)
with open(sys.argv[1], "rb") as f: d = f.read()
print(GMLDisassembler(d).disasm_all())