54a81f2984b60bbc99c6090168f28ac11c1d219b
[zzz-pokedex.git] / pokedex / struct / __init__.py
1 # encoding: utf8
2 u"""
3 Handles reading and encryption/decryption of Pokémon save file data.
4
5 See: http://projectpokemon.org/wiki/Pokemon_NDS_Structure
6
7 Kudos to LordLandon for his pkmlib.py, from which this module was originally
8 derived.
9 """
10
11 import struct
12
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
17
18 def pokemon_prng(seed):
19 u"""Creates a generator that simulates the main Pokémon PRNG."""
20 while True:
21 seed = 0x41C64E6D * seed + 0x6073
22 seed &= 0xFFFFFFFF
23 yield seed >> 16
24
25
26 class SaveFilePokemon(object):
27 u"""Represents an individual Pokémon, from the game's point of view.
28
29 Handles translating between the on-disk encrypted form, the in-RAM blob
30 (also used by pokesav), and something vaguely intelligible.
31 """
32
33 Stat = namedtuple('Stat', ['stat', 'base', 'gene', 'exp', 'calc'])
34
35 def __init__(self, blob, encrypted=False):
36 u"""Wraps a Pokémon save struct in a friendly object.
37
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
40 is left alone.
41
42 `session` is an optional database session.
43 """
44
45 if encrypted:
46 # Decrypt it.
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) )
50
51 # Apply standard Pokémon decryption, undo the block shuffling, and
52 # done
53 self.reciprocal_crypt(shuffled)
54 words = self.shuffle_chunks(shuffled, reverse=True)
55 self.blob = struct.pack(struct_def, *words)
56
57 else:
58 # Already decrypted
59 self.blob = blob
60
61 self.structure = pokemon_struct.parse(self.blob)
62
63 @property
64 def as_struct(self):
65 u"""Returns a decrypted struct, aka .pkm file."""
66 return self.blob
67
68 @property
69 def as_encrypted(self):
70 u"""Returns an encrypted struct the game expects in a save file."""
71
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) )
75
76 # Apply the block shuffle and standard Pokémon encryption
77 shuffled = self.shuffle_chunks(words)
78 self.reciprocal_crypt(shuffled)
79
80 # Stuff back into a string, and done
81 return struct.pack(struct_def, *shuffled)
82
83 ### Delicious data
84 @property
85 def is_shiny(self):
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
92 return (
93 self.structure.original_trainer_id
94 ^ self.structure.original_trainer_secret_id
95 ^ personality_msdw
96 ^ personality_lsdw
97 ) < 8
98
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
102 like `species`, etc.
103 """
104 self._session = session
105
106 st = self.structure
107 self._pokemon = session.query(tables.Pokemon).get(st.national_id)
108 self._ability = self._session.query(tables.Ability).get(st.ability_id)
109
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()) \
115 [0]
116 level = self._experience_rung.level
117
118 self._next_experience_rung = None
119 if level < 100:
120 self._next_experience_rung = session.query(tables.Experience) \
121 .filter(tables.Experience.growth_rate == growth_rate) \
122 .filter(tables.Experience.level == level + 1) \
123 .one()
124
125 self._held_item = None
126 if st.held_item_id:
127 self._held_item = session.query(tables.ItemInternalID) \
128 .filter_by(generation_id = 4, internal_id = st.held_item_id).one().item
129
130 self._stats = []
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]
135
136 if pokemon_stat.stat.name == u'HP':
137 calc = calculated_hp
138 else:
139 calc = calculated_stat
140
141 stat_tup = self.Stat(
142 stat = pokemon_stat.stat,
143 base = pokemon_stat.base_stat,
144 gene = gene,
145 exp = exp,
146 calc = calc(
147 pokemon_stat.base_stat,
148 level = level,
149 iv = gene,
150 effort = exp,
151 ),
152 )
153
154 self._stats.append(stat_tup)
155
156
157 move_ids = (
158 self.structure.move1_id,
159 self.structure.move2_id,
160 self.structure.move3_id,
161 self.structure.move4_id,
162 )
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)
165
166 self._moves = [moves_dict.get(move_id, None) for move_id in move_ids]
167
168 if st.hgss_pokeball >= 17:
169 pokeball_id = st.hgss_pokeball - 17 + 492
170 else:
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
174
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
177
178 self._egg_location = None
179 if egg_loc_id:
180 self._egg_location = session.query(tables.LocationInternalID) \
181 .filter_by(generation_id = 4, internal_id = egg_loc_id).one().location
182
183 self._met_location = session.query(tables.LocationInternalID) \
184 .filter_by(generation_id = 4, internal_id = met_loc_id).one().location
185
186 @property
187 def species(self):
188 # XXX forme!
189 return self._pokemon
190
191 @property
192 def pokeball(self):
193 return self._pokeball
194
195 @property
196 def egg_location(self):
197 return self._egg_location
198
199 @property
200 def met_location(self):
201 return self._met_location
202
203 @property
204 def shiny_leaves(self):
205 return (
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,
211 )
212
213 @property
214 def level(self):
215 return self._experience_rung.level
216
217 @property
218 def exp_to_next(self):
219 if self._next_experience_rung:
220 return self._next_experience_rung.experience - self.structure.exp
221 else:
222 return 0
223
224 @property
225 def progress_to_next(self):
226 if self._next_experience_rung:
227 return 1.0 \
228 * (self.structure.exp - self._experience_rung.experience) \
229 / (self._next_experience_rung.experience - self._experience_rung.experience)
230 else:
231 return 0.0
232
233 @property
234 def ability(self):
235 return self._ability
236
237 @property
238 def held_item(self):
239 return self._held_item
240
241 @property
242 def stats(self):
243 return self._stats
244
245 @property
246 def moves(self):
247 return self._moves
248
249 @property
250 def move_pp(self):
251 return (
252 self.structure.move1_pp,
253 self.structure.move2_pp,
254 self.structure.move3_pp,
255 self.structure.move4_pp,
256 )
257
258
259 ### Utility methods
260
261 shuffle_orders = list( permutations(range(4)) )
262
263 @classmethod
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.
270
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
273 unshuffle instead.
274 """
275
276 pid = words[0]
277 shuffle_index = (pid >> 0xD & 0x1F) % 24
278
279 shuffle_order = cls.shuffle_orders[shuffle_index]
280 if reverse:
281 # Decoding requires going the other way; invert the order
282 shuffle_order = [shuffle_order.index(i) for i in range(4)]
283
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
288
289 return shuffled
290
291 @classmethod
292 def reciprocal_crypt(cls, words):
293 u"""Applies the reciprocal Pokémon save file cipher to the provided
294 list of words.
295
296 Returns nothing; the list is changed in-place.
297 """
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
300 # encrypted.
301
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)
306
307 if len(words) > 67:
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)
312
313 return