[WIP] Laying some foundations for multi-ROM support
This commit is contained in:
parent
f0c76049bb
commit
6287bcee05
|
@ -1 +1,3 @@
|
||||||
*.sfc
|
*.sfc
|
||||||
|
*.gba
|
||||||
|
__pycache__
|
||||||
|
|
|
@ -107,7 +107,7 @@ def unflatten_table(headers: list[str], entries: list):
|
||||||
return entries
|
return entries
|
||||||
# This could be an array of an array of an array of an...
|
# This could be an array of an array of an array of an...
|
||||||
id0 = entries[0]['ID']
|
id0 = entries[0]['ID']
|
||||||
if '.' not in id0 and ':' not in id0:
|
if isinstance(id0, int) or ('.' not in id0 and ':' not in id0):
|
||||||
return entries
|
return entries
|
||||||
# Treat this as a nested array
|
# Treat this as a nested array
|
||||||
table = {tuple(decode_nested_ids(entry['ID'])): entry for entry in entries}
|
table = {tuple(decode_nested_ids(entry['ID'])): entry for entry in entries}
|
||||||
|
@ -147,13 +147,16 @@ def dump_tsv(filename, table, id_column=True) -> None:
|
||||||
file.write('\t'.join([str(entry[key]) for key in headers]) + '\n')
|
file.write('\t'.join([str(entry[key]) for key in headers]) + '\n')
|
||||||
|
|
||||||
|
|
||||||
def load_tsv(filename) -> list:
|
def load_tsv(filename: str, unflatten: bool = True) -> list:
|
||||||
with open(filename, 'r') as file:
|
with open(filename, 'r') as file:
|
||||||
lines = file.read().rstrip().split('\n')
|
lines = file.read().rstrip().split('\n')
|
||||||
if len(lines) < 2:
|
if len(lines) < 2:
|
||||||
return []
|
return []
|
||||||
headers = lines[0].split('\t')
|
headers = lines[0].split('\t')
|
||||||
|
|
||||||
|
if not unflatten:
|
||||||
|
return [{key: try_int(value) for key, value in zip(headers, line.split('\t'))} for line in lines[1:]]
|
||||||
|
|
||||||
# Simple line-by-line unflatten
|
# Simple line-by-line unflatten
|
||||||
entries = []
|
entries = []
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
|
|
|
@ -28,16 +28,21 @@ class ROMHandler:
|
||||||
self.build(table, existing_data, out_buffer)
|
self.build(table, existing_data, out_buffer)
|
||||||
|
|
||||||
|
|
||||||
def load_ff5_snes_struct_definitions() -> dict:
|
def load_struct_definitions(*filenames) -> dict:
|
||||||
existing_structs = get_base_structarraytypes()
|
existing_structs = get_base_structarraytypes()
|
||||||
parse_struct_definitions_from_tsv_filename('ChocolateBirdData/structs_SNES_stubs.tsv', existing_structs)
|
for filename in filenames:
|
||||||
parse_struct_definitions_from_tsv_filename('ChocolateBirdData/5/structs/SNES_stubs.tsv', existing_structs)
|
parse_struct_definitions_from_tsv_filename(filename, existing_structs)
|
||||||
parse_struct_definitions_from_tsv_filename('ChocolateBirdData/5/structs/SNES.tsv', existing_structs)
|
|
||||||
parse_struct_definitions_from_tsv_filename('ChocolateBirdData/5/structs/SNES_save.tsv', existing_structs)
|
|
||||||
return existing_structs
|
return existing_structs
|
||||||
|
|
||||||
|
|
||||||
class FF5SNESHandler(ROMHandler):
|
class FF5SNESHandler(ROMHandler):
|
||||||
offset_key: str = 'SNES'
|
offset_key: str = 'SNES'
|
||||||
struct_definitions: dict = load_ff5_snes_struct_definitions()
|
struct_definitions: dict = load_struct_definitions('ChocolateBirdData/structs_SNES_stubs.tsv', 'ChocolateBirdData/5/structs/SNES_stubs.tsv', 'ChocolateBirdData/5/structs/SNES.tsv', 'ChocolateBirdData/5/structs/SNES_save.tsv')
|
||||||
addresses: dict = {entry['Label']: entry for entry in load_tsv('ChocolateBirdData/5/addresses_SNES_PSX.tsv')}
|
addresses: dict = {entry['Label']: entry for entry in load_tsv('ChocolateBirdData/5/addresses_SNES_PSX.tsv')}
|
||||||
|
|
||||||
|
|
||||||
|
class FF5GBAHandler(ROMHandler):
|
||||||
|
def __init__(self, region: str) -> None:
|
||||||
|
self.offset_key = region
|
||||||
|
struct_definitions: dict = load_struct_definitions('ChocolateBirdData/structs_SNES_stubs.tsv', 'ChocolateBirdData/5/structs/SNES_stubs.tsv', 'ChocolateBirdData/5/structs/GBA.tsv', 'ChocolateBirdData/5/structs/SNES_save.tsv')
|
||||||
|
addresses: dict = {entry['Label']: entry for entry in load_tsv('ChocolateBirdData/5/addresses_GBA.tsv')}
|
||||||
|
|
75
tabcomp.py
75
tabcomp.py
|
@ -1,23 +1,54 @@
|
||||||
from includes.helpers import load_tsv, dump_tsv
|
from includes.helpers import load_tsv, dump_tsv
|
||||||
from includes.rom_serde import FF5SNESHandler
|
from includes.rom_serde import FF5SNESHandler, FF5GBAHandler
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from glob import glob
|
||||||
|
import re
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from argparse import ArgumentParser
|
parser = ArgumentParser(description='The ROMhacking Table Compiler.')
|
||||||
parser = ArgumentParser(description='The ROMhacking Table Compiler.')
|
parser.add_argument('action', choices=['extract', 'build'])
|
||||||
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('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('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.')
|
||||||
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.project:
|
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 = args.project.rstrip('/') + '/'
|
||||||
project_folder_len = len(project_folder)
|
project_folder_len = len(project_folder)
|
||||||
|
|
||||||
from glob import glob
|
|
||||||
from configparser import ConfigParser
|
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config['TabComp.Project'] = {'Game': 'Final Fantasy V', 'Platform': 'SNES', 'Region': 'any'}
|
config['TabComp.Project'] = rom_id # {'Game': 'Final Fantasy V', 'Platform': 'SNES', 'Region': 'any'}
|
||||||
try:
|
try:
|
||||||
with open(f'{project_folder}project.ini', 'r') as configfile:
|
with open(f'{project_folder}project.ini', 'r') as configfile:
|
||||||
config.read_file(configfile)
|
config.read_file(configfile)
|
||||||
|
@ -26,16 +57,15 @@ if __name__ == '__main__':
|
||||||
with open(f'{project_folder}project.ini', 'w') as configfile:
|
with open(f'{project_folder}project.ini', 'w') as configfile:
|
||||||
config.write(configfile)
|
config.write(configfile)
|
||||||
|
|
||||||
def run():
|
|
||||||
game = config['TabComp.Project']['Game']
|
game = config['TabComp.Project']['Game']
|
||||||
platform = config['TabComp.Project']['Platform']
|
platform = config['TabComp.Project']['Platform']
|
||||||
if game != 'Final Fantasy V' or platform != 'SNES':
|
region = config['TabComp.Project']['Region']
|
||||||
print(f'Unsupported ROM for project - "{game}" on "{platform}"')
|
try:
|
||||||
return
|
handler = known_roms[game][platform][region]['handler']
|
||||||
handler = FF5SNESHandler()
|
except IndexError:
|
||||||
if not args.rom:
|
print(f'Unsupported ROM for project - "{game}" on "{platform}" with region "{region}"')
|
||||||
print('No ROM specified!')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(args.rom, 'rb') as file:
|
with open(args.rom, 'rb') as file:
|
||||||
rom_bytes = file.read()
|
rom_bytes = file.read()
|
||||||
in_buffer = bytearray(rom_bytes)
|
in_buffer = bytearray(rom_bytes)
|
||||||
|
@ -68,4 +98,7 @@ if __name__ == '__main__':
|
||||||
case _:
|
case _:
|
||||||
'Invalid action!'
|
'Invalid action!'
|
||||||
return
|
return
|
||||||
run()
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
Loading…
Reference in New Issue