- Add xscript-cli: Command-line validator for XScript definition files - Syntax checking and reference validation - Directory batch processing - Type listing and info commands - Add gml_disasm.py: Python disassembler for Avatar GML bytecode - Update tools.pro for new subprojects Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
69 lines
3.5 KiB
Python
69 lines
3.5 KiB
Python
#!/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())
|