"""Dumps data from Omega Ruby and Alpha Sapphire. Filesystem reference: http://www.projectpokemon.org/wiki/ORAS_File_System """ import argparse from collections import OrderedDict from contextlib import contextmanager import itertools import math from pathlib import Path import shutil import struct from construct import Array, BitField, Bitwise, Magic, OptionalGreedyRange, Padding, Pointer, Struct, SLInt8, SLInt16, ULInt8, ULInt16, ULInt32 import yaml from .lib.garc import GARCFile, decrypt_xy_text from .lib.text import merge_japanese_texts # TODO fix some hardcoding in here # TODO finish converting garc parsing to use construct, if possible, i think (i would not miss substream) # way way more sprite work in here... CANON_LANGUAGES = ('ja', 'en', 'fr', 'it', 'de', 'es', 'ko') ORAS_SCRIPT_FILES = { 'ja-kana': 'rom/a/0/7/1', 'ja-kanji': 'rom/a/0/7/2', 'en': 'rom/a/0/7/3', 'fr': 'rom/a/0/7/4', 'it': 'rom/a/0/7/5', 'de': 'rom/a/0/7/6', 'es': 'rom/a/0/7/7', 'ko': 'rom/a/0/7/8', } ORAS_SCRIPT_ENTRIES = { 'form-names': 5, # TODO these might be backwards, i'm just guessing 'species-flavor-alpha-sapphire': 6, 'species-flavor-omega-ruby': 7, 'move-contest-flavor': 13, 'move-names': 14, # Note: table 15 is also a list of move names, but with a few at the end # missing? XY leftovers? 'move-flavor': 16, 'type-names': 18, 'ability-flavor': 36, 'ability-names': 37, 'nature-names': 51, 'species-names': 98, } # The first element in each list is the name of the BASE form -- if it's not # None, the base form will be saved under two filenames ORAS_EXTRA_SPRITE_NAMES = { # Cosplay Pikachu 25: (None, 'rockstar', 'belle', 'popstar', 'phd', 'libre', 'cosplay'), # Unown 201: tuple('abcdefghijklmnopqrstuvwxyz') + ('exclamation', 'question'), # Castform 351: (None, 'sunny', 'rainy', 'snowy'), # Kyogre and Groudon 382: (None, 'primal',), 383: (None, 'primal',), # Deoxys 386: ('normal', 'attack', 'defense', 'speed'), # Burmy and Wormadam 412: ('plant', 'sandy', 'trash'), 413: ('plant', 'sandy', 'trash'), # Cherrim 421: ('overcast', 'sunshine',), # Shellos and Gastrodon 422: ('west', 'east',), 423: ('west', 'east',), # Rotom 479: (None, 'heat', 'wash', 'frost', 'fan', 'mow'), # Giratina 487: ('altered', 'origin',), # Shaymin 492: ('land', 'sky',), # Arceus 493: ( 'normal', 'fighting', 'flying', 'poison', 'ground', 'rock', 'bug', 'ghost', 'steel', 'fire', 'water', 'grass', 'electric', 'psychic', 'ice', 'dragon', 'dark', 'fairy', ), # Basculin 550: ('red-striped', 'blue-striped',), # Darmanitan 555: ('standard', 'zen',), # Deerling and Sawsbuck 585: ('sprint', 'summer', 'autumn', 'winter'), 586: ('sprint', 'summer', 'autumn', 'winter'), # Tornadus, Thundurus, and Landorus 641: ('incarnate', 'therian'), 642: ('incarnate', 'therian'), 645: ('incarnate', 'therian'), # Kyurem 646: (None, 'white', 'black'), # Keldeo 647: ('ordinary', 'resolute'), # Meloetta 648: ('aria', 'pirouette'), # Genesect 649: (None, 'douse', 'shock', 'burn', 'chill'), # Vivillon 666: ( 'icy-snow', 'polar', 'tundra', 'continental', 'garden', 'elegant', 'meadow', 'modern', 'marine', 'archipelago', 'high-plains', 'sandstorm', 'river', 'monsoon', 'savanna', 'sun', 'ocean', 'jungle', 'fancy', 'poke-ball', ), # Flabébé/Floette/Florges 669: ('red', 'yellow', 'orange', 'blue', 'white'), 670: ('red', 'yellow', 'orange', 'blue', 'white', 'eternal'), 671: ('red', 'yellow', 'orange', 'blue', 'white'), # Furfrou 676: ( 'natural', 'heart', 'star', 'diamond', 'debutante', 'matron', 'dandy', 'la-reine', 'kabuki', 'pharaoh', ), # Meowstic #678: [male, female] # Aegislash 681: ('shield', 'blade'), # Pumpkaboo/Gourgeist 710: ('average', 'small', 'large', 'super'), 711: ('average', 'small', 'large', 'super'), # Xerneas 716: ('neutral', 'active'), # Hoopa 720: ('confined', 'unbound'), } pokemon_struct = Struct( 'pokemon', ULInt8('stat_hp'), ULInt8('stat_atk'), ULInt8('stat_def'), ULInt8('stat_speed'), ULInt8('stat_spatk'), ULInt8('stat_spdef'), ULInt8('type1'), ULInt8('type2'), ULInt8('catch_rate'), ULInt8('stage'), ULInt16('effort'), ULInt16('held_item1'), ULInt16('held_item2'), ULInt16('held_item3'), # dark grass from bw, unused in oras? ULInt8('gender_rate'), ULInt8('steps_to_hatch'), ULInt8('base_happiness'), ULInt8('exp_curve'), ULInt8('egg_group1'), ULInt8('egg_group2'), ULInt8('ability1'), ULInt8('ability2'), ULInt8('ability_dream'), ULInt8('safari_escape'), ULInt16('form_species_start'), ULInt16('form_sprite_start'), ULInt8('form_count'), ULInt8('color'), ULInt16('base_exp'), ULInt16('height'), ULInt16('weight'), Bitwise( BitField('machines', 14 * 8, swapped=True), ), Padding(2), ULInt32('tutors'), ULInt16('mystery1'), ULInt16('mystery2'), ULInt32('bp_tutors1'), ULInt32('bp_tutors2'), ULInt32('bp_tutors3'), ULInt32('bp_tutors4'), ) pokemon_mega_evolutions_struct = Array( 3, Struct( 'pokemon_mega_evolutions', ULInt16('number'), ULInt16('mode'), ULInt16('mega_stone_itemid'), Padding(2), ) ) egg_moves_struct = Struct( 'egg_moves', ULInt16('count'), Array( lambda ctx: ctx.count, ULInt16('moveids'), ), ) level_up_moves_struct = OptionalGreedyRange( Struct( 'level_up_pair', SLInt16('moveid'), SLInt16('level'), ), ) move_struct = Struct( 'move', ULInt8('type'), ULInt8('category'), ULInt8('damage_class'), ULInt8('power'), ULInt8('accuracy'), ULInt8('pp'), SLInt8('priority'), ULInt8('min_max_hits'), SLInt16('caused_effect'), ULInt8('effect_chance'), ULInt8('status'), ULInt8('min_turns'), ULInt8('max_turns'), ULInt8('crit_rate'), ULInt8('flinch_chance'), ULInt16('effect'), SLInt8('recoil'), ULInt8('healing'), ULInt8('range'), # ok Bitwise( BitField('stat_change', 24), ), Bitwise( BitField('stat_amount', 24), ), Bitwise( BitField('stat_chance', 24), ), ULInt8('padding0'), # ok ULInt8('padding1'), # ok ULInt16('flags'), ULInt8('padding2'), # ok ULInt8('extra'), ) move_container_struct = Struct( 'move_container', Magic(b'WD'), # waza... descriptions? ULInt16('record_ct'), Array( lambda ctx: ctx.record_ct, Struct( 'records', ULInt32('offset'), Pointer(lambda ctx: ctx.offset, move_struct), ), ), ) pokemon_sprite_struct = Struct( 'pokemon_sprite_config', ULInt16('index'), ULInt16('female_index'), ULInt32('form_index_offset'), ULInt32('right_index_offset'), ULInt16('form_count'), ULInt16('right_count'), ) # There are 63 tutor move bits in use, but only 60 move tutors -- the moves # appear to be largely inherited from B2W2 but these are just not exposed in # ORAS ORAS_UNUSED_MOVE_TUTORS = {'dark-pulse', 'roost', 'sleep-talk'} # Unsure where this is in the binary ORAS_NORMAL_MOVE_TUTORS = ( 'grass-pledge', 'fire-pledge', 'water-pledge', 'frenzy-plant', 'blast-burn', 'hydro-cannon', 'draco-meteor', 'dragon-ascent', ) @contextmanager def read_garc(path): with path.open('rb') as f: yield GARCFile(f) # XXX christ lol. taken from SO. fodder for camel maybe def represent_ordereddict(dumper, data): value = [] for item_key, item_value in data.items(): node_key = dumper.represent_data(item_key) node_value = dumper.represent_data(item_value) value.append((node_key, node_value)) return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', value) yaml.add_representer(OrderedDict, represent_ordereddict) def represent_tuple(dumper, data): return yaml.nodes.SequenceNode( u'tag:yaml.org,2002:seq', [dumper.represent_data(item) for item in data], flow_style=True, ) yaml.add_representer(tuple, represent_tuple) def dump_to_yaml(data, f): # TODO gonna need a better way to handle flow style yaml.dump( data, f, default_flow_style=False, allow_unicode=True, ) def extract_data(root, out): # TODO big conceptual question for the yaml thing: how do we decide how the # identifiers work in the per-version data? the "global" identifiers are # in theory based on the names from the latest version, and the game dump # scripts shouldn't have to care about what the latest version is # 1. make the canon data not be keyed by identifier (makes it hard to # follow what's going on in flavor text files etc, and unclear how to match # up items across versions) # 2. make each version's data keyed by its own identifiers (makes it hard # to align them all when loading everything, and unclear how to match up # items whose names change across versions) # 3. hardcode a mapping of version+identifier pairs to their current # identifiers, when they changed, which is a little ugly but also solves # all the match-up problems and is what we'd basically have to do anyway # ------------------------------------------------------------------------- # Names and flavor text texts = {} for lang, fn in ORAS_SCRIPT_FILES.items(): texts[lang] = {} with read_garc(root / fn) as garc: for entryname, entryid in ORAS_SCRIPT_ENTRIES.items(): entry = garc[entryid][0] texts[lang][entryname] = decrypt_xy_text(entry.read()) # Japanese text is special! It's written in both kanji and kana, and we # want to combine them texts['ja'] = {} for entryname in ORAS_SCRIPT_ENTRIES: kanjis = texts['ja-kanji'][entryname] kanas = texts['ja-kana'][entryname] # But not if they're names of things. # (TODO this might not be true in the case of, say, towns? in which # case, what do we do? we want to ultimately put these in urls and # whatnot, right, but we don't want furigana there :S do we need a # separate "identifier" field /per language/?) if entryname.endswith('names'): assert kanjis == kanas texts['ja'][entryname] = kanjis else: assert len(kanas) == len(kanjis) texts['ja'][entryname] = [ merge_japanese_texts(kanji, kana) for (kanji, kana) in zip(kanjis, kanas) ] del texts['ja-kanji'] del texts['ja-kana'] identifiers = {} identifiers['species'] = [ # TODO better identifier creation, to be determined later, but surely # want to lose . and ' # TODO handling forms here is awkward since the form names are # contained in the personal struct ((species_name or '') + '-' + form_name).lower().replace(' ', '-') for (species_name, form_name) in itertools.zip_longest( texts['en']['species-names'], texts['en']['form-names'], ) ] identifiers['move'] = [ # TODO better identifier creation, to be determined later, but surely # want to lose . and ' name.lower().replace(' ', '-') for name in texts['en']['move-names'] ] textdir = out / 'script' if not textdir.exists(): textdir.mkdir() for lang in CANON_LANGUAGES: with (textdir / (lang + '.yaml')).open('w') as f: # TODO this should use identifiers, not be lists # TODO need to skip slot 0 which is junk dump_to_yaml(texts[lang], f) # ------------------------------------------------------------------------- # Scrape some useful bits from the binary with (root / 'exe/code.bin').open('rb') as f: # Tutored moves tutor_moves = dict(tutors=ORAS_NORMAL_MOVE_TUTORS) f.seek(0x004960f8) for n in range(1, 5): key = "bp_tutors{}".format(n) moves = tutor_moves[key] = [] while True: moveid, = struct.unpack('= len(identifiers['move']): break moves.append(identifiers['move'][moveid]) # TMs machines = [] f.seek(0x004a67ee) machineids = struct.unpack('<107H', f.read(2 * 107)) # Order appears to be based on some gen 4 legacy: TMs 1 through 92, HMs # 1 through 6, then the other eight TMs and the last HM. But the bits # in the Pokémon structs are in the expected order of 1 through 100, 1 # through 7 machines = [ identifiers['move'][moveid] for moveid in machineids[0:92] + machineids[98:106] + machineids[92:98] + machineids[106:] ] # ------------------------------------------------------------------------- # Pokémon structs pokemon_data = [] with read_garc(root / 'rom/a/1/9/5') as garc: personals = [subfile[0].read() for subfile in garc] _pokemon_forms = {} # "real" species id => (base species id, form name id) _next_name_form_id = 723 for i, personal in enumerate(personals[:-1]): record = pokemon_struct.parse(personal) # TODO transform to an OD somehow probably pokemon_data.append(record) #print("{:3d} {:15s} {} {:5d} {:5d}".format( # i, # identifiers['species'][baseid], # ('0'*16 + bin(record.mystery1)[2:])[-16:], # record.mystery2, # record.stage, #)) # TODO some pokemon have sprite starts but no species start, because their sprites vary obv if record.form_count > 1: # The form names appear to be all just jammed at the end in order, # completely unrelated to either of the "start" offsets here for offset in range(record.form_count - 1): #form_name = texts['en']['form-names'][_next_name_form_id] if record.form_species_start: # TODO still no idea how "intangible" forms are being # handled in the new schema _pokemon_forms[record.form_species_start + offset] = i, _next_name_form_id _next_name_form_id += 1 if record.form_species_start: for offset in range(record.form_count - 1): # TODO grab the form names argh identifiers['species'][record.form_species_start + offset] = identifiers['species'][i] #for i in range(723, 825 + 1): # base_species_id, form_name_id = _pokemon_forms[i] # species_name = texts['en']['species-names'][base_species_id] # form_name = texts['en']['form-names'][form_name_id] # print(i, species_name, '/', form_name) # ------------------------------------------------------------------------- # Move stats movesets = OrderedDict() with read_garc(root / 'rom/a/1/8/9') as garc: # Only one subfile data = garc[0][0].read() container = move_container_struct.parse(data) for n, record in enumerate(container.records): m = record.move # TODO with the release of oras all moves have contest types and effects again! where are they?? #print("{:3d} {:20s} | {m.type:3d} {m.power:3d} {m.pp:2d} {m.accuracy:3d} / {m.priority:2d} {m.range:2d} {m.damage_class:1d} / {m.effect:3d} {m.caused_effect:3d} {m.effect_chance:3d} -- {m.status:3d} {m.min_turns:3d} {m.max_turns:3d} {m.crit_rate:3d} {m.flinch_chance:3d} {m.recoil:4d} {m.healing:3d} / {m.stat_change:06x} {m.stat_amount:06x} {m.stat_chance:06x} / {m.padding0:3d} {m.padding1:3d} {m.flags:04x} {m.padding2:3d} {m.extra:3d}".format( # n, # identifiers['move'][n], # m=record.move, #)) # Egg moves with read_garc(root / 'rom/a/1/9/0') as garc: for i, subfile in enumerate(garc): ident = identifiers['species'][i] data = subfile[0].read() if not data: continue container = egg_moves_struct.parse(data) moveset = movesets.setdefault(ident, OrderedDict()) eggset = moveset['egg'] = [] for moveid in container.moveids: eggset.append(identifiers['move'][moveid]) # Level-up moves with read_garc(root / 'rom/a/1/9/1') as garc: for i, subfile in enumerate(garc): ident = identifiers['species'][i] level_up_moves = subfile[0].read() moveset = movesets.setdefault(ident, OrderedDict()) levelset = moveset['level'] = [] lastlevel = None order = 1 for pair in level_up_moves_struct.parse(level_up_moves): # End is indicated with -1, -1 if pair.moveid <= 0: break levelset.append(( pair.level, identifiers['move'][pair.moveid], )) if pair.level == lastlevel: order += 1 else: lastlevel = pair.level order = 1 # Evolution #with read_garc(root / 'rom/a/1/9/2') as garc: # for subfile in garc: # evolution = subfile[0].read() # print(repr(evolution)) # Mega evolution #with read_garc(root / 'rom/a/1/9/3') as garc: # for subfile in garc: # evolution = subfile[0].read() # print(repr(evolution)) # TODO what is a/1/9/4? 8 files of 404 bytes each # Baby Pokémon #with read_garc(root / 'rom/a/1/9/6') as garc: # for subfile in garc: # baby_pokemon = subfile[0].read() # print(repr(baby_pokemon)) # Item stats #with read_garc(root / 'rom/a/1/9/7') as garc: # for subfile in garc: # item_stats = subfile[0].read() # print(repr(item_stats)) # Tutor moves (from the personal structs) for i, datum in enumerate(pokemon_data): ident = identifiers['species'][i] moveset = movesets.setdefault(ident, OrderedDict()) tutorset = moveset['tutor'] = [] for key, tutors in tutor_moves.items(): for bit, moveident in enumerate(tutors): if moveident in ORAS_UNUSED_MOVE_TUTORS: continue if not datum[key] & (1 << bit): continue tutorset.append(moveident) # TMs machineset = moveset['machine'] = [] for bit, moveident in enumerate(machines): if not datum['machines'] & (1 << bit): continue machineset.append(moveident) with (out / 'movesets.yaml').open('w') as f: dump_to_yaml(movesets, f) def extract_box_sprites(root, out): filenames = {} with (root / 'exe/code.bin').open('rb') as f: # Form configuration, used to put sprites in the right order # NOTE: in x/y the address is 0x0043ea98 f.seek(0x0047d650) # TODO need to do a different thing for main sprites # TODO magic number for n in range(722): sprite = pokemon_sprite_struct.parse_stream(f) assert sprite.index not in filenames filenames[sprite.index] = "{}".format(n) if sprite.female_index != sprite.index: assert sprite.female_index not in filenames filenames[sprite.female_index] = "{}-female".format(n) # Note that these addresses are relative to RAM, and the binary is # loaded into RAM starting at 0x100000, so we need to subtract that # to get a file position pos = f.tell() form_indices = () right_indices = () if sprite.form_index_offset: f.seek(sprite.form_index_offset - 0x100000) form_indices = struct.unpack( "<{}H".format(sprite.form_count), f.read(2 * sprite.form_count), ) for form, form_idx in enumerate(form_indices): # Ignore the first form, since it's the default and thus # covered by `index` already if form == 0: continue if form_idx == sprite.index: continue assert form_idx not in filenames filenames[form_idx] = "{}-form{}".format(n, form) if sprite.right_index_offset: f.seek(sprite.right_index_offset - 0x100000) right_indices = struct.unpack( "<{}H".format(sprite.right_count), f.read(2 * sprite.right_count), ) if sprite.form_count: assert sprite.right_count == sprite.form_count for form, (form_idx, right_idx) in enumerate(zip(form_indices, right_indices)): if form_idx == right_idx: continue if form != 0: suffix = "form{}-right".format(form) else: suffix = 'right' assert right_idx not in filenames filenames[right_idx] = "{}-{}".format(n, suffix) else: assert sprite.right_count == 2 assert right_indices[0] == right_indices[1] if right_indices[0] != sprite.index: assert right_indices[0] not in filenames filenames[right_indices[0]] = "{}-right".format(n) f.seek(pos) pokemon_sprites_dir = out if not pokemon_sprites_dir.exists(): pokemon_sprites_dir.mkdir() with read_garc(root / 'rom/a/0/9/1') as garc: from .lib.clim import decode_clim for i, subfile in enumerate(garc): if i == 0: # Dummy blank sprite, not interesting to us continue elif i in filenames: filename = filenames[i] + '.png' elif i == len(garc) - 1: # Very last one is egg filename = 'egg.png' else: # This is a duplicate Entei sprite that's not used assert i in (333,) continue data = subfile[0].read() width, height, color_depth, pixels = decode_clim(data) png_writer = png.Writer( width=width, height=height, alpha=True, ) # this library is so fucking stupid # TODO strictly speaking we could just write out a paletted PNG directly # TODO add sBIT chunk indicating original bit depth with (pokemon_sprites_dir / filename).open('wb') as f: png_writer.write(f, (itertools.chain(*row) for row in pixels)) def extract_dex_sprites(root, out): # Some Pokémon have dex sprites for their forms, too, and they're all # clustered together, so we have to do a little work to fix the numbering. # Luckily the dex sprites are in the same order as the models # (unsurprising, as they're just model renders), which also tells us what # Pokémon have female forms. The mega evolution map tells us which forms # are megas, and the rest are listed manually above as # ORAS_EXTRA_SPRITE_NAMES. # Grab the list of megas first num_megas = {} # pokemonid => number of mega evos with read_garc(root / 'rom/a/1/9/3') as garc: for pokemonid, subfile in enumerate(garc): mega_evos = pokemon_mega_evolutions_struct.parse_stream(subfile[0]) num_megas[pokemonid] = max( mega_evo.number for mega_evo in mega_evos) # Then construct filenames, using num_megas plus information from the model # index filenames = {} # model/sprite number => filename, sans extension duplicate_filenames = [] # pairs of (copy from, copy to) with read_garc(root / 'rom/a/0/0/8') as garc: f = garc[0][0] # TODO magic number for n in range(721): # Unlike /virtually everywhere else/, Pokémon are zero-indexed here pokemonid = n + 1 # Index of the first model (also zero-indexed), how many models the # Pokémon has, and some flags start, count, flags = struct.unpack('= 717: model_num += 1 filenames[model_num] = str(pokemonid) form_count = count - 1 # discount "base" form total_model_count = model_num + count - 1 # Some "forms" have no real default, so we save the sprite both as # nnn.png and nnn-form.png, to guarantee that nnn.png always exists if pokemonid in ORAS_EXTRA_SPRITE_NAMES: if ORAS_EXTRA_SPRITE_NAMES[pokemonid][0] is not None: duplicate_filenames.append(( str(pokemonid), "{}-{}".format( pokemonid, ORAS_EXTRA_SPRITE_NAMES[pokemonid][0]), )) # Don't know what flag 1 is; everything has it. # Flag 2 means the first alternate form is a female variant. if flags & 2: assert form_count > 0 form_count -= 1 model_num += 1 filenames[model_num] = "female/{}".format(pokemonid) # Flag 4 just means there are more forms? if flags & 4: assert form_count assert 1 or 1 == sum(( form_count == 0, num_megas[pokemonid] > 0, pokemonid in ORAS_EXTRA_SPRITE_NAMES, )) if num_megas[pokemonid]: assert form_count == num_megas[pokemonid] assert pokemonid not in ORAS_EXTRA_SPRITE_NAMES model_num += 1 if form_count == 1: filenames[model_num] = "{}-mega".format(pokemonid) else: # Charizard and Mewtwo assert form_count == 2 filenames[model_num] = "{}-mega-x".format(pokemonid) filenames[model_num + 1] = "{}-mega-y".format(pokemonid) elif pokemonid in ORAS_EXTRA_SPRITE_NAMES: for form_name in ORAS_EXTRA_SPRITE_NAMES[pokemonid][1:]: model_num += 1 filenames[model_num] = "{}-{}".format(pokemonid, form_name) # And now, do the ripping # TODO This will save Unown A as 201.png, and not create a 201-a.png pokemon_sprites_dir = out with read_garc(root / 'rom/a/2/6/3') as garc: from .lib.clim import decode_clim for i, subfile in enumerate(garc): shiny_prefix = '' if i > total_model_count: i -= total_model_count shiny_prefix = 'shiny/' if i == 0: # Dummy blank sprite, not interesting to us continue elif 37 <= i <= 41: # Cosplay Pikachu's outfits -- the sprites are blank, so saving # these is not particularly useful continue elif i in filenames: filename = shiny_prefix + filenames[i] + '.png' else: raise ValueError( "Can't find a filename for sprite number {}".format(i)) data = subfile[0].read() width, height, color_depth, pixels = decode_clim(data) png_writer = png.Writer( width=width, height=height, alpha=True, ) # this library is so fucking stupid # TODO strictly speaking we could just write out a paletted PNG directly # TODO add sBIT chunk indicating original bit depth path = pokemon_sprites_dir / filename parent = path.parent if not parent.exists(): parent.mkdir(parents=False) with path.open('wb') as f: png_writer.write(f, (itertools.chain(*row) for row in pixels)) for source, dest in duplicate_filenames: shutil.copyfile( str(pokemon_sprites_dir / source) + '.png', str(pokemon_sprites_dir / dest) + '.png', ) def _munge_source_arg(strpath): path = Path(strpath) if not path.is_dir(): raise argparse.ArgumentTypeError( "{!r} is not a directory".format(strpath)) # TODO something something romfs, exefs return path def make_arg_parser(): p = argparse.ArgumentParser() p.add_argument('what', choices=('data', 'dex-sprites', 'box-sprites'), help='what to extract') # TODO should verify that this is an actual game dump, and find the rom/exe p.add_argument('source', type=_munge_source_arg, help='path to an unpacked game image') p.add_argument('dest', type=_munge_source_arg, help='directory to dump the results into') return p def main(args): parser = make_arg_parser() args = parser.parse_args(args) # TODO support 'all', and just make some subdirectories per thing # TODO or maybe merge all the sprite things together since stuff will need moving around anyway idk if args.what == 'data': extract_data(args.source, args.dest) elif args.what == 'dex-sprites': extract_dex_sprites(args.source, args.dest) elif args.what == 'box-sprites': extract_box_sprites(args.source, args.dest) if __name__ == '__main__': import sys main(sys.argv[1:])