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
._ability
= self
._session
.query(tables
.Ability
).get(st
.ability_id
)
110 growth_rate
= self
._pokemon
.evolution_chain
.growth_rate
111 self
._experience_rung
= session
.query(tables
.Experience
) \
112 .filter(tables
.Experience
.growth_rate
== growth_rate
) \
113 .filter(tables
.Experience
.experience
<= st
.exp
) \
114 .order_by(tables
.Experience
.level
.desc()) \
116 level
= self
._experience_rung
.level
118 self
._next_experience_rung
= None
120 self
._next_experience_rung
= session
.query(tables
.Experience
) \
121 .filter(tables
.Experience
.growth_rate
== growth_rate
) \
122 .filter(tables
.Experience
.level
== level
+ 1) \
125 self
._held_item
= None
127 self
._held_item
= session
.query(tables
.ItemInternalID
) \
128 .filter_by(generation_id
= 4, internal_id
= st
.held_item_id
).one().item
131 for pokemon_stat
in self
._pokemon
.stats
:
132 structure_name
= pokemon_stat
.stat
.name
.lower().replace(' ', '_')
133 gene
= st
.ivs
['iv_' + structure_name
]
134 exp
= st
['effort_' + structure_name
]
136 if pokemon_stat
.stat
.name
== u
'HP':
139 calc
= calculated_stat
141 stat_tup
= self
.Stat(
142 stat
= pokemon_stat
.stat
,
143 base
= pokemon_stat
.base_stat
,
147 pokemon_stat
.base_stat
,
154 self
._stats
.append(stat_tup
)
158 self
.structure
.move1_id
,
159 self
.structure
.move2_id
,
160 self
.structure
.move3_id
,
161 self
.structure
.move4_id
,
163 move_rows
= self
._session
.query(tables
.Move
).filter(tables
.Move
.id.in_(move_ids
))
164 moves_dict
= dict((move
.id, move
) for move
in move_rows
)
166 self
._moves
= [moves_dict
.get(move_id
, None) for move_id
in move_ids
]
168 if st
.hgss_pokeball
>= 17:
169 pokeball_id
= st
.hgss_pokeball
- 17 + 492
171 pokeball_id
= st
.dppt_pokeball
172 self
._pokeball
= session
.query(tables
.ItemInternalID
) \
173 .filter_by(generation_id
= 4, internal_id
= pokeball_id
).one().item
175 egg_loc_id
= st
.pt_egg_location_id
or st
.dp_egg_location_id
176 met_loc_id
= st
.pt_met_location_id
or st
.dp_met_location_id
178 self
._egg_location
= None
180 self
._egg_location
= session
.query(tables
.LocationInternalID
) \
181 .filter_by(generation_id
= 4, internal_id
= egg_loc_id
).one().location
183 self
._met_location
= session
.query(tables
.LocationInternalID
) \
184 .filter_by(generation_id
= 4, internal_id
= met_loc_id
).one().location
193 return self
._pokeball
196 def egg_location(self
):
197 return self
._egg_location
200 def met_location(self
):
201 return self
._met_location
204 def shiny_leaves(self
):
206 self
.structure
.shining_leaves
.leaf1
,
207 self
.structure
.shining_leaves
.leaf2
,
208 self
.structure
.shining_leaves
.leaf3
,
209 self
.structure
.shining_leaves
.leaf4
,
210 self
.structure
.shining_leaves
.leaf5
,
215 return self
._experience_rung
.level
218 def exp_to_next(self
):
219 if self
._next_experience_rung
:
220 return self
._next_experience_rung
.experience
- self
.structure
.exp
225 def progress_to_next(self
):
226 if self
._next_experience_rung
:
228 * (self
.structure
.exp
- self
._experience_rung
.experience
) \
229 / (self
._next_experience_rung
.experience
- self
._experience_rung
.experience
)
239 return self
._held_item
252 self
.structure
.move1_pp
,
253 self
.structure
.move2_pp
,
254 self
.structure
.move3_pp
,
255 self
.structure
.move4_pp
,
261 shuffle_orders
= list( permutations(range(4)) )
264 def shuffle_chunks(cls
, words
, reverse
=False):
265 """The main 128 encrypted bytes (or 64 words) in a save block are split
266 into four chunks and shuffled around in some order, based on
267 personality. The actual order of shuffling is a permutation of four
268 items in order, indexed by the shuffle index. That is, 0 yields 0123,
269 1 yields 0132, 2 yields 0213, etc.
271 Given a list of words (the first of which should be the pid), this
272 function returns the words in shuffled order. Pass reverse=True to
277 shuffle_index
= (pid
>> 0xD & 0x1F) %
24
279 shuffle_order
= cls
.shuffle_orders
[shuffle_index
]
281 # Decoding requires going the other way; invert the order
282 shuffle_order
= [shuffle_order
.index(i
) for i
in range(4)]
284 shuffled
= words
[:3] # skip the unencrypted stuff
285 for chunk
in shuffle_order
:
286 shuffled
+= words
[ chunk
* 16 + 3 : chunk
* 16 + 19 ]
287 shuffled
+= words
[67:] # extra bytes are also left alone
292 def reciprocal_crypt(cls
, words
):
293 u
"""Applies the reciprocal Pokémon save file cipher to the provided
296 Returns nothing; the list is changed in-place.
298 # Apply regular Pokémon "encryption": xor everything with the output of
299 # the PRNG. First three items are pid/unused/checksum and are not
302 # Main data is encrypted using the checksum as a seed
303 prng
= pokemon_prng(words
[2])
304 for i
in range(3, 67):
305 words
[i
] ^
= next(prng
)
308 # Extra bytes are encrypted using the pid as a seed
309 prng
= pokemon_prng(words
[0])
310 for i
in range(67, len(words
)):
311 words
[i
] ^
= next(prng
)