3 Handles reading and encryption/decryption of Pokémon save file data.
5 See: http://projectpokemon.org/wiki/Pokemon_NDS_Structure
7 Kudos to LordLandon for his pkmlib.py, from which this module was originally
13 from pokedex
.db
import tables
14 from pokedex
.formulae
import calculated_hp
, calculated_stat
15 from pokedex
.util
import namedtuple
, permutations
16 from pokedex
.struct
._pokemon_struct
import pokemon_struct
18 def pokemon_prng(seed
):
19 u
"""Creates a generator that simulates the main Pokémon PRNG."""
21 seed
= 0x41C64E6D * seed
+ 0x6073
26 class SaveFilePokemon(object):
27 u
"""Represents an individual Pokémon, from the game's point of view.
29 Handles translating between the on-disk encrypted form, the in-RAM blob
30 (also used by pokesav), and something vaguely intelligible.
33 Stat
= namedtuple('Stat', ['stat', 'base', 'gene', 'exp', 'calc'])
35 def __init__(self
, blob
, encrypted
=False):
36 u
"""Wraps a Pokémon save struct in a friendly object.
38 If `encrypted` is True, the blob will be decrypted as though it were an
39 on-disk save. Otherwise, the blob is taken to be already decrypted and
42 `session` is an optional database session.
47 # Interpret as one word (pid), followed by a bunch of shorts
48 struct_def
= "I" + "H" * ((len(blob
) - 4) / 2)
49 shuffled
= list( struct
.unpack(struct_def
, blob
) )
51 # Apply standard Pokémon decryption, undo the block shuffling, and
53 self
.reciprocal_crypt(shuffled
)
54 words
= self
.shuffle_chunks(shuffled
, reverse
=True)
55 self
.blob
= struct
.pack(struct_def
, *words
)
61 self
.structure
= pokemon_struct
.parse(self
.blob
)
65 u
"""Returns a decrypted struct, aka .pkm file."""
69 def as_encrypted(self
):
70 u
"""Returns an encrypted struct the game expects in a save file."""
72 # Interpret as one word (pid), followed by a bunch of shorts
73 struct_def
= "I" + "H" * ((len(self
.blob
) - 4) / 2)
74 words
= list( struct
.unpack(struct_def
, self
.blob
) )
76 # Apply the block shuffle and standard Pokémon encryption
77 shuffled
= self
.shuffle_chunks(words
)
78 self
.reciprocal_crypt(shuffled
)
80 # Stuff back into a string, and done
81 return struct
.pack(struct_def
, *shuffled
)
86 u
"""Returns true iff this Pokémon is shiny."""
87 # See http://bulbapedia.bulbagarden.net/wiki/Personality#Shininess
88 # But don't see it too much, because the above is super over
89 # complicated. Do this instead!
90 personality_msdw
= self
.structure
.personality
>> 16
91 personality_lsdw
= self
.structure
.personality
& 0xffff
93 self
.structure
.original_trainer_id
94 ^ self
.structure
.original_trainer_secret_id
99 def use_database_session(self
, session
):
100 """Remembers the given database session, and prefetches a bunch of
101 database stuff. Gotta call this before you use the database properties
104 self
._session
= session
107 self
._pokemon
= session
.query(tables
.Pokemon
).get(st
.national_id
)
108 self
._pokemon_form
= session
.query(tables
.PokemonForm
) \
109 .with_parent(self
._pokemon
) \
110 .filter_by(name
=st
.alternate_form
) \
112 self
._ability
= self
._session
.query(tables
.Ability
).get(st
.ability_id
)
114 growth_rate
= self
._pokemon
.evolution_chain
.growth_rate
115 self
._experience_rung
= session
.query(tables
.Experience
) \
116 .filter(tables
.Experience
.growth_rate
== growth_rate
) \
117 .filter(tables
.Experience
.experience
<= st
.exp
) \
118 .order_by(tables
.Experience
.level
.desc()) \
120 level
= self
._experience_rung
.level
122 self
._next_experience_rung
= None
124 self
._next_experience_rung
= session
.query(tables
.Experience
) \
125 .filter(tables
.Experience
.growth_rate
== growth_rate
) \
126 .filter(tables
.Experience
.level
== level
+ 1) \
129 self
._held_item
= None
131 self
._held_item
= session
.query(tables
.ItemGameIndex
) \
132 .filter_by(generation_id
= 4, game_index
= st
.held_item_id
).one().item
135 for pokemon_stat
in self
._pokemon
.stats
:
136 structure_name
= pokemon_stat
.stat
.name
.lower().replace(' ', '_')
137 gene
= st
.ivs
['iv_' + structure_name
]
138 exp
= st
['effort_' + structure_name
]
140 if pokemon_stat
.stat
.name
== u
'HP':
143 calc
= calculated_stat
145 stat_tup
= self
.Stat(
146 stat
= pokemon_stat
.stat
,
147 base
= pokemon_stat
.base_stat
,
151 pokemon_stat
.base_stat
,
158 self
._stats
.append(stat_tup
)
162 self
.structure
.move1_id
,
163 self
.structure
.move2_id
,
164 self
.structure
.move3_id
,
165 self
.structure
.move4_id
,
167 move_rows
= self
._session
.query(tables
.Move
).filter(tables
.Move
.id.in_(move_ids
))
168 moves_dict
= dict((move
.id, move
) for move
in move_rows
)
170 self
._moves
= [moves_dict
.get(move_id
, None) for move_id
in move_ids
]
172 if st
.hgss_pokeball
>= 17:
173 pokeball_id
= st
.hgss_pokeball
- 17 + 492
175 pokeball_id
= st
.dppt_pokeball
176 self
._pokeball
= session
.query(tables
.ItemGameIndex
) \
177 .filter_by(generation_id
= 4, game_index
= pokeball_id
).one().item
179 egg_loc_id
= st
.pt_egg_location_id
or st
.dp_egg_location_id
180 met_loc_id
= st
.pt_met_location_id
or st
.dp_met_location_id
182 self
._egg_location
= None
184 self
._egg_location
= session
.query(tables
.LocationGameIndex
) \
185 .filter_by(generation_id
= 4, game_index
= egg_loc_id
).one().location
187 self
._met_location
= session
.query(tables
.LocationGameIndex
) \
188 .filter_by(generation_id
= 4, game_index
= met_loc_id
).one().location
196 def species_form(self
):
197 return self
._pokemon_form
201 return self
._pokeball
204 def egg_location(self
):
205 return self
._egg_location
208 def met_location(self
):
209 return self
._met_location
212 def shiny_leaves(self
):
214 self
.structure
.shining_leaves
.leaf1
,
215 self
.structure
.shining_leaves
.leaf2
,
216 self
.structure
.shining_leaves
.leaf3
,
217 self
.structure
.shining_leaves
.leaf4
,
218 self
.structure
.shining_leaves
.leaf5
,
223 return self
._experience_rung
.level
226 def exp_to_next(self
):
227 if self
._next_experience_rung
:
228 return self
._next_experience_rung
.experience
- self
.structure
.exp
233 def progress_to_next(self
):
234 if self
._next_experience_rung
:
236 * (self
.structure
.exp
- self
._experience_rung
.experience
) \
237 / (self
._next_experience_rung
.experience
- self
._experience_rung
.experience
)
247 return self
._held_item
260 self
.structure
.move1_pp
,
261 self
.structure
.move2_pp
,
262 self
.structure
.move3_pp
,
263 self
.structure
.move4_pp
,
269 shuffle_orders
= list( permutations(range(4)) )
272 def shuffle_chunks(cls
, words
, reverse
=False):
273 """The main 128 encrypted bytes (or 64 words) in a save block are split
274 into four chunks and shuffled around in some order, based on
275 personality. The actual order of shuffling is a permutation of four
276 items in order, indexed by the shuffle index. That is, 0 yields 0123,
277 1 yields 0132, 2 yields 0213, etc.
279 Given a list of words (the first of which should be the pid), this
280 function returns the words in shuffled order. Pass reverse=True to
285 shuffle_index
= (pid
>> 0xD & 0x1F) %
24
287 shuffle_order
= cls
.shuffle_orders
[shuffle_index
]
289 # Decoding requires going the other way; invert the order
290 shuffle_order
= [shuffle_order
.index(i
) for i
in range(4)]
292 shuffled
= words
[:3] # skip the unencrypted stuff
293 for chunk
in shuffle_order
:
294 shuffled
+= words
[ chunk
* 16 + 3 : chunk
* 16 + 19 ]
295 shuffled
+= words
[67:] # extra bytes are also left alone
300 def reciprocal_crypt(cls
, words
):
301 u
"""Applies the reciprocal Pokémon save file cipher to the provided
304 Returns nothing; the list is changed in-place.
306 # Apply regular Pokémon "encryption": xor everything with the output of
307 # the PRNG. First three items are pid/unused/checksum and are not
310 # Main data is encrypted using the checksum as a seed
311 prng
= pokemon_prng(words
[2])
312 for i
in range(3, 67):
313 words
[i
] ^
= next(prng
)
316 # Extra bytes are encrypted using the pid as a seed
317 prng
= pokemon_prng(words
[0])
318 for i
in range(67, len(words
)):
319 words
[i
] ^
= next(prng
)