mirror of
https://github.com/veekun/pokedex.git
synced 2024-08-20 18:16:34 +00:00
684aef2506
Box sprites are now ripped paletted, complete with an sBIT chunk. Box sprites are also saved with the right form names. A lot of name-mangling and error-checking code was pulled out of the binary parsing stuff and shared between both types of sprites.
931 lines
32 KiB
Python
931 lines
32 KiB
Python
"""Dumps data from Omega Ruby and Alpha Sapphire.
|
|
|
|
Filesystem reference: http://www.projectpokemon.org/wiki/ORAS_File_System
|
|
"""
|
|
import argparse
|
|
from collections import Counter
|
|
from collections import OrderedDict
|
|
from collections import defaultdict
|
|
from contextlib import contextmanager
|
|
import io
|
|
import itertools
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import struct
|
|
|
|
from construct import Array, BitField, Bitwise, Magic, OptionalGreedyRange, Padding, Pointer, Struct, SLInt8, SLInt16, ULInt8, ULInt16, ULInt32
|
|
import png
|
|
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: ('spring', 'summer', 'autumn', 'winter'),
|
|
586: ('spring', '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
|
|
# TODO uh oh, this is handled as forms in boxes but as gender in sprites, maybe?
|
|
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('<H', f.read(2))
|
|
if moveid >= 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 get_mega_counts(root):
|
|
"""Return a dict mapping Pokémon ids to how many mega evolutions each one
|
|
has.
|
|
"""
|
|
mega_counts = {} # 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])
|
|
mega_counts[pokemonid] = max(
|
|
mega_evo.number for mega_evo in mega_evos)
|
|
|
|
return mega_counts
|
|
|
|
|
|
class SpriteFileNamer:
|
|
"""Do you have a big set of sprites, and a separate list of stuff
|
|
identifying them, as happens in XY and ORAS? I will sort that all out for
|
|
you.
|
|
"""
|
|
def __init__(self, out, mega_counts, form_names):
|
|
self.out = out
|
|
self.mega_counts = mega_counts
|
|
self.form_names = form_names
|
|
|
|
self.index_to_filenames = defaultdict(list)
|
|
self.seen = set()
|
|
|
|
def add(self, index, pokemonid, formid=0, right=False, back=False, shiny=False, female=False):
|
|
# Check that we don't try to do the same one twice
|
|
if index in self.index_to_filenames:
|
|
raise ValueError("Index {} is already {}".format(
|
|
index, self.index_to_filenames[index]))
|
|
|
|
key = (pokemonid, formid, right, back, shiny, female)
|
|
if key in self.seen:
|
|
raise ValueError("Duplicate sprite: {!r}".format(key))
|
|
self.seen.add(key)
|
|
|
|
# Figure out the form name
|
|
# TODO this assumes a Pokémon cannot have both forms and mega
|
|
# evolutions, which is true... for now
|
|
if pokemonid in self.form_names:
|
|
form = self.form_names[pokemonid][formid]
|
|
elif formid == 0:
|
|
form = None
|
|
elif self.mega_counts[pokemonid]:
|
|
if self.mega_counts[pokemonid] == 1:
|
|
form = ['mega'][formid - 1]
|
|
elif self.mega_counts[pokemonid] == 2:
|
|
form = ['mega-x', 'mega-y'][formid - 1]
|
|
else:
|
|
raise ValueError(
|
|
"Don't know how to name {} mega evolutions for Pokémon {}"
|
|
.format(self.mega_counts[pokemonid], pokemonid))
|
|
else:
|
|
raise ValueError("Pokemon {} doesn't have forms".format(pokemonid))
|
|
|
|
# Construct the directory
|
|
parts = []
|
|
if right:
|
|
parts.append('right')
|
|
if back:
|
|
parts.append('back')
|
|
if shiny:
|
|
parts.append('shiny')
|
|
if female:
|
|
parts.append('female')
|
|
|
|
# Build the final filename
|
|
bare_filename = "{}.png".format(pokemonid)
|
|
if form:
|
|
parts.append("{}-{}.png".format(pokemonid, form))
|
|
else:
|
|
parts.append(bare_filename)
|
|
filename = '/'.join(parts)
|
|
self.index_to_filenames[index].append(filename)
|
|
|
|
# For named "default" forms, create two output files
|
|
if form and formid == 0:
|
|
parts[-1] = bare_filename
|
|
self.index_to_filenames[index].append('/'.join(parts))
|
|
|
|
# Special case for Meowstic: duplicate its female form as a formless
|
|
# female sprite
|
|
if form == 'female' and not female:
|
|
parts.insert(-1, 'female')
|
|
parts[-1] = bare_filename
|
|
self.index_to_filenames[index].append('/'.join(parts))
|
|
|
|
def inject(self, index, filename):
|
|
"""Manually specify the filename for an index. Helpful for edge cases
|
|
like egg sprites.
|
|
"""
|
|
if index in self.index_to_filenames:
|
|
raise ValueError("Index {} is already {}".format(
|
|
index, self.index_to_filenames[index]))
|
|
|
|
self.index_to_filenames[index].append(filename)
|
|
|
|
# TODO we oughta create aliases for any that are missing?
|
|
# pumpkaboo/gourgeist and arceus don't have separate box icons, for
|
|
# example.
|
|
@contextmanager
|
|
def open(self, index, prefix=None):
|
|
out = self.out
|
|
if prefix:
|
|
out /= prefix
|
|
|
|
filenames = self.index_to_filenames[index]
|
|
|
|
if len(filenames) == 0:
|
|
raise RuntimeError("Don't have filenames for index {}".format(index))
|
|
|
|
fn = out / filenames[0]
|
|
if not fn.parent.exists():
|
|
fn.parent.mkdir(parents=True)
|
|
with fn.open('wb') as f:
|
|
yield f
|
|
|
|
for path in filenames[1:]:
|
|
fn2 = out / path
|
|
# TODO this duplication is annoying and we can probably do it in
|
|
# one fell swoop instead of constantly rechecking, maybe during the
|
|
# same timeframe that we fill in missing forms
|
|
if not fn2.parent.exists():
|
|
fn2.parent.mkdir(parents=True)
|
|
shutil.copyfile(str(fn), str(fn2))
|
|
|
|
|
|
def extract_box_sprites(root, out):
|
|
namer = SpriteFileNamer(
|
|
out, get_mega_counts(root), ORAS_EXTRA_SPRITE_NAMES)
|
|
|
|
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 magic number
|
|
for n in range(722):
|
|
sprite = pokemon_sprite_struct.parse_stream(f)
|
|
namer.add(sprite.index, n)
|
|
if sprite.female_index != sprite.index:
|
|
namer.add(sprite.female_index, n, female=True)
|
|
# 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
|
|
namer.add(form_idx, 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
|
|
namer.add(right_idx, n, form, right=True)
|
|
else:
|
|
assert sprite.right_count == 2
|
|
assert right_indices[0] == right_indices[1]
|
|
if right_indices[0] != sprite.index:
|
|
namer.add(right_indices[0], n, right=True)
|
|
|
|
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 == 333:
|
|
# Duplicate Entei sprite that's not used
|
|
continue
|
|
elif i == len(garc) - 1:
|
|
# Very last one is egg
|
|
namer.inject(i, 'egg.png')
|
|
|
|
data = subfile[0].read()
|
|
width, height, color_depth, palette, pixels = decode_clim(data)
|
|
png_writer = png.Writer(
|
|
width=width,
|
|
height=height,
|
|
palette=palette,
|
|
)
|
|
|
|
# TODO this is bad.
|
|
if 'right/' in namer.index_to_filenames[i][0]:
|
|
for row in pixels:
|
|
row.reverse()
|
|
|
|
# I want to preserve Zhorken's good idea of indicating the original
|
|
# bit depth with an sBIT chunk, but PyPNG can't do that directly,
|
|
# so we need to do a bit of nonsense.
|
|
buf = io.BytesIO()
|
|
png_writer.write(buf, pixels)
|
|
|
|
# Read the PNG as chunks, and manually add an sBIT chunk
|
|
buf.seek(0)
|
|
png_reader = png.Reader(buf)
|
|
chunks = list(png_reader.chunks())
|
|
sbit = bytes([color_depth] * 3)
|
|
chunks.insert(1, ('sBIT', sbit))
|
|
|
|
# Write chunks to an actual file
|
|
with namer.open(i) as f:
|
|
png.write_chunks(f, chunks)
|
|
|
|
|
|
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.
|
|
|
|
namer = SpriteFileNamer(
|
|
out, get_mega_counts(root), ORAS_EXTRA_SPRITE_NAMES)
|
|
|
|
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('<HBB', f.read(4))
|
|
model_num = start + 1
|
|
# For some asinine reason, Xerneas is counted as two separate
|
|
# Pokémon in the dex sprites but not the models, so we have to
|
|
# shift everything after it back by 1
|
|
if pokemonid == 716:
|
|
count = 2
|
|
elif pokemonid >= 717:
|
|
model_num += 1
|
|
|
|
namer.add(model_num, pokemonid)
|
|
form_count = count - 1 # discount "base" form
|
|
total_model_count = model_num + count - 1
|
|
|
|
# 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
|
|
namer.add(model_num, pokemonid, female=True)
|
|
# Flag 4 just means there are more forms?
|
|
if flags & 4:
|
|
assert form_count
|
|
|
|
for formid in range(1, form_count + 1):
|
|
model_num += 1
|
|
namer.add(model_num, pokemonid, formid)
|
|
|
|
# And now, do the ripping
|
|
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 = None
|
|
if i > total_model_count:
|
|
i -= total_model_count
|
|
# TODO this should be a real feature, as should the 'right'
|
|
# hack in the other code
|
|
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
|
|
|
|
data = subfile[0].read()
|
|
width, height, color_depth, palette, pixels = decode_clim(data)
|
|
assert not palette
|
|
png_writer = png.Writer(
|
|
width=width,
|
|
height=height,
|
|
alpha=True,
|
|
)
|
|
|
|
with namer.open(i, prefix=shiny_prefix) as f:
|
|
png_writer.write(f, (itertools.chain(*row) for row in pixels))
|
|
|
|
|
|
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:])
|