from pathlib import Path import os import sys import subprocess from distutils.util import strtobool import json from assets.palette_group import PaletteGroup from assets.gfx_group import GfxGroup from assets.fixed_type_gfx import FixedTypeGfx from assets.frame_obj_lists import FrameObjLists from assets.extra_frame_offsets import ExtraFrameOffsets from assets.animation import Animation from assets.exit_list import ExitList from assets.entity_list import EntityList verbose = False def extract_assets(variant, assets_folder): print(f'Extract assets from {variant}.', flush=True) map = { 'USA': 'baserom.gba', 'EU': 'baserom_eu.gba', 'JP': 'baserom_jp.gba', 'DEMO_USA': 'baserom_demo.gba', 'DEMO_JP': 'baserom_demo_jp.gba', } if not os.path.exists(map[variant]): print(f'Error: Baserom {map[variant]} is missing.', file=sys.stderr) exit(1) baserom = None baserom_path = map[variant] with open(baserom_path, 'rb') as file: baserom = bytearray(file.read()) config_modified = os.path.getmtime('assets.json') # json_modified = os.path.getmtime('assets.json') # if json_modified < config_modified: # print('Convert yaml to json...', flush=True) # subprocess.check_call('cat assets.yaml | yq . > assets.json', shell=True) with open('assets.json') as file: current_offset = 0 #print('Parsing yaml...', flush=True) #assets = yaml.safe_load(file) #print('done', flush=True) print('Parsing json...', flush=True) assets = json.load(file) print('done', flush=True) for asset in assets: if 'offsets' in asset: # Offset definition if variant in asset['offsets']: current_offset = asset['offsets'][variant] elif 'path' in asset: # Asset definition if 'variants' in asset: if variant not in asset['variants']: # This asset is not used in the current variant continue path = os.path.join(assets_folder, asset['path']) extract_file = False if os.path.isfile(path): file_modified = os.path.getmtime(path) if file_modified < config_modified: if verbose: print(f'{path} was created before the config was modified.') extract_file = True # TODO Extract when source file (depends on type) was modified after target file #print(f'{file_modified} {config_modified}') else: if verbose: print(f'{path} does not yet exist.') extract_file = True if extract_file: if verbose: print(f'Extracting {path}...') start = 0 if 'start' in asset: # Apply offset to the start of the USA variant start = asset['start'] + current_offset elif 'starts' in asset: # Use start for the current variant start = asset['starts'][variant] mode = '' if 'type' in asset: mode = asset['type'] Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True) if 'size' in asset: # The asset has a size and want to be extracted first. size = asset['size'] # TODO can different sizes for the different variants ever occur? with open(path, 'wb') as output: output.write(baserom[start:start+size]) # If an asset has no size, the extraction tool reads the baserom iself. options = asset['options'] if 'options' in asset else [] if mode == 'tileset': extract_tileset(path) elif mode == 'palette': extract_palette(path) elif mode == 'graphic': extract_graphic(path, options) elif mode == 'midi': extract_midi(path, baserom_path, start, options) elif mode == 'aif': extract_aif(path, options) elif mode == 'palette_group': palette_group = PaletteGroup(path, start, size, options) palette_group.extract_binary(baserom) elif mode == 'gfx_group': gfx_group = GfxGroup(path, start, size, options) gfx_group.extract_binary(baserom) elif mode == 'fixed_type_gfx': fixed_type_gfx = FixedTypeGfx(path, start, size, options) fixed_type_gfx.extract_binary(baserom) elif mode == 'frame_obj_lists': frame_obj_lists = FrameObjLists(path, start, size, options) frame_obj_lists.extract_binary(baserom) elif mode == 'extra_frame_offsets': extra_frame_offsets = ExtraFrameOffsets(path, start, size, options) extra_frame_offsets.extract_binary(baserom) elif mode == 'animation': animation = Animation(path, start, size, options) animation.extract_binary(baserom) elif mode == 'exit_list': exit_list = ExitList(path, start, size, options) exit_list.extract_binary(baserom) elif mode == 'entity_list': entity_list = EntityList(path, start, size, options) entity_list.extract_binary(baserom) elif mode != '': print(f'Asset type {mode} not yet implemented') def run_gbagfx(path_in, path_out, options): subprocess.check_call([os.path.join('tools', 'gbagfx', 'gbagfx'), path_in, path_out] + options) def extract_tileset(path): assert(path.endswith('.4bpp.lz')) base = path[0:-8] # subprocess.call(['cp', path, path+'.bkp']) run_gbagfx(path, base+'.4bpp', []) # decompress run_gbagfx(base+'.4bpp', base+'.png', ['-mwidth', '32']) # convert to png # TODO automatically generate tileset entries from tileset_headers.s # TODO Possible to set the correct palette? Or not, because there is a set of palettes that can be chosen and the correct palette is only defined by the metatile? def extract_palette(path): assert(path.endswith('.gbapal')) base = path[0:-7] run_gbagfx(path, base+'.pal', []) def extract_graphic(path, options): assert(path.endswith('.4bpp')) base = path[0:-5] params = [] for key in options: params.append('-'+key) params.append(str(options[key])) run_gbagfx(path, base+'.png', params) def extract_midi(path, baserom_path, start, options): assert(path.endswith('.s')) base = path[0:-2] common_params = [] agb2mid_params = [] exactGateTime = True # Set exactGateTime by default for key in options: if key == 'group' or key == 'G': common_params.append('-G') common_params.append(str(options[key])) elif key == 'priority' or key == 'P': common_params.append('-P') common_params.append(str(options[key])) elif key == 'reverb' or key == 'R': common_params.append('-R') common_params.append(str(options[key])) elif key == 'nominator': agb2mid_params.append('-n') agb2mid_params.append(str(options[key])) elif key == 'denominator': agb2mid_params.append('-d') agb2mid_params.append(str(options[key])) elif key == 'timeChanges': changes = options['timeChanges'] if isinstance(changes, list): # Multiple time changes for change in changes: agb2mid_params.append('-t') agb2mid_params.append(str(change['nominator'])) agb2mid_params.append(str(change['denominator'])) agb2mid_params.append(str(change['time'])) else: agb2mid_params.append('-t') agb2mid_params.append(str(changes['nominator'])) agb2mid_params.append(str(changes['denominator'])) agb2mid_params.append(str(changes['time'])) elif key == 'exactGateTime': if options[key] == 1: exactGateTime = True elif options[key] == 0: exactGateTime = False else: exactGateTime = strtobool(options[key]) else: common_params.append('-'+key) common_params.append(str(options[key])) if exactGateTime: common_params.append('-E') # To midi subprocess.check_call([os.path.join('tools', 'agb2mid', 'agb2mid'), baserom_path, hex(start), baserom_path, base+'.mid'] + common_params + agb2mid_params) # To assembly (TODO only do in build step, not if only extracting) subprocess.check_call([os.path.join('tools', 'mid2agb', 'mid2agb'), base+'.mid', path] + common_params) def extract_aif(path, options): assert(path.endswith('.bin')) base = path[0:-4] subprocess.check_call([os.path.join('tools', 'aif2pcm', 'aif2pcm'), path, base+'.aif']) def main(): if len(sys.argv) == 1: extract_assets('USA') elif len(sys.argv) == 3: extract_assets(sys.argv[1].upper(), sys.argv[2]) else: print('Usage: asset_extractor.py VARIANT BUILD_FOLDER') if __name__ == '__main__': main()