2010-04-17 05:45:44 +00:00
|
|
|
# encoding: utf8
|
|
|
|
u"""
|
|
|
|
Handles reading and encryption/decryption of Pokémon save file data.
|
|
|
|
|
|
|
|
See: http://projectpokemon.org/wiki/Pokemon_NDS_Structure
|
|
|
|
|
|
|
|
Kudos to LordLandon for his pkmlib.py, from which this module was originally
|
|
|
|
derived.
|
|
|
|
"""
|
|
|
|
|
2010-04-25 07:14:29 +00:00
|
|
|
import struct
|
2012-10-13 14:16:02 +00:00
|
|
|
import base64
|
|
|
|
import datetime
|
|
|
|
import contextlib
|
2012-10-13 23:00:56 +00:00
|
|
|
from operator import attrgetter
|
2010-06-18 04:43:50 +00:00
|
|
|
|
2012-10-13 20:01:49 +00:00
|
|
|
import sqlalchemy.orm.exc
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
from pokedex.db import tables, util
|
2010-08-20 03:48:03 +00:00
|
|
|
from pokedex.formulae import calculated_hp, calculated_stat
|
2011-04-14 10:46:12 +00:00
|
|
|
from pokedex.compatibility import namedtuple, permutations
|
2012-10-13 20:01:49 +00:00
|
|
|
from pokedex.struct._pokemon_struct import (make_pokemon_struct, pokemon_forms,
|
|
|
|
StringWithOriginal)
|
2010-04-17 05:45:44 +00:00
|
|
|
|
|
|
|
def pokemon_prng(seed):
|
|
|
|
u"""Creates a generator that simulates the main Pokémon PRNG."""
|
|
|
|
while True:
|
|
|
|
seed = 0x41C64E6D * seed + 0x6073
|
|
|
|
seed &= 0xFFFFFFFF
|
|
|
|
yield seed >> 16
|
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def struct_proxy(name, dependent=[]):
|
2012-10-13 23:56:38 +00:00
|
|
|
"""Proxies to self.structure.<name>
|
|
|
|
|
|
|
|
"blob" is autometically reset by the setter.
|
|
|
|
The setter deletes all attributes named in ``dependent``.
|
|
|
|
"""
|
2012-06-24 21:54:00 +00:00
|
|
|
def getter(self):
|
2012-10-13 23:56:38 +00:00
|
|
|
return self.structure[name]
|
2012-06-24 21:54:00 +00:00
|
|
|
|
|
|
|
def setter(self, value):
|
2012-10-13 23:56:38 +00:00
|
|
|
self.structure[name] = value
|
2012-10-13 14:16:02 +00:00
|
|
|
for dep in dependent:
|
|
|
|
delattr(self, dep)
|
|
|
|
del self.blob
|
2012-06-24 21:54:00 +00:00
|
|
|
|
|
|
|
return property(getter, setter)
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
|
|
|
|
def struct_frozenset_proxy(name):
|
2012-10-13 23:56:38 +00:00
|
|
|
"""Proxy for sets like ribbons or markings
|
|
|
|
|
|
|
|
"blob" is autometically reset by the setter.
|
|
|
|
"""
|
2012-06-24 21:54:00 +00:00
|
|
|
def getter(self):
|
2012-10-13 23:56:38 +00:00
|
|
|
bitstruct = self.structure[name]
|
2012-06-24 21:54:00 +00:00
|
|
|
return frozenset(k for k, v in bitstruct.items() if v)
|
|
|
|
|
2012-10-13 21:52:51 +00:00
|
|
|
def setter(self, new_set):
|
|
|
|
new_set = set(new_set)
|
2012-10-13 23:56:38 +00:00
|
|
|
struct = self.structure[name]
|
2012-10-13 20:01:49 +00:00
|
|
|
for key in struct:
|
2012-10-13 23:56:38 +00:00
|
|
|
struct[key] = (key in new_set)
|
2012-10-13 21:52:51 +00:00
|
|
|
new_set.discard(key)
|
|
|
|
if new_set:
|
|
|
|
raise ValueError('Unknown values: {0}'.format(', '.join(ribbons)))
|
2012-10-13 14:16:02 +00:00
|
|
|
del self.blob
|
2012-06-24 21:54:00 +00:00
|
|
|
|
|
|
|
return property(getter, setter)
|
|
|
|
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
class cached_property(object):
|
2012-10-13 23:56:38 +00:00
|
|
|
"""Caching property. Use del to remove the cache."""
|
2012-10-13 14:16:02 +00:00
|
|
|
def __init__(self, getter, setter=None):
|
|
|
|
self._getter = getter
|
|
|
|
self._setter = setter
|
|
|
|
self.cache_setter_value = True
|
|
|
|
|
|
|
|
def setter(self, func):
|
|
|
|
"""With this setter, the value being set is automatically cached
|
2012-10-13 23:56:38 +00:00
|
|
|
|
|
|
|
"blob" is autometically reset by the setter.
|
2012-10-13 14:16:02 +00:00
|
|
|
"""
|
|
|
|
self._setter = func
|
|
|
|
self.cache_setter_value = True
|
|
|
|
return self
|
|
|
|
|
|
|
|
def complete_setter(self, func):
|
2012-10-13 23:56:38 +00:00
|
|
|
"""Setter without automatic caching of the set value"""
|
2012-10-13 14:16:02 +00:00
|
|
|
self._setter = func
|
|
|
|
self.cache_setter_value = False
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __get__(self, instance, owner):
|
|
|
|
if instance is None:
|
|
|
|
return self
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
return instance._cached_properties[self]
|
|
|
|
except AttributeError:
|
|
|
|
instance._cached_properties = {}
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
result = self._getter(instance)
|
|
|
|
instance._cached_properties[self] = result
|
|
|
|
return result
|
|
|
|
|
|
|
|
def __set__(self, instance, value):
|
|
|
|
if self._setter is None:
|
|
|
|
raise AttributeError('Cannot set attribute')
|
|
|
|
else:
|
|
|
|
self._setter(instance, value)
|
|
|
|
if self.cache_setter_value:
|
|
|
|
try:
|
|
|
|
instance._cached_properties[self] = value
|
|
|
|
except AttributeError:
|
|
|
|
instance._cached_properties = {self: value}
|
|
|
|
del instance.blob
|
|
|
|
|
|
|
|
def __delete__(self, instance):
|
|
|
|
try:
|
|
|
|
del instance._cached_properties[self]
|
|
|
|
except (AttributeError, KeyError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2010-06-18 04:43:50 +00:00
|
|
|
class SaveFilePokemon(object):
|
2012-06-10 21:01:15 +00:00
|
|
|
u"""Base class for an individual Pokémon, from the game's point of view.
|
2010-04-17 05:45:44 +00:00
|
|
|
|
|
|
|
Handles translating between the on-disk encrypted form, the in-RAM blob
|
|
|
|
(also used by pokesav), and something vaguely intelligible.
|
|
|
|
"""
|
2010-08-20 03:48:03 +00:00
|
|
|
Stat = namedtuple('Stat', ['stat', 'base', 'gene', 'exp', 'calc'])
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def __init__(self, blob=None, dict_=None, encrypted=False, session=None):
|
2010-04-17 05:45:44 +00:00
|
|
|
u"""Wraps a Pokémon save struct in a friendly object.
|
|
|
|
|
|
|
|
If `encrypted` is True, the blob will be decrypted as though it were an
|
|
|
|
on-disk save. Otherwise, the blob is taken to be already decrypted and
|
|
|
|
is left alone.
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
`session` is an optional database session. Either give it or fill it
|
|
|
|
later with `use_database_session`
|
2010-04-17 05:45:44 +00:00
|
|
|
"""
|
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
try:
|
|
|
|
self.generation_id
|
|
|
|
except AttributeError:
|
|
|
|
raise NotImplementedError(
|
|
|
|
"Use generation-specific subclass of SaveFilePokemon")
|
|
|
|
|
|
|
|
if blob:
|
|
|
|
if encrypted:
|
|
|
|
# Decrypt it.
|
|
|
|
# Interpret as one word (pid), followed by a bunch of shorts
|
|
|
|
struct_def = "I" + "H" * ((len(blob) - 4) / 2)
|
|
|
|
shuffled = list( struct.unpack(struct_def, blob) )
|
|
|
|
|
|
|
|
# Apply standard Pokémon decryption, undo the block shuffling, and
|
|
|
|
# done
|
|
|
|
self.reciprocal_crypt(shuffled)
|
|
|
|
words = self.shuffle_chunks(shuffled, reverse=True)
|
|
|
|
self.blob = struct.pack(struct_def, *words)
|
2010-04-17 05:45:44 +00:00
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
else:
|
|
|
|
# Already decrypted
|
|
|
|
self.blob = blob
|
2010-04-17 05:45:44 +00:00
|
|
|
else:
|
2012-10-13 14:16:02 +00:00
|
|
|
self.blob = '\0' * (32 * 4 + 8)
|
2010-04-17 05:45:44 +00:00
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
if session:
|
2012-10-13 14:16:02 +00:00
|
|
|
self.session = session
|
2012-06-24 21:54:00 +00:00
|
|
|
else:
|
2012-10-13 14:16:02 +00:00
|
|
|
self.session = None
|
|
|
|
|
|
|
|
if dict_:
|
|
|
|
self.update(dict_)
|
2010-06-18 04:43:50 +00:00
|
|
|
|
2010-04-17 05:45:44 +00:00
|
|
|
@property
|
|
|
|
def as_struct(self):
|
|
|
|
u"""Returns a decrypted struct, aka .pkm file."""
|
|
|
|
return self.blob
|
|
|
|
|
|
|
|
@property
|
|
|
|
def as_encrypted(self):
|
|
|
|
u"""Returns an encrypted struct the game expects in a save file."""
|
|
|
|
|
|
|
|
# Interpret as one word (pid), followed by a bunch of shorts
|
|
|
|
struct_def = "I" + "H" * ((len(self.blob) - 4) / 2)
|
|
|
|
words = list( struct.unpack(struct_def, self.blob) )
|
|
|
|
|
|
|
|
# Apply the block shuffle and standard Pokémon encryption
|
|
|
|
shuffled = self.shuffle_chunks(words)
|
|
|
|
self.reciprocal_crypt(shuffled)
|
|
|
|
|
|
|
|
# Stuff back into a string, and done
|
|
|
|
return struct.pack(struct_def, *shuffled)
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def export_dict(self):
|
2012-06-24 21:54:00 +00:00
|
|
|
"""Exports the pokemon as a YAML/JSON-compatible dict
|
|
|
|
"""
|
|
|
|
st = self.structure
|
|
|
|
|
2012-10-13 23:00:56 +00:00
|
|
|
NO_VALUE = object()
|
|
|
|
def save(target_dict, key, value=NO_VALUE, transform=None,
|
|
|
|
condition=lambda x: x):
|
|
|
|
"""Set a dict key to a value, if a condition is true
|
|
|
|
|
|
|
|
If value is not given, it is looked up on self.
|
|
|
|
The value can be transformed by a function before setting.
|
|
|
|
"""
|
|
|
|
if value is NO_VALUE:
|
|
|
|
attrname = key.replace(' ', '_')
|
|
|
|
value = getattr(self, attrname)
|
|
|
|
if condition(value):
|
|
|
|
if transform:
|
|
|
|
value = transform(value)
|
|
|
|
target_dict[key] = value
|
|
|
|
|
|
|
|
def save_string(target_dict, string_key, trash_key, string):
|
|
|
|
"""Save a string, including trash bytes"""
|
|
|
|
target_dict[string_key] = unicode(string)
|
2012-10-13 20:01:49 +00:00
|
|
|
trash = getattr(string, 'original', None)
|
|
|
|
if trash:
|
|
|
|
expected = (string + u'\uffff').encode('utf-16LE')
|
|
|
|
if trash.rstrip('\0') != expected:
|
2012-10-13 23:00:56 +00:00
|
|
|
target_dict[trash_key] = base64.b64encode(trash)
|
|
|
|
|
|
|
|
def save_object(target_dict, key, value=NO_VALUE, **extra):
|
|
|
|
"""Objects are represented as dicts with "name" and a bunch of IDs
|
|
|
|
|
|
|
|
The name is for humans. The ID is the number from the struct.
|
|
|
|
"""
|
|
|
|
save(target_dict, key, value=value, transform=lambda value:
|
|
|
|
dict(name=value.name, **extra))
|
2012-10-13 20:01:49 +00:00
|
|
|
|
2012-10-14 00:14:47 +00:00
|
|
|
def any_values(d):
|
|
|
|
return any(d.values())
|
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
result = dict(
|
|
|
|
species=dict(id=self.species.id, name=self.species.name),
|
|
|
|
)
|
2012-10-13 23:00:56 +00:00
|
|
|
if self.form != self.species.default_form:
|
2012-10-14 11:47:51 +00:00
|
|
|
result['form'] = dict(
|
|
|
|
id=st.alternate_form_id, name=self.form.form_name)
|
2012-10-13 23:00:56 +00:00
|
|
|
|
|
|
|
save_object(result, 'ability', id=st.ability_id)
|
|
|
|
save_object(result, 'held item', id=st.held_item_id)
|
2012-10-14 00:14:47 +00:00
|
|
|
|
|
|
|
ball_dict = {}
|
|
|
|
save(ball_dict, 'id_dppt', st.dppt_pokeball)
|
|
|
|
save(ball_dict, 'id_hgss', st.hgss_pokeball)
|
2012-10-14 14:16:02 +00:00
|
|
|
save(ball_dict, 'name', self.pokeball, transform=attrgetter('name'))
|
2012-10-14 00:14:47 +00:00
|
|
|
save(result, 'pokeball', ball_dict, condition=any_values)
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
trainer = dict(
|
2012-06-24 21:54:00 +00:00
|
|
|
id=self.original_trainer_id,
|
|
|
|
secret=self.original_trainer_secret_id,
|
|
|
|
name=unicode(self.original_trainer_name),
|
|
|
|
gender=self.original_trainer_gender
|
|
|
|
)
|
2012-10-13 23:00:56 +00:00
|
|
|
save_string(trainer, 'name', 'name trash', self.original_trainer_name)
|
2012-10-13 14:16:02 +00:00
|
|
|
if (trainer['id'] or trainer['secret'] or
|
|
|
|
trainer['name'].strip('\0') or trainer['gender'] != 'male'):
|
|
|
|
result['oiginal trainer'] = trainer
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-10-13 23:00:56 +00:00
|
|
|
save(result, 'exp')
|
|
|
|
save(result, 'happiness')
|
|
|
|
save(result, 'original country')
|
|
|
|
save(result, 'original version')
|
2012-10-13 23:56:38 +00:00
|
|
|
save(result, 'met at level')
|
|
|
|
save(result, 'is egg')
|
|
|
|
save(result, 'fateful encounter')
|
|
|
|
save(result, 'personality')
|
|
|
|
|
|
|
|
save(result, 'markings', transform=sorted)
|
2012-10-13 23:00:56 +00:00
|
|
|
save(result, 'encounter type', condition=lambda et:
|
|
|
|
(et and et != 'special'))
|
|
|
|
save_string(result, 'nickname', 'nickname trash', self.nickname)
|
|
|
|
save(result, 'egg received', self.date_egg_received,
|
|
|
|
transform=lambda x: x.isoformat())
|
|
|
|
save(result, 'date met',
|
|
|
|
transform=lambda x: x.isoformat())
|
|
|
|
save(result, 'pokerus data', self.pokerus)
|
|
|
|
save(result, 'nicknamed', self.is_nicknamed)
|
|
|
|
save(result, 'gender', condition=lambda g: g != 'genderless')
|
2012-10-14 11:51:40 +00:00
|
|
|
for name in 'sinnoh ribbons', 'sinnoh contest ribbons', 'hoenn ribbons':
|
|
|
|
save(result, name, transform=lambda ribbons:
|
|
|
|
sorted(r.replace('_', ' ') for r in ribbons))
|
2012-10-13 23:00:56 +00:00
|
|
|
|
|
|
|
for loc_type in 'egg', 'met':
|
|
|
|
loc_dict = dict()
|
|
|
|
save(loc_dict, 'id_pt', st['pt_{0}_location_id'.format(loc_type)])
|
|
|
|
save(loc_dict, 'id_dp', st['dp_{0}_location_id'.format(loc_type)])
|
|
|
|
save(loc_dict, 'name',
|
|
|
|
getattr(self, '{0}_location'.format(loc_type)),
|
|
|
|
transform=attrgetter('name'))
|
|
|
|
save(result, '{0} location'.format(loc_type), loc_dict)
|
2012-10-13 20:01:49 +00:00
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
moves = result['moves'] = []
|
|
|
|
for i, move_object in enumerate(self.moves, 1):
|
|
|
|
move = {}
|
2012-10-13 23:00:56 +00:00
|
|
|
save(move, 'id', move_object, transform=attrgetter('id'))
|
|
|
|
save(move, 'name', move_object, transform=attrgetter('name'))
|
|
|
|
save(move, 'pp ups', st['move%s_pp_ups' % i])
|
|
|
|
pp = st['move%s_pp' % i]
|
|
|
|
if move or pp:
|
|
|
|
move['pp'] = pp
|
2012-06-24 21:54:00 +00:00
|
|
|
moves.append(move)
|
|
|
|
|
|
|
|
effort = {}
|
|
|
|
genes = {}
|
|
|
|
contest_stats = {}
|
2012-10-13 14:16:02 +00:00
|
|
|
for pokemon_stat in self.pokemon.stats:
|
|
|
|
stat_identifier = pokemon_stat.stat.identifier
|
|
|
|
st_stat_identifier = stat_identifier.replace('-', '_')
|
|
|
|
dct_stat_identifier = stat_identifier.replace('-', ' ')
|
2012-10-13 20:01:49 +00:00
|
|
|
genes[dct_stat_identifier] = st['iv_' + st_stat_identifier]
|
|
|
|
effort[dct_stat_identifier] = st['effort_' + st_stat_identifier]
|
2012-06-24 21:54:00 +00:00
|
|
|
for contest_stat in 'cool', 'beauty', 'cute', 'smart', 'tough', 'sheen':
|
2012-10-13 20:01:49 +00:00
|
|
|
contest_stats[contest_stat] = st['contest_' + contest_stat]
|
2012-10-13 23:56:38 +00:00
|
|
|
save(result, 'effort', effort, condition=any_values)
|
|
|
|
save(result, 'genes', genes, condition=any_values)
|
|
|
|
save(result, 'contest stats', contest_stats, condition=any_values)
|
2012-10-13 23:00:56 +00:00
|
|
|
|
2012-10-14 12:49:21 +00:00
|
|
|
trash = []
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
trash.append(st['trash_{0}'.format(len(trash))])
|
|
|
|
except KeyError:
|
|
|
|
break
|
|
|
|
save(result, 'trash values', trash, condition=any)
|
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
return result
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def update(self, dct, **kwargs):
|
|
|
|
"""Updates the pokemon from a YAML/JSON-compatible dict
|
|
|
|
|
|
|
|
Dicts that don't specify all the data are allowed. They update the
|
|
|
|
structure with the information they contain.
|
|
|
|
|
|
|
|
Keyword arguments with single keys are allowed. The semantics are
|
|
|
|
similar to dict.update.
|
|
|
|
|
|
|
|
Unlike setting properties directly, the this method tries more to keep
|
|
|
|
the result sensible, e.g. when species is updated, it can switch
|
|
|
|
to/from genderless.
|
|
|
|
"""
|
|
|
|
st = self.structure
|
|
|
|
session = self.session
|
|
|
|
dct.update(kwargs)
|
|
|
|
if 'ability' in dct:
|
|
|
|
st.ability_id = dct['ability']['id']
|
|
|
|
del self.ability
|
2012-10-13 20:01:49 +00:00
|
|
|
reset_form = False
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'form' in dct:
|
2012-10-14 11:47:51 +00:00
|
|
|
st.alternate_form_id = dct['form']['id']
|
2012-10-13 14:16:02 +00:00
|
|
|
reset_form = True
|
|
|
|
if 'species' in dct:
|
|
|
|
st.national_id = dct['species']['id']
|
|
|
|
if 'form' not in dct:
|
|
|
|
st.alternate_form = 0
|
|
|
|
reset_form = True
|
|
|
|
if reset_form:
|
|
|
|
del self.form
|
|
|
|
if not self.is_nicknamed:
|
|
|
|
self.nickname = self.species.name
|
|
|
|
self.is_nicknamed = False
|
|
|
|
if self.species.gender_rate == -1:
|
|
|
|
self.gender = 'genderless'
|
|
|
|
elif self.gender == 'genderless':
|
|
|
|
# make id=0 the default, sorry if it looks sexist
|
|
|
|
self.gender = 'male'
|
|
|
|
if 'held item' in dct:
|
2012-10-13 20:01:49 +00:00
|
|
|
st.held_item_id = dct['held item']['id']
|
|
|
|
del self.held_item
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'pokeball' in dct:
|
2012-10-14 00:14:47 +00:00
|
|
|
if 'id_dppt' in dct['pokeball']:
|
|
|
|
st.dppt_pokeball = dct['pokeball']['id_dppt']
|
|
|
|
if 'id_hgss' in dct['pokeball']:
|
|
|
|
st.hgss_pokeball = dct['pokeball']['id_hgss']
|
2012-10-13 14:16:02 +00:00
|
|
|
del self.pokeball
|
2012-10-13 23:56:38 +00:00
|
|
|
def load_values(source, **values):
|
2012-10-13 14:16:02 +00:00
|
|
|
for attrname, key in values.iteritems():
|
|
|
|
try:
|
|
|
|
value = source[key]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
setattr(self, attrname, value)
|
2012-10-13 20:01:49 +00:00
|
|
|
def load_name(attr_name, dct, string_key, trash_key):
|
|
|
|
if string_key in dct:
|
|
|
|
if trash_key in dct:
|
|
|
|
name = StringWithOriginal(unicode(dct[string_key]))
|
|
|
|
name.original = base64.b64decode(dct[trash_key])
|
|
|
|
setattr(self, attr_name, name)
|
|
|
|
else:
|
|
|
|
setattr(self, attr_name, unicode(dct[string_key]))
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'oiginal trainer' in dct:
|
2012-10-13 20:01:49 +00:00
|
|
|
trainer = dct['oiginal trainer']
|
2012-10-13 23:56:38 +00:00
|
|
|
load_values(trainer,
|
2012-10-13 14:16:02 +00:00
|
|
|
original_trainer_id='id',
|
|
|
|
original_trainer_secret_id='secret',
|
|
|
|
original_trainer_gender='gender',
|
|
|
|
)
|
2012-10-13 20:01:49 +00:00
|
|
|
load_name('original_trainer_name', trainer, 'name', 'name trash')
|
|
|
|
was_nicknamed = self.is_nicknamed
|
2012-10-13 23:56:38 +00:00
|
|
|
load_values(dct,
|
2012-10-13 14:16:02 +00:00
|
|
|
exp='exp',
|
|
|
|
happiness='happiness',
|
|
|
|
markings='markings',
|
|
|
|
original_country='original country',
|
|
|
|
original_version='original version',
|
|
|
|
encounter_type='encounter type',
|
|
|
|
pokerus='pokerus data',
|
|
|
|
met_at_level='met at level',
|
|
|
|
is_egg='is egg',
|
|
|
|
fateful_encounter='fateful encounter',
|
|
|
|
gender='gender',
|
2012-10-13 20:01:49 +00:00
|
|
|
personality='personality',
|
|
|
|
)
|
|
|
|
load_name('nickname', dct, 'nickname', 'nickname trash')
|
|
|
|
self.is_nicknamed = was_nicknamed
|
2012-10-13 23:56:38 +00:00
|
|
|
load_values(dct, is_nicknamed='nicknamed')
|
2012-10-13 20:01:49 +00:00
|
|
|
for loc_type in 'egg', 'met':
|
2012-10-13 23:00:56 +00:00
|
|
|
loc_dict = dct.get('{0} location'.format(loc_type))
|
|
|
|
if loc_dict:
|
2012-10-13 20:01:49 +00:00
|
|
|
dp_attr = 'dp_{0}_location_id'.format(loc_type)
|
|
|
|
pt_attr = 'pt_{0}_location_id'.format(loc_type)
|
2012-10-13 23:00:56 +00:00
|
|
|
if 'id_dp' in loc_dict:
|
|
|
|
st[dp_attr] = loc_dict['id_dp']
|
|
|
|
if 'id_pt' in loc_dict:
|
|
|
|
st[pt_attr] = loc_dict['id_pt']
|
2012-10-13 20:01:49 +00:00
|
|
|
delattr(self, '{0}_location'.format(loc_type))
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'date met' in dct:
|
|
|
|
self.date_met = datetime.datetime.strptime(
|
|
|
|
dct['date met'], '%Y-%m-%d').date()
|
2012-10-13 20:01:49 +00:00
|
|
|
if 'egg received' in dct:
|
|
|
|
self.date_egg_received = datetime.datetime.strptime(
|
|
|
|
dct['egg received'], '%Y-%m-%d').date()
|
2012-10-14 11:51:40 +00:00
|
|
|
for name in 'sinnoh ribbons', 'sinnoh contest ribbons', 'hoenn ribbons':
|
|
|
|
if name in dct:
|
|
|
|
setattr(self, name.replace(' ', '_'),
|
|
|
|
(r.replace(' ', '_') for r in dct[name]))
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'moves' in dct:
|
|
|
|
pp_reset_indices = []
|
2012-10-14 14:16:02 +00:00
|
|
|
i = -1
|
2012-10-13 14:16:02 +00:00
|
|
|
for i, movedict in enumerate(dct['moves']):
|
2012-10-14 11:47:51 +00:00
|
|
|
if 'id' in movedict:
|
|
|
|
st['move{0}_id'.format(i + 1)] = movedict['id']
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'pp' in movedict:
|
2012-10-13 20:01:49 +00:00
|
|
|
st['move{0}_pp'.format(i + 1)] = movedict['pp']
|
2012-10-13 14:16:02 +00:00
|
|
|
else:
|
2012-10-13 20:01:49 +00:00
|
|
|
pp_reset_indices.append(i)
|
|
|
|
if 'pp ups' in movedict:
|
|
|
|
st['move{0}_pp_ups'.format(i + 1)] = movedict['pp ups']
|
2012-10-13 14:16:02 +00:00
|
|
|
for i in range(i + 1, 4):
|
|
|
|
# Reset the rest of the moves
|
2012-10-13 20:01:49 +00:00
|
|
|
st['move{0}_id'.format(i + 1)] = 0
|
|
|
|
st['move{0}_pp'.format(i + 1)] = 0
|
|
|
|
st['move{0}_pp_up'.format(i + 1)] = 0
|
2012-10-13 14:16:02 +00:00
|
|
|
del self.moves
|
|
|
|
del self.move_pp
|
|
|
|
for i in pp_reset_indices:
|
|
|
|
# Set default PP here, when the moves dict is regenerated
|
2012-10-13 20:01:49 +00:00
|
|
|
st['move{0}_pp'.format(i + 1)] = self.moves[i].pp
|
|
|
|
for key, prefix in (('genes', 'iv'), ('effort', 'effort'),
|
2012-10-13 14:16:02 +00:00
|
|
|
('contest stats', 'contest')):
|
|
|
|
for name, value in dct.get(key, {}).items():
|
|
|
|
st['{}_{}'.format(prefix, name.replace(' ', '_'))] = value
|
2012-10-14 12:49:21 +00:00
|
|
|
if 'trash values' in dct:
|
|
|
|
for i, data in enumerate(dct['trash values']):
|
|
|
|
st['trash_{0}'.format(i)] = data
|
2012-10-13 23:56:38 +00:00
|
|
|
del self.stats
|
|
|
|
del self.blob
|
2012-10-13 14:16:02 +00:00
|
|
|
return self
|
|
|
|
|
2010-07-08 05:29:30 +00:00
|
|
|
### Delicious data
|
|
|
|
@property
|
|
|
|
def is_shiny(self):
|
|
|
|
u"""Returns true iff this Pokémon is shiny."""
|
|
|
|
# See http://bulbapedia.bulbagarden.net/wiki/Personality#Shininess
|
|
|
|
# But don't see it too much, because the above is super over
|
|
|
|
# complicated. Do this instead!
|
|
|
|
personality_msdw = self.structure.personality >> 16
|
|
|
|
personality_lsdw = self.structure.personality & 0xffff
|
|
|
|
return (
|
|
|
|
self.structure.original_trainer_id
|
|
|
|
^ self.structure.original_trainer_secret_id
|
|
|
|
^ personality_msdw
|
|
|
|
^ personality_lsdw
|
|
|
|
) < 8
|
|
|
|
|
2010-08-20 03:48:03 +00:00
|
|
|
def use_database_session(self, session):
|
|
|
|
"""Remembers the given database session, and prefetches a bunch of
|
2012-06-24 21:54:00 +00:00
|
|
|
database stuff. Gotta call this (or give session to `__init__`) before
|
2012-06-10 21:01:15 +00:00
|
|
|
you use the database properties like `species`, etc.
|
2010-08-20 03:48:03 +00:00
|
|
|
"""
|
2012-10-13 14:16:02 +00:00
|
|
|
if self.session and self.session is not session:
|
|
|
|
raise ValueError('Re-setting a session is not supported')
|
|
|
|
self.session = session
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def stats(self):
|
|
|
|
stats = []
|
|
|
|
for pokemon_stat in self.pokemon.stats:
|
|
|
|
stat_identifier = pokemon_stat.stat.identifier.replace('-', '_')
|
|
|
|
gene = st['iv_' + stat_identifier]
|
|
|
|
exp = st['effort_' + stat_identifier]
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
if pokemon_stat.stat.identifier == u'hp':
|
|
|
|
calc = calculated_hp
|
|
|
|
else:
|
|
|
|
calc = calculated_stat
|
|
|
|
|
|
|
|
stat_tup = self.Stat(
|
|
|
|
stat = pokemon_stat.stat,
|
|
|
|
base = pokemon_stat.base_stat,
|
|
|
|
gene = gene,
|
|
|
|
exp = exp,
|
|
|
|
calc = calc(
|
|
|
|
pokemon_stat.base_stat,
|
|
|
|
level = level,
|
|
|
|
iv = gene,
|
|
|
|
effort = exp,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
stats.append(stat_tup)
|
|
|
|
return tuple(stats)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def alternate_form(self):
|
2010-08-20 03:48:03 +00:00
|
|
|
st = self.structure
|
2012-10-13 14:16:02 +00:00
|
|
|
forms = pokemon_forms.get(st.national_id)
|
|
|
|
if forms:
|
|
|
|
return forms[st.alternate_form_id]
|
|
|
|
else:
|
|
|
|
return None
|
2012-06-10 21:01:15 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@alternate_form.setter
|
|
|
|
def alternate_form(self, alternate_form):
|
|
|
|
st = self.structure
|
|
|
|
forms = pokemon_forms.get(st.national_id)
|
|
|
|
if forms:
|
|
|
|
st.alternate_form_id = forms.index(alternate_form)
|
2012-06-10 21:01:15 +00:00
|
|
|
else:
|
2012-10-13 14:16:02 +00:00
|
|
|
st.alternate_form_id = 0
|
|
|
|
del self.form
|
2012-10-13 23:56:38 +00:00
|
|
|
del self.blob
|
2012-06-10 21:01:15 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@property
|
|
|
|
def species(self):
|
|
|
|
if self.form:
|
|
|
|
return self.form.species
|
2012-06-10 21:01:15 +00:00
|
|
|
else:
|
2012-10-13 14:16:02 +00:00
|
|
|
return None
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@species.setter
|
|
|
|
def species(self, species):
|
|
|
|
self.form = species.default_form
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@property
|
|
|
|
def pokemon(self):
|
|
|
|
if self.form:
|
|
|
|
return self.form.pokemon
|
|
|
|
else:
|
|
|
|
return None
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@pokemon.setter
|
|
|
|
def pokemon(self, pokemon):
|
|
|
|
self.form = pokemon.default_form
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def form(self):
|
|
|
|
st = self.structure
|
|
|
|
session = self.session
|
|
|
|
if st.national_id:
|
|
|
|
pokemon = session.query(tables.Pokemon).get(st.national_id)
|
2012-10-13 20:01:49 +00:00
|
|
|
if self.alternate_form:
|
|
|
|
return session.query(tables.PokemonForm) \
|
|
|
|
.with_parent(pokemon) \
|
|
|
|
.filter_by(form_identifier=self.alternate_form) \
|
|
|
|
.one()
|
|
|
|
else:
|
|
|
|
return pokemon.default_form
|
2012-10-13 14:16:02 +00:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
@form.setter
|
|
|
|
def form(self, form):
|
|
|
|
self.structure.national_id = form.species.id
|
|
|
|
self.structure.alternate_form = form.form_identifier
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 20:01:49 +00:00
|
|
|
@cached_property
|
2012-10-13 14:16:02 +00:00
|
|
|
def pokeball(self):
|
|
|
|
st = self.structure
|
2010-08-20 03:48:03 +00:00
|
|
|
if st.hgss_pokeball >= 17:
|
|
|
|
pokeball_id = st.hgss_pokeball - 17 + 492
|
2012-06-10 21:01:15 +00:00
|
|
|
elif st.dppt_pokeball:
|
2010-08-20 03:48:03 +00:00
|
|
|
pokeball_id = st.dppt_pokeball
|
2012-06-10 21:01:15 +00:00
|
|
|
else:
|
2012-10-13 20:01:49 +00:00
|
|
|
return None
|
|
|
|
return self._get_pokeball(pokeball_id)
|
|
|
|
|
|
|
|
def _get_pokeball(self, pokeball_id):
|
|
|
|
return (self.session.query(tables.ItemGameIndex)
|
|
|
|
.filter_by(generation_id=4, game_index = pokeball_id).one().item)
|
|
|
|
|
|
|
|
@pokeball.setter
|
|
|
|
def pokeball(self, pokeball):
|
|
|
|
st = self.structure
|
|
|
|
st.hgss_pokeball = st.dppt_pokeball = 0
|
|
|
|
if pokeball:
|
|
|
|
pokeball_id = pokeball.id
|
|
|
|
boundary = 492 - 17
|
|
|
|
if pokeball_id >= boundary:
|
2012-10-14 00:14:47 +00:00
|
|
|
st.dppt_pokeball = 0
|
2012-10-13 20:01:49 +00:00
|
|
|
st.hgss_pokeball = pokeball_id - boundary
|
|
|
|
else:
|
|
|
|
st.dppt_pokeball = pokeball_id
|
2012-10-14 00:14:47 +00:00
|
|
|
st.hgss_pokeball = 0
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@cached_property
|
|
|
|
def egg_location(self):
|
|
|
|
st = self.structure
|
2010-08-20 03:48:03 +00:00
|
|
|
egg_loc_id = st.pt_egg_location_id or st.dp_egg_location_id
|
|
|
|
if egg_loc_id:
|
2012-10-13 20:01:49 +00:00
|
|
|
try:
|
|
|
|
return self.session.query(tables.LocationGameIndex) \
|
|
|
|
.filter_by(generation_id=4,
|
|
|
|
game_index = egg_loc_id).one().location
|
|
|
|
except sqlalchemy.orm.exc.NoResultFound:
|
|
|
|
return None
|
2012-06-10 21:01:15 +00:00
|
|
|
else:
|
2012-10-13 14:16:02 +00:00
|
|
|
return None
|
2011-03-06 00:04:05 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@cached_property
|
2010-08-20 03:48:03 +00:00
|
|
|
def met_location(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
st = self.structure
|
|
|
|
met_loc_id = st.pt_met_location_id or st.dp_met_location_id
|
|
|
|
if met_loc_id:
|
2012-10-13 20:01:49 +00:00
|
|
|
try:
|
|
|
|
return self.session.query(tables.LocationGameIndex) \
|
|
|
|
.filter_by(generation_id=4,
|
|
|
|
game_index=met_loc_id).one().location
|
|
|
|
except sqlalchemy.orm.exc.NoResultFound:
|
|
|
|
return None
|
2012-10-13 14:16:02 +00:00
|
|
|
else:
|
|
|
|
return None
|
2010-08-20 03:48:03 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def level(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
return self.experience_rung.level
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def experience_rung(self):
|
|
|
|
growth_rate = self.species.growth_rate
|
2012-10-14 14:16:02 +00:00
|
|
|
return (self.session.query(tables.Experience)
|
2012-10-13 14:16:02 +00:00
|
|
|
.filter(tables.Experience.growth_rate == growth_rate)
|
|
|
|
.filter(tables.Experience.experience <= self.exp)
|
|
|
|
.order_by(tables.Experience.level.desc())
|
|
|
|
[0])
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def next_experience_rung(self):
|
|
|
|
level = self.level
|
2012-10-14 14:16:02 +00:00
|
|
|
growth_rate = self.species.growth_rate
|
2012-10-13 14:16:02 +00:00
|
|
|
if level < 100:
|
2012-10-14 14:16:02 +00:00
|
|
|
return (self.session.query(tables.Experience)
|
2012-10-13 14:16:02 +00:00
|
|
|
.filter(tables.Experience.growth_rate == growth_rate)
|
|
|
|
.filter(tables.Experience.level == level + 1)
|
|
|
|
.one())
|
|
|
|
else:
|
|
|
|
return None
|
2010-08-20 03:48:03 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def exp_to_next(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
if self.next_experience_rung:
|
|
|
|
return self.next_experience_rung.experience - self.exp
|
2010-08-20 03:48:03 +00:00
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def progress_to_next(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
if self.next_experience_rung:
|
|
|
|
rung = self.experience_rung
|
|
|
|
return (1.0 *
|
|
|
|
(self.exp - rung.experience) /
|
|
|
|
(self.next_experience_rung.experience - rung.experience))
|
2010-08-20 03:48:03 +00:00
|
|
|
else:
|
|
|
|
return 0.0
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@cached_property
|
2010-08-20 03:48:03 +00:00
|
|
|
def ability(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
return self.session.query(tables.Ability).get(self.structure.ability_id)
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@ability.setter
|
|
|
|
def ability(self, ability):
|
|
|
|
self.structure.ability_id = ability.id
|
|
|
|
|
|
|
|
@cached_property
|
2010-08-20 03:48:03 +00:00
|
|
|
def held_item(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
held_item_id = self.structure.held_item_id
|
|
|
|
if held_item_id:
|
2012-10-13 20:01:49 +00:00
|
|
|
return self.session.query(tables.ItemGameIndex) \
|
2012-10-13 14:16:02 +00:00
|
|
|
.filter_by(generation_id=self.generation_id,
|
|
|
|
game_index=held_item_id) \
|
|
|
|
.one().item
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def moves(self):
|
|
|
|
move_ids = (
|
|
|
|
self.structure.move1_id,
|
|
|
|
self.structure.move2_id,
|
|
|
|
self.structure.move3_id,
|
|
|
|
self.structure.move4_id,
|
|
|
|
)
|
|
|
|
move_rows = (self.session.query(tables.Move)
|
|
|
|
.filter(tables.Move.id.in_(move_ids)))
|
|
|
|
moves_dict = dict((move.id, move) for move in move_rows)
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 23:56:38 +00:00
|
|
|
result = tuple(
|
2012-10-14 14:16:02 +00:00
|
|
|
[moves_dict.get(move_id, None) for move_id in move_ids if move_id])
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
return result
|
2010-08-20 03:48:03 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@moves.complete_setter
|
|
|
|
def moves(self, new_moves):
|
2012-10-14 14:16:02 +00:00
|
|
|
(
|
|
|
|
self.structure.move1_id,
|
|
|
|
self.structure.move2_id,
|
|
|
|
self.structure.move3_id,
|
|
|
|
self.structure.move4_id,
|
|
|
|
) = ([m.id for m in new_moves] + [0, 0, 0, 0])[:4]
|
2012-10-13 23:56:38 +00:00
|
|
|
del self.moves
|
2012-10-13 14:16:02 +00:00
|
|
|
|
|
|
|
@cached_property
|
2010-08-20 03:48:03 +00:00
|
|
|
def move_pp(self):
|
|
|
|
return (
|
|
|
|
self.structure.move1_pp,
|
|
|
|
self.structure.move2_pp,
|
|
|
|
self.structure.move3_pp,
|
|
|
|
self.structure.move4_pp,
|
|
|
|
)
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@move_pp.complete_setter
|
|
|
|
def move_pp(self, new_pps):
|
2012-10-14 14:16:02 +00:00
|
|
|
(
|
|
|
|
self.structure.move1_pp,
|
|
|
|
self.structure.move2_pp,
|
|
|
|
self.structure.move3_pp,
|
|
|
|
self.structure.move4_pp,
|
|
|
|
) = (list(new_pps) + [0, 0, 0, 0])[:4]
|
|
|
|
del self.move_pp
|
2012-10-13 14:16:02 +00:00
|
|
|
|
|
|
|
original_trainer_id = struct_proxy('original_trainer_id')
|
|
|
|
original_trainer_secret_id = struct_proxy('original_trainer_secret_id')
|
|
|
|
original_trainer_name = struct_proxy('original_trainer_name')
|
|
|
|
exp = struct_proxy('exp',
|
|
|
|
dependent=['experience_rung', 'next_experience_rung'])
|
|
|
|
happiness = struct_proxy('happiness')
|
|
|
|
original_country = struct_proxy('original_country')
|
|
|
|
is_nicknamed = struct_proxy('is_nicknamed')
|
|
|
|
is_egg = struct_proxy('is_egg')
|
|
|
|
fateful_encounter = struct_proxy('fateful_encounter')
|
|
|
|
gender = struct_proxy('gender')
|
|
|
|
original_version = struct_proxy('original_version')
|
|
|
|
date_egg_received = struct_proxy('date_egg_received')
|
|
|
|
date_met = struct_proxy('date_met')
|
|
|
|
pokerus = struct_proxy('pokerus')
|
|
|
|
met_at_level = struct_proxy('met_at_level')
|
|
|
|
original_trainer_gender = struct_proxy('original_trainer_gender')
|
|
|
|
encounter_type = struct_proxy('encounter_type')
|
2012-10-13 20:01:49 +00:00
|
|
|
personality = struct_proxy('personality')
|
2012-10-13 14:16:02 +00:00
|
|
|
|
|
|
|
markings = struct_frozenset_proxy('markings')
|
|
|
|
sinnoh_ribbons = struct_frozenset_proxy('sinnoh_ribbons')
|
|
|
|
hoenn_ribbons = struct_frozenset_proxy('hoenn_ribbons')
|
|
|
|
sinnoh_contest_ribbons = struct_frozenset_proxy('sinnoh_contest_ribbons')
|
2012-06-24 21:54:00 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def nickname(self):
|
|
|
|
return self.structure.nickname
|
|
|
|
|
|
|
|
@nickname.setter
|
|
|
|
def nickname(self, value):
|
|
|
|
self.structure.nickname = value
|
2012-10-13 14:16:02 +00:00
|
|
|
self.is_nicknamed = True
|
|
|
|
del self.blob
|
2012-06-24 21:54:00 +00:00
|
|
|
|
|
|
|
@nickname.deleter
|
|
|
|
def nickname(self, value):
|
|
|
|
self.structure.nickname = ''
|
2012-10-13 14:16:02 +00:00
|
|
|
self.is_nicknamed = False
|
|
|
|
del self.blob
|
2010-07-08 05:29:30 +00:00
|
|
|
|
2010-04-17 05:45:44 +00:00
|
|
|
### Utility methods
|
|
|
|
|
2010-04-25 07:14:29 +00:00
|
|
|
shuffle_orders = list( permutations(range(4)) )
|
2010-04-17 05:45:44 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def shuffle_chunks(cls, words, reverse=False):
|
|
|
|
"""The main 128 encrypted bytes (or 64 words) in a save block are split
|
|
|
|
into four chunks and shuffled around in some order, based on
|
|
|
|
personality. The actual order of shuffling is a permutation of four
|
|
|
|
items in order, indexed by the shuffle index. That is, 0 yields 0123,
|
|
|
|
1 yields 0132, 2 yields 0213, etc.
|
|
|
|
|
|
|
|
Given a list of words (the first of which should be the pid), this
|
|
|
|
function returns the words in shuffled order. Pass reverse=True to
|
|
|
|
unshuffle instead.
|
|
|
|
"""
|
|
|
|
|
|
|
|
pid = words[0]
|
|
|
|
shuffle_index = (pid >> 0xD & 0x1F) % 24
|
|
|
|
|
|
|
|
shuffle_order = cls.shuffle_orders[shuffle_index]
|
|
|
|
if reverse:
|
|
|
|
# Decoding requires going the other way; invert the order
|
|
|
|
shuffle_order = [shuffle_order.index(i) for i in range(4)]
|
|
|
|
|
|
|
|
shuffled = words[:3] # skip the unencrypted stuff
|
|
|
|
for chunk in shuffle_order:
|
|
|
|
shuffled += words[ chunk * 16 + 3 : chunk * 16 + 19 ]
|
|
|
|
shuffled += words[67:] # extra bytes are also left alone
|
|
|
|
|
|
|
|
return shuffled
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def reciprocal_crypt(cls, words):
|
|
|
|
u"""Applies the reciprocal Pokémon save file cipher to the provided
|
|
|
|
list of words.
|
|
|
|
|
|
|
|
Returns nothing; the list is changed in-place.
|
|
|
|
"""
|
|
|
|
# Apply regular Pokémon "encryption": xor everything with the output of
|
|
|
|
# the PRNG. First three items are pid/unused/checksum and are not
|
|
|
|
# encrypted.
|
|
|
|
|
|
|
|
# Main data is encrypted using the checksum as a seed
|
|
|
|
prng = pokemon_prng(words[2])
|
|
|
|
for i in range(3, 67):
|
|
|
|
words[i] ^= next(prng)
|
|
|
|
|
|
|
|
if len(words) > 67:
|
|
|
|
# Extra bytes are encrypted using the pid as a seed
|
|
|
|
prng = pokemon_prng(words[0])
|
|
|
|
for i in range(67, len(words)):
|
|
|
|
words[i] ^= next(prng)
|
|
|
|
|
|
|
|
return
|
2012-06-10 21:01:15 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@cached_property
|
|
|
|
def blob(self):
|
|
|
|
blob = self.pokemon_struct.build(self.structure)
|
|
|
|
self.structure = self.pokemon_struct.parse(blob)
|
|
|
|
checksum = sum(struct.unpack('H' * 0x40, blob[8:0x88])) & 0xffff
|
2012-06-24 21:54:00 +00:00
|
|
|
self.structure.checksum = checksum
|
2012-10-13 14:16:02 +00:00
|
|
|
blob = blob[:6] + struct.pack('H', checksum) + blob[8:]
|
|
|
|
return blob
|
|
|
|
|
|
|
|
@blob.setter
|
|
|
|
def blob(self, blob):
|
|
|
|
self.structure = self.pokemon_struct.parse(blob)
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
|
|
|
|
class SaveFilePokemonGen4(SaveFilePokemon):
|
|
|
|
generation_id = 4
|
|
|
|
pokemon_struct = make_pokemon_struct(generation=generation_id)
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def export_dict(self):
|
2012-10-14 00:14:47 +00:00
|
|
|
result = super(SaveFilePokemonGen4, self).export_dict()
|
2012-06-24 21:54:00 +00:00
|
|
|
if any(self.shiny_leaves):
|
|
|
|
result['shiny leaves'] = self.shiny_leaves
|
|
|
|
return result
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def update(self, dct, **kwargs):
|
|
|
|
dct.update(kwargs)
|
|
|
|
if 'shiny leaves' in dct:
|
|
|
|
self.shiny_leaves = dct['shiny leaves']
|
|
|
|
super(SaveFilePokemonGen4, self).update(dct)
|
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
@property
|
|
|
|
def shiny_leaves(self):
|
|
|
|
return (
|
|
|
|
self.structure.shining_leaves.leaf1,
|
|
|
|
self.structure.shining_leaves.leaf2,
|
|
|
|
self.structure.shining_leaves.leaf3,
|
|
|
|
self.structure.shining_leaves.leaf4,
|
|
|
|
self.structure.shining_leaves.leaf5,
|
2012-06-24 21:54:00 +00:00
|
|
|
self.structure.shining_leaves.crown,
|
2012-06-10 21:01:15 +00:00
|
|
|
)
|
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
@shiny_leaves.setter
|
|
|
|
def shiny_leaves(self, new_values):
|
|
|
|
(
|
|
|
|
self.structure.shining_leaves.leaf1,
|
|
|
|
self.structure.shining_leaves.leaf2,
|
|
|
|
self.structure.shining_leaves.leaf3,
|
|
|
|
self.structure.shining_leaves.leaf4,
|
|
|
|
self.structure.shining_leaves.leaf5,
|
|
|
|
self.structure.shining_leaves.crown,
|
|
|
|
) = new_values
|
2012-10-13 14:16:02 +00:00
|
|
|
del self.blob
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
|
|
|
|
class SaveFilePokemonGen5(SaveFilePokemon):
|
|
|
|
generation_id = 5
|
|
|
|
pokemon_struct = make_pokemon_struct(generation=generation_id)
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def export_dict(self):
|
|
|
|
result = super(SaveFilePokemonGen5, self).export_dict()
|
|
|
|
if self.nature:
|
|
|
|
result['nature'] = dict(
|
|
|
|
id=self.structure.nature_id, name=self.nature.name)
|
2012-10-14 14:16:02 +00:00
|
|
|
ability_is_hidden = (self.ability == self.pokemon.dream_ability)
|
|
|
|
if (ability_is_hidden != bool(self.hidden_ability) or
|
|
|
|
self.pokemon.dream_ability in self.pokemon.abilities):
|
|
|
|
result['has hidden ability'] = self.hidden_ability
|
2012-06-24 21:54:00 +00:00
|
|
|
return result
|
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
def update(self, dct, **kwargs):
|
|
|
|
dct.update(kwargs)
|
2012-10-13 20:01:49 +00:00
|
|
|
super(SaveFilePokemonGen5, self).update(dct)
|
2012-10-13 14:16:02 +00:00
|
|
|
if 'nature' in dct:
|
|
|
|
self.structure.nature_id = dct['nature']['id']
|
2012-10-14 14:16:02 +00:00
|
|
|
if any(x in dct for x in
|
|
|
|
('has hidden ability', 'species', 'form', 'ability')):
|
|
|
|
if 'has hidden ability' in dct:
|
|
|
|
self.hidden_ability = dct['has hidden ability']
|
|
|
|
else:
|
|
|
|
self.hidden_ability = (
|
|
|
|
self.ability == self.pokemon.dream_ability and
|
|
|
|
self.ability not in self.pokemon.abilities)
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-10-13 14:16:02 +00:00
|
|
|
@cached_property
|
2012-06-10 21:01:15 +00:00
|
|
|
def nature(self):
|
2012-10-13 14:16:02 +00:00
|
|
|
st = self.structure
|
|
|
|
if st.nature_id:
|
|
|
|
return (self.session.query(tables.Nature)
|
|
|
|
.filter_by(game_index = st.nature_id).one())
|
|
|
|
else:
|
|
|
|
return None
|
2012-06-10 21:01:15 +00:00
|
|
|
|
2012-06-24 21:54:00 +00:00
|
|
|
@nature.setter
|
|
|
|
def nature(self, new_nature):
|
|
|
|
self.structure.nature_id = int(new_nature.game_index)
|
2012-10-13 23:56:38 +00:00
|
|
|
del self.blob
|
2012-06-24 21:54:00 +00:00
|
|
|
|
2012-10-13 20:01:49 +00:00
|
|
|
hidden_ability = struct_proxy('hidden_ability')
|
|
|
|
|
2012-06-10 21:01:15 +00:00
|
|
|
|
|
|
|
save_file_pokemon_classes = {
|
|
|
|
4: SaveFilePokemonGen4,
|
|
|
|
5: SaveFilePokemonGen5,
|
|
|
|
}
|