Files
gen1recomp/tools/gen_firered_anim_names.py
bryanthaboi 96d23ab93b battle stuff
2026-09-18 13:11:33 -04:00

485 lines
18 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import os
import re
import sys
ROM_BASE = 0x08000000
DEFAULT_PRET = os.path.expanduser("~/Documents/development/pokefirered")
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VERSIONS = os.path.join(REPO, "src", "import", "gba", "versions.lua")
MOVES_TABLE = 0x1C68F4
OPCODES = {
"loadspritegfx": 0x00, "unloadspritegfx": 0x01, "createsprite": 0x02,
"createvisualtask": 0x03, "delay": 0x04, "waitforvisualfinish": 0x05,
"nop": 0x06, "nop2": 0x07, "end": 0x08, "playse": 0x09, "monbg": 0x0A,
"clearmonbg": 0x0B, "setalpha": 0x0C, "blendoff": 0x0D, "call": 0x0E,
"return": 0x0F, "setarg": 0x10, "choosetwoturnanim": 0x11,
"jumpifmoveturn": 0x12, "goto": 0x13, "fadetobg": 0x14, "restorebg": 0x15,
"waitbgfadeout": 0x16, "waitbgfadein": 0x17, "changebg": 0x18,
"playsewithpan": 0x19, "setpan": 0x1A, "panse": 0x1B,
"loopsewithpan": 0x1C, "waitplaysewithpan": 0x1D, "setbldcnt": 0x1E,
"createsoundtask": 0x1F, "waitsound": 0x20, "jumpargeq": 0x21,
"monbg_static": 0x22, "clearmonbg_static": 0x23, "jumpifcontest": 0x24,
"fadetobgfromset": 0x25, "panse_adjustnone": 0x26, "panse_adjustall": 0x27,
"splitbgprio": 0x28, "splitbgprio_all": 0x29, "splitbgprio_foes": 0x2A,
"invisible": 0x2B, "visible": 0x2C, "teamattack_moveback": 0x2D,
"teamattack_movefwd": 0x2E, "stopsound": 0x2F,
}
FIXED = {
0x00: 3, 0x01: 3, 0x04: 2, 0x05: 1, 0x06: 1, 0x07: 1, 0x08: 1, 0x09: 3,
0x0A: 2, 0x0B: 2, 0x0C: 3, 0x0D: 1, 0x0E: 5, 0x0F: 1, 0x10: 4, 0x11: 9,
0x12: 6, 0x13: 5, 0x14: 2, 0x15: 1, 0x16: 1, 0x17: 1, 0x18: 2, 0x19: 4,
0x1A: 2, 0x1B: 7, 0x1C: 6, 0x1D: 5, 0x1E: 3, 0x20: 1, 0x21: 8, 0x22: 2,
0x23: 2, 0x24: 5, 0x25: 4, 0x26: 7, 0x27: 7, 0x28: 2, 0x29: 1, 0x2A: 2,
0x2B: 2, 0x2C: 2, 0x2D: 2, 0x2E: 2, 0x2F: 1,
}
TABLES = [
("moves", "gBattleAnims_Moves"),
("status", "gBattleAnims_StatusConditions"),
("general", "gBattleAnims_General"),
("special", "gBattleAnims_Special"),
]
def split_args(s):
out, depth, cur = [], 0, ""
for ch in s:
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if ch == "," and depth == 0:
out.append(cur.strip())
cur = ""
else:
cur += ch
if cur.strip():
out.append(cur.strip())
return out
def parse_scripts(path, revision=0):
stream, labels, tables = [], {}, {}
cur_table = None
cond = []
with open(path) as f:
lines = f.readlines()
for lineno, raw in enumerate(lines, 1):
line = raw.split("@", 1)[0].rstrip()
comment = raw.split("@", 1)[1].strip() if "@" in raw else ""
s = line.strip()
if not s or s.startswith("#"):
continue
m = re.match(r"^\.if\s+REVISION\s*>=\s*(\S+)$", s)
if m:
cond.append(revision >= int(m.group(1), 0))
continue
if s == ".else":
cond[-1] = not cond[-1]
continue
if s == ".endif":
cond.pop()
continue
if cond and not all(cond):
continue
m = re.match(r"^(\w+)\s*::?\s*$", s)
if m:
name = m.group(1)
if name.startswith("gBattleAnims_"):
cur_table = name
tables[name] = []
else:
cur_table = None
labels[name] = len(stream)
continue
if s.startswith("."):
if s.startswith(".4byte") and cur_table:
tables[cur_table].append((s.split(None, 1)[1].strip(), comment))
elif s.startswith(".align") or s.startswith(".2byte") or s.startswith(".include") or s.startswith(".section"):
pass
else:
raise SystemExit("unhandled directive %s:%d %s" % (path, lineno, s))
continue
parts = s.split(None, 1)
mnem = parts[0]
args = split_args(parts[1]) if len(parts) > 1 else []
if mnem == "jumpreteq":
mnem, args = "jumpargeq", ["ARG_RET_ID", args[0], args[1]]
elif mnem == "jumprettrue":
mnem, args = "jumpargeq", ["ARG_RET_ID", "TRUE", args[0]]
elif mnem == "jumpretfalse":
mnem, args = "jumpargeq", ["ARG_RET_ID", "FALSE", args[0]]
if mnem not in OPCODES:
raise SystemExit("unknown mnemonic %s:%d %s" % (path, lineno, mnem))
stream.append({"m": mnem, "a": args, "line": lineno})
return stream, labels, tables
class Rom:
def __init__(self, path):
with open(path, "rb") as f:
self.b = f.read()
def u8(self, o):
return self.b[o]
def u16(self, o):
return self.b[o] | (self.b[o + 1] << 8)
def u32(self, o):
return self.u16(o) | (self.u16(o + 2) << 16)
def off(self, ptr):
o = ptr - ROM_BASE
if 0 <= o < len(self.b):
return o
return None
def rom_len(rom, o):
op = rom.u8(o)
if op in (0x02, 0x03):
return 7 + rom.u8(o + 6) * 2
if op == 0x1F:
return 6 + rom.u8(o + 5) * 2
if op not in FIXED:
raise ValueError("bad opcode 0x%02X at 0x%X" % (op, o))
return FIXED[op]
def align(rom, stream, labels, tables, table_addrs):
templates, tasks, soundtasks, errors = {}, {}, {}, []
seen = {}
work = []
seg = {}
roots = []
for key, tname in TABLES:
base = table_addrs[key]
for i, (lab, _) in enumerate(tables[tname]):
ptr = rom.u32(base + i * 4)
o = rom.off(ptr)
if o is None:
errors.append("%s[%d] bad ptr 0x%08X" % (tname, i, ptr))
continue
if lab not in labels:
if lab == "Move_COUNT":
continue
errors.append("missing label %s" % lab)
continue
work.append((o, labels[lab], "%s:%d" % (key, i), lab))
roots.append(("%s:%d" % (key, i), o))
def pair(store, addr, name, where):
prev = store.get(addr)
if prev and prev != name:
errors.append("addr 0x%08X named %s and %s (%s)" % (addr, prev, name, where))
store[addr] = name
def branch(ptr, lab, owner, where, src):
o = rom.off(ptr)
if o is None or lab not in labels:
errors.append("bad branch %s -> %s at %s" % (lab, hex(ptr), where))
return
seg[src]["succ"].append(o)
work.append((o, labels[lab], owner, lab))
while work:
o, idx, owner, lab = work.pop()
key = o
if key in seen:
if seen[key][0] != idx:
errors.append("rom 0x%X reached as %s and stream %d" % (o, lab, seen[key][0]))
seen[key][1].add(owner)
continue
seen[key] = (idx, {owner})
seg[o] = {"t": set(), "k": set(), "succ": []}
pos = o
while True:
if idx >= len(stream):
errors.append("ran off stream from %s" % lab)
break
cmd = stream[idx]
want = OPCODES[cmd["m"]]
op = rom.u8(pos)
where = "%s line %d rom 0x%X" % (lab, cmd["line"], pos)
if op != want:
errors.append("opcode mismatch %s: rom 0x%02X pret %s" % (where, op, cmd["m"]))
break
a = cmd["a"]
if op == 0x02:
if rom.u8(pos + 6) != len(a) - 3:
errors.append("argc mismatch %s" % where)
pair(templates, rom.u32(pos + 1), a[0], where)
seg[o]["t"].add(a[0])
elif op == 0x03:
if rom.u8(pos + 6) != len(a) - 2:
errors.append("argc mismatch %s" % where)
pair(tasks, rom.u32(pos + 1), a[0], where)
seg[o]["k"].add(a[0])
elif op == 0x1F:
if rom.u8(pos + 5) != len(a) - 1:
errors.append("argc mismatch %s" % where)
pair(soundtasks, rom.u32(pos + 1), a[0], where)
seg[o]["k"].add(a[0])
elif op == 0x04 and re.match(r"^-?\d+$", a[0]):
if rom.u8(pos + 1) != int(a[0]) & 0xFF:
errors.append("delay mismatch %s" % where)
elif op in (0x0E, 0x13, 0x24):
branch(rom.u32(pos + 1), a[0], owner, where, o)
elif op == 0x21:
branch(rom.u32(pos + 4), a[2], owner, where, o)
elif op == 0x12:
branch(rom.u32(pos + 2), a[1], owner, where, o)
elif op == 0x11:
branch(rom.u32(pos + 1), a[0], owner, where, o)
branch(rom.u32(pos + 5), a[1], owner, where, o)
if op in (0x08, 0x0F, 0x13):
break
pos += rom_len(rom, pos)
idx += 1
usage_t, usage_k = {}, {}
for owner, r in roots:
stack, vis = [r], set()
while stack:
x = stack.pop()
if x in vis or x not in seg:
continue
vis.add(x)
for t in seg[x]["t"]:
usage_t.setdefault(t, set()).add(owner)
for k in seg[x]["k"]:
usage_k.setdefault(k, set()).add(owner)
stack.extend(seg[x]["succ"])
return templates, tasks, soundtasks, errors, seen, usage_t, usage_k
def find_template_defs(pret):
defs = {}
srcdir = os.path.join(pret, "src")
for fn in sorted(os.listdir(srcdir)):
if not fn.endswith(".c"):
continue
text = open(os.path.join(srcdir, fn)).read()
for m in re.finditer(r"SpriteTemplate\s+(\w+)\s*=\s*\{(.*?)\};", text, re.S):
body = m.group(2)
cb = re.search(r"\.callback\s*=\s*(\w+)", body)
tile = re.search(r"\.tileTag\s*=\s*(\w+)", body)
if not cb:
fields = split_args(re.sub(r"/\*.*?\*/|//[^\n]*", "", body, flags=re.S))
cbname = fields[6].strip() if len(fields) >= 7 else None
tilename = fields[0].strip() if fields else None
else:
cbname = cb.group(1)
tilename = tile.group(1) if tile else None
line = text.count("\n", 0, m.start()) + 1
defs[m.group(1)] = {"callback": cbname, "tileTag": tilename, "file": "src/" + fn, "line": line}
return defs
def find_func_defs(pret, names):
out = {}
srcdir = os.path.join(pret, "src")
want = set(names)
for fn in sorted(os.listdir(srcdir)):
if not fn.endswith(".c"):
continue
lines = open(os.path.join(srcdir, fn)).read().split("\n")
for i, l in enumerate(lines):
m = re.match(r"^(?:static\s+)?(?:void|u8|bool8|u16|s16)\s+(\w+)\s*\(([^;]*)$", l)
if m and m.group(1) in want and m.group(1) not in out:
nxt = lines[i + 1].strip() if i + 1 < len(lines) else ""
if l.rstrip().endswith("{") or nxt.startswith("{"):
out[m.group(1)] = "src/%s:%d" % (fn, i + 1)
return out
def tag_values(pret):
vals = {}
for l in open(os.path.join(pret, "include", "constants", "battle_anim.h")):
m = re.match(r"#define\s+(ANIM_TAG_\w+)\s+\(ANIM_SPRITES_START\s*\+\s*(\d+)\)", l)
if m:
vals[m.group(1)] = 10000 + int(m.group(2))
vals["ANIM_TAG_NONE"] = 0
vals["TAG_NONE"] = 0xFFFF
return vals
def short_cb(n):
if re.match(r"^Anim[A-Z]", n):
return n[4:]
return n
def short_task(n):
if n.startswith("AnimTask_"):
return n[len("AnimTask_"):]
return n
def lua_table(name, d):
rows = ["Versions.%s = {" % name]
for k in sorted(d):
rows.append(' [0x%08X] = "%s",' % (k, d[k]))
rows.append("}")
return "\n".join(rows)
def lua_names(name, lst):
rows = ["Versions.%s = {" % name]
for i, n in enumerate(lst):
rows.append(' [%d] = "%s",' % (i, n))
rows.append("}")
return "\n".join(rows)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--rom", required=True)
ap.add_argument("--pret", default=DEFAULT_PRET)
ap.add_argument("--write", action="store_true")
ap.add_argument("--json")
args = ap.parse_args()
rom = Rom(args.rom)
stream, labels, tables = parse_scripts(os.path.join(args.pret, "data", "battle_anim_scripts.s"))
addrs = {"moves": MOVES_TABLE}
cur = MOVES_TABLE
for key, tname in TABLES:
addrs[key] = cur
cur += 4 * len(tables[tname])
templates, tasks, soundtasks, errors, seen, usage_t, usage_k = align(rom, stream, labels, tables, addrs)
defs = find_template_defs(args.pret)
tagv = tag_values(args.pret)
callbacks = {}
tmpl_info = {}
for addr, tname in templates.items():
d = defs.get(tname)
if not d:
errors.append("no C definition for %s" % tname)
continue
o = rom.off(addr)
cbptr = rom.u32(o + 20)
tile = rom.u16(o)
if d["tileTag"] in tagv and tagv[d["tileTag"]] != tile:
errors.append("tileTag mismatch %s rom %d pret %s" % (tname, tile, d["tileTag"]))
prev = callbacks.get(cbptr)
if prev and prev != d["callback"]:
errors.append("callback 0x%08X named %s and %s" % (cbptr, prev, d["callback"]))
callbacks[cbptr] = d["callback"]
tmpl_info[tname] = {"addr": addr, "callback": d["callback"], "file": d["file"], "line": d["line"]}
alltasks = dict(tasks)
for k, v in soundtasks.items():
if k in alltasks and alltasks[k] != v:
errors.append("task/sound clash 0x%08X" % k)
alltasks[k] = v
def check_injective(d, fn, label):
rev = {}
for k, v in d.items():
s = fn(v)
if s in rev and rev[s] != v:
errors.append("%s short-name collision %s: %s / %s" % (label, s, rev[s], v))
rev[s] = v
for lbl, d in (("template", templates), ("callback", callbacks), ("task", alltasks)):
rev = {}
for k, v in d.items():
if v in rev and rev[v] != k:
errors.append("%s %s at two addresses 0x%08X 0x%08X" % (lbl, v, rev[v], k))
rev[v] = k
check_injective(callbacks, short_cb, "callback")
check_injective(alltasks, short_task, "task")
for addr in list(callbacks) + list(alltasks):
if addr & 1 == 0 or rom.off(addr) is None:
errors.append("non-thumb/non-rom fn 0x%08X" % addr)
names = {}
for key, tname in TABLES:
if key == "moves":
continue
lst = []
for lab, comment in tables[tname]:
m = re.match(r"B_ANIM_(\w+)", comment)
if not m:
errors.append("no B_ANIM_ comment for %s" % lab)
lst.append(lab)
else:
lst.append(m.group(1))
names[key] = lst
funcs = find_func_defs(args.pret, list(callbacks.values()) + list(alltasks.values()))
unnamed_cb = [a for a in callbacks if not callbacks[a]]
missing_src = sorted(set(n for n in list(callbacks.values()) + list(alltasks.values()) if n not in funcs))
print("tables: moves=0x%X status=0x%X general=0x%X special=0x%X" % (
addrs["moves"], addrs["status"], addrs["general"], addrs["special"]))
print("counts: moves=%d status=%d general=%d special=%d" % tuple(
len(tables[t]) for _, t in TABLES))
print("script entry points aligned: %d" % len(seen))
print("templates=%d callbacks=%d tasks=%d soundtasks=%d" % (
len(templates), len(callbacks), len(tasks), len(soundtasks)))
print("unnamed: templates=0 callbacks=%d tasks=0" % len(unnamed_cb))
if missing_src:
print("no C definition line found for: %s" % ", ".join(missing_src))
if errors:
print("ERRORS (%d):" % len(errors))
for e in errors[:80]:
print(" " + e)
sys.exit(1)
print("alignment OK, 0 errors")
if args.json:
owners_t = {k: sorted(v) for k, v in usage_t.items()}
owners_k = {k: sorted(v) for k, v in usage_k.items()}
with open(args.json, "w") as f:
json.dump({
"tables": addrs, "names": names, "templates": tmpl_info,
"templateUsage": owners_t, "taskUsage": owners_k,
"tasks": {("0x%08X" % k): v for k, v in alltasks.items()},
"callbacks": {("0x%08X" % k): v for k, v in callbacks.items()},
"funcDefs": funcs,
"moveLabels": [lab for lab, _ in tables["gBattleAnims_Moves"]],
}, f, indent=1, sort_keys=True)
block = "\n\n".join([
lua_table("ANIM_TEMPLATE_NAMES", templates),
lua_table("ANIM_CALLBACK_NAMES", {k: short_cb(v) for k, v in callbacks.items()}),
lua_table("ANIM_TASK_NAMES", {k: short_task(v) for k, v in alltasks.items()}),
lua_names("BATTLE_ANIM_STATUS_NAMES", names["status"]),
lua_names("BATTLE_ANIM_GENERAL_NAMES", names["general"]),
lua_names("BATTLE_ANIM_SPECIAL_NAMES", names["special"]),
]) + "\n"
if args.write:
src = open(VERSIONS).read()
start_marks = ["Versions.ANIM_TEMPLATE_NAMES = {\n", "Versions.ANIM_CALLBACK_NAMES = {\n"]
start = -1
for sm in start_marks:
start = src.find(sm)
if start >= 0:
break
end_mark = "\n-- Wild encounters (FireRed USA 1.0)"
end = src.find(end_mark)
assert start >= 0 and end > start, "versions.lua anchors not found"
assert src.count(end_mark) == 1
new = src[:start] + block + src[end:]
with open(VERSIONS, "w") as f:
f.write(new)
print("wrote %s" % VERSIONS)
else:
sys.stdout.write(block)
if __name__ == "__main__":
main()