veekun_pokedex/pokedex/struct/__init__.py

959 lines
33 KiB
Python
Raw Normal View History

# 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
import base64
import datetime
import contextlib
2012-10-13 23:00:56 +00:00
from operator import attrgetter
2012-10-13 20:01:49 +00:00
import sqlalchemy.orm.exc
from pokedex.db import tables, util
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)
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
def struct_proxy(name, dependent=[]):
def getter(self):
return getattr(self.structure, name)
def setter(self, value):
setattr(self.structure, name, value)
for dep in dependent:
delattr(self, dep)
del self.blob
return property(getter, setter)
def struct_frozenset_proxy(name):
def getter(self):
bitstruct = getattr(self.structure, name)
return frozenset(k for k, v in bitstruct.items() if v)
def setter(self, new_set):
new_set = set(new_set)
2012-10-13 20:01:49 +00:00
struct = getattr(self.structure, name)
for key in struct:
setattr(struct, key, key in new_set)
new_set.discard(key)
if new_set:
raise ValueError('Unknown values: {0}'.format(', '.join(ribbons)))
del self.blob
return property(getter, setter)
class cached_property(object):
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
"""
self._setter = func
self.cache_setter_value = True
return self
def complete_setter(self, func):
"""Setter without automatic caching of the set value
"""
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
class InstrumentedList(object):
def __init__(self, callback, initial=()):
self.list = list(initial)
self.callback = callback
def __getitem__(self, index):
return self.list[index]
def __setitem__(self, index, value):
self.list[index] = value
self.callback()
def __delitem__(self, index, value):
self.list[index] = value
self.callback()
def append(self, item):
self.list.append(item)
self.callback()
def extend(self, extralist):
self.list.extend(extralist)
self.callback()
def __iter__(self):
return iter(self.list)
class SaveFilePokemon(object):
u"""Base class for an individual Pokémon, from the game's point of view.
Handles translating between the on-disk encrypted form, the in-RAM blob
(also used by pokesav), and something vaguely intelligible.
"""
Stat = namedtuple('Stat', ['stat', 'base', 'gene', 'exp', 'calc'])
def __init__(self, blob=None, dict_=None, encrypted=False, session=None):
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.
`session` is an optional database session. Either give it or fill it
later with `use_database_session`
"""
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)
else:
# Already decrypted
self.blob = blob
else:
self.blob = '\0' * (32 * 4 + 8)
if session:
self.session = session
else:
self.session = None
if dict_:
self.update(dict_)
@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)
def export_dict(self):
"""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
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:
result['form'] = dict(id=st.form_id, name=self.form.form_name)
save_object(result, 'ability', id=st.ability_id)
save_object(result, 'held item', id=st.held_item_id)
save_object(result, 'pokeball', id=st.dppt_pokeball or st.hgss_pokeball)
trainer = dict(
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)
if (trainer['id'] or trainer['secret'] or
trainer['name'].strip('\0') or trainer['gender'] != 'male'):
result['oiginal trainer'] = trainer
2012-10-13 23:00:56 +00:00
save(result, 'exp')
save(result, 'happiness')
save(result, 'markings', transform=sorted)
save(result, 'original country')
save(result, 'original version')
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, 'met at level')
save(result, 'nicknamed', self.is_nicknamed)
save(result, 'is egg')
save(result, 'fateful encounter')
save(result, 'personality')
save(result, 'gender', condition=lambda g: g != 'genderless')
save(result, 'has hidden ability', self.hidden_ability)
save(result, 'ribbons',
sorted(r.replace('_', ' ') for r in self.ribbons))
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
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
moves.append(move)
effort = {}
genes = {}
contest_stats = {}
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]
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:00:56 +00:00
save(result, 'effort', effort, condition=any)
save(result, 'genes', genes, condition=any)
save(result, 'contest stats', contest_stats, condition=any)
return result
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
if 'form' in dct:
st.alternate_form = dct['form']
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
if 'pokeball' in dct:
2012-10-13 20:01:49 +00:00
self.pokeball = self._get_pokeball(dct['pokeball']['id'])
del self.pokeball
def _load_values(source, **values):
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]))
if 'oiginal trainer' in dct:
2012-10-13 20:01:49 +00:00
trainer = dct['oiginal trainer']
_load_values(trainer,
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
_load_values(dct,
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',
hidden_ability='has hidden ability',
)
load_name('nickname', dct, 'nickname', 'nickname trash')
self.is_nicknamed = was_nicknamed
_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))
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()
if 'ribbons' in dct:
self.ribbons = (r.replace(' ', '_') for r in dct['ribbons'])
if 'moves' in dct:
pp_reset_indices = []
for i, movedict in enumerate(dct['moves']):
2012-10-13 20:01:49 +00:00
st['move{0}_id'.format(i + 1)] = movedict['id']
if 'pp' in movedict:
2012-10-13 20:01:49 +00:00
st['move{0}_pp'.format(i + 1)] = movedict['pp']
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']
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
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'),
('contest stats', 'contest')):
for name, value in dct.get(key, {}).items():
st['{}_{}'.format(prefix, name.replace(' ', '_'))] = value
return self
### 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
def use_database_session(self, session):
"""Remembers the given database session, and prefetches a bunch of
database stuff. Gotta call this (or give session to `__init__`) before
you use the database properties like `species`, etc.
"""
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]
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):
st = self.structure
forms = pokemon_forms.get(st.national_id)
if forms:
return forms[st.alternate_form_id]
else:
return None
@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)
else:
st.alternate_form_id = 0
del self.form
@property
def species(self):
if self.form:
return self.form.species
else:
return None
@species.setter
def species(self, species):
self.form = species.default_form
@property
def pokemon(self):
if self.form:
return self.form.pokemon
else:
return None
@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
else:
return None
@form.setter
def form(self, form):
self.structure.national_id = form.species.id
self.structure.alternate_form = form.form_identifier
del self.species
del self.pokemon
self._reset()
2012-10-13 20:01:49 +00:00
@cached_property
def pokeball(self):
st = self.structure
if st.hgss_pokeball >= 17:
pokeball_id = st.hgss_pokeball - 17 + 492
elif st.dppt_pokeball:
pokeball_id = st.dppt_pokeball
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:
st.hgss_pokeball = pokeball_id - boundary
else:
st.dppt_pokeball = pokeball_id
@cached_property
def egg_location(self):
st = self.structure
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
else:
return None
@cached_property
def met_location(self):
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
else:
return None
@property
def level(self):
return self.experience_rung.level
@cached_property
def experience_rung(self):
growth_rate = self.species.growth_rate
return (session.query(tables.Experience)
.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
if level < 100:
return (session.query(tables.Experience)
.filter(tables.Experience.growth_rate == growth_rate)
.filter(tables.Experience.level == level + 1)
.one())
else:
return None
@property
def exp_to_next(self):
if self.next_experience_rung:
return self.next_experience_rung.experience - self.exp
else:
return 0
@property
def progress_to_next(self):
if self.next_experience_rung:
rung = self.experience_rung
return (1.0 *
(self.exp - rung.experience) /
(self.next_experience_rung.experience - rung.experience))
else:
return 0.0
@cached_property
def ability(self):
return self.session.query(tables.Ability).get(self.structure.ability_id)
@ability.setter
def ability(self, ability):
self.structure.ability_id = ability.id
@cached_property
def held_item(self):
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) \
.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)
def callback():
def get(index):
try:
return result[x].id
except AttributeError:
return 0
self.structure.move1_id = get(0)
self.structure.move2_id = get(1)
self.structure.move3_id = get(2)
self.structure.move4_id = get(3)
self._reset()
result = InstrumentedList(
callback,
[moves_dict.get(move_id, None) for move_id in move_ids])
return result
@moves.complete_setter
def moves(self, new_moves):
self.moves[:] = new_moves
@cached_property
def move_pp(self):
return (
self.structure.move1_pp,
self.structure.move2_pp,
self.structure.move3_pp,
self.structure.move4_pp,
)
@move_pp.complete_setter
def move_pp(self, new_pps):
self.move_pp[:] = new_pps
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')
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')
@property
def ribbons(self):
return frozenset(
self.sinnoh_ribbons |
self.hoenn_ribbons |
self.sinnoh_contest_ribbons)
@ribbons.setter
def ribbons(self, ribbons):
ribbons = set(ribbons)
for ribbonset_name in (
'sinnoh_ribbons', 'hoenn_ribbons', 'sinnoh_contest_ribbons'):
ribbonset = self.structure[ribbonset_name]
for ribbon_name in ribbonset:
ribbonset[ribbon_name] = (ribbon_name in ribbons)
ribbons.discard(ribbon_name)
if ribbons:
raise ValueError('Unknown ribbons: {0}'.format(', '.join(ribbons)))
@property
def nickname(self):
return self.structure.nickname
@nickname.setter
def nickname(self, value):
self.structure.nickname = value
self.is_nicknamed = True
del self.blob
@nickname.deleter
def nickname(self, value):
self.structure.nickname = ''
self.is_nicknamed = False
del self.blob
### Utility methods
2010-04-25 07:14:29 +00:00
shuffle_orders = list( permutations(range(4)) )
@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
@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
self.structure.checksum = checksum
blob = blob[:6] + struct.pack('H', checksum) + blob[8:]
return blob
@blob.setter
def blob(self, blob):
self.structure = self.pokemon_struct.parse(blob)
class SaveFilePokemonGen4(SaveFilePokemon):
generation_id = 4
pokemon_struct = make_pokemon_struct(generation=generation_id)
def export_dict(self):
result = super(SaveFilePokemonGen5, self).export_dict()
if any(self.shiny_leaves):
result['shiny leaves'] = self.shiny_leaves
return result
def update(self, dct, **kwargs):
dct.update(kwargs)
if 'shiny leaves' in dct:
self.shiny_leaves = dct['shiny leaves']
super(SaveFilePokemonGen4, self).update(dct)
@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,
self.structure.shining_leaves.crown,
)
@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
del self.blob
class SaveFilePokemonGen5(SaveFilePokemon):
generation_id = 5
pokemon_struct = make_pokemon_struct(generation=generation_id)
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)
return result
def update(self, dct, **kwargs):
dct.update(kwargs)
2012-10-13 20:01:49 +00:00
super(SaveFilePokemonGen5, self).update(dct)
if 'nature' in dct:
self.structure.nature_id = dct['nature']['id']
2012-10-13 20:01:49 +00:00
if 'has hidden ability' not in dct:
2012-10-13 23:00:56 +00:00
self.hidden_ability = (self.ability == self.pokemon.dream_ability
and self.ability not in self.pokemon.abilities)
@cached_property
def nature(self):
st = self.structure
if st.nature_id:
return (self.session.query(tables.Nature)
.filter_by(game_index = st.nature_id).one())
else:
return None
@nature.setter
def nature(self, new_nature):
self.structure.nature_id = int(new_nature.game_index)
2012-10-13 20:01:49 +00:00
hidden_ability = struct_proxy('hidden_ability')
save_file_pokemon_classes = {
4: SaveFilePokemonGen4,
5: SaveFilePokemonGen5,
}