2016-02-26 18:05:51 +00:00
""" Dumps data from Omega Ruby and Alpha Sapphire.
Filesystem reference : http : / / www . projectpokemon . org / wiki / ORAS_File_System
"""
import argparse
from collections import OrderedDict
2016-10-16 01:16:41 +00:00
from collections import defaultdict
2016-02-26 18:05:51 +00:00
from contextlib import contextmanager
2017-06-23 20:32:18 +00:00
import itertools
2016-02-26 18:05:51 +00:00
from pathlib import Path
2016-10-16 01:16:41 +00:00
import re
2016-02-26 18:05:51 +00:00
import shutil
import struct
2017-01-05 12:57:05 +00:00
import warnings
2017-01-27 00:29:44 +00:00
from camel import Camel
2017-01-05 12:57:05 +00:00
from construct import (
# Simple fields
Const , Flag , Int16sl , Int16ul , Int8sl , Int8ul , Int32ul , Padding ,
# Structures and meta stuff
2017-06-25 22:57:38 +00:00
Array , BitsInteger , BitsSwapped , Bitwise , Embedded , Enum , Filter ,
2017-07-11 23:29:22 +00:00
FlagsEnum , FocusedSeq , GreedyRange , Pointer , PrefixedArray , Range , Struct ,
Terminated , this ,
2017-01-05 12:57:05 +00:00
# temp
Peek , Bytes ,
)
2016-02-26 18:05:51 +00:00
import yaml
2017-01-27 00:29:44 +00:00
import pokedex . schema as schema
2016-02-26 18:05:51 +00:00
from . lib . garc import GARCFile , decrypt_xy_text
2017-08-22 21:29:07 +00:00
from . lib . pc import PokemonContainerFile
2017-01-05 12:57:05 +00:00
# TODO: ribbons! 080 in sumo
2016-02-26 18:05:51 +00:00
2016-12-07 14:29:44 +00:00
# TODO auto-detect rom vs romfs vs... whatever
2016-02-26 18:05:51 +00:00
# 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...
2016-12-07 14:29:44 +00:00
# TODO would be nice to have meaningful names for the file structure instead of sprinkling hardcoded ones throughout
2017-01-05 12:57:05 +00:00
# SUMO file list:
# a/2/8/1 "photos" from the credits
2016-02-26 18:05:51 +00:00
2017-01-05 12:57:05 +00:00
GROWTH_RATES = {
0 : ' gr.medium ' ,
1 : ' gr.slow-then-very-fast ' ,
2 : ' gr.fast-then-very-slow ' ,
3 : ' gr.medium-slow ' ,
4 : ' gr.fast ' ,
5 : ' gr.slow ' ,
}
TYPES = {
0 : ' t.normal ' ,
1 : ' t.fighting ' ,
2 : ' t.flying ' ,
3 : ' t.poison ' ,
4 : ' t.ground ' ,
5 : ' t.rock ' ,
6 : ' t.bug ' ,
7 : ' t.ghost ' ,
8 : ' t.steel ' ,
9 : ' t.fire ' ,
10 : ' t.water ' ,
11 : ' t.grass ' ,
12 : ' t.electric ' ,
13 : ' t.psychic ' ,
14 : ' t.ice ' ,
15 : ' t.dragon ' ,
16 : ' t.dark ' ,
17 : ' t.fairy ' ,
}
2017-06-25 22:57:38 +00:00
EGG_GROUPS = {
0 : ' eg.egg ' , # FIXME dummy value only egg has
1 : ' eg.monster ' ,
2 : ' eg.water1 ' ,
3 : ' eg.bug ' ,
4 : ' eg.flying ' ,
5 : ' eg.ground ' ,
6 : ' eg.fairy ' ,
7 : ' eg.plant ' ,
8 : ' eg.humanshape ' ,
9 : ' eg.water3 ' ,
10 : ' eg.mineral ' ,
11 : ' eg.indeterminate ' ,
12 : ' eg.water2 ' ,
13 : ' eg.ditto ' ,
14 : ' eg.dragon ' ,
15 : ' eg.no-eggs ' ,
}
2017-07-11 23:29:22 +00:00
# TODO the order of these in the veekun db doesn't match the order in the games
COLORS = {
4 : ' pc.black ' ,
1 : ' pc.blue ' ,
5 : ' pc.brown ' ,
7 : ' pc.gray ' ,
3 : ' pc.green ' ,
9 : ' pc.pink ' ,
6 : ' pc.purple ' ,
0 : ' pc.red ' ,
8 : ' pc.white ' ,
2 : ' pc.yellow ' ,
}
2017-08-22 21:29:07 +00:00
# NOTE: these are listed in veekun order, which doesn't match the games, just
# as with colors
SHAPES = {
0 : ' ps.ball ' ,
12 : ' ps.squiggle ' ,
2 : ' ps.fish ' ,
13 : ' ps.arms ' ,
8 : ' ps.blob ' ,
9 : ' ps.upright ' ,
1 : ' ps.legs ' ,
4 : ' ps.quadruped ' ,
11 : ' ps.wings ' ,
7 : ' ps.tentacles ' ,
6 : ' ps.heads ' , # NOTE: this is really multi-body
10 : ' ps.humanoid ' ,
5 : ' ps.bug-wings ' ,
3 : ' ps.armor ' , # NOTE/TODO?: this is really just bug-shaped
}
2017-06-25 19:40:52 +00:00
DAMAGE_CLASSES = {
0 : ' dc.status ' ,
1 : ' dc.physical ' ,
2 : ' dc.special ' ,
}
2017-07-11 23:29:22 +00:00
# TODO the order of these in the veekun db doesn't match the order in the games
2017-06-25 19:40:52 +00:00
MOVE_RANGES = {
13 : ' mr.specific-move ' ,
3 : ' mr.selected-pokemon-me-first ' ,
2 : ' mr.ally ' ,
2017-07-11 23:29:22 +00:00
12 : ' mr.users-field ' ,
2017-06-25 19:40:52 +00:00
1 : ' mr.user-or-ally ' ,
11 : ' mr.opponents-field ' ,
7 : ' mr.user ' ,
9 : ' mr.random-opponent ' ,
4 : ' mr.all-other-pokemon ' ,
0 : ' mr.selected-pokemon ' ,
5 : ' mr.all-opponents ' ,
10 : ' mr.entire-field ' ,
2017-07-11 23:29:22 +00:00
6 : ' mr.user-and-allies ' ,
2017-06-25 19:40:52 +00:00
8 : ' mr.all-pokemon ' ,
}
2017-07-11 23:29:22 +00:00
AILMENTS = {
- 1 : ' ma.unknown ' ,
0 : ' ma.none ' ,
1 : ' ma.paralysis ' ,
2 : ' ma.sleep ' ,
3 : ' ma.freeze ' ,
4 : ' ma.burn ' ,
5 : ' ma.poison ' ,
6 : ' ma.confusion ' ,
7 : ' ma.infatuation ' ,
8 : ' ma.trap ' ,
9 : ' ma.nightmare ' ,
12 : ' ma.torment ' ,
13 : ' ma.disable ' ,
14 : ' ma.yawn ' ,
15 : ' ma.heal-block ' ,
17 : ' ma.no-type-immunity ' ,
18 : ' ma.leech-seed ' ,
19 : ' ma.embargo ' ,
20 : ' ma.perish-song ' ,
21 : ' ma.ingrain ' ,
24 : ' ma.silence ' ,
}
MOVE_CATEGORIES = {
0 : ' mc.damage ' ,
1 : ' mc.ailment ' ,
2 : ' mc.net-good-stats ' ,
3 : ' mc.heal ' ,
4 : ' mc.damage+ailment ' ,
5 : ' mc.swagger ' ,
6 : ' mc.damage+lower ' ,
7 : ' mc.damage+raise ' ,
8 : ' mc.damage+heal ' ,
9 : ' mc.ohko ' ,
10 : ' mc.whole-field-effect ' ,
11 : ' mc.field-effect ' ,
12 : ' mc.force-switch ' ,
13 : ' mc.unique ' ,
}
MOVE_FLAGS = {
1 : ' mf.contact ' ,
2 : ' mf.charge ' ,
3 : ' mf.recharge ' ,
4 : ' mf.protect ' ,
5 : ' mf.reflectable ' ,
6 : ' mf.snatch ' ,
7 : ' mf.mirror ' ,
8 : ' mf.punch ' ,
9 : ' mf.sound ' ,
10 : ' mf.gravity ' ,
11 : ' mf.defrost ' ,
12 : ' mf.distance ' ,
13 : ' mf.heal ' ,
14 : ' mf.authentic ' ,
# FIXME this was powder before? whenever "before" was? also this name, is bad.
15 : ' mf.non-sky-battle ' ,
# NOTE: 16 indicates a distinct animation when the move is used on an ally,
# I think; doesn't seem like something we care about
#16: 'mf.unknown16',
# FIXME this is new
17 : ' mf.dance ' ,
# FIXME these are either gone, or in a different order? the last four seem
# to be inventions by surskitty, not sure where they came from
#16: 'mf.bite',
#17: 'mf.pulse',
#18: 'mf.ballistics',
#19: 'mf.mental',
#20: 'mf.non-sky-battle',
}
# FIXME hokey; "all" in particular is not great
MOVE_STATS = {
1 : ' st.attack ' ,
2 : ' st.defense ' ,
3 : ' st.special-attack ' ,
4 : ' st.special-defense ' ,
5 : ' st.speed ' ,
6 : ' st.accuracy ' ,
7 : ' st.evasion ' ,
8 : ' st.all ' ,
}
2017-06-25 19:40:52 +00:00
2017-01-05 12:57:05 +00:00
# ja-Hrkt: hiragana/katakana
# zh-Hans: simplified
# zh-Hant: traditional
CANON_LANGUAGES = ( ' ja-Hrkt ' , ' ja ' , ' en ' , ' fr ' , ' it ' , ' de ' , ' es ' , ' ko ' , ' zh-Hans ' , ' zh-Hant ' )
2016-02-26 18:05:51 +00:00
ORAS_SCRIPT_FILES = {
2017-01-05 12:57:05 +00:00
' ja-Hrkt ' : ' rom/a/0/7/1 ' ,
' ja ' : ' rom/a/0/7/2 ' ,
2016-02-26 18:05:51 +00:00
' 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 ' ,
}
2016-12-07 14:29:44 +00:00
SUMO_SCRIPT_FILES = {
2017-01-05 12:57:05 +00:00
' ja-Hrkt ' : ' rom/a/0/3/0 ' ,
' ja ' : ' rom/a/0/3/1 ' ,
2016-12-07 14:29:44 +00:00
' en ' : ' rom/a/0/3/2 ' ,
' fr ' : ' rom/a/0/3/3 ' ,
' it ' : ' rom/a/0/3/4 ' ,
' de ' : ' rom/a/0/3/5 ' ,
' es ' : ' rom/a/0/3/6 ' ,
' ko ' : ' rom/a/0/3/7 ' ,
2017-01-05 12:57:05 +00:00
' zh-Hans ' : ' rom/a/0/3/8 ' ,
' zh-Hant ' : ' rom/a/0/3/9 ' ,
2016-12-07 14:29:44 +00:00
}
2016-02-26 18:05:51 +00:00
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 ,
2016-12-07 14:29:44 +00:00
# Note that these place names come in pairs, in order to support X/Y's
# routes, which had both numbers and traditional street names
# TODO oughta rip those too!
' zone-names ' : 90 ,
2016-02-26 18:05:51 +00:00
' species-names ' : 98 ,
2016-12-07 14:29:44 +00:00
# 113: item names, with macros to branch for pluralization
# 114: copy of item names, but with "PP" in latin in korean (?!)
# 115: item names in plural (maybe interesting?)
' item-names ' : 116 , # singular
' item-flavor ' : 117 ,
}
SUMO_SCRIPT_ENTRIES = {
# 2: bag pockets
# 81: ribbons
' form-names ' : 114 ,
# TODO a lot of these are missing
' species-flavor-sun ' : 119 ,
' species-flavor-moon ' : 120 ,
' move-contest-flavor ' : 109 ,
' move-names ' : 113 ,
# TODO 19 is z-move names
# Note: table 15 is also a list of move names, but with a few at the end
# missing? XY leftovers?
' move-flavor ' : 112 ,
' type-names ' : 107 ,
' ability-flavor ' : 97 ,
' ability-names ' : 96 ,
' nature-names ' : 87 ,
# Note that these place names come in pairs, in order to support X/Y's
# routes, which had both numbers and traditional street names
# TODO oughta rip those too!
' zone-names ' : 67 ,
# NOTE: 67 through 70 could be zone names, but could also be "where caught"
# names for Pokémon
' species-names ' : 55 ,
' pokemon-height-flavor ' : 115 ,
' genus-names ' : 116 ,
' pokemon-weight-flavor ' : 117 ,
' trainer-class-names ' : 106 ,
' berry-names ' : 65 ,
2017-08-22 21:29:07 +00:00
# 49 appears to be clothing dye colors + a set of clothes patterns?
2016-12-07 14:29:44 +00:00
# 38: item names, with macros to branch for pluralization
# 114: copy of item names, but with "PP" in latin in korean (?!)
# 37: item names in plural (maybe interesting?)
' item-names ' : 36 , # singular
' item-flavor ' : 35 ,
2016-02-26 18:05:51 +00:00
}
2017-01-05 12:57:05 +00:00
# The first element in each list is the name of the BASE form.
# If it's None, then the base form is a true default in some sense, and it'll
# have the same name as the species. Mega Evolutions are a good example.
# Otherwise, there is no default; the form name will differ from the species
# name, and the first sprite will be saved under both names, e.g., Shellos.
# Note that this does NOT include megas -- those are pulled from game data.
FORM_NAMES = {
# TODO alolan are of course new in SUMO
# Rattata and Raticate
19 : ( None , ' alola ' ) ,
20 : ( None , ' alola ' , ' totem-alola ' ) ,
2016-02-26 18:05:51 +00:00
# Cosplay Pikachu
2017-01-05 12:57:05 +00:00
# TODO not in SUMO
#25: (None, 'rock-star', 'belle', 'pop-star', 'phd', 'libre', 'cosplay'),
25 : ( None , ' original-cap ' , ' hoenn-cap ' , ' sinnoh-cap ' , ' unova-cap ' , ' kalos-cap ' , ' alola-cap ' ) ,
# Raichu
26 : ( None , ' alola ' ) ,
# Sandshrew and Sandslash
27 : ( None , ' alola ' ) ,
28 : ( None , ' alola ' ) ,
# Vulpix and Ninetales
37 : ( None , ' alola ' ) ,
38 : ( None , ' alola ' ) ,
# Diglett and Dugtrio
50 : ( None , ' alola ' ) ,
51 : ( None , ' alola ' ) ,
# Meowth and Persian
52 : ( None , ' alola ' ) ,
53 : ( None , ' alola ' ) ,
# Geodude, Graveler, and Golem
74 : ( None , ' alola ' ) ,
75 : ( None , ' alola ' ) ,
76 : ( None , ' alola ' ) ,
# Geodude, Graveler, and Golem
88 : ( None , ' alola ' ) ,
89 : ( None , ' alola ' ) ,
# Exeggutor
103 : ( None , ' alola ' ) ,
# Marowak
105 : ( None , ' alola ' ) ,
2016-02-26 18:05:51 +00:00
# Unown
201 : tuple ( ' abcdefghijklmnopqrstuvwxyz ' ) + ( ' exclamation ' , ' question ' ) ,
# Castform
351 : ( None , ' sunny ' , ' rainy ' , ' snowy ' ) ,
# Kyogre and Groudon
2017-01-05 12:57:05 +00:00
382 : ( None , ' primal ' ) ,
383 : ( None , ' primal ' ) ,
2016-02-26 18:05:51 +00:00
# Deoxys
386 : ( ' normal ' , ' attack ' , ' defense ' , ' speed ' ) ,
# Burmy and Wormadam
412 : ( ' plant ' , ' sandy ' , ' trash ' ) ,
413 : ( ' plant ' , ' sandy ' , ' trash ' ) ,
# Cherrim
2017-01-05 12:57:05 +00:00
421 : ( ' overcast ' , ' sunshine ' ) ,
2016-02-26 18:05:51 +00:00
# Shellos and Gastrodon
2017-01-05 12:57:05 +00:00
422 : ( ' west ' , ' east ' ) ,
423 : ( ' west ' , ' east ' ) ,
2016-02-26 18:05:51 +00:00
# Rotom
479 : ( None , ' heat ' , ' wash ' , ' frost ' , ' fan ' , ' mow ' ) ,
# Giratina
2017-01-05 12:57:05 +00:00
487 : ( ' altered ' , ' origin ' ) ,
2016-02-26 18:05:51 +00:00
# Shaymin
2017-01-05 12:57:05 +00:00
492 : ( ' land ' , ' sky ' ) ,
2016-02-26 18:05:51 +00:00
# Arceus
493 : (
' normal ' , ' fighting ' , ' flying ' , ' poison ' , ' ground ' , ' rock ' , ' bug ' ,
' ghost ' , ' steel ' , ' fire ' , ' water ' , ' grass ' , ' electric ' , ' psychic ' ,
' ice ' , ' dragon ' , ' dark ' , ' fairy ' ,
) ,
# Basculin
2017-01-05 12:57:05 +00:00
550 : ( ' red-striped ' , ' blue-striped ' ) ,
2016-02-26 18:05:51 +00:00
# Darmanitan
2017-01-05 12:57:05 +00:00
555 : ( ' standard ' , ' zen ' ) ,
2016-02-26 18:05:51 +00:00
# Deerling and Sawsbuck
2016-10-16 01:16:41 +00:00
585 : ( ' spring ' , ' summer ' , ' autumn ' , ' winter ' ) ,
586 : ( ' spring ' , ' summer ' , ' autumn ' , ' winter ' ) ,
2016-02-26 18:05:51 +00:00
# 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 ' ) ,
2017-01-05 12:57:05 +00:00
# Greninja
# TODO SUMO only
# FIXME why is the second one here at all?
658 : ( None , ' dupe ' , ' ash ' ) ,
2016-02-26 18:05:51 +00:00
# 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
2016-10-16 01:16:41 +00:00
# TODO uh oh, this is handled as forms in boxes but as gender in sprites, maybe?
678 : ( ' male ' , ' female ' ) ,
2016-02-26 18:05:51 +00:00
# Aegislash
681 : ( ' shield ' , ' blade ' ) ,
# Pumpkaboo/Gourgeist
710 : ( ' average ' , ' small ' , ' large ' , ' super ' ) ,
711 : ( ' average ' , ' small ' , ' large ' , ' super ' ) ,
# Xerneas
716 : ( ' neutral ' , ' active ' ) ,
2017-01-05 12:57:05 +00:00
# Zygarde
# TODO SUMO only
# TODO why are 10 and 50 duplicated?
718 : ( None , ' 10 ' , ' 10 ' , ' 50 ' , ' complete ' ) ,
2016-02-26 18:05:51 +00:00
# Hoopa
2017-08-22 21:29:07 +00:00
# TODO should the default form be 'confined'?
720 : ( None , ' unbound ' ) ,
2017-01-05 12:57:05 +00:00
# Gumshoos
735 : ( None , ' totem ' ) ,
# Vikavolt
738 : ( None , ' totem ' ) ,
# Oricorio
741 : ( ' baile ' , ' pom-pom ' , ' pau ' , ' sensu ' ) ,
# Lycanroc
745 : ( ' midday ' , ' midnight ' ) ,
# Wishiwashi
746 : ( ' solo ' , ' school ' ) ,
# Lurantis
754 : ( None , ' totem ' ) ,
# Salazzle
758 : ( None , ' totem ' ) ,
# Silvally
773 : (
' normal ' , ' fighting ' , ' flying ' , ' poison ' , ' ground ' , ' rock ' , ' bug ' ,
' ghost ' , ' steel ' , ' fire ' , ' water ' , ' grass ' , ' electric ' , ' psychic ' ,
' ice ' , ' dragon ' , ' dark ' , ' fairy ' ,
) ,
# Minior
774 : (
' red-meteor ' , ' orange-meteor ' , ' yellow-meteor ' , ' green-meteor ' ,
' blue-meteor ' , ' indigo-meteor ' , ' violet-meteor ' ,
' red ' , ' orange ' , ' yellow ' , ' green ' , ' blue ' , ' indigo ' , ' violet ' ,
) ,
# Mimikyu
778 : ( ' disguised ' , ' busted ' , ' totem-disguised ' , ' totem-busted ' ) ,
# Kommo-o
784 : ( None , ' totem ' ) ,
# Magearna
801 : ( None , ' original ' ) ,
2016-02-26 18:05:51 +00:00
}
2017-06-25 19:40:52 +00:00
def VeekunEnum ( subcon , enumdict ) :
return Enum ( subcon , * * { v : k for ( k , v ) in enumdict . items ( ) } )
2016-02-26 18:05:51 +00:00
pokemon_struct = Struct (
2017-01-05 12:57:05 +00:00
' stat_hp ' / Int8ul ,
' stat_atk ' / Int8ul ,
' stat_def ' / Int8ul ,
' stat_speed ' / Int8ul ,
' stat_spatk ' / Int8ul ,
' stat_spdef ' / Int8ul ,
2017-06-25 22:57:38 +00:00
' type1 ' / VeekunEnum ( Int8ul , TYPES ) ,
' type2 ' / VeekunEnum ( Int8ul , TYPES ) ,
2017-01-05 12:57:05 +00:00
' capture_rate ' / Int8ul ,
' stage ' / Int8ul ,
2017-06-25 22:57:38 +00:00
' effort ' / Peek ( Int16ul ) ,
Embedded ( Bitwise ( Struct (
# FIXME this is byte-swapped and i had to manually compensate for that,
# which i dislike
' effort_speed ' / BitsInteger ( 2 ) ,
' effort_defense ' / BitsInteger ( 2 ) ,
' effort_attack ' / BitsInteger ( 2 ) ,
' effort_hp ' / BitsInteger ( 2 ) ,
' effort_padding ' / BitsInteger ( 4 ) ,
' effort_special_defense ' / BitsInteger ( 2 ) ,
' effort_special_attack ' / BitsInteger ( 2 ) ,
) ) ) ,
2017-01-05 12:57:05 +00:00
' held_item1 ' / Int16ul ,
' held_item2 ' / Int16ul ,
' held_item3 ' / Int16ul , # dark grass from bw, unused in oras?
' gender_rate ' / Int8ul ,
' steps_to_hatch ' / Int8ul ,
' base_happiness ' / Int8ul ,
2017-06-25 19:40:52 +00:00
' growth_rate ' / VeekunEnum ( Int8ul , GROWTH_RATES ) ,
2017-06-25 22:57:38 +00:00
' egg_group1 ' / VeekunEnum ( Int8ul , EGG_GROUPS ) ,
' egg_group2 ' / VeekunEnum ( Int8ul , EGG_GROUPS ) ,
2017-01-05 12:57:05 +00:00
' ability1 ' / Int8ul ,
' ability2 ' / Int8ul ,
' ability_hidden ' / Int8ul ,
' safari_escape ' / Int8ul ,
' form_species_start ' / Int16ul ,
' form_sprite_start ' / Int16ul ,
' form_count ' / Int8ul ,
2017-07-11 23:29:22 +00:00
' color ' / VeekunEnum ( Int8ul , COLORS ) ,
2017-01-05 12:57:05 +00:00
' base_exp ' / Int16ul ,
' height ' / Int16ul ,
' weight ' / Int16ul ,
2017-07-11 23:29:22 +00:00
' machines ' / BitsSwapped ( Bitwise ( Array ( 16 * 8 , Flag ) ) ) ,
2017-08-22 21:29:07 +00:00
# TODO this is clearly not tutors (in sumo anyway)? bulb is 1/1/9, char 2/2/18, squirtle 4/4/36, then all zeroes until dratini 64/64/64, then same with next starters...
2017-01-05 12:57:05 +00:00
' tutors ' / Int32ul ,
2017-08-22 21:29:07 +00:00
# TODO appear to be all zeroes, at least in sumo
2017-01-05 12:57:05 +00:00
' mystery1 ' / Int16ul ,
' mystery2 ' / Int16ul ,
2017-07-11 23:29:22 +00:00
# TODO these are unused in sumo
' bp_tutors1 ' / Const ( b ' \x00 \x00 \x00 \x00 ' ) ,
' bp_tutors2 ' / Const ( b ' \x00 \x00 \x00 \x00 ' ) ,
' bp_tutors3 ' / Const ( b ' \x00 \x00 \x00 \x00 ' ) ,
# FIXME this is bp_tutors4 in oras
' z_crystal ' / Int16ul ,
' z_base_move ' / Int16ul ,
# FIXME oras ends here
' z_move ' / Int16ul ,
# Not sure where this is used but it seems to be 1 for Alolan Pokémon only
# (but not their totem versions)
' is_alolan ' / Int16ul ,
2016-02-26 18:05:51 +00:00
)
2017-01-05 12:57:05 +00:00
pokemon_mega_evolutions_struct = Filter ( this . number != 0 , Range (
# XY and ORAS have 3 of these, but the third never seems to be populated.
# SUMO just has 2.
2 , 3 ,
2016-02-26 18:05:51 +00:00
Struct (
2017-01-05 12:57:05 +00:00
' number ' / Int16ul ,
' mode ' / Int16ul ,
' mega_stone_itemid ' / Int16ul ,
2016-02-26 18:05:51 +00:00
Padding ( 2 ) ,
)
2017-01-05 12:57:05 +00:00
) )
2016-02-26 18:05:51 +00:00
2017-06-18 06:15:11 +00:00
# FIXME need a thing for "pad to 8 junk entries"?
pokemon_evolutions_struct = Filter ( this . method != 0 , Array ( 8 , Struct (
' method ' / Int16ul ,
' param ' / Int16ul ,
' into_species ' / Int16ul ,
' form ' / Int8sl ,
' level ' / Int8sl ,
) ) )
2016-02-26 18:05:51 +00:00
egg_moves_struct = Struct (
2017-01-05 12:57:05 +00:00
' moveids ' / PrefixedArray ( Int16ul , Int16ul ) ,
2016-02-26 18:05:51 +00:00
)
2016-12-07 14:29:44 +00:00
egg_moves_struct = Struct (
2017-01-05 12:57:05 +00:00
' first_form_id ' / Int16ul , # TODO SUMO ONLY
' moveids ' / PrefixedArray ( Int16ul , Int16ul ) ,
2016-12-07 14:29:44 +00:00
)
2017-01-05 12:57:05 +00:00
level_up_moves_struct = GreedyRange (
2016-02-26 18:05:51 +00:00
Struct (
2017-01-05 12:57:05 +00:00
' moveid ' / Int16sl ,
' level ' / Int16sl ,
2016-02-26 18:05:51 +00:00
) ,
)
move_struct = Struct (
2017-06-25 19:40:52 +00:00
' type ' / VeekunEnum ( Int8ul , TYPES ) ,
2017-07-11 23:29:22 +00:00
' category ' / VeekunEnum ( Int8ul , MOVE_CATEGORIES ) ,
2017-06-25 19:40:52 +00:00
' damage_class ' / VeekunEnum ( Int8ul , DAMAGE_CLASSES ) ,
2017-01-05 12:57:05 +00:00
' power ' / Int8ul ,
' accuracy ' / Int8ul ,
' pp ' / Int8ul ,
' priority ' / Int8sl ,
' min_max_hits ' / Int8ul ,
2017-07-11 23:29:22 +00:00
' ailment ' / VeekunEnum ( Int16sl , AILMENTS ) ,
' ailment_chance ' / Int8ul ,
2017-01-05 12:57:05 +00:00
' status ' / Int8ul ,
' min_turns ' / Int8ul ,
' max_turns ' / Int8ul ,
' crit_rate ' / Int8ul ,
' flinch_chance ' / Int8ul ,
' effect ' / Int16ul ,
2017-07-11 23:29:22 +00:00
' drain ' / Int8sl ,
' healing ' / Int8sl ,
2017-06-25 19:40:52 +00:00
' range ' / VeekunEnum ( Int8ul , MOVE_RANGES ) ,
2017-07-11 23:29:22 +00:00
' stat_change ' / Array ( 3 , Int8ul ) ,
' stat_amount ' / Array ( 3 , Int8sl ) ,
' stat_chance ' / Array ( 3 , Int8ul ) ,
2017-06-25 19:40:52 +00:00
# FIXME sumo only; padding in oras i think
2017-07-11 23:29:22 +00:00
' z_move_id ' / Int16ul ,
' z_move_power ' / Int8ul ,
' z_move_effect ' / Int8ul ,
# FIXME this cuts off somewhere in ORAS, unsure where... but the flags are last, so, ??
' mystery3 ' / Int8ul , # 0-4
' mystery4 ' / Int8ul , # 0, 25, 50, or 100
' flags ' / FlagsEnum ( Int32ul , * * { v : 1 << ( k - 1 ) for ( k , v ) in MOVE_FLAGS . items ( ) } ) ,
2016-02-26 18:05:51 +00:00
)
2017-01-05 12:57:05 +00:00
move_container_struct = FocusedSeq ( ' records ' ,
Const ( b ' WD ' ) , # waza... descriptions?
' records ' / PrefixedArray ( Int16ul , FocusedSeq ( ' move ' ,
' offset ' / Int32ul ,
' move ' / Pointer ( this . offset , move_struct ) ,
) ) ,
2016-02-26 18:05:51 +00:00
)
2017-06-15 05:55:28 +00:00
item_struct = Struct (
' price ' / Int16ul ,
' held_effect ' / Int8ul ,
' held_param ' / Int8ul ,
' natural_gift_effect ' / Int8ul ,
' fling_effect ' / Int8ul ,
' fling_power ' / Int8ul ,
' natural_gift_power ' / Int8ul ,
2017-06-18 03:21:26 +00:00
# actually only the low 5 bits are natural gift type, and i think 31 means none, or something
# high bit is... usable on a pokémon, maybe? but, it's not on tms and IS on z-crystals
# a lot of items are 63, including tons (all?) of key items and all tms
# also some items are 127??? exp share, vs recorder, mach bike, acro bike, pokeblock kit, eon flute, rods, dowsing machine
# so out of top three bits the only combinations i see are 000, 001, 011, 100
' natural_gift_type ' / Int8ul ,
2017-06-15 05:55:28 +00:00
# 1 - TMs and berries
# 2 - key items and one set of z-crystals
# 3 - another set of z-crystals??
# 8 - pokeballs
# 16 - battle items
# 32 - healing items
# 64 - status items
# 33, 65, 96 - as you'd expect
' maybe_flags ' / Int8ul ,
# seems to be about interaction with pokémon?
# 24 - form changers?
# 26 - dna splicers??
# 18 - rods
# 10 - black/white flute (more/fewer wild encounters)
# 9 - eon flute
# 8 - some berries, zygarde cube, pokéblock kit,
# 7 - mail
# 6 - stones
# 5 - repel and exp share??
# 4 - honey
# 3 - bike
# 2 - tms
# 1 - healing items and z-crystals
# possibly use effect??
' maybe_flags3 ' / Int8ul ,
# maybe part of battle item ui?
# 1 - balls
# 2 - medicine (no berries)
# 3 - three escape items
' mystery0b ' / Int8ul ,
# 1 - fossils, shards, relics, heart scale, mulch, special battle items, medicine, stones, berries (holdable??)
# 2 - balls (but not in xy apparently?)
' mystery0c ' / Int8ul ,
# ALMOST pocket?
# 0 - medicine
# 1 - held items (including ites and z-crystals)
# 2 - apricorns, data cards, some reward items, fossils, nectar, relics, shards, vendor trash, mulch, stones
# 3 - battle items
# 4 - balls
# 5 - mail
# 6 - tms
# 7 - berries
# 8 - key items
2017-08-22 21:29:07 +00:00
' pocketish ' / Int8ul ,
2017-06-15 05:55:28 +00:00
# 1 - can be used (+ consumed) by pokémon (maybe for recycle purposes)
# 16 - black/white/yellow/red/blue flutes?? (not in xy?)
' mystery0e ' / Int8ul ,
# note that 1 appears nine times (not counting dupe normalium z) but there aren't nine pockets
# here they are with the almost-pocket value, feel free to investigate
# 0: super potion
# 1: smoke ball / normalium z
# 2: super repel
# 3: x defense
# 4: great ball
# 5: -
# 6: tm02
# 7: chesto berry
# 8: exp share
# of course, 0 appears a bunch of times (28!!), since it includes a bunch of junk items
' pocket_order ' / Int8ul ,
# 1 - slp
# 2 - psn
# 4 - brn
# 8 - frz
# 16 - par
# 32 - confusion
# 64 - attraction
# 128 - guard spec
# NOTE: balls and z-crystals seem to use this as a regular number
' status_cured ' / Int8ul ,
# low nybble:
# 1 - revive
# 3 - revive all
# 5 - level up
# 8 - evo stone
# high nybble: attack increase
' base_stats1 ' / Int8sl ,
# defense / special attack
' base_stats2 ' / Int8sl ,
# special defense / speed
' base_stats3 ' / Int8sl ,
# accuracy / critical hit / pp up / pp max
' base_stats4 ' / Int8sl ,
# 1 - ether
# 2 - elixir
# 4 - heal
# 8 - hp up (i.e., effort)
# 16 - atk up
# 32 - def up
# 64 - spd up
# 128 - spatk up
# 1 - spdef up
# 2 - is a wing (combines with others)
# 4, 8, 16 - friendship up? maybe for "can affect in range 1, 2, 3" -- only ones without 16 are the x's, which also have 0 for friendship3
' heal_slash_effort ' / Bytes ( 2 ) ,
' effort_hp ' / Int8sl ,
' effort_attack ' / Int8sl ,
' effort_defense ' / Int8sl ,
' effort_speed ' / Int8sl ,
' effort_special_attack ' / Int8sl ,
' effort_special_defense ' / Int8sl ,
' hp_restore ' / Int8sl , # TODO negative means fraction of total
' pp_restore ' / Int8sl , # TODO negative means fraction of total
' friendship1 ' / Int8sl ,
' friendship2 ' / Int8sl ,
' friendship3 ' / Int8sl ,
Padding ( 2 , strict = True ) ,
Terminated ,
)
2016-02-26 18:05:51 +00:00
pokemon_sprite_struct = Struct (
2017-01-05 12:57:05 +00:00
' index ' / Int16ul ,
' female_index ' / Int16ul ,
' form_index_offset ' / Int32ul ,
' right_index_offset ' / Int32ul ,
' form_count ' / Int16ul ,
' right_count ' / Int16ul ,
2016-02-26 18:05:51 +00:00
)
2016-12-07 14:29:44 +00:00
encounter_struct = Struct (
# TODO top 5 bits are form stuff
2017-01-05 12:57:05 +00:00
' pokemon_id ' / Int16ul ,
' min_level ' / Int8ul ,
' max_level ' / Int8ul ,
2016-12-07 14:29:44 +00:00
)
encounter_table_struct = Struct (
2017-01-05 12:57:05 +00:00
' walk_rate ' / Int8ul ,
' long_grass_rate ' / Int8ul ,
' hidden_rate ' / Int8ul ,
' surf_rate ' / Int8ul ,
' rock_smash_rate ' / Int8ul ,
' old_rod_rate ' / Int8ul ,
' good_rod_rate ' / Int8ul ,
' super_rod_rate ' / Int8ul ,
' horde_rate ' / Int8ul ,
Const ( b ' \x00 ' * 5 ) ,
2016-12-07 14:29:44 +00:00
Array ( 61 , encounter_struct ) ,
2017-01-05 12:57:05 +00:00
Const ( b ' \x00 ' * 2 ) ,
2016-12-07 14:29:44 +00:00
)
ORAS_ENCOUNTER_SLOTS = [
( ' walk ' , ( 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 5 , 4 , 1 ) ) ,
( ' long-grass ' , ( 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 10 , 5 , 4 , 1 ) ) ,
( ' hidden ' , ( 60 , 35 , 5 ) ) , # TODO guessing here!
( ' surf ' , ( 50 , 30 , 15 , 4 , 1 ) ) ,
( ' rock-smash ' , ( 50 , 30 , 15 , 4 , 1 ) ) ,
( ' old-rod ' , ( 60 , 35 , 5 ) ) ,
( ' good-rod ' , ( 60 , 35 , 5 ) ) ,
( ' super-rod ' , ( 60 , 35 , 5 ) ) ,
( ' horde ' , ( ( 60 , 5 ) , ( 35 , 5 ) , ( 5 , 5 ) ) ) ,
]
# The only thing really linking ORAS zones together is that they share the same
# overall location/place name, so use the index of that name as a key to match
# to an extant location
ORAS_ZONE_NAME_INDEX_TO_VEEKUN_LOCATION = {
#170: Littleroot Town
#172: Oldale Town
174 : ' dewford-town ' ,
#176: Lavaridge Town
#178: Fallarbor Town
#180: Verdanturf Town
#182: Pacifidlog Town
184 : ' petalburg-city ' ,
186 : ' slateport-city ' ,
#188: Mauville City
#190: Rustboro City
#192: Fortree City
194 : ' lilycove-city ' ,
196 : ' mossdeep-city ' ,
198 : ' sootopolis-city ' ,
200 : ' ever-grande-city ' ,
#202: Pokémon League
204 : ' hoenn-route-101 ' ,
206 : ' hoenn-route-102 ' ,
208 : ' hoenn-route-103 ' ,
210 : ' hoenn-route-104 ' ,
212 : ' hoenn-route-105 ' ,
214 : ' hoenn-route-106 ' ,
216 : ' hoenn-route-107 ' ,
218 : ' hoenn-route-108 ' ,
220 : ' hoenn-route-109 ' ,
222 : ' hoenn-route-110 ' ,
224 : ' hoenn-route-111 ' ,
226 : ' hoenn-route-112 ' ,
228 : ' hoenn-route-113 ' ,
230 : ' hoenn-route-114 ' ,
232 : ' hoenn-route-115 ' ,
234 : ' hoenn-route-116 ' ,
236 : ' hoenn-route-117 ' ,
238 : ' hoenn-route-118 ' ,
240 : ' hoenn-route-119 ' ,
242 : ' hoenn-route-120 ' ,
244 : ' hoenn-route-121 ' ,
246 : ' hoenn-route-122 ' ,
248 : ' hoenn-route-123 ' ,
250 : ' hoenn-route-124 ' ,
252 : ' hoenn-route-125 ' ,
254 : ' hoenn-route-126 ' ,
256 : ' hoenn-route-127 ' ,
258 : ' hoenn-route-128 ' ,
260 : ' hoenn-route-129 ' ,
262 : ' hoenn-route-130 ' ,
264 : ' hoenn-route-131 ' ,
266 : ' hoenn-route-132 ' ,
268 : ' hoenn-route-133 ' ,
270 : ' hoenn-route-134 ' ,
272 : ' meteor-falls ' ,
274 : ' rusturf-tunnel ' ,
#276: ???
#278: Desert Ruins
280 : ' granite-cave ' ,
282 : ' petalburg-woods ' ,
#284: Mt. Chimney
286 : ' jagged-pass ' ,
288 : ' fiery-path ' ,
290 : ' mt-pyre ' ,
#292: Team Aqua Hideout
294 : ' seafloor-cavern ' ,
296 : ' cave-of-origin ' ,
298 : ' hoenn-victory-road ' ,
300 : ' shoal-cave ' ,
302 : ' new-mauville ' ,
#304: Sea Mauville
#306: Island Cave
#308: Ancient Tomb
#310: Sealed Chamber
#312: Scorched Slab
#314: Team Magma Hideout
316 : ' sky-pillar ' ,
#318: Battle Resort
#320: Southern Island
# TODO is this "abandoned-ship" from rse?
#322: S.S. Tidal
324 : ' hoenn-safari-zone ' ,
#326: Mirage Forest
#328: Mirage Cave
#330: Mirage Island
#332: Mirage Mountain
#334: Trackless Forest
#336: Pathless Plain
#338: Nameless Cavern
#340: Fabled Cave
#342: Gnarled Den
#344: Crescent Isle
#346: Secret Islet
#348: Soaring in the sky
#350: Secret Shore
#352: Secret Meadow
#354: Secret Base
}
# TODO wait, in the yaml thing, where do the fanon names for these go?
ORAS_ZONE_INDEX_TO_VEEKUN_AREA = {
# TODO oops i should be actually mapping these to areas in rse. many of
# them aren't split the same way, though. uh oh. if we make areas a more
# first-class thing, then... how do we deal with this? e.g. route 104 is
# two zones in oras but only one zone in rse. it's easy enough to fudge
# that with encounters, but what do you do about events etc?
26 : ' hoenn-route-104--north ' ,
27 : ' hoenn-route-104--south ' ,
# TODO should i, maybe, indicate the type of terrain an area has...?
30 : ' hoenn-route-107 ' ,
64 : ' hoenn-route-107--underwater ' ,
# NOTE: split from rse
38 : ' hoenn-route-112--north ' , # route 111 side
39 : ' hoenn-route-112--south ' , # lavaridge town side
35 : ' hoenn-route-111 ' ,
# NOTE: split from rse
37 : ' hoenn-route-111--desert ' ,
48 : ' hoenn-route-120 ' ,
# NOTE: new
49 : ' hoenn-route-120--tomb-area ' ,
53 : ' hoenn-route-124 ' ,
65 : ' hoenn-route-124--underwater ' ,
55 : ' hoenn-route-126 ' ,
66 : ' hoenn-route-126--underwater ' ,
57 : ' hoenn-route-128 ' ,
# NOTE: new
68 : ' hoenn-route-128--underwater ' ,
58 : ' hoenn-route-129 ' ,
# NOTE: new
69 : ' hoenn-route-129--underwater ' ,
59 : ' hoenn-route-130 ' ,
# NOTE: new
70 : ' hoenn-route-130--underwater ' ,
71 : ' meteor-falls ' ,
74 : ' meteor-falls--backsmall-room ' , # TODO this name is dumb
# NOTE: indistinguishable
72 : ' meteor-falls--back ' ,
73 : ' meteor-falls--b1f ' ,
78 : ' granite-cave--1f ' ,
79 : ' granite-cave--b1f ' ,
80 : ' granite-cave--b2f ' ,
# NOTE: indistinguishable
86 : ' mt-pyre--1f ' ,
87 : ' mt-pyre--2f ' ,
88 : ' mt-pyre--3f ' ,
89 : ' mt-pyre--4f ' ,
90 : ' mt-pyre--outside ' ,
# NOTE: indistinguishable; split from rse
91 : ' mt-pyre--summit-south ' ,
533 : ' mt-pyre--summit-north ' ,
# NOTE: many sets of these are indistinguishable; ALL split from rse
99 : ' seafloor-cavern--entrance ' ,
100 : ' seafloor-cavern--room-1 ' ,
101 : ' seafloor-cavern--room-2 ' ,
102 : ' seafloor-cavern--room-5 ' ,
103 : ' seafloor-cavern--room-6 ' ,
104 : ' seafloor-cavern--room-3 ' ,
105 : ' seafloor-cavern--room-7 ' ,
106 : ' seafloor-cavern--room-4 ' ,
107 : ' seafloor-cavern--room-8 ' ,
108 : ' seafloor-cavern--room-9 ' ,
109 : ' seafloor-cavern--room-10 ' ,
# NOTE: indistinguishable
112 : ' cave-of-origin--entrance ' ,
113 : ' cave-of-origin--1f ' ,
114 : ' cave-of-origin--b1f ' ,
115 : ' cave-of-origin--b2f ' ,
116 : ' cave-of-origin--b3f ' ,
# NOTE: new? rse had this room but had no encounters in it
452 : ' cave-of-origin--b4f ' ,
# NOTE: indistinguishable
123 : ' hoenn-victory-road--entrance ' , # NOTE: new
124 : ' hoenn-victory-road--1f ' ,
125 : ' hoenn-victory-road--b1f ' ,
# NOTE: new; rse had b2f instead
126 : ' hoenn-victory-road--2f ' ,
}
2016-02-26 18:05:51 +00:00
# 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 ' ,
)
2016-12-07 14:29:44 +00:00
# TODO ripe for being put in the pokedex codebase itself
def make_identifier ( english_name ) :
# TODO do nidoran too
2017-07-11 23:29:22 +00:00
return re . sub (
' [^a-zA-Z0-9-]+ ' ,
' - ' ,
2017-08-22 21:29:07 +00:00
english_name . lower ( ) . replace ( ' é ' , ' e ' ) . replace ( ' ’ ' , ' ' ) . replace ( ' ♂ ' , ' -m ' ) . replace ( ' ♀ ' , ' -f ' ) ,
) . rstrip ( ' - ' )
2016-12-07 14:29:44 +00:00
2016-02-26 18:05:51 +00:00
@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 ,
)
2017-01-27 00:29:44 +00:00
def collect_text ( texts , text_type , id ) :
return OrderedDict (
( language , texts [ language ] [ text_type ] [ id ] )
for language in CANON_LANGUAGES )
2016-02-26 18:05:51 +00:00
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
2016-12-07 14:29:44 +00:00
2016-02-26 18:05:51 +00:00
texts = { }
2016-12-07 14:29:44 +00:00
#for lang, fn in ORAS_SCRIPT_FILES.items():
for lang , fn in SUMO_SCRIPT_FILES . items ( ) :
2016-02-26 18:05:51 +00:00
texts [ lang ] = { }
with read_garc ( root / fn ) as garc :
2016-12-07 14:29:44 +00:00
#for entryname, entryid in ORAS_SCRIPT_ENTRIES.items():
for entryname , entryid in SUMO_SCRIPT_ENTRIES . items ( ) :
2016-02-26 18:05:51 +00:00
entry = garc [ entryid ] [ 0 ]
texts [ lang ] [ entryname ] = decrypt_xy_text ( entry . read ( ) )
identifiers = { }
2017-01-05 12:57:05 +00:00
# FIXME should use a known list, mayyybe compare against this
identifiers [ ' species ' ] = list ( map ( make_identifier , texts [ ' en ' ] [ ' species-names ' ] ) )
# This is totally wrong, but the Pokémon loop below fixes it as it goes
# FIXME okay that bit at the end is dumb
identifiers [ ' pokémon ' ] = identifiers [ ' species ' ] [ : ] + [ None ] * 1000
identifiers [ ' move ' ] = list ( map ( make_identifier , texts [ ' en ' ] [ ' move-names ' ] ) )
identifiers [ ' item ' ] = list ( map ( make_identifier , texts [ ' en ' ] [ ' item-names ' ] ) )
identifiers [ ' ability ' ] = list ( map ( make_identifier , texts [ ' en ' ] [ ' ability-names ' ] ) )
2016-02-26 18:05:51 +00:00
2017-07-11 23:29:22 +00:00
# De-duplicate some moves and items with identical names
2017-06-15 05:55:28 +00:00
# TODO eventually these should be a separate manual list that these scripts
# can update when necessary for new games
2017-07-11 23:29:22 +00:00
# Generic Z-moves come in both physical and special, with the same names
for i in range ( 622 , 658 ) :
if i % 2 == 0 :
identifiers [ ' move ' ] [ i ] + = ' --physical '
else :
identifiers [ ' move ' ] [ i ] + = ' --special '
2017-06-15 05:55:28 +00:00
# TODO or maybe we should name /both/ items in each pair... but for old
# items that requires matching up with older versions in some sensible way,
# right? maybe?
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 621 ] = ' xtranceiver--red '
identifiers [ ' item ' ] [ 626 ] = ' xtranceiver--yellow '
identifiers [ ' item ' ] [ 450 ] = ' bike--green '
identifiers [ ' item ' ] [ 713 ] = ' bike--yellow '
identifiers [ ' item ' ] [ 641 ] = ' holo-caster--green '
identifiers [ ' item ' ] [ 714 ] = ' holo-caster--red '
identifiers [ ' item ' ] [ 739 ] = ' contest-costume--jacket '
identifiers [ ' item ' ] [ 740 ] = ' contest-costume--dress '
identifiers [ ' item ' ] [ 636 ] = ' dropped-item--red '
identifiers [ ' item ' ] [ 637 ] = ' dropped-item--yellow '
2017-06-15 05:55:28 +00:00
# Storage Key is tricky!
# It was in gen 3, where it opened a storage hold, but then it was either
# co-opted or replaced (?) in gen 4 to open the team galactic warehouse,
# but then we got gen 3 remakes so now it's two items
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 463 ] = ' storage-key--galactic-warehouse '
identifiers [ ' item ' ] [ 734 ] = ' storage-key--sea-mauville '
2017-06-15 05:55:28 +00:00
# One DNA Splicers merges Kyurem, the other separates it, and either will
# transparently turn into the other when used.
# TODO i'm /guessing/ that the first one is the merge one, but i'm not sure
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 628 ] = ' dna-splicers--merge '
identifiers [ ' item ' ] [ 629 ] = ' dna-splicers--split '
2017-06-15 05:55:28 +00:00
# The meteorite in ORAS gradually changes over time; in gen 3 only the first one existed
identifiers [ ' item ' ] [ 729 ] = ' meteorite '
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 751 ] = ' meteorite--2 '
identifiers [ ' item ' ] [ 771 ] = ' meteorite--3 '
identifiers [ ' item ' ] [ 772 ] = ' meteorite--4 '
2017-06-15 05:55:28 +00:00
# I have absolutely no idea when or why the S S Ticket was cloned, but I
# assume the new one is intended to be the same as the one in gen 3
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 456 ] = ' ss-ticket '
identifiers [ ' item ' ] [ 736 ] = ' ss-ticket--hoenn '
2017-06-15 05:55:28 +00:00
# Another key that was reused, then remade. Curiously, it doesn't look
# like the New Mauville version of the key was ever actually used, though
# its sprite was resurrected
2017-06-18 03:21:26 +00:00
identifiers [ ' item ' ] [ 476 ] = ' basement-key--goldenrod '
identifiers [ ' item ' ] [ 723 ] = ' basement-key--new-mauville '
# There are two of every Z crystal: one when it's in the bag, one when it's
# held by a Pokémon. I don't know exactly how to distinguish them from the
# data, but they have different icons; bag versions are larger and show a
# symbol.
identifiers [ ' item ' ] [ 776 ] = ' normalium-z--held '
identifiers [ ' item ' ] [ 777 ] = ' firium-z--held '
identifiers [ ' item ' ] [ 778 ] = ' waterium-z--held '
identifiers [ ' item ' ] [ 779 ] = ' electrium-z--held '
identifiers [ ' item ' ] [ 780 ] = ' grassium-z--held '
identifiers [ ' item ' ] [ 781 ] = ' icium-z--held '
identifiers [ ' item ' ] [ 782 ] = ' fightinium-z--held '
identifiers [ ' item ' ] [ 783 ] = ' poisonium-z--held '
identifiers [ ' item ' ] [ 784 ] = ' groundium-z--held '
identifiers [ ' item ' ] [ 785 ] = ' flyinium-z--held '
identifiers [ ' item ' ] [ 786 ] = ' psychium-z--held '
identifiers [ ' item ' ] [ 787 ] = ' buginium-z--held '
identifiers [ ' item ' ] [ 788 ] = ' rockium-z--held '
identifiers [ ' item ' ] [ 789 ] = ' ghostium-z--held '
identifiers [ ' item ' ] [ 790 ] = ' dragonium-z--held '
identifiers [ ' item ' ] [ 791 ] = ' darkinium-z--held '
identifiers [ ' item ' ] [ 792 ] = ' steelium-z--held '
identifiers [ ' item ' ] [ 793 ] = ' fairium-z--held '
identifiers [ ' item ' ] [ 794 ] = ' pikanium-z--held '
identifiers [ ' item ' ] [ 798 ] = ' decidium-z--held '
identifiers [ ' item ' ] [ 799 ] = ' incinium-z--held '
identifiers [ ' item ' ] [ 800 ] = ' primarium-z--held '
identifiers [ ' item ' ] [ 801 ] = ' tapunium-z--held '
identifiers [ ' item ' ] [ 802 ] = ' marshadium-z--held '
identifiers [ ' item ' ] [ 803 ] = ' aloraichium-z--held '
identifiers [ ' item ' ] [ 804 ] = ' snorlium-z--held '
identifiers [ ' item ' ] [ 805 ] = ' eevium-z--held '
identifiers [ ' item ' ] [ 806 ] = ' mewnium-z--held '
identifiers [ ' item ' ] [ 807 ] = ' normalium-z--bag '
identifiers [ ' item ' ] [ 808 ] = ' firium-z--bag '
identifiers [ ' item ' ] [ 809 ] = ' waterium-z--bag '
identifiers [ ' item ' ] [ 810 ] = ' electrium-z--bag '
identifiers [ ' item ' ] [ 811 ] = ' grassium-z--bag '
identifiers [ ' item ' ] [ 812 ] = ' icium-z--bag '
identifiers [ ' item ' ] [ 813 ] = ' fightinium-z--bag '
identifiers [ ' item ' ] [ 814 ] = ' poisonium-z--bag '
identifiers [ ' item ' ] [ 815 ] = ' groundium-z--bag '
identifiers [ ' item ' ] [ 816 ] = ' flyinium-z--bag '
identifiers [ ' item ' ] [ 817 ] = ' psychium-z--bag '
identifiers [ ' item ' ] [ 818 ] = ' buginium-z--bag '
identifiers [ ' item ' ] [ 819 ] = ' rockium-z--bag '
identifiers [ ' item ' ] [ 820 ] = ' ghostium-z--bag '
identifiers [ ' item ' ] [ 821 ] = ' dragonium-z--bag '
identifiers [ ' item ' ] [ 822 ] = ' darkinium-z--bag '
identifiers [ ' item ' ] [ 823 ] = ' steelium-z--bag '
identifiers [ ' item ' ] [ 824 ] = ' fairium-z--bag '
identifiers [ ' item ' ] [ 825 ] = ' pikanium-z--bag '
identifiers [ ' item ' ] [ 826 ] = ' decidium-z--bag '
identifiers [ ' item ' ] [ 827 ] = ' incinium-z--bag '
identifiers [ ' item ' ] [ 828 ] = ' primarium-z--bag '
identifiers [ ' item ' ] [ 829 ] = ' tapunium-z--bag '
identifiers [ ' item ' ] [ 830 ] = ' marshadium-z--bag '
identifiers [ ' item ' ] [ 831 ] = ' aloraichium-z--bag '
identifiers [ ' item ' ] [ 832 ] = ' snorlium-z--bag '
identifiers [ ' item ' ] [ 833 ] = ' eevium-z--bag '
identifiers [ ' item ' ] [ 834 ] = ' mewnium-z--bag '
identifiers [ ' item ' ] [ 835 ] = ' pikashunium-z--bag '
identifiers [ ' item ' ] [ 836 ] = ' pikashunium-z--held '
2017-06-15 05:55:28 +00:00
2016-02-26 18:05:51 +00:00
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 )
2016-12-07 14:29:44 +00:00
"""
# Encounters
2017-01-05 12:57:05 +00:00
22 : 42 < magical > note to self : X / Y ambush encounters are found in DllField . cro , starting at 0xf40d0
23 : 02 < magical > friend safari pokemon at 0x13d34a
2016-12-07 14:29:44 +00:00
# TODO move mee elsewheeere -- actually all of these should be in their own pieces
places = OrderedDict ( )
name_index_to_place = { }
name_index_counts = Counter ( )
zones = { }
zone_to_name_index = { }
with read_garc ( root / ' rom/a/0/1/3 ' ) as garc :
# Fetch the pointer table from the encounter file first, mostly so we
# can figure out which zones have no encounters at all. For whatever
# reason, a zone with no encounters still has data -- but it uses the
# same pointer as the following zone. I don't know if the pointers
# were intended to be used as ranges or what, but it's a handy signal.
f = garc [ - 1 ] [ 0 ]
# TODO SIGH, translate this to construct, i guess
magic = f . read ( 2 )
assert magic == b ' EN '
num_records = int . from_bytes ( f . read ( 2 ) , ' little ' )
encounter_pointers = [ ]
for n in range ( num_records ) :
encounter_pointers . append ( int . from_bytes ( f . read ( 4 ) , ' little ' ) )
empty_zones = set ( )
for n in range ( num_records - 1 ) :
if encounter_pointers [ n ] == encounter_pointers [ n + 1 ] :
empty_zones . add ( n )
# Every file in this GARC is ZO (zonedata) except the last one, which
# is a table of encounters for each zone.
num_zones = len ( garc ) - 1
for z in range ( num_zones ) :
if z in empty_zones :
# TODO later we may want these, to hang events off of etc
continue
zone = OrderedDict ( )
zone [ ' game-index ' ] = z
zones [ z ] = zone
# TODO probably worth trying to parse this stuff for real later
data = garc [ z ] [ 0 ] . read ( )
name_index = int . from_bytes ( data [ 56 : 58 ] , ' little ' )
name_bits = name_index >> 9
name_index & = 0x1ff
zone_to_name_index [ z ] = name_index
name_index_counts [ name_index ] + = 1
# Create places as we go, but DO NOT assign zones to places yet,
# since the logic for figuring out zone identifiers is different
# for places with only one zone
if name_index not in name_index_to_place :
place = OrderedDict ( )
place [ ' unknown--gen6-name-bits ' ] = name_bits
place [ ' name ' ] = OrderedDict ( )
place [ ' alternate-name ' ] = OrderedDict ( )
for language in CANON_LANGUAGES :
name , altname = (
texts [ language ] [ ' zone-names ' ] [ name_index : name_index + 2 ] )
place [ ' name ' ] [ language ] = name
if altname :
place [ ' alternate-name ' ] [ language ] = altname
# Drop this dict entirely if there are no alt names
if not place [ ' alternate-name ' ] :
del place [ ' alternate-name ' ]
name_index_to_place [ name_index ] = place
ident = ORAS_ZONE_NAME_INDEX_TO_VEEKUN_LOCATION . get ( name_index )
if not ident :
# Not in veekun yet...
place [ ' veekun--new ' ] = True
ident = make_identifier ( place [ ' name ' ] [ ' en ' ] )
places [ ident ] = place
# TODO ugh
place [ ' _identifier ' ] = ident
place [ ' zones ' ] = OrderedDict ( )
# Some encounters are used more than once
seen_encounters = { }
for z , ptr in enumerate ( encounter_pointers ) :
if z in empty_zones :
continue
zone = zones [ z ]
name_index = zone_to_name_index [ z ]
place = name_index_to_place [ name_index ]
# Now we have all the zones, so we can figure out identifiers and
# assign the zone to its parent place
identifier = place [ ' _identifier ' ]
if name_index_counts [ name_index ] > 1 :
# TODO are these names /sometimes/ official? e.g. doesn't
# "B1F" appear sometimes?
subidentifier = ORAS_ZONE_INDEX_TO_VEEKUN_AREA . get ( z )
if not subidentifier :
subidentifier = " oras-unknown- {} " . format ( z )
identifier = " {} -- {} " . format ( identifier , subidentifier )
place [ ' zones ' ] [ identifier ] = zone
# Snag the actual encounters, if any.
zone [ ' encounters ' ] = OrderedDict ( )
# TODO dumb hack for soaring through the sky, which is... nothing
if not f . read ( 1 ) :
continue
f . seek ( ptr )
encounter_table = encounter_table_struct . parse_stream ( f )
n = 0
for method , chances in ORAS_ENCOUNTER_SLOTS :
rate_attr = method . replace ( ' - ' , ' _ ' ) + ' _rate '
rate = getattr ( encounter_table , rate_attr )
# TODO where does rate fit in here?
if rate == 0 :
# TODO wrong for hordes
n + = len ( chances )
continue
encounters = zone [ ' encounters ' ] [ method ] = [ ]
for chance in chances :
if isinstance ( chance , tuple ) :
chance , groupsize = chance
else :
groupsize = 1
encounter = [ ]
for _ in range ( groupsize ) :
enc = encounter_table . encounter [ n ]
# TODO assert always zero when rate is zero, never zero when rate isn't
if enc . pokemon_id != 0 :
if enc . min_level == enc . max_level :
levels = str ( enc . min_level )
else :
levels = " {} - {} " . format ( enc . min_level , enc . max_level )
2017-01-05 12:57:05 +00:00
pokemon_ident = identifiers [ ' pokémon ' ] [ enc . pokemon_id & 0x1ff ]
2016-12-07 14:29:44 +00:00
pokemon_form_bits = enc . pokemon_id >> 9
# TODO maybe turn this into, i have no idea, a
# custom type? something forcibly short??
# TODO what do i do with the form bits?
encounter . append ( " {} {} " . format ( pokemon_ident , levels ) )
n + = 1
if groupsize == 1 :
encounters . extend ( encounter )
else :
encounters . append ( encounter )
with ( out / ' places.yaml ' ) . open ( ' w ' ) as f :
dump_to_yaml ( places , f )
return
"""
2016-02-26 18:05:51 +00:00
# -------------------------------------------------------------------------
# Scrape some useful bits from the binary
with ( root / ' exe/code.bin ' ) . open ( ' rb ' ) as f :
# Tutored moves
2016-12-07 14:29:44 +00:00
# TODO i think these are oras only? do they exist in sumo? xy?
2016-02-26 18:05:51 +00:00
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 = [ ]
2016-12-07 14:29:44 +00:00
#f.seek(0x004a67ee) # ORAS
f . seek ( 0x0049795a ) # SUMO
2017-01-05 12:57:05 +00:00
# TODO magic number (107)
2016-02-26 18:05:51 +00:00
machineids = struct . unpack ( ' <107H ' , f . read ( 2 * 107 ) )
2017-08-22 21:29:07 +00:00
# FIXME this is no longer true in sun/moon, of course
2016-02-26 18:05:51 +00:00
# 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 ]
2017-08-22 21:29:07 +00:00
for moveid in machineids
#machineids[0:92] +
#machineids[98:106] +
#machineids[92:98] +
#machineids[106:]
2016-02-26 18:05:51 +00:00
]
2017-06-15 05:55:28 +00:00
# TODO Pokémon box sprite map
#0x43EAA8 XY
2017-06-23 20:32:18 +00:00
#f.seek(0x0047d650) # ORAS
#f.seek(0x004999d0) # SUMO
2017-06-15 05:55:28 +00:00
# TODO Pickup
# 0x4455A8 XY
2017-01-27 00:29:44 +00:00
# -------------------------------------------------------------------------
# Abilities
2017-06-15 05:55:28 +00:00
2017-01-27 00:29:44 +00:00
all_abilities = OrderedDict ( )
for i , identifier in enumerate ( identifiers [ ' ability ' ] ) :
if i == 0 :
# Dummy non-ability
continue
ability = all_abilities [ identifier ] = schema . Ability ( )
ability . name = collect_text ( texts , ' ability-names ' , i )
ability . flavor_text = collect_text ( texts , ' ability-flavor ' , i )
with ( out / ' abilities.yaml ' ) . open ( ' w ' ) as f :
f . write ( Camel ( [ schema . POKEDEX_TYPES ] ) . dump ( all_abilities ) )
2017-06-15 05:55:28 +00:00
# -------------------------------------------------------------------------
# Items
all_items = OrderedDict ( )
# a/1/9/7 in ORAS
with read_garc ( root / ' rom/a/0/1/9 ' ) as garc : # SUMO
item_ct = len ( garc )
for i , subfile in enumerate ( garc ) :
identifier = identifiers [ ' item ' ] [ i ]
2017-08-22 21:29:07 +00:00
if identifier == ' ' :
2017-06-15 05:55:28 +00:00
# Junk non-item
2017-08-22 21:29:07 +00:00
# TODO striiictly speaking, maybe we should dump these
# anyway... but we have no way of knowing what they'd do, so,
# eh
2017-06-15 05:55:28 +00:00
continue
item = all_items [ identifier ] = schema . Item ( )
2017-08-22 21:29:07 +00:00
item . game_index = i
2017-06-15 05:55:28 +00:00
item . name = collect_text ( texts , ' item-names ' , i )
item . flavor_text = collect_text ( texts , ' item-flavor ' , i )
raw_item = item_struct . parse_stream ( subfile [ 0 ] )
2017-08-22 21:29:07 +00:00
item . price = raw_item . price * 10
2017-06-15 05:55:28 +00:00
item . fling_power = raw_item . fling_power
2017-01-27 00:29:44 +00:00
2017-08-22 21:29:07 +00:00
subfile [ 0 ] . seek ( 0 )
data = subfile [ 0 ] . read ( )
print ( f " { identifiers [ ' item ' ] [ i ] : 24s } { raw_item . price : 5d } { raw_item . fling_effect : 3d } { raw_item . natural_gift_effect : 3d } { raw_item . natural_gift_power : 3d } { raw_item . natural_gift_type & 31 : 2d } | { raw_item . pocketish : 1d } { data . hex ( ) [ : 32 ] } " )
2017-06-15 05:55:28 +00:00
with ( out / ' items.yaml ' ) . open ( ' w ' ) as f :
f . write ( Camel ( [ schema . POKEDEX_TYPES ] ) . dump ( all_items ) )
2017-01-27 00:29:44 +00:00
2017-06-15 05:55:28 +00:00
# TODO shouldn't this be, um, not here
# Grab the item icons, but don't save them yet...
image_datae = [ ]
# with read_garc(root / 'rom/a/0/9/2') as garc: # ORAS
with read_garc ( root / ' rom/a/0/6/1 ' ) as garc : # SUMO
from . lib . clim import decode_clim
for i , subfile in enumerate ( garc ) :
image_data = decode_clim ( subfile [ 0 ] . read ( ) )
image_datae . append ( image_data )
# The item icons are /almost/ parallel with the items themselves, but not
# quite. The binary has a simple array of the icon for each item
with ( root / ' exe/code.bin ' ) . open ( ' rb ' ) as f :
#f.seek(0x0043db74) # XY
f . seek ( 0x00498660 ) # SUMO
item_icon_map = struct . unpack (
" < {} I " . format ( item_ct ) , f . read ( item_ct * 4 ) )
# And now write the icons out under the correct names
print ( len ( image_datae ) , max ( item_icon_map ) )
unused_image_datae = set ( range ( len ( image_datae ) ) )
for i , identifier in enumerate ( identifiers [ ' item ' ] ) :
# As per usual, there's an off-by-one problem here
if i == 0 :
continue
item_icon_id = item_icon_map [ i - 1 ]
item_icon = image_datae [ item_icon_id ]
unused_image_datae . discard ( item_icon_id )
with ( out / " items/ {} - {} .png " . format ( i , identifier ) ) . open ( ' wb ' ) as f :
item_icon . write_to_png ( f )
assert not unused_image_datae
2016-02-26 18:05:51 +00:00
# -------------------------------------------------------------------------
2017-08-22 21:29:07 +00:00
# Pokémon
# Shapes are stored separately alongside some Pokédex sorting data, since
# they're only used in the Pokédex search. In SUMO, at least, every flavor
# form has its own shape.
# TODO where is this in other games?
# TODO what are the other records in these two files? file 0 has 11 records, file 1 has 7
with read_garc ( root / ' rom/a/1/5/2 ' ) as garc : # SUMO
subfile = PokemonContainerFile ( garc [ 1 ] [ 0 ] )
shape_data = subfile [ 1 ] . read ( )
# One byte per form, no parsing required!
pokémon_form_shapes = [ SHAPES [ shape_id ] for shape_id in shape_data ]
2017-06-23 20:32:18 +00:00
# TODO document this properly sometime, somewhere, but the gist is:
# - a species may have multiple forms
# - forms may be "concrete" (e.g. deoxys) or flavor only (e.g. unown)
# - either all the forms are concrete, or all the forms are not (for now)
# - concrete forms get an entry in the personal file
# - flavor forms do not, but the count of them is still listed
# - the order of flavor forms is not explicitly given anywhere, but it
# seems to be just all the forms for all the species in order
# - it's arbitrary whether a form is referred to by concrete order, flavor
# order, or as a (species, form number) pair
# - the identifiers in identifiers['pokémon'] assume concrete order
# - so here are a few data structures for storing that info
# TODO i wonder how i'll reuse this code in the future, since it needs to
# go all the way back to gen 4 ha
# species id => {forms (identifiers), is_concrete, concrete_ids, flavor_ids}
species_forms = [ ]
# id => (species id, form number); only dicts for ease of construction
flavor_form_order = { }
concrete_form_order = { }
2017-01-05 12:57:05 +00:00
mega_evolutions = get_mega_evolutions ( root )
2017-06-23 20:32:18 +00:00
# Note that despite being called "all", this is really just concrete forms
2017-01-05 12:57:05 +00:00
all_pokémon = OrderedDict ( )
2016-02-26 18:05:51 +00:00
pokemon_data = [ ]
2016-12-07 14:29:44 +00:00
with read_garc ( root / ' rom/a/0/1/7 ' ) as garc : # SUMO
#with read_garc(root / 'rom/a/1/9/5') as garc: # ORAS
2016-02-26 18:05:51 +00:00
personals = [ subfile [ 0 ] . read ( ) for subfile in garc ]
2017-06-23 20:32:18 +00:00
next_flavor_form_id = 804 #723 # TODO magic numbers -- also skip one because of the alt egg, i guess??
2017-01-05 12:57:05 +00:00
print ( " number of flavor texts " , len ( texts [ ' en ' ] [ ' species-flavor-moon ' ] ) )
2017-06-23 20:32:18 +00:00
print ( " number of form names " , len ( texts [ ' en ' ] [ ' form-names ' ] ) )
2016-02-26 18:05:51 +00:00
for i , personal in enumerate ( personals [ : - 1 ] ) :
record = pokemon_struct . parse ( personal )
2017-01-05 12:57:05 +00:00
2017-06-23 20:32:18 +00:00
# FIRST THINGS FIRST: let's deal with forms, but only once per species
if i < 803 : # TODO magic number but i bet i can get rid of it
# TODO some pokemon, like unown, /only/ have sprite variations, so they
# don't have a form_species_start here. what do i do about them?
if ( record . form_count > 1 ) != bool ( record . form_species_start ) :
print ( " !!! sprite-only forms, argh " )
# Name megas automatically, or pull from our manual list of form
# identifiers
# TODO maybe this is silly and we should just manually list megas too
2017-01-05 12:57:05 +00:00
megas = mega_evolutions [ i ]
if len ( megas ) == 1 :
assert i not in FORM_NAMES
2017-06-23 20:32:18 +00:00
form_names = [ None , ' mega ' ]
2017-01-05 12:57:05 +00:00
elif len ( megas ) == 2 :
assert i not in FORM_NAMES
2017-06-23 20:32:18 +00:00
form_names = [ None , ' mega-x ' , ' mega-y ' ]
elif record . form_count > 1 :
2017-01-05 12:57:05 +00:00
assert not megas
2017-06-23 20:32:18 +00:00
form_names = FORM_NAMES [ i ]
2017-01-05 12:57:05 +00:00
# Fix our own name if necessary
2017-06-23 20:32:18 +00:00
if form_names [ 0 ] :
identifiers [ ' pokémon ' ] [ i ] + = ' - ' + form_names [ 0 ]
else :
form_names = [ None ]
2016-02-26 18:05:51 +00:00
2017-06-23 20:32:18 +00:00
if record . form_count != len ( form_names ) :
print ( " !!!!! MISMATCH " , record . form_count , len ( form_names ) )
2017-01-05 12:57:05 +00:00
2017-06-23 20:32:18 +00:00
form_data = dict (
forms = form_names ,
is_concrete = record . form_species_start != 0 ,
concrete_ids = [ i ] ,
flavor_ids = [ i ] ,
)
species_forms . append ( form_data )
# Concrete ids start at the given species start, if any. Skip 0
2017-06-25 18:05:14 +00:00
# because that refers to the current record. Also populate
# concrete Pokémon identifiers
2017-06-23 20:32:18 +00:00
concrete_form_order [ i ] = ( i , 0 )
if record . form_species_start :
for f in range ( 1 , record . form_count ) :
concrete_id = record . form_species_start + f - 1
form_data [ ' concrete_ids ' ] . append ( concrete_id )
assert concrete_id not in concrete_form_order
concrete_form_order [ concrete_id ] = ( i , f )
2017-06-25 18:05:14 +00:00
identifiers [ ' pokémon ' ] [ concrete_id ] = identifiers [ ' species ' ] [ i ] + ' - ' + form_names [ f ]
2017-06-23 20:32:18 +00:00
# Flavor ids all just go in species order
flavor_form_order [ i ] = ( i , 0 )
for f in range ( 1 , record . form_count ) :
form_data [ ' flavor_ids ' ] . append ( next_flavor_form_id )
flavor_form_order [ next_flavor_form_id ] = ( i , f )
next_flavor_form_id + = 1
# OK, now let's scrape data for this concrete form
2017-01-27 00:29:44 +00:00
pokémon = schema . Pokémon ( )
2017-01-05 12:57:05 +00:00
all_pokémon [ identifiers [ ' pokémon ' ] [ i ] ] = pokémon
pokémon . game_index = i
2017-06-23 20:32:18 +00:00
base_species_id , form_name_id = concrete_form_order [ i ]
2017-08-22 21:29:07 +00:00
flavor_id = species_forms [ base_species_id ] [ ' flavor_ids ' ] [ form_name_id ]
2017-01-05 12:57:05 +00:00
# TODO i observe this is explicitly a species name, the one thing that
# really is shared between forms
2017-01-27 00:29:44 +00:00
pokémon . name = collect_text ( texts , ' species-names ' , base_species_id )
pokémon . genus = collect_text ( texts , ' genus-names ' , base_species_id )
2017-01-05 12:57:05 +00:00
# FIXME ho ho, hang on a second, forms have their own flavor text too!!
2017-01-27 00:29:44 +00:00
# TODO well this depends on which game you're dumping
2017-08-22 21:29:07 +00:00
pokémon . flavor_text = collect_text ( texts , ' species-flavor-moon ' , flavor_id )
2017-06-25 18:05:14 +00:00
# FIXME this is pretty temporary hackery; ideally the file would be
# arranged around species, not concrete forms
pokémon . form_base_species = identifiers [ ' species ' ] [ base_species_id ]
pokémon . form_number = form_name_id
2017-08-22 21:29:07 +00:00
pokémon . form_identifier = species_forms [ base_species_id ] [ ' forms ' ] [ form_name_id ]
2017-06-25 18:05:14 +00:00
if i < len ( species_forms ) and not species_forms [ i ] [ ' is_concrete ' ] :
pokémon . form_appearances = species_forms [ i ] [ ' forms ' ]
else :
pokémon . form_appearances = [ ]
# FIXME this skips over form names for non-concrete forms, ugh
pokémon . form_name = collect_text ( texts , ' form-names ' , species_forms [ base_species_id ] [ ' flavor_ids ' ] [ form_name_id ] )
2017-01-05 12:57:05 +00:00
2017-07-11 23:29:22 +00:00
# TODO stage? i'm iffy on that since it's a computed thing and what if
# it's incorrect? same problem as trusting the move meta stuff i
# suppose
2017-01-05 12:57:05 +00:00
pokémon . base_stats = {
' hp ' : record . stat_hp ,
' attack ' : record . stat_atk ,
' defense ' : record . stat_def ,
' special-attack ' : record . stat_spatk ,
' special-defense ' : record . stat_spdef ,
' speed ' : record . stat_speed ,
}
2017-06-25 22:57:38 +00:00
pokémon . effort = {
' hp ' : record . effort_hp ,
' attack ' : record . effort_attack ,
' defense ' : record . effort_defense ,
' special-attack ' : record . effort_special_attack ,
' special-defense ' : record . effort_special_defense ,
' speed ' : record . effort_speed ,
}
if record . type1 == record . type2 :
pokémon . types = [ record . type1 ]
else :
pokémon . types = [ record . type1 , record . type2 ]
2017-01-05 12:57:05 +00:00
pokémon . capture_rate = record . capture_rate
# Held items are a bit goofy; if the same item is in all three slots, it always appears!
pokémon . held_items = { }
if 0 != record . held_item1 == record . held_item2 == record . held_item3 :
pokémon . held_items [ identifiers [ ' item ' ] [ record . held_item1 ] ] = 100
else :
if record . held_item1 :
pokémon . held_items [ identifiers [ ' item ' ] [ record . held_item1 ] ] = 50
if record . held_item2 :
pokémon . held_items [ identifiers [ ' item ' ] [ record . held_item2 ] ] = 5
if record . held_item3 :
pokémon . held_items [ identifiers [ ' item ' ] [ record . held_item3 ] ] = 1
# TODO i think this needs some normalizing? maybe renaming because
# this doesn't at all imply what it means
pokémon . gender_rate = record . gender_rate
pokémon . hatch_counter = record . steps_to_hatch
pokémon . base_happiness = record . base_happiness
pokémon . growth_rate = record . growth_rate
2017-06-25 22:57:38 +00:00
if record . egg_group1 == record . egg_group2 :
pokémon . egg_groups = [ record . egg_group1 ]
else :
pokémon . egg_groups = [ record . egg_group1 , record . egg_group2 ]
2017-01-05 12:57:05 +00:00
pokémon . abilities = [
identifiers [ ' ability ' ] [ ability ]
for ability in ( record . ability1 , record . ability2 , record . ability_hidden )
]
# FIXME safari escape??
pokémon . base_experience = record . base_exp
2017-07-11 23:29:22 +00:00
pokémon . color = record . color
2017-08-22 21:29:07 +00:00
pokémon . shape = pokémon_form_shapes [ flavor_id ]
2017-01-05 12:57:05 +00:00
# FIXME what units are these!
pokémon . height = record . height
pokémon . weight = record . weight
pokémon . moves = { }
2017-06-18 06:15:11 +00:00
pokémon . evolutions = [ ]
2017-01-05 12:57:05 +00:00
# TODO transform to an OD somehow probably
pokemon_data . append ( record )
2017-08-22 21:29:07 +00:00
print ( " {:4d} {:25s} {} {:5d} {:5d} {:4d} {:4d} {:2d} / {p.z_crystal:3d} {p.z_base_move:3d} {p.z_move:3d} | {:10s} - {p.effort_padding:2d} - {p.safari_escape:3d} {p.mystery1:5d} {p.mystery2:5d} {p.held_item3:3d} {p.tutors:10} {shape} " . format (
2017-01-05 12:57:05 +00:00
i ,
identifiers [ ' pokémon ' ] [ i ] ,
( ' 0 ' * 16 + bin ( record . mystery1 ) [ 2 : ] ) [ - 16 : ] ,
record . mystery2 ,
record . stage ,
record . form_species_start ,
record . form_sprite_start ,
record . form_count ,
2017-06-25 22:57:38 +00:00
record . color ,
p = record ,
2017-08-22 21:29:07 +00:00
shape = pokémon_form_shapes [ flavor_id ] ,
2017-01-05 12:57:05 +00:00
) )
2016-02-26 18:05:51 +00:00
# -------------------------------------------------------------------------
# Move stats
2017-06-25 19:40:52 +00:00
all_moves = OrderedDict ( )
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/1/8/9') as garc: # ORAS
2017-01-05 12:57:05 +00:00
with read_garc ( root / ' rom/a/0/1/1 ' ) as garc : # SUMO
2016-02-26 18:05:51 +00:00
# Only one subfile
2017-06-25 19:40:52 +00:00
# TODO assert only one file wherever i do this
records = move_container_struct . parse_stream ( garc [ 0 ] [ 0 ] )
2017-01-05 12:57:05 +00:00
for i , record in enumerate ( records ) :
2017-07-11 23:29:22 +00:00
if i == 0 :
# Skip the dummy zeroth move, which has no useful properties
continue
2016-02-26 18:05:51 +00:00
# TODO with the release of oras all moves have contest types and effects again! where are they??
2017-07-11 23:29:22 +00:00
print ( f " { i : 3d } { texts [ ' en ' ] [ ' move-names ' ] [ i ] : 30s } | { record . type : 10s } / { record . effect : 3d } { record . ailment_chance : 3d } -- { record . status : 3d } ~ { record . z_move_id : 3d } { record . z_move_power : 3d } { record . z_move_effect : 4d } ~ { record . mystery3 : 3d } { record . mystery4 : 3d } ~ { ' ' . join ( k for ( k , v ) in record . flags . items ( ) if v ) } | { [ x for x in zip ( record . stat_change , record . stat_amount , record . stat_chance ) if x [ 0 ] ] } " )
2017-06-25 19:40:52 +00:00
ident = identifiers [ ' move ' ] [ i ]
move = all_moves [ ident ] = schema . Move ( )
move . game_index = i
move . name = collect_text ( texts , ' move-names ' , i )
2017-06-25 22:57:38 +00:00
move . flavor_text = collect_text ( texts , ' move-flavor ' , i )
2017-06-25 19:40:52 +00:00
move . type = record . type
# TODO does this need munging somehow?
move . power = record . power
move . pp = record . pp
# TODO does this need munging somehow?
move . accuracy = record . accuracy
move . priority = record . priority
move . damage_class = record . damage_class
move . range = record . range
move . effect = record . effect
2017-07-11 23:29:22 +00:00
# NOTE that this is now a weird computed thing, which raises the
# question of exactly how much of the meta stuff is used by code vs
# how much isn't; are there decompiles of this?
move . effect_chance = record . ailment_chance or record . flinch_chance or record . stat_chance [ 0 ] or record . stat_chance [ 1 ] or record . stat_chance [ 2 ] or None
2017-06-25 19:40:52 +00:00
# FIXME need to identify whether THIS move is a z-move?
if record . z_move_id :
move . z_move = identifiers [ ' move ' ] [ record . z_move_id ]
else :
# TODO what does it mean if the z-move is missing?? (how do z
# moves work for status moves anyway?)
pass
move . min_hits = record . min_max_hits & 0x0f
move . max_hits = record . min_max_hits >> 4
move . category = record . category
2017-07-11 23:29:22 +00:00
move . ailment = record . ailment
move . ailment_chance = record . ailment_chance
2017-06-25 19:40:52 +00:00
# FIXME what is record.status???
2017-07-11 23:29:22 +00:00
# FIXME this is nonsense, it should be per-stat?? also it's often
# zero. also one of the stats is "all". this may take a little
# caressing
2017-06-25 19:40:52 +00:00
#move.stat_chance = _Value(int)
move . min_turns = record . min_turns
move . max_turns = record . max_turns
2017-07-11 23:29:22 +00:00
move . drain = record . drain
2017-06-25 19:40:52 +00:00
move . healing = record . healing
move . crit_rate = record . crit_rate
move . flinch_chance = record . flinch_chance
2017-07-11 23:29:22 +00:00
move . flags = set ( k for ( k , v ) in record . flags . items ( ) if v )
print ( )
moves_by_status = defaultdict ( list )
for i , record in enumerate ( records ) :
if record . status :
moves_by_status [ record . status ] . append ( i )
for status in sorted ( moves_by_status ) :
print ( f " moves with status == { status } : " )
print ( * ( identifiers [ ' move ' ] [ i ] for i in moves_by_status [ status ] ) )
print ( )
moves_by_mystery4 = defaultdict ( list )
for i , record in enumerate ( records ) :
if record . mystery4 :
moves_by_mystery4 [ record . mystery4 ] . append ( i )
for mystery4 in sorted ( moves_by_mystery4 ) :
print ( f " moves with mystery4 == { mystery4 } : " )
print ( * ( identifiers [ ' move ' ] [ i ] for i in moves_by_mystery4 [ mystery4 ] ) )
2017-06-25 19:40:52 +00:00
with ( out / ' moves.yaml ' ) . open ( ' w ' ) as f :
f . write ( Camel ( [ schema . POKEDEX_TYPES ] ) . dump ( all_moves ) )
2016-02-26 18:05:51 +00:00
# Egg moves
2016-12-07 14:29:44 +00:00
with read_garc ( root / ' rom/a/0/1/2 ' ) as garc : # SUMO
#with read_garc(root / 'rom/a/1/9/0') as garc: # ORAS
2017-01-05 12:57:05 +00:00
print ( " number of egg moves: " , len ( garc ) )
2016-02-26 18:05:51 +00:00
for i , subfile in enumerate ( garc ) :
2017-01-05 12:57:05 +00:00
ident = identifiers [ ' pokémon ' ] [ i ]
2016-02-26 18:05:51 +00:00
data = subfile [ 0 ] . read ( )
if not data :
continue
container = egg_moves_struct . parse ( data )
2017-01-05 12:57:05 +00:00
# FIXME: 961 pokémon, 1063 named forms, but 1048 egg movesets.
# what? they get completely out of order after 802 and i don't
# know how to fix this. didn't magical write some code...?
if i > len ( identifiers [ ' species ' ] ) :
continue
moveset = all_pokémon [ ident ] . moves
2017-08-22 21:29:07 +00:00
eggseen = set ( )
2016-02-26 18:05:51 +00:00
eggset = moveset [ ' egg ' ] = [ ]
for moveid in container . moveids :
2017-08-22 21:29:07 +00:00
# Swinub has Mud Shot listed twice, for some reason
if moveid not in eggseen :
eggset . append ( identifiers [ ' move ' ] [ moveid ] )
eggseen . add ( moveid )
2016-02-26 18:05:51 +00:00
# Level-up moves
2016-12-07 14:29:44 +00:00
with read_garc ( root / ' rom/a/0/1/3 ' ) as garc : # SUMO
#with read_garc(root / 'rom/a/1/9/1') as garc: # ORAS
2017-01-05 12:57:05 +00:00
print ( " number of level-up moves " , len ( garc ) )
2016-02-26 18:05:51 +00:00
for i , subfile in enumerate ( garc ) :
2017-01-05 12:57:05 +00:00
ident = identifiers [ ' pokémon ' ] [ i ]
2016-02-26 18:05:51 +00:00
level_up_moves = subfile [ 0 ] . read ( )
2017-01-05 12:57:05 +00:00
moveset = all_pokémon [ ident ] . moves
2017-06-18 03:21:26 +00:00
levelset = moveset [ ' level-up ' ] = [ ]
2016-02-26 18:05:51 +00:00
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
2017-01-05 12:57:05 +00:00
# FIXME this is a goofy-looking structure, but it makes the
# yaml come out nicely?
levelset . append ( {
pair . level : identifiers [ ' move ' ] [ pair . moveid ] ,
} )
2016-02-26 18:05:51 +00:00
if pair . level == lastlevel :
order + = 1
else :
lastlevel = pair . level
order = 1
# Evolution
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/1/9/2') as garc: # ORAS
2017-06-15 05:55:28 +00:00
with read_garc ( root / ' rom/a/0/1/4 ' ) as garc : # SUMO?
2017-06-18 06:15:11 +00:00
for i , subfile in enumerate ( garc ) :
identifier = identifiers [ ' pokémon ' ] [ i ]
for raw_evo in pokemon_evolutions_struct . parse_stream ( subfile [ 0 ] ) :
# FIXME how do i include form in here? especially when it
# could be -1?? i guess that's a null or optional key or
# something... do i even have separate identifiers for pokémon
# vs forms yet? really gotta figure that junk out. i don't
# even remember if all the unowns count as separate forms in
# sumo
# TODO should make this its own schema sub-node, probably, so
# its structure is actually documented
evo = OrderedDict ( )
evo [ ' into ' ] = identifiers [ ' pokémon ' ] [ raw_evo . into_species ]
if raw_evo . method == 1 :
2017-08-22 21:29:07 +00:00
evo [ ' trigger ' ] = ' ev.level-up '
2017-06-18 06:15:11 +00:00
evo [ ' minimum-friendship ' ] = 220
elif raw_evo . method == 2 :
2017-08-22 21:29:07 +00:00
evo [ ' trigger ' ] = ' ev.level-up '
2017-06-18 06:15:11 +00:00
# FIXME is this an enum? also really it's morning OR day
evo [ ' time-of-day ' ] = ' day '
2017-08-22 21:29:07 +00:00
evo [ ' minimum-friendship ' ] = 220
2017-06-18 06:15:11 +00:00
elif raw_evo . method == 3 :
2017-08-22 21:29:07 +00:00
evo [ ' trigger ' ] = ' ev.level-up '
2017-06-18 06:15:11 +00:00
# FIXME is this an enum?
evo [ ' time-of-day ' ] = ' night '
2017-08-22 21:29:07 +00:00
evo [ ' minimum-friendship ' ] = 220
2017-06-18 06:15:11 +00:00
elif raw_evo . method == 4 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 5 :
evo [ ' trigger ' ] = ' ev.trade '
elif raw_evo . method == 6 :
evo [ ' trigger ' ] = ' ev.trade '
evo [ ' held-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
elif raw_evo . method == 7 :
evo [ ' trigger ' ] = ' ev.trade '
2017-08-22 21:29:07 +00:00
# FIXME uhh this is always zero. karrablast and shelmet do
# not actually mention each other here. guess it's
# hardcoded?? awesome
2017-06-18 06:15:11 +00:00
evo [ ' traded-with ' ] = identifiers [ ' pokémon ' ] [ raw_evo . param ]
elif raw_evo . method == 8 :
evo [ ' trigger ' ] = ' ev.use-item '
evo [ ' trigger-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
elif raw_evo . method == 9 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' higher-physical-stat ' ] = ' defense '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 10 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME hm, attack and defense are both names of stats, but
# this isn't? i am bothered
evo [ ' higher-physical-stat ' ] = ' equal '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 11 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' higher-physical-stat ' ] = ' attack '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 12 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
# FIXME this is wurmple -> silcoon
elif raw_evo . method == 13 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
# FIXME this is wurmple -> cascoon
elif raw_evo . method == 14 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
# FIXME this is ninjask; where does shedinja come in? i
# think it used to be in the data but it doesn't seem to be
# any more
elif raw_evo . method == 16 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-beauty ' ] = raw_evo . param
elif raw_evo . method == 17 :
evo [ ' trigger ' ] = ' ev.use-item '
evo [ ' trigger-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
evo [ ' gender ' ] = ' male '
elif raw_evo . method == 18 :
evo [ ' trigger ' ] = ' ev.use-item '
evo [ ' trigger-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
evo [ ' gender ' ] = ' female '
elif raw_evo . method == 19 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' time-of-day ' ] = ' day '
evo [ ' held-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
elif raw_evo . method == 20 :
evo [ ' trigger ' ] = ' ev.level-up '
2017-08-22 21:29:07 +00:00
evo [ ' time-of-day ' ] = ' night '
2017-06-18 06:15:11 +00:00
evo [ ' held-item ' ] = identifiers [ ' item ' ] [ raw_evo . param ]
elif raw_evo . method == 21 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' known-move ' ] = identifiers [ ' move ' ] [ raw_evo . param ]
elif raw_evo . method == 22 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' party-member ' ] = identifiers [ ' pokémon ' ] [ raw_evo . param ]
elif raw_evo . method == 23 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' gender ' ] = ' male '
elif raw_evo . method == 24 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' gender ' ] = ' female '
elif raw_evo . method == 25 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME this is magnetic field, which means i need to know
# the identifier of the appropriate place in this game!
# (for sumo it's vast poni canyon; oras, new mauville)
# OR... is that a property of the places??
elif raw_evo . method == 26 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME this is level up near mossy stone
elif raw_evo . method == 27 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME this is level up near icy stone
elif raw_evo . method == 28 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' upside-down ' ] = True
elif raw_evo . method == 29 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME i would prefer if this used the actual value rather
# than the number of hearts, but
evo [ ' minimum-affection ' ] = 2
evo [ ' known-move-type ' ] = TYPES [ raw_evo . param ]
elif raw_evo . method == 30 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
2017-08-22 21:29:07 +00:00
# This is Pancham (needs Dark-type party member) but the
# type seems to be hardcoded, ugh
evo [ ' party-member-type ' ] = ' t.dark '
2017-06-18 06:15:11 +00:00
elif raw_evo . method == 31 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' overworld-weather ' ] = ' rain '
elif raw_evo . method == 32 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' time-of-day ' ] = ' day '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 33 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' time-of-day ' ] = ' night '
evo [ ' minimum-level ' ] = raw_evo . level
elif raw_evo . method == 34 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
# FIXME this is a little confusing. it's espurr's second
# evolution, but neither of them have either params or forms?
elif raw_evo . method == 36 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' minimum-level ' ] = raw_evo . level
# FIXME should probably have a list somewhere of these; see
# https://bulbapedia.bulbagarden.net/wiki/User:SnorlaxMonster/Game_of_origin
# XXX should this actually be a field, or should we just
# omit in all but the applicable game?
evo [ ' game ' ] = { 30 : ' sun ' , 31 : ' moon ' } [ raw_evo . param ]
# FIXME this is cosmoem; again, 30 for sun, 31 for moon
elif raw_evo . method == 37 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' time-of-day ' ] = ' day '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' game ' ] = { 30 : ' sun ' , 31 : ' moon ' } [ raw_evo . param ]
elif raw_evo . method == 38 :
evo [ ' trigger ' ] = ' ev.level-up '
evo [ ' time-of-day ' ] = ' night '
evo [ ' minimum-level ' ] = raw_evo . level
evo [ ' game ' ] = { 30 : ' sun ' , 31 : ' moon ' } [ raw_evo . param ]
elif raw_evo . method == 39 :
evo [ ' trigger ' ] = ' ev.level-up '
# FIXME this is crabrawler, which only evolves when leveled
# up at mount lanakila; need an identifier for that area
else :
raise ValueError ( f " Unrecognized evolution method: { raw_evo . method } " )
all_pokémon [ identifier ] . evolutions . append ( evo )
2017-06-15 05:55:28 +00:00
2017-08-22 21:29:07 +00:00
# Shedinja is an exceptionally special case that isn't listed in the data
# TODO having to lay this out explicitly bugs me
all_pokémon [ ' nincada ' ] . evolutions . append ( dict (
into = ' shedinja ' ,
trigger = ' shed ' ,
) )
2016-02-26 18:05:51 +00:00
# Mega evolution
2017-06-18 06:15:11 +00:00
# TODO
# already parsed a/0/1/5 as mega_evolutions... but... that only lists
# items? what lists the actual megas? OH is the number a form number??
# wow i really need a list of species -> forms eh
2016-12-07 14:29:44 +00:00
# TODO what is a/1/9/4 (ORAS) or a/0/1/6 (SUMO)? 8 files of 404 bytes each
2017-08-22 21:29:07 +00:00
# in both versions, so not dependent on the number of loci
2016-02-26 18:05:51 +00:00
# Baby Pokémon
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/1/9/6') as garc: # ORAS
2017-08-22 21:29:07 +00:00
with read_garc ( root / ' rom/a/0/1/8 ' ) as garc : # SUMO?
# Last record shows something else
last_data = garc [ - 1 ] [ 0 ] . read ( )
for i , subfile in enumerate ( garc [ : - 1 ] ) :
data = subfile [ 0 ] . read ( )
baby_id = int . from_bytes ( data [ : 2 ] , ' little ' )
print ( identifiers [ ' species ' ] [ i ] , ' -> ' , identifiers [ ' species ' ] [ baby_id ] )
if data [ 2 : ] != b ' \xff \xff ' :
print ( " !!! " , repr ( data ) )
other_id = int . from_bytes ( last_data [ i * 2 : i * 2 + 2 ] , ' little ' )
if other_id != baby_id :
print ( " !!! " , i , baby_id , other_id )
2016-12-07 14:29:44 +00:00
2016-02-26 18:05:51 +00:00
# Tutor moves (from the personal structs)
2017-06-23 20:32:18 +00:00
# FIXME why is this down here and not just in the personal loop?
2016-02-26 18:05:51 +00:00
for i , datum in enumerate ( pokemon_data ) :
2017-01-05 12:57:05 +00:00
ident = identifiers [ ' pokémon ' ] [ i ]
moveset = all_pokémon [ ident ] . moves
2016-02-26 18:05:51 +00:00
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 ) :
2017-01-05 12:57:05 +00:00
if not datum [ ' machines ' ] [ bit ] :
2016-02-26 18:05:51 +00:00
continue
machineset . append ( moveident )
2017-01-05 12:57:05 +00:00
with ( out / ' pokemon.yaml ' ) . open ( ' w ' ) as f :
f . write ( Camel ( [ schema . POKEDEX_TYPES ] ) . dump ( all_pokémon ) )
2016-02-26 18:05:51 +00:00
2017-01-05 12:57:05 +00:00
def get_mega_evolutions ( root ) :
""" Return a dict mapping Pokémon ids to a list of mega evolution records.
2016-10-16 01:16:41 +00:00
"""
2017-01-05 12:57:05 +00:00
megas = { }
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/1/9/3') as garc: # oras
with read_garc ( root / ' rom/a/0/1/5 ' ) as garc : # SUMO
2016-10-16 01:16:41 +00:00
for pokemonid , subfile in enumerate ( garc ) :
2017-01-05 12:57:05 +00:00
megas [ pokemonid ] = pokemon_mega_evolutions_struct . parse_stream ( subfile [ 0 ] )
2016-10-16 01:16:41 +00:00
2017-01-05 12:57:05 +00:00
return megas
2016-10-16 01:16:41 +00:00
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 :
2016-12-07 14:29:44 +00:00
# TODO should use warnings for this so it works for new games
2017-01-05 12:57:05 +00:00
warnings . warn ( " Don ' t know any forms for Pokemon {} " . format ( pokemonid ) )
2016-12-07 14:29:44 +00:00
form = " form- {} " . format ( formid )
2016-10-16 01:16:41 +00:00
# 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 ) )
2016-02-26 18:05:51 +00:00
def extract_box_sprites ( root , out ) :
2017-01-05 12:57:05 +00:00
mega_counts = {
id : len ( megas )
for ( id , megas ) in get_mega_evolutions ( root ) . items ( )
}
namer = SpriteFileNamer ( out , mega_counts , FORM_NAMES )
2016-10-16 01:16:41 +00:00
2016-02-26 18:05:51 +00:00
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
2016-12-07 14:29:44 +00:00
#f.seek(0x0047d650) # ORAS
f . seek ( 0x004999d0 ) # SUMO
2017-01-05 12:57:05 +00:00
# Discard dummy zero sprite
pokemon_sprite_struct . parse_stream ( f )
n = 0
while True :
2016-02-26 18:05:51 +00:00
sprite = pokemon_sprite_struct . parse_stream ( f )
2017-01-05 12:57:05 +00:00
# This is not particularly reliable, but the data immediately
# following this list is some small 32-bit values, so the female
# index will be (illegally) zero
if not sprite . female_index :
break
n + = 1
2016-10-16 01:16:41 +00:00
namer . add ( sprite . index , n )
2016-02-26 18:05:51 +00:00
if sprite . female_index != sprite . index :
2016-10-16 01:16:41 +00:00
namer . add ( sprite . female_index , n , female = True )
2016-02-26 18:05:51 +00:00
# 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
2016-10-16 01:16:41 +00:00
namer . add ( form_idx , n , form )
2016-02-26 18:05:51 +00:00
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
2016-10-16 01:16:41 +00:00
namer . add ( right_idx , n , form , right = True )
2016-02-26 18:05:51 +00:00
else :
assert sprite . right_count == 2
assert right_indices [ 0 ] == right_indices [ 1 ]
if right_indices [ 0 ] != sprite . index :
2016-10-16 01:16:41 +00:00
namer . add ( right_indices [ 0 ] , n , right = True )
2016-02-26 18:05:51 +00:00
f . seek ( pos )
pokemon_sprites_dir = out
if not pokemon_sprites_dir . exists ( ) :
pokemon_sprites_dir . mkdir ( )
2016-12-07 14:29:44 +00:00
# with read_garc(root / 'rom/a/0/9/1') as garc: # ORAS
# TODO what's in 2/5/3?
with read_garc ( root / ' rom/a/0/6/2 ' ) as garc : # SUMO
2016-02-26 18:05:51 +00:00
from . lib . clim import decode_clim
for i , subfile in enumerate ( garc ) :
2017-01-05 12:57:05 +00:00
# TODO ORAS ONLY
#elif i == 333:
# # Duplicate Entei sprite that's not used
# continue
if i == len ( garc ) - 1 :
2016-02-26 18:05:51 +00:00
# Very last one is egg
2016-10-16 01:16:41 +00:00
namer . inject ( i , ' egg.png ' )
2016-02-26 18:05:51 +00:00
2017-01-05 12:57:05 +00:00
# TODO this is bad.
if not namer . index_to_filenames [ i ] :
# Unused sprite -- e.g. index 0, or one of the dummies in SUMO
continue
2016-02-26 18:05:51 +00:00
data = subfile [ 0 ] . read ( )
2017-01-05 12:57:05 +00:00
image_data = decode_clim ( data )
2016-02-26 18:05:51 +00:00
2016-10-16 01:16:41 +00:00
# TODO this is bad.
if ' right/ ' in namer . index_to_filenames [ i ] [ 0 ] :
2017-01-05 12:57:05 +00:00
image_data . mirror ( )
2016-10-16 01:16:41 +00:00
with namer . open ( i ) as f :
2017-01-05 12:57:05 +00:00
image_data . write_to_png ( f )
2016-02-26 18:05:51 +00:00
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
2017-01-05 12:57:05 +00:00
# are megas, and the rest are listed manually above as FORM_NAMES.
2016-02-26 18:05:51 +00:00
2017-01-05 12:57:05 +00:00
mega_counts = {
id : len ( megas )
for ( id , megas ) in get_mega_evolutions ( root ) . items ( )
}
namer = SpriteFileNamer ( out , mega_counts , FORM_NAMES )
2016-02-26 18:05:51 +00:00
2016-12-07 14:29:44 +00:00
# TODO Meowstic is counted as simply female in here, but should probably be
# saved with a form filename as well
2017-01-05 12:57:05 +00:00
# TODO should skip the extra komala and the totem forms
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/0/0/8') as garc: # ORAS
with read_garc ( root / ' rom/a/0/9/4 ' ) as garc : # SUMO
2016-02-26 18:05:51 +00:00
f = garc [ 0 ] [ 0 ]
2017-01-05 12:57:05 +00:00
pokemonid = 0
while True :
pokemonid + = 1
data = f . read ( 4 )
# All zeroes means we're done. Maybe. More data follows after
# this, but it doesn't seem to be the same format, and I don't know
# what exactly it's for.
if data == b ' \x00 \x00 \x00 \x00 ' :
break
2016-02-26 18:05:51 +00:00
# Index of the first model (also zero-indexed), how many models the
# Pokémon has, and some flags
2017-01-05 12:57:05 +00:00
start , count , flags = struct . unpack ( ' <HBB ' , data )
# TODO this was CHANGED for SUMO -- for ORAS all the shiny sprites are a second block at the end!
#model_num = start + 1
model_num = start * 2 + 1
#print("pokemon {:3d} -- start {:4d} ({:4d}) -- count {:2d} -- flags {:08b}".format(pokemonid, start, model_num, count, flags))
# Fix a few odd disconnects between the model listing and the
# actual dex sprites.
# TODO there must be a dex sprite index somewhere, this is silly
# Xerneas has two models, but three dex sprites
2016-02-26 18:05:51 +00:00
if pokemonid == 716 :
count = 2
2017-01-05 12:57:05 +00:00
# Lurantis has two models, but one dex sprite
if pokemonid == 754 :
count = 1
flags & = ~ 4
# Salazzle has two models, but one dex sprite
if pokemonid == 758 :
count = 1
flags & = ~ 4
# Komala has one model, but two dex sprites
# FIXME probably skip extracting it at all
if pokemonid == 775 :
count = 2
# The above all naturally throw later numbering off; compensate
if 716 < pokemonid < = 754 :
model_num + = 2
elif 758 < pokemonid < = 775 :
model_num - = 2
2016-02-26 18:05:51 +00:00
2016-10-16 01:16:41 +00:00
namer . add ( model_num , pokemonid )
2017-01-05 12:57:05 +00:00
# TODO SUMO ONLY (should be += 1 for ORAS)
namer . add ( model_num + 1 , pokemonid , shiny = True )
model_num + = 2
2016-02-26 18:05:51 +00:00
form_count = count - 1 # discount "base" form
2017-01-05 12:57:05 +00:00
# TODO this is only used for ORAS, and should be done another way anyway
2016-02-26 18:05:51 +00:00
total_model_count = model_num + count - 1
# Don't know what flag 1 is; everything has it.
2017-01-05 12:57:05 +00:00
# Flag 2 means the first alternate form is female.
2016-02-26 18:05:51 +00:00
if flags & 2 :
assert form_count > 0
form_count - = 1
2016-10-16 01:16:41 +00:00
namer . add ( model_num , pokemonid , female = True )
2017-01-05 12:57:05 +00:00
namer . add ( model_num + 1 , pokemonid , female = True , shiny = True )
model_num + = 2
2016-02-26 18:05:51 +00:00
# Flag 4 just means there are more forms?
if flags & 4 :
assert form_count
2016-10-16 01:16:41 +00:00
for formid in range ( 1 , form_count + 1 ) :
namer . add ( model_num , pokemonid , formid )
2017-01-05 12:57:05 +00:00
namer . add ( model_num + 1 , pokemonid , formid , shiny = True )
model_num + = 2
2016-02-26 18:05:51 +00:00
# And now, do the ripping
2016-12-07 14:29:44 +00:00
#with read_garc(root / 'rom/a/2/6/3') as garc: # ORAS
2017-01-05 12:57:05 +00:00
with read_garc ( root / ' rom/a/2/4/0 ' ) as garc : # SUMO
2016-02-26 18:05:51 +00:00
from . lib . clim import decode_clim
2016-12-07 14:29:44 +00:00
from . lib . etc1 import decode_etc1
2016-02-26 18:05:51 +00:00
for i , subfile in enumerate ( garc ) :
2017-01-05 12:57:05 +00:00
if i == 0 :
# Dummy sprite, not interesting to us
continue
data = subfile [ 0 ] . read ( )
"""
with open ( " {} / {} .png " . format ( str ( out ) , i ) , ' wb ' ) as f :
write_clim_to_png ( f , * decode_etc1 ( data ) )
continue
# TODO THIS IS ALL ORAS ONLY
2016-10-16 01:16:41 +00:00
shiny_prefix = None
2016-02-26 18:05:51 +00:00
if i > total_model_count :
i - = total_model_count
2016-10-16 01:16:41 +00:00
# TODO this should be a real feature, as should the 'right'
# hack in the other code
2016-02-26 18:05:51 +00:00
shiny_prefix = ' shiny/ '
elif 37 < = i < = 41 :
# Cosplay Pikachu's outfits -- the sprites are blank, so saving
# these is not particularly useful
continue
2017-01-05 12:57:05 +00:00
"""
2016-02-26 18:05:51 +00:00
data = subfile [ 0 ] . read ( )
2017-01-05 12:57:05 +00:00
with namer . open ( i ) as f :
decode_etc1 ( data ) . write_to_png ( f )
# TODO ORAS
#with namer.open(i, prefix=shiny_prefix) as f:
# decode_clim(data).write_to_png(f)
2016-02-26 18:05:51 +00:00
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 : ] )