from includes.helpers import load_tsv, dump_tsv from includes.rom_serde import FF5SNESHandler, FF5GBAHandler from argparse import ArgumentParser from configparser import ConfigParser from glob import glob import re parser = ArgumentParser(description='The ROMhacking Table Compiler.') parser.add_argument('action', choices=['extract', 'build']) parser.add_argument('rom', help='The ROM to use as a basis for extracting data.') parser.add_argument('project', help='The project folder to extract data to, or compile data from.') parser.add_argument('tables', nargs='*', help='Specify which tables to extract or compile, separated by spaces. If left empty, nothing will be extracted, or all tables in a project will be compiled. See the labels in https://git.ufeff.net/birdulon/ChocolateBirdData/src/branch/master/5/addresses_SNES_PSX.tsv for a list of values which may be used, though bear in mind things such as graphics and maps are currently not supported in a sensible way.') known_roms = { 'Final Fantasy V': { 'SNES': { 'any': {'filename': r'.*Final Fantasy [V5].*\.sfc', 'handler': FF5SNESHandler()}, }, 'GBA': { 'U': {'filename': r'2564 - .*\.gba', 'handler': FF5GBAHandler('U')}, 'E': {'filename': r'2727 - .*\.gba', 'handler': FF5GBAHandler('E')}, }, }, } def guess_rom(filename: str) -> dict: for game, gd in known_roms.items(): for platform, pd in gd.items(): for region, rd in pd.items(): if re.fullmatch(rd['filename'], filename): return {'Game': game, 'Platform': platform, 'Region': region} def main(): args = parser.parse_args() if not args.rom: print('No ROM specified!') return if not args.project: return rom_id = guess_rom(args.rom) project_folder = args.project.rstrip('/') + '/' project_folder_len = len(project_folder) config = ConfigParser() config['TabComp.Project'] = rom_id # {'Game': 'Final Fantasy V', 'Platform': 'SNES', 'Region': 'any'} try: with open(f'{project_folder}project.ini', 'r') as configfile: config.read_file(configfile) except FileNotFoundError: pass with open(f'{project_folder}project.ini', 'w') as configfile: config.write(configfile) game = config['TabComp.Project']['Game'] platform = config['TabComp.Project']['Platform'] region = config['TabComp.Project']['Region'] try: handler = known_roms[game][platform][region]['handler'] except IndexError: print(f'Unsupported ROM for project - "{game}" on "{platform}" with region "{region}"') return with open(args.rom, 'rb') as file: rom_bytes = file.read() in_buffer = bytearray(rom_bytes) match args.action: case 'extract': if not args.tables: print('Must specify tables to extract!') return tables = [table for table in args.tables] print(f'Attempting to extract tables {tables}') for table in tables: data = handler.extract(table, in_buffer) dump_tsv(f'{project_folder}{table}.tsv', data) print('Done extracting!') case 'build': tables = [table for table in args.tables] if not args.tables: # Find all .tsv files in project folder tables = [file[project_folder_len:-4] for file in glob(f'{project_folder}*.tsv')] print(f'Attempting to build tables {tables}') out_buffer = bytearray(rom_bytes) for table in tables: data = load_tsv(f'{project_folder}{table}.tsv') handler.build_partial(table, data, in_buffer, out_buffer) out_filename = f'{project_folder}rom.sfc' with open(out_filename, 'wb') as file: file.write(out_buffer) print(f'Compiled to "{out_filename}", make your own .ips from this') case _: 'Invalid action!' return if __name__ == '__main__': main()