mirror of
https://github.com/pret/poketcg.git
synced 2026-04-23 05:17:22 -05:00
most useful when parsing many starting addresses at once and wanting to get as much valid output as possible
394 lines
18 KiB
Python
Executable File
394 lines
18 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
import argparse
|
|
|
|
from constants import boosters
|
|
from constants import cards
|
|
from constants import decks
|
|
from constants import directions
|
|
from constants import events
|
|
from constants import maps
|
|
from constants import npcs
|
|
from constants import sfxs
|
|
from constants import songs
|
|
|
|
args = None
|
|
rom = None
|
|
|
|
# script command names and parameter lists
|
|
script_commands = {
|
|
0xe7: { "name": "start_script", "params": [] },
|
|
|
|
0x00: { "name": "end_script_loop", "params": [] },
|
|
0x01: { "name": "close_advanced_text_box", "params": [] },
|
|
0x02: { "name": "print_text_string", "params": [ "text" ] },
|
|
0x03: { "name": "Func_ccdc", "params": [ "text" ] }, # print text and ???
|
|
0x04: { "name": "ask_question_jump", "params": [ "text", "label" ] },
|
|
0x05: { "name": "start_battle", "params": [ "prizes", "deck", "song" ] },
|
|
0x06: { "name": "print_variable_text", "params": [ "text", "text" ] },
|
|
0x07: { "name": "Func_cda8", "params": [ "text", "text" ] }, # print variable text and ???
|
|
0x08: { "name": "print_text_quit_fully", "params": [ "text" ] },
|
|
0x09: { "name": "Func_cdcb", "params": [] },
|
|
0x0a: { "name": "move_active_npc_by_direction", "params": [ "movement_table" ] },
|
|
0x0b: { "name": "close_text_box", "params": [] },
|
|
0x0c: { "name": "give_booster_packs", "params": [ "booster", "booster", "booster" ] },
|
|
0x0d: { "name": "jump_if_card_owned", "params": [ "card", "label" ] },
|
|
0x0e: { "name": "jump_if_card_in_collection", "params": [ "card", "label" ] },
|
|
0x0f: { "name": "give_card", "params": [ "card" ] },
|
|
0x10: { "name": "take_card", "params": [ "card" ] },
|
|
0x11: { "name": "Func_cf53", "params": [ "label" ] }, # jump if any energy cards in collection
|
|
0x12: { "name": "Func_cf7b", "params": [] }, # remove all energy cards from collection
|
|
0x13: { "name": "jump_if_enough_cards_owned", "params": [ "word_decimal", "label" ] },
|
|
0x14: { "name": "fight_club_pupil_jump", "params": [ "label", "label", "label", "label", "label" ] },
|
|
0x15: { "name": "Func_cfc6", "params": [ "byte" ] },
|
|
0x16: { "name": "Func_cfd4", "params": [] },
|
|
0x17: { "name": "Func_d00b", "params": [] }, # get card name for EVENT_FLAG_2B
|
|
0x18: { "name": "Func_d025", "params": [ "label" ] }, # jump if card in EVENT_FLAG_2B is in collection or decks
|
|
0x19: { "name": "Func_d032", "params": [ "label" ] }, # jump if card in EVENT_FLAG_2B is in collection
|
|
0x1a: { "name": "Func_d03f", "params": [] }, # remove card in EVENT_FLAG_2B from collection
|
|
0x1b: { "name": "script_jump", "params": [ "label" ] },
|
|
0x1c: { "name": "try_give_medal_pc_packs", "params": [] },
|
|
0x1d: { "name": "set_player_direction", "params": [ "direction" ] },
|
|
0x1e: { "name": "move_player", "params": [ "direction", "byte_decimal" ] },
|
|
0x1f: { "name": "show_card_received_screen", "params": [ "card" ] },
|
|
0x20: { "name": "set_dialog_npc", "params": [ "npc" ] },
|
|
0x21: { "name": "set_next_npc_and_script", "params": [ "npc", "label" ] },
|
|
0x22: { "name": "Func_d095", "params": [ "byte", "byte", "byte" ] }, # LOADED_NPC_FIELD_05 and LOADED_NPC_FIELD_06
|
|
0x23: { "name": "Func_d0be", "params": [ "byte", "byte" ] }, # coords
|
|
0x24: { "name": "do_frames", "params": [ "byte_decimal" ] },
|
|
0x25: { "name": "Func_d0d9", "params": [ "byte", "byte", "label" ] }, # coords, jump if npc coords match
|
|
0x26: { "name": "jump_if_player_coords_match", "params": [ "byte_decimal", "byte_decimal", "label" ] },
|
|
0x27: { "name": "move_active_npc", "params": [ "movement" ] },
|
|
0x28: { "name": "give_one_of_each_trainer_booster", "params": [] },
|
|
0x29: { "name": "Func_d103", "params": [ "npc", "label" ] }, # jump if npc loaded
|
|
0x2a: { "name": "Func_d125", "params": [ "event" ] }, # give medal
|
|
0x2b: { "name": "Func_d135", "params": [ "byte" ] }, # load current map name into tx ram slot
|
|
0x2c: { "name": "Func_d16b", "params": [ "byte" ] }, # load current npc name into tx ram slot
|
|
0x2d: { "name": "Func_cd4f", "params": [ "prizes", "deck", "song" ] },
|
|
0x2e: { "name": "Func_cd94", "params": [ "text", "text", "text" ] },
|
|
0x2f: { "name": "move_wram_npc", "params": [ "movement" ] },
|
|
0x30: { "name": "Func_cdd8", "params": [] },
|
|
0x31: { "name": "Func_cdf5", "params": [ "byte", "byte" ] }, # coords
|
|
0x32: { "name": "Func_d195", "params": [] }, # pick challenge hall opponent
|
|
0x33: { "name": "Func_d1ad", "params": [] }, # open menu
|
|
0x34: { "name": "Func_d1b3", "params": [] }, # pick trade card
|
|
0x35: { "name": "quit_script_fully", "params": [] },
|
|
0x36: { "name": "Func_d244", "params": [ "byte" ] },
|
|
0x37: { "name": "choose_deck_to_duel_against_multichoice", "params": [] },
|
|
0x38: { "name": "open_deck_machine", "params": [ "byte" ] },
|
|
0x39: { "name": "choose_starter_deck_multichoice", "params": [] },
|
|
0x3a: { "name": "enter_map", "params": [ "byte", "map", "byte_decimal", "byte_decimal", "direction" ] },
|
|
0x3b: { "name": "move_arbitrary_npc", "params": [ "npc", "movement" ] },
|
|
0x3c: { "name": "Func_d209", "params": [] }, # pick legendary card
|
|
0x3d: { "name": "Func_d38f", "params": [ "byte" ] },
|
|
0x3e: { "name": "Func_d396", "params": [ "byte" ] },
|
|
0x3f: { "name": "Func_cd76", "params": [] },
|
|
0x40: { "name": "Func_d39d", "params": [ "byte" ] },
|
|
0x41: { "name": "Func_d3b9", "params": [] },
|
|
0x42: { "name": "try_give_pc_pack", "params": [ "byte" ] },
|
|
0x43: { "name": "script_nop", "params": [] },
|
|
0x44: { "name": "Func_d3d4", "params": [] },
|
|
0x45: { "name": "Func_d3e0", "params": [] },
|
|
0x46: { "name": "Func_d3fe", "params": [ "song" ] },
|
|
0x47: { "name": "Func_d408", "params": [ "song" ] }, # set default song
|
|
0x48: { "name": "play_song", "params": [ "song" ] },
|
|
0x49: { "name": "play_sfx", "params": [ "sfx" ] },
|
|
0x4a: { "name": "pause_song", "params": [] },
|
|
0x4b: { "name": "resume_song", "params": [] },
|
|
0x4c: { "name": "Func_d41d", "params": [] }, # play default song
|
|
0x4d: { "name": "wait_for_song_to_finish", "params": [] },
|
|
0x4e: { "name": "Func_d435", "params": [ "byte" ] },
|
|
0x4f: { "name": "ask_question_jump_default_yes", "params": [ "text", "label" ] },
|
|
0x50: { "name": "show_sam_normal_multichoice", "params": [] },
|
|
0x51: { "name": "show_sam_tutorial_multichoice", "params": [] },
|
|
0x52: { "name": "Func_d43d", "params": [] },
|
|
0x53: { "name": "end_script_loop_2", "params": [] },
|
|
0x54: { "name": "end_script_loop_3", "params": [] },
|
|
0x55: { "name": "end_script_loop_4", "params": [] },
|
|
0x56: { "name": "end_script_loop_5", "params": [] },
|
|
0x57: { "name": "end_script_loop_6", "params": [] },
|
|
0x58: { "name": "script_set_flag_value", "params": [ "event", "byte" ] },
|
|
0x59: { "name": "jump_if_flag_zero_1", "params": [ "event", "label" ] },
|
|
0x5a: { "name": "jump_if_flag_nonzero_1", "params": [ "event", "label" ] },
|
|
0x5b: { "name": "jump_if_flag_equal", "params": [ "event", "byte", "label" ] },
|
|
0x5c: { "name": "jump_if_flag_not_equal", "params": [ "event", "byte", "label" ] },
|
|
0x5d: { "name": "jump_if_flag_not_less_than", "params": [ "event", "byte", "label" ] },
|
|
0x5e: { "name": "jump_if_flag_less_than", "params": [ "event", "byte", "label" ] },
|
|
0x5f: { "name": "max_out_flag_value", "params": [ "event" ] },
|
|
0x60: { "name": "zero_out_flag_value", "params": [ "event" ] },
|
|
0x61: { "name": "jump_if_flag_nonzero_2", "params": [ "event", "label"] },
|
|
0x62: { "name": "jump_if_flag_zero_2", "params": [ "event", "label" ] },
|
|
0x63: { "name": "increment_flag_value", "params": [ "event" ] },
|
|
0x64: { "name": "end_script_loop_7", "params": [] },
|
|
0x65: { "name": "end_script_loop_8", "params": [] },
|
|
0x66: { "name": "end_script_loop_9", "params": [] },
|
|
0x67: { "name": "end_script_loop_10", "params": [] },
|
|
}
|
|
|
|
quit_commands = [
|
|
0x00,
|
|
0x08,
|
|
0x1b,
|
|
0x35,
|
|
0x53,
|
|
0x54,
|
|
0x55,
|
|
0x56,
|
|
0x57,
|
|
0x64,
|
|
0x65,
|
|
0x66,
|
|
0x67,
|
|
]
|
|
|
|
# length in bytes of each type of parameter
|
|
param_lengths = {
|
|
"byte": 1,
|
|
"byte_decimal": 1,
|
|
"booster": 1,
|
|
"card": 1,
|
|
"deck": 1,
|
|
"direction": 1,
|
|
"event": 1,
|
|
"map": 1,
|
|
"npc": 1,
|
|
"prizes": 1,
|
|
"sfx": 1,
|
|
"song": 1,
|
|
"word_decimal": 2,
|
|
"movement": 2,
|
|
"movement_table": 2,
|
|
"text": 2,
|
|
"label": 2,
|
|
}
|
|
|
|
def get_bank(address):
|
|
return int(address / 0x4000)
|
|
|
|
def get_relative_address(address):
|
|
if address < 0x4000:
|
|
return address
|
|
return (address % 0x4000) + 0x4000
|
|
|
|
# get absolute pointer stored at an address in the rom
|
|
# if bank is None, assumes the pointer refers to the same bank as the bank it is located in
|
|
def get_pointer(address, bank=None):
|
|
raw_pointer = rom[address + 1] * 0x100 + rom[address]
|
|
if raw_pointer < 0x4000:
|
|
bank = 0
|
|
if bank is None:
|
|
bank = get_bank(address)
|
|
return (raw_pointer % 0x4000) + bank * 0x4000
|
|
|
|
def make_address_comment(address):
|
|
return " ; {:x} ({:x}:{:x})\n".format(address, get_bank(address), get_relative_address(address))
|
|
|
|
def make_blob(start, output, end=None):
|
|
return { "start": start, "output": output, "end": end if end else start }
|
|
|
|
def dump_movement(address):
|
|
blobs = []
|
|
blobs.append(make_blob(address, "NPCMovement_{:x}:".format(address) + make_address_comment(address)))
|
|
while 1:
|
|
movement = rom[address]
|
|
if movement == 0xff:
|
|
blobs.append(make_blob(address, "\tdb ${:02x}\n\n".format(movement), address + 1))
|
|
break
|
|
if movement == 0xfe:
|
|
jump = rom[address + 1]
|
|
if jump > 127:
|
|
jump -= 256
|
|
blobs.append(make_blob(address, "\tdb ${:02x}, {}\n\n".format(movement, jump), address + 2))
|
|
break
|
|
blobs.append(make_blob(address, "\tdb {}".format(directions[movement & 0b01111111]) + (" | NO_MOVE\n" if movement & 0b10000000 else "\n"), address + 1))
|
|
address += 1
|
|
return blobs
|
|
|
|
def dump_movement_table(address):
|
|
blobs = []
|
|
blobs.append(make_blob(address, "NPCMovementTable_{:x}:".format(address) + make_address_comment(address)))
|
|
for i in range(4):
|
|
pointer = get_pointer(address)
|
|
blobs.append(make_blob(address, "\tdw NPCMovement_{:x}\n".format(pointer) + ("\n" if i == 3 else ""), address + 2))
|
|
blobs += dump_movement(pointer)
|
|
address += 2
|
|
return blobs
|
|
|
|
# parse a script starting at the given address
|
|
# returns a list of all commands
|
|
def dump_script(start_address, address=None, visited=set()):
|
|
blobs = []
|
|
branches = set()
|
|
if address is None:
|
|
blobs.append(make_blob(start_address, "Script_{:x}:".format(start_address) + make_address_comment(start_address)))
|
|
address = start_address
|
|
else:
|
|
blobs.append(make_blob(address, ".ows_{:x}\n".format(address)))
|
|
if address in visited:
|
|
return blobs
|
|
visited.add(address)
|
|
while 1:
|
|
command_address = address
|
|
command_id = rom[command_address]
|
|
command = script_commands[command_id]
|
|
address += 1
|
|
macro_mode = not command["name"].startswith("Func_")
|
|
if macro_mode:
|
|
output = "\t{}".format(command["name"])
|
|
else:
|
|
output = "\trun_command {}".format(command["name"])
|
|
# print all params for current command
|
|
for i in range(len(command["params"])):
|
|
param = rom[address]
|
|
param_type = command["params"][i]
|
|
param_length = param_lengths[param_type]
|
|
if param_type == "byte":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " ${:02x}".format(param)
|
|
elif param_type == "byte_decimal":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(param)
|
|
elif param_type == "booster":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(boosters[param])
|
|
elif param_type == "card":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(cards[param])
|
|
elif param_type == "deck":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(decks[param])
|
|
elif param_type == "direction":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(directions[param])
|
|
elif param_type == "event":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(events[param])
|
|
elif param_type == "map":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(maps[param])
|
|
elif param_type == "npc":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(npcs[param])
|
|
elif param_type == "prizes":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " PRIZES_{}".format(param)
|
|
elif param_type == "sfx":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(sfxs[param])
|
|
elif param_type == "song":
|
|
if not macro_mode:
|
|
output += "\n\tdb"
|
|
output += " {}".format(songs[param])
|
|
elif param_type == "word_decimal":
|
|
if not macro_mode:
|
|
output += "\n\tdw"
|
|
output += " {}".format(param + rom[address + 1] * 0x100)
|
|
elif param_type == "movement":
|
|
param = get_pointer(address)
|
|
label = "NPCMovement_{:x}".format(param)
|
|
if not macro_mode:
|
|
output += "\n\tdw"
|
|
output += " {}".format(label)
|
|
blobs += dump_movement(param)
|
|
elif param_type == "movement_table":
|
|
param = get_pointer(address)
|
|
label = "NPCMovementTable_{:x}".format(param)
|
|
if not macro_mode:
|
|
output += "\n\tdw"
|
|
output += " {}".format(label)
|
|
blobs += dump_movement_table(param)
|
|
elif param_type == "text":
|
|
text_id = param + rom[address + 1] * 0x100
|
|
if not macro_mode:
|
|
if text_id == 0x0000:
|
|
output += "\n\tdw"
|
|
else:
|
|
output += "\n\ttx"
|
|
if text_id == 0x0000:
|
|
output += " NULL"
|
|
else:
|
|
output += " Text{:04x}".format(text_id)
|
|
elif param_type == "label":
|
|
param = get_pointer(address)
|
|
if param == 0x0000:
|
|
label = "NULL"
|
|
elif param == start_address:
|
|
label = "Script_{:x}".format(param)
|
|
else:
|
|
label = ".ows_{:x}".format(param)
|
|
if param > start_address or args.allow_backward_jumps:
|
|
branches.add(param)
|
|
if not macro_mode:
|
|
output += "\n\tdw"
|
|
output += " {}".format(label)
|
|
address += param_length
|
|
if macro_mode and i < len(command["params"]) - 1:
|
|
output += ","
|
|
output += "\n"
|
|
blobs.append(make_blob(command_address, output, address))
|
|
if command_id in quit_commands:
|
|
if rom[address] == 0xc9:
|
|
blobs.append(make_blob(address, "\tret\n", address + 1))
|
|
address += 1
|
|
blobs.append(make_blob(address, "; 0x{:x}\n\n".format(address)))
|
|
break
|
|
for branch in branches:
|
|
blobs += dump_script(start_address, branch, visited)
|
|
return blobs
|
|
|
|
def fill_gap(start, end):
|
|
output = ""
|
|
for address in range(start, end):
|
|
output += "\tdb ${:x}\n".format(rom[address])
|
|
output += "\n"
|
|
return output
|
|
|
|
def sort_and_filter(blobs):
|
|
blobs.sort(key=lambda b: (b["start"], b["end"], not b["output"].startswith(";")))
|
|
filtered = []
|
|
for blob, next in zip(blobs, blobs[1:]+[None]):
|
|
if next and blob["start"] == next["start"] and blob["output"] == next["output"]:
|
|
continue
|
|
if next and blob["end"] < next["start"] and get_bank(blob["end"]) == get_bank(next["start"]):
|
|
if args.fill_gaps:
|
|
blob["output"] += fill_gap(blob["end"], next["start"])
|
|
else:
|
|
blob["output"] += "; gap from 0x{:x} to 0x{:x}\n\n".format(blob["end"], next["start"])
|
|
filtered.append(blob)
|
|
if len(filtered) > 0:
|
|
filtered[-1]["output"] = filtered[-1]["output"].rstrip("\n")
|
|
return filtered
|
|
|
|
if __name__ == "__main__":
|
|
ap = argparse.ArgumentParser(description="Pokemon TCG Script Extractor")
|
|
ap.add_argument("-b", "--allow-backward-jumps", action="store_true", help="extract scripts that are found before the starting address")
|
|
ap.add_argument("-g", "--fill-gaps", action="store_true", help="use 'db's to fill the gaps between visited locations")
|
|
ap.add_argument("-i", "--ignore-errors", action="store_true", help="silently proceed to the next address if an error occurs")
|
|
ap.add_argument("-r", "--rom", default="baserom.gbc", help="rom file to extract script from")
|
|
ap.add_argument("addresses", nargs="+", help="addresses to extract from")
|
|
args = ap.parse_args()
|
|
rom = bytearray(open(args.rom, "rb").read())
|
|
blobs = []
|
|
for address in args.addresses:
|
|
try:
|
|
blobs += dump_script(int(address, 16))
|
|
except:
|
|
if not args.ignore_errors:
|
|
raise
|
|
blobs = sort_and_filter(blobs)
|
|
output = ""
|
|
for blob in blobs:
|
|
output += blob["output"]
|
|
print(output)
|