mirror of https://github.com/zeldaret/mm.git
267 lines
8.5 KiB
Python
Executable File
267 lines
8.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# MM Event Script Disassembler
|
|
#
|
|
|
|
import argparse, os, struct, math
|
|
from actor_symbols import resolve_symbol
|
|
|
|
cmd_info = [
|
|
('SCHEDULE_CMD_CHECK_WEEK_EVENT_REG_S', 0x04, '>Hb', (1, )),
|
|
('SCHEDULE_CMD_CHECK_WEEK_EVENT_REG_L', 0x05, '>Hh', (1, )),
|
|
('SCHEDULE_CMD_CHECK_TIME_RANGE_S', 0x06, '>BBBBb', (4, )),
|
|
('SCHEDULE_CMD_CHECK_TIME_RANGE_L', 0x07, '>BBBBh', (4, )),
|
|
('SCHEDULE_CMD_RET_VAL_L', 0x03, '>H', ( )),
|
|
('SCHEDULE_CMD_RET_NONE', 0x01, '', ( )),
|
|
('SCHEDULE_CMD_RET_EMPTY', 0x01, '', ( )),
|
|
('SCHEDULE_CMD_NOP', 0x04, '>BBB', ( )),
|
|
('SCHEDULE_CMD_CHECK_MISC_S', 0x03, '>Bb', (1, )),
|
|
('SCHEDULE_CMD_RET_VAL_S', 0x02, '>B', ( )),
|
|
('SCHEDULE_CMD_CHECK_NOT_IN_SCENE_S', 0x04, '>Hb', (1, )),
|
|
('SCHEDULE_CMD_CHECK_NOT_IN_SCENE_L', 0x05, '>Hh', (1, )),
|
|
('SCHEDULE_CMD_CHECK_NOT_IN_DAY_S', 0x04, '>Hb', (1, )),
|
|
('SCHEDULE_CMD_CHECK_NOT_IN_DAY_L', 0x05, '>Hh', (1, )),
|
|
('SCHEDULE_CMD_RET_TIME', 0x06, '>BBBBB', ( )),
|
|
('SCHEDULE_CMD_CHECK_BEFORE_TIME_S', 0x04, '>BBb', (2, )),
|
|
('SCHEDULE_CMD_CHECK_BEFORE_TIME_L', 0x05, '>BBh', (2, )),
|
|
('SCHEDULE_CMD_BRANCH_S', 0x02, '>b', (0, )),
|
|
('SCHEDULE_CMD_BRANCH_L', 0x03, '>h', (0, )),
|
|
]
|
|
|
|
scene_names = [
|
|
'SCENE_20SICHITAI2',
|
|
'SCENE_UNSET_1',
|
|
'SCENE_UNSET_2',
|
|
'SCENE_UNSET_3',
|
|
'SCENE_UNSET_4',
|
|
'SCENE_UNSET_5',
|
|
'SCENE_UNSET_6',
|
|
'SCENE_KAKUSIANA',
|
|
'SCENE_SPOT00',
|
|
'SCENE_UNSET_9',
|
|
'SCENE_WITCH_SHOP',
|
|
'SCENE_LAST_BS',
|
|
'SCENE_HAKASHITA',
|
|
'SCENE_AYASHIISHOP',
|
|
'SCENE_UNSET_E',
|
|
'SCENE_UNSET_F',
|
|
'SCENE_OMOYA',
|
|
'SCENE_BOWLING',
|
|
'SCENE_SONCHONOIE',
|
|
'SCENE_IKANA',
|
|
'SCENE_KAIZOKU',
|
|
'SCENE_MILK_BAR',
|
|
'SCENE_INISIE_N',
|
|
'SCENE_TAKARAYA',
|
|
'SCENE_INISIE_R',
|
|
'SCENE_OKUJOU',
|
|
'SCENE_OPENINGDAN',
|
|
'SCENE_MITURIN',
|
|
'SCENE_13HUBUKINOMITI',
|
|
'SCENE_CASTLE',
|
|
'SCENE_DEKUTES',
|
|
'SCENE_MITURIN_BS',
|
|
'SCENE_SYATEKI_MIZU',
|
|
'SCENE_HAKUGIN',
|
|
'SCENE_ROMANYMAE',
|
|
'SCENE_PIRATE',
|
|
'SCENE_SYATEKI_MORI',
|
|
'SCENE_SINKAI',
|
|
'SCENE_YOUSEI_IZUMI',
|
|
'SCENE_KINSTA1',
|
|
'SCENE_KINDAN2',
|
|
'SCENE_TENMON_DAI',
|
|
'SCENE_LAST_DEKU',
|
|
'SCENE_22DEKUCITY',
|
|
'SCENE_KAJIYA',
|
|
'SCENE_00KEIKOKU',
|
|
'SCENE_POSTHOUSE',
|
|
'SCENE_LABO',
|
|
'SCENE_DANPEI2TEST',
|
|
'SCENE_UNSET_31',
|
|
'SCENE_16GORON_HOUSE',
|
|
'SCENE_33ZORACITY',
|
|
'SCENE_8ITEMSHOP',
|
|
'SCENE_F01',
|
|
'SCENE_INISIE_BS',
|
|
'SCENE_30GYOSON',
|
|
'SCENE_31MISAKI',
|
|
'SCENE_TAKARAKUJI',
|
|
'SCENE_UNSET_3A',
|
|
'SCENE_TORIDE',
|
|
'SCENE_FISHERMAN',
|
|
'SCENE_GORONSHOP',
|
|
'SCENE_DEKU_KING',
|
|
'SCENE_LAST_GORON',
|
|
'SCENE_24KEMONOMITI',
|
|
'SCENE_F01_B',
|
|
'SCENE_F01C',
|
|
'SCENE_BOTI',
|
|
'SCENE_HAKUGIN_BS',
|
|
'SCENE_20SICHITAI',
|
|
'SCENE_21MITURINMAE',
|
|
'SCENE_LAST_ZORA',
|
|
'SCENE_11GORONNOSATO2',
|
|
'SCENE_SEA',
|
|
'SCENE_35TAKI',
|
|
'SCENE_REDEAD',
|
|
'SCENE_BANDROOM',
|
|
'SCENE_11GORONNOSATO',
|
|
'SCENE_GORON_HAKA',
|
|
'SCENE_SECOM',
|
|
'SCENE_10YUKIYAMANOMURA',
|
|
'SCENE_TOUGITES',
|
|
'SCENE_DANPEI',
|
|
'SCENE_IKANAMAE',
|
|
'SCENE_DOUJOU',
|
|
'SCENE_MUSICHOUSE',
|
|
'SCENE_IKNINSIDE',
|
|
'SCENE_MAP_SHOP',
|
|
'SCENE_F40',
|
|
'SCENE_F41',
|
|
'SCENE_10YUKIYAMANOMURA2',
|
|
'SCENE_14YUKIDAMANOMITI',
|
|
'SCENE_12HAKUGINMAE',
|
|
'SCENE_17SETUGEN',
|
|
'SCENE_17SETUGEN2',
|
|
'SCENE_SEA_BS',
|
|
'SCENE_RANDOM',
|
|
'SCENE_YADOYA',
|
|
'SCENE_KONPEKI_ENT',
|
|
'SCENE_INSIDETOWER',
|
|
'SCENE_26SARUNOMORI',
|
|
'SCENE_LOST_WOODS',
|
|
'SCENE_LAST_LINK',
|
|
'SCENE_SOUGEN',
|
|
'SCENE_BOMYA',
|
|
'SCENE_KYOJINNOMA',
|
|
'SCENE_KOEPONARACE',
|
|
'SCENE_GORONRACE',
|
|
'SCENE_TOWN',
|
|
'SCENE_ICHIBA',
|
|
'SCENE_BACKTOWN',
|
|
'SCENE_CLOCKTOWER',
|
|
'SCENE_ALLEY'
|
|
]
|
|
|
|
def read_bytes(data_file, offset, len):
|
|
data_file.seek(offset)
|
|
return bytearray(data_file.read(len))
|
|
|
|
def calc_length(data_file, offset):
|
|
off = 0
|
|
cmd = None
|
|
branch_targets = []
|
|
|
|
# Parse script, just keeping track of branches, until we find the end
|
|
while any([branch >= off for branch in branch_targets]) or cmd not in [0x04, 0x05, 0x06, 0x09]:
|
|
cmd = read_bytes(data_file, offset + off, 1)[0]
|
|
|
|
cmd_len = cmd_info[cmd][1]
|
|
cmd_args = cmd_info[cmd][2]
|
|
|
|
if len(cmd_args) > 1:
|
|
arg_values = struct.unpack(cmd_args, read_bytes(data_file, offset + off + 1, cmd_len - 1))
|
|
for i in cmd_info[cmd][3]:
|
|
branch_targets.append(off + arg_values[i] + cmd_len)
|
|
|
|
off += cmd_len
|
|
|
|
return off
|
|
|
|
def disassemble_unk_script(data_file, offset):
|
|
off = 0
|
|
cmd = None
|
|
branch_targets = []
|
|
script_len = calc_length(data_file, offset);
|
|
script_len_num_digits = 0 if script_len == 1 else int(math.ceil(math.log(script_len - 1, 16)))
|
|
|
|
out = "static ScheduleScript sScheduleScript[] = {\n"
|
|
|
|
# Keep trying to disassemble until it hits a terminator and no commands branch past it
|
|
while any([branch >= off for branch in branch_targets]) or cmd not in [0x04, 0x05, 0x06, 0x09]:
|
|
cmd = read_bytes(data_file, offset + off, 1)[0]
|
|
|
|
cmd_name = cmd_info[cmd][0]
|
|
cmd_len = cmd_info[cmd][1]
|
|
cmd_args = cmd_info[cmd][2]
|
|
|
|
args_formatted = []
|
|
if len(cmd_args) > 1: # handle args
|
|
arg_values = struct.unpack(cmd_args, read_bytes(data_file, offset + off + 1, cmd_len - 1))
|
|
for i,(arg_value,arg_type) in enumerate(zip(arg_values, cmd_args[1:]),0):
|
|
arg_formatted = ""
|
|
if arg_type == "h":
|
|
arg_formatted = f"{arg_value:#x}"
|
|
elif arg_type == "b":
|
|
arg_formatted = f"{arg_value:#x}"
|
|
elif arg_type == "H":
|
|
if (cmd == 0x0A or cmd == 0x0B) and (arg_value < len(scene_names)):
|
|
arg_formatted = scene_names[arg_value]
|
|
elif cmd == 0x00 or cmd == 0x01:
|
|
arg_formatted = f"WEEKEVENTREG_{arg_value>>8:02}_{arg_value&0xFF:02X}"
|
|
else:
|
|
arg_formatted = f"{arg_value}"
|
|
elif arg_type == "B":
|
|
if cmd == 0x00 or cmd == 0x01:
|
|
arg_formatted = f"0x{arg_value:02X}"
|
|
elif cmd == 0x08:
|
|
if arg_value == 0:
|
|
arg_formatted = "SCHEDULE_CHECK_MISC_ROOM_KEY"
|
|
elif arg_value == 1:
|
|
arg_formatted = "SCHEDULE_CHECK_MISC_LETTER_TO_KAFEI"
|
|
elif arg_value == 2:
|
|
arg_formatted = "SCHEDULE_CHECK_MISC_MASK_ROMANI"
|
|
else:
|
|
arg_formatted = f"0x{arg_value:02X}"
|
|
else:
|
|
arg_formatted = f"{arg_value:2}"
|
|
elif arg_type == "S":
|
|
if arg_value < len(scene_names):
|
|
arg_formatted = scene_names[arg_value]
|
|
else:
|
|
arg_formatted = f"0x{arg_value:04X}"
|
|
|
|
if i in cmd_info[cmd][3]: # skip argument indices
|
|
# add skip args to branch targets, and change the output to absolute differences rather than relative offsets
|
|
arg_value += cmd_len
|
|
branch_targets.append(off + arg_value)
|
|
if arg_value == cmd_len:
|
|
arg_formatted == "0"
|
|
else:
|
|
arg_formatted = f"0x{off + arg_value:0{script_len_num_digits}X} - 0x{off + cmd_len:0{script_len_num_digits}X}"
|
|
args_formatted.append(arg_formatted)
|
|
|
|
args = ", ".join(args_formatted) if len(args_formatted) != 0 else ""
|
|
|
|
out += f" /* 0x{off:0{script_len_num_digits}X} */ {cmd_name}({args}),\n"
|
|
|
|
off += cmd_len
|
|
|
|
out = out.strip()
|
|
out += "\n};"
|
|
|
|
print(out)
|
|
|
|
def hex_parse(s):
|
|
return int(s, 16)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Disassembles MM event scripts")
|
|
parser.add_argument('address', help="VRAM or ROM address to disassemble at", type=hex_parse)
|
|
args = parser.parse_args()
|
|
|
|
file_result = resolve_symbol(args.address)
|
|
|
|
if file_result[0] is None:
|
|
print("Invalid address")
|
|
return
|
|
|
|
print(hex(file_result[1]), "in", file_result[0].split(os.sep)[-1])
|
|
|
|
with open(file_result[0],"rb") as data_file:
|
|
disassemble_unk_script(data_file, file_result[1])
|
|
|
|
if __name__ == "__main__":
|
|
main()
|