Commits

ceol  committed d8e828f Merge

Merge branch 'development'

  • Participants
  • Parent commits f7dbd7c, 896dc5b

Comments (0)

Files changed (14)

 editing. A proper API reference will be made available in time. Until then,
 refer to the Struct declaration in `pypkm.structs`.
 
+If you have GTS data, you can edit that as well:
+
+>>> gts_data = open('/path/to/gts_pkm.pkm.proper', 'r').open()
+>>> proper_pkm = pypkm.load(gen=4, data=gts_data)
+
+>>> proper_pkm.level
+5
+
+>>> proper_pkm.ot_name
+u'Trainer'
+
+PyPKM also handles converting data. You can convert PC box data to party data,
+to/from GTS server-sent data, and to/from GTS client-sent data. The list of
+conversion methods are as follows:
+
+* Gen4Pkm.togen5() - converts Gen 4 data to Gen 5
+* .topkm() - converts GTS data to a Pkm object
+* .toparty() - adds battle data
+* .togtsserver() - converts to data sent by the GTS server
+* .togtsclient() - converts to data sent by the DS
+
 If you've edited the data, you probably want to save. PyPKM does not handle
 saving data; you must save the file yourself. However, to convert an object
 into a string of byte data:
 
 ## Thanks
 
-Many thanks to the folks at [Project Pokemon][7] for all of their research
+The folks at [Project Pokemon][7] for all of their research
 into the structure of Pokémon data.
 
-A big thanks to [Stephen Anthony Uy][8] for his pycrypto module. Somehow I
-came across this module whilst looking for a way to encrypt and decrypt
-Pokémon data, and it's been a huge help (the `shuffle()` function comes
-directly from his work).
+[maxg][8] for answering my questions.
+
+[Veekun][9] for looking over my code, introducing me to [Construct][5], and
+providing an awesome database of Pokémon information.
+
+[Stephen Anthony Uy][10] for his pycrypto module. Somehow I came across this
+module whilst looking for a way to encrypt and decrypt Pokémon data, and
+it's been a huge help (the `shuffle()` function comes directly from his work).
 
 [7]: http://projectpokemon.org/
-[8]: mailto:tsanth@iname.com
+[8]: http://www.pokecheck.org/
+[9]: http://veekun.com/
+[10]: mailto:tsanth@iname.com

File pypkm/__init__.py

 """
 
 __author__ = 'Patrick Jacobs <ceolwulf@gmail.com>'
-__version__ = '0.4'
+__version__ = '0.5'
 
-from pypkm.pkm import Gen4Pkm, Gen5Pkm
+from pypkm.pkm import get_pkmobj
 
 def load(gen, data):
     """Load PKM data.
     gen (int) -- the file's game generation
     data (str) -- the file's binary data
     """
-    if gen == 5:
-        return Gen5Pkm(data)
-    elif gen == 4:
-        return Gen4Pkm(data)
+    return get_pkmobj(gen, data)
 
 def new(gen):
     """Create a new PKM file.
     Keyword arguments:
     gen (int) -- the file's game generation
     """
-    if gen == 5:
-        return Gen5Pkm()
-    elif gen == 4:
-        return Gen4Pkm()
+    return get_pkmobj(gen, '\x00' * 136)

File pypkm/adapters/gen1.py

Empty file removed.

File pypkm/adapters/gen2.py

Empty file removed.

File pypkm/adapters/gen3.py

Empty file removed.

File pypkm/crypto.py

 
 import struct
 from array import array
-from pypkm.rng import Prng, Grng
+from pypkm.rng import Prng, Arng, Grng
 
-def _checksum(data, size='H'):
+def checksum(data, size='H'):
     """Calculate the checksum of data using size as word-length.
     
     This defaults to 'H' (a two-byte word) because it's what the
     ]
     return ''.join(chunks)
 
-def _crypt(seed, data):
+def _crypt(seed, data, obj=Prng):
     """Encrypts/decrypts data with the given seed.
 
     Keyword arguments:
     """
 
     data = array('H', data)
-    lc = Prng(seed)
+    lc = obj(seed)
 
     new_data = array('H')
     for word in data:
     chksum = _checksum(box_data)
     box_data = _crypt(chksum, box_data)
 
-    party_data = _shuffle(pv, party_data)
     party_data = _crypt(pv, party_data)
 
     return _pack(pv, chksum, box_data, party_data)
     box_data = _unshuffle(pv, box_data)
 
     party_data = _crypt(pv, party_data)
-    party_data = _unshuffle(pv, party_data)
 
     return _pack(pv, chksum, box_data, party_data)
 
     Keyword arguments:
     data (string) -- the Pokémon data to encrypt
     """
-    pass
+    (pv, chksum, box_data, party_data) = _unpack(data)
+
+    box_data = _shuffle(pv, box_data)
+    chksum = _checksum(box_data)
+    box_data = _crypt(chksum, box_data, obj=Grng)
+
+    party_data = _crypt(pv, party_data, obj=Grng)
+
+    return _pack(pv, chksum, box_data, party_data)
 
 def decrypt_gts(data):
     """Decrypt PKM bin sent over the GTS.
     Keyword arguments:
     data (string) -- the Pokémon binary to decrypt
     """
-    pass
+    (pv, chksum, box_data, party_data) = _unpack(data)
+    
+    box_data = _crypt(chksum, box_data, obj=Grng)
+    box_data = _unshuffle(pv, box_data)
+
+    party_data = _crypt(pv, party_data, obj=Grng)
+
+    return _pack(pv, chksum, box_data, party_data)

File pypkm/pkm.py

 
 __author__ = 'Patrick Jacobs <ceolwulf@gmail.com>'
 
+import datetime
+import struct
 from pypkm.structs import gen4, gen5
-from pypkm.crypto import encrypt, decrypt
+from pypkm.crypto import checksum, encrypt, decrypt
 from pypkm.sqlite import get_level, get_nature, get_basestats
-from pypkm.util import calcstat
+from pypkm.util import calcstat, LengthError
+        
 
-class BasePkm(object):
+class StructData(object):
+    """A wrapper class for the construct Container object."""
 
     # Constructor Struct object
     _strc = None
         self._ctnr = self._strc.parse(data)
     
     def tostring(self):
-        return self._strc.build(self._ctnr)
+        data = self._strc.build(self._ctnr)
+
+        return data
+
+class PkmData(StructData):
+    """A base class for Pokémon data."""
+    
+    def tostring(self):
+        data = super(PkmData, self).tostring()
+
+        # we need to recalculate the checksum when we build...
+        # since building removes any trash bytes in the nickname and
+        # ot name fields, if you're just reading the file, you should
+        # store the loaded data instead of using tostring()
+        chksum = checksum(data[0x08:])
+        packed = struct.pack('<H', chksum)
+        data = ''.join([
+            data[:0x06],
+            packed,
+            data[0x08:],
+        ])
 
-class Gen4Pkm(BasePkm):
+        return data
+
+class Gen4BoxPkm(PkmData):
 
     def __init__(self, data=None):
         if data is None:
             data = '\x00' * 136
+        elif len(data) != 136:
+            raise LengthError(136, len(data))
         
-        if len(data) == 136:
-            strc = gen4.pkm_struct
-        elif len(data) == 236:
-            strc = gen4.pkm_party_struct
-        else:
-            raise ValueError('Unsupported PKM file length: expected 136 or 236, received {}'.format(len(data)))
-        
-        self._load(strc, data)
+        self._load(gen4.pkm_struct, data)
     
     def toparty(self):
         # even if it's already a party file, we should process it
         data = self.tostring()[:136]
+        
         # create empty data to load into Struct
         data = ''.join([data, '\x00' * 100])
-        new_pkm = Gen4Pkm(data)
+        new_pkm = Gen4PartyPkm(data)
 
         new_pkm.level = get_level(pokemon_id=new_pkm.id, exp=new_pkm.exp)
 
                             level=new_pkm.level, nature_stat=nature[6])
         
         return new_pkm
+    
+    def togtsserver(self):
+        # have to do this in case our old data isn't party
+        obj = self
+        # check if it's a party file (this hopefully saves us from
+        # building data twice)
+        try:
+            obj.level
+        except AttributeError:
+            obj = self.toparty()
+        
+        # pkm data sent over the GTS is both party and encrypted
+        data = encrypt(obj.tostring())
 
-    def togts(self):
-        pass
+        # create empty data to load into Struct
+        gts = Gen4ServerPkm('\x00' * 292)
+
+        gts.encrypted_pkm = data
+
+        gts.id = obj.id
+        if obj.is_genderless:
+            gts.gender = 0x03
+        elif obj.is_female:
+            gts.gender = 0x02
+        else:
+            gts.gender = 0x01
+        gts.level = obj.level
 
+        gts.requested.id = 1 # bulbasaur
+        gts.requested.gender = 0x02 # female
+        gts.requested.min_level = 0 # any
+        gts.requested.max_level = 0 # any
+        
+        if obj.ot_is_female:
+            gts.ot_gender = 0x01
+
+        now = datetime.datetime.now()
+        gts.deposited_time.year = now.year
+        gts.deposited_time.month = now.month
+        gts.deposited_time.day = now.day
+        gts.deposited_time.hour = now.hour
+        gts.deposited_time.minute = now.minute
+        gts.deposited_time.second = now.second
+
+        gts.traded_time.year = now.year
+        gts.traded_time.month = now.month
+        gts.traded_time.day = now.day
+        gts.traded_time.hour = now.hour
+        gts.traded_time.minute = now.minute
+        gts.traded_time.second = now.second
+
+        gts.pv = obj.pv
+        gts.ot_name = obj.ot_name
+        gts.ot_id = obj.ot_id
+
+        gts.country = 0xDB # not sure, taken from ir-gts
+        gts.city = 0x02 # not sure, taken from ir-gts
+        
+        if gts.ot_gender == 0x01:
+            # if female, set to lass; if male, leave at 0 for youngster
+            gts.ot_sprite = 0x08
+        
+        gts.is_exchanged = True
+        gts.version = 0x08 # soulsilver version
+        gts.language = obj.language
+
+        return gts
+    
+    def togtsclient(self):
+        # have to do this in case our old data isn't party
+        obj = self
+        # check if it's a party file (this hopefully saves us from
+        # building data twice)
+        try:
+            obj.level
+        except AttributeError:
+            obj = self.toparty()
+        
+        # pkm data sent over the GTS is both party and encrypted
+        data = encrypt(obj.tostring())
+
+        # create empty data to load into Struct
+        gts = Gen4ClientPkm('\x00' * 296)
+
+        gts.encrypted_pkm = data
+
+        gts.id = obj.id
+        if obj.is_genderless:
+            gts.gender = 0x03
+        elif obj.is_female:
+            gts.gender = 0x02
+        else:
+            gts.gender = 0x01
+        gts.level = obj.level
+
+        gts.requested.id = 1 # bulbasaur
+        gts.requested.gender = 0x02 # female
+        gts.requested.min_level = 0 # any
+        gts.requested.max_level = 0 # any
+        
+        if obj.ot_is_female:
+            gts.ot_gender = 0x01
+
+        now = datetime.datetime.now()
+        gts.deposited_time.year = now.year
+        gts.deposited_time.month = now.month
+        gts.deposited_time.day = now.day
+        gts.deposited_time.hour = now.hour
+        gts.deposited_time.minute = now.minute
+        gts.deposited_time.second = now.second
+
+        gts.traded_time.year = now.year
+        gts.traded_time.month = now.month
+        gts.traded_time.day = now.day
+        gts.traded_time.hour = now.hour
+        gts.traded_time.minute = now.minute
+        gts.traded_time.second = now.second
+
+        gts.pv = obj.pv
+        gts.ot_name = obj.ot_name
+        gts.ot_id = obj.ot_id
+
+        gts.country = 0xDB # not sure, taken from ir-gts
+        gts.city = 0x02 # not sure, taken from ir-gts
+        
+        if gts.ot_gender == 0x01:
+            # if female, set to lass; if male, leave at 0 for youngster
+            gts.ot_sprite = 0x08
+        
+        gts.is_exchanged = True
+        gts.version = 0x08 # soulsilver version
+        gts.language = obj.language
+
+        return gts
+    
     def togen5(self):
         data = self.tostring()
         if len(data) == 236:
             # need to for pc files because they're the same size
             # between gens
             data = data[:220]
-        new_pkm = Gen5Pkm(data)
+            new_pkm = Gen5PartyPkm(data)
+        else:
+            new_pkm = Gen5BoxPkm(data)
 
         # nature gets its own byte in gen 5
         new_pkm.nature = new_pkm.pv % 25
 
         return new_pkm
 
-class Gen5Pkm(BasePkm):
+class Gen4PartyPkm(Gen4BoxPkm):
     
     def __init__(self, data=None):
         if data is None:
-            data = '\x00' * 136
+            data = '\x00' * 236
+        elif len(data) != 236:
+            raise LengthError(236, len(data))
         
-        if len(data) == 136:
-            strc = gen5.pkm_struct
-        elif len(data) == 220:
-            strc = gen5.pkm_party_struct
-        else:
-            raise ValueError('Unsupported PKM file length: expected 136 or 220, received {}'.format(len(data)))
+        self._load(gen4.pkm_party_struct, data)
+    
+class Gen5BoxPkm(PkmData):
+
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 136
+        elif len(data) != 136:
+            raise LengthError(136, len(data))
         
-        self._load(strc, data)
+        self._load(gen5.pkm_struct, data)
     
     def toparty(self):
         # even if it's already a party file, we should process it
         data = self.tostring()[:136]
+        
         # create empty data to load into Struct
-        data = ''.join([data, '\x00' * 84])
-        new_pkm = Gen5Pkm(data)
+        data = ''.join([data, '\x00' * 100])
+        new_pkm = Gen5PartyPkm(data)
 
         new_pkm.level = get_level(pokemon_id=new_pkm.id, exp=new_pkm.exp)
 
         
         return new_pkm
     
-    def togts(self):
-        pass
+    def togtsserver(self):
+        # have to do this in case our old data isn't party
+        obj = self
+        # check if it's a party file (this hopefully saves us from
+        # building data twice)
+        try:
+            obj.level
+        except AttributeError:
+            obj = self.toparty()
+        
+        # pkm data sent over the GTS is both party and encrypted
+        data = encrypt(obj.tostring())
+
+        # create empty data to load into Struct
+        gts = Gen5ServerPkm('\x00' * 296)
+
+        gts.encrypted_pkm = data
+
+        gts.id = obj.id
+        if obj.is_genderless:
+            gts.gender = 0x03
+        elif obj.is_female:
+            gts.gender = 0x02
+        else:
+            gts.gender = 0x01
+        gts.level = obj.level
+
+        gts.requested.id = 1 # bulbasaur
+        gts.requested.gender = 0x02 # female
+        gts.requested.min_level = 0 # any
+        gts.requested.max_level = 0 # any
+        
+        if obj.ot_is_female:
+            gts.ot_gender = 0x01
+
+        now = datetime.datetime.now()
+        gts.deposited_time.year = now.year
+        gts.deposited_time.month = now.month
+        gts.deposited_time.day = now.day
+        gts.deposited_time.hour = now.hour
+        gts.deposited_time.minute = now.minute
+        gts.deposited_time.second = now.second
+
+        gts.traded_time.year = now.year
+        gts.traded_time.month = now.month
+        gts.traded_time.day = now.day
+        gts.traded_time.hour = now.hour
+        gts.traded_time.minute = now.minute
+        gts.traded_time.second = now.second
+
+        gts.pv = obj.pv
+        gts.ot_id = obj.ot_id
+        gts.ot_secret_id = obj.ot_secret_id
+        gts.ot_name = obj.ot_name
+
+        gts.country = 0xDB # not sure, taken from ir-gts
+        gts.city = 0x02 # not sure, taken from ir-gts
+        
+        if gts.ot_gender == 0x01:
+            # if female, set to lass; if male, leave at 0 for youngster
+            gts.ot_sprite = 0x08
+        
+        gts.is_exchanged = True
+        gts.version = 0x14 # white version
+        gts.language = obj.language
+
+        return gts
+    
+    def togtsclient(self):
+        # have to do this in case our old data isn't party
+        obj = self
+        # check if it's a party file (this hopefully saves us from
+        # building data twice)
+        try:
+            obj.level
+        except AttributeError:
+            obj = self.toparty()
+        
+        # pkm data sent over the GTS is both party and encrypted
+        data = encrypt(obj.tostring())
+
+        # create empty data to load into Struct
+        gts = Gen5ClientPkm('\x00' * 444)
+
+        gts.encrypted_pkm = data
+
+        gts.pv = obj.pv
+        gts.length = 432
+
+        gts.id = obj.id
+        if obj.is_genderless:
+            gts.gender = 0x03
+        elif obj.is_female:
+            gts.gender = 0x02
+        else:
+            gts.gender = 0x01
+        gts.level = obj.level
+
+        gts.requested.id = 1 # bulbasaur
+        gts.requested.gender = 0x02 # female
+        gts.requested.min_level = 0 # any
+        gts.requested.max_level = 0 # any
+        
+        if obj.ot_is_female:
+            gts.ot_gender = 0x01
+        gts.ot_nature = 24 # quirky
+
+        now = datetime.datetime.now()
+        gts.deposited_time.year = now.year
+        gts.deposited_time.month = now.month
+        gts.deposited_time.day = now.day
+        gts.deposited_time.hour = now.hour
+        gts.deposited_time.minute = now.minute
+        gts.deposited_time.second = now.second
+
+        gts.traded_time.year = now.year
+        gts.traded_time.month = now.month
+        gts.traded_time.day = now.day
+        gts.traded_time.hour = now.hour
+        gts.traded_time.minute = now.minute
+        gts.traded_time.second = now.second
+
+        gts.ot_id = obj.ot_id
+        gts.ot_secret_id = obj.ot_secret_id
+        gts.ot_name = obj.ot_name
+
+        gts.country = 0xDB # not sure, taken from ir-gts
+        gts.city = 0x02 # not sure, taken from ir-gts
+        
+        if gts.ot_gender == 0x01:
+            # if female, set to lass; if male, leave at 0 for youngster
+            gts.ot_sprite = 0x08
+        
+        gts.is_exchanged = True
+        gts.version = 0x14 # white version
+        gts.language = obj.language
+
+        gts.unknown_0to8 = 8 # no idea!
+
+        gts.terminator = 128
+
+        return gts
+
+class Gen5PartyPkm(Gen5BoxPkm):
+    
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 220
+        elif len(data) != 220:
+            raise LengthError(220, len(data))
+        
+        self._load(gen5.pkm_party_struct, data)
+
+class GTSData(StructData):
+    """A base class for data sent to or from the GTS server."""
+    pass
+
+class Gen4ServerData(GTSData):
+    
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 292
+        elif len(data) != 292:
+            raise LengthError(292, len(data))
+        
+        self._load(gen4.pkm_gtsserver_struct, data)
+    
+    def topkm(self):
+        data = decrypt(self.encrypted_pkm)
+
+        return Gen4PartyPkm(data)
+
+class Gen4ClientData(GTSData):
+    
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 296
+        elif len(data) != 296:
+            raise LengthError(296, len(data))
+        
+        self._load(gen4.pkm_gtsclient_struct, data)
+    
+    def topkm(self):
+        data = decrypt(self.encrypted_pkm)
+
+        return Gen4PartyPkm(data)
+
+class Gen5ServerData(GTSData):
+    
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 296
+        elif len(data) != 296:
+            raise LengthError(296, len(data))
+        
+        self._load(gen5.pkm_gtsserver_struct, data)
+    
+    def topkm(self):
+        data = decrypt(self.encrypted_pkm)
+
+        return Gen5PartyPkm(data)
+
+class Gen5ClientData(GTSData):
+    
+    def __init__(self, data=None):
+        if data is None:
+            data = '\x00' * 444
+        elif len(data) != 444:
+            raise LengthError(444, len(data))
+        
+        self._load(gen5.pkm_gtsclient_struct, data)
+    
+    def topkm(self):
+        data = decrypt(self.encrypted_pkm)
+
+        return Gen5PartyPkm(data)
+
+def get_pkmobj(gen, data):
+    objs = {
+        4: {
+            136: Gen4BoxPkm,
+            236: Gen4PartyPkm,
+            292: Gen4ServerData,
+            296: Gen4ClientData,
+        },
+        5: {
+            136: Gen5BoxPkm,
+            220: Gen5PartyPkm,
+            296: Gen5ServerData,
+            444: Gen5ClientData,
+        }
+    }
+    
+    return objs.get(gen).get(len(data))(data)

File pypkm/sqlite.py

 
     if row is not None:
         return row[0]
+    
+    return ''
 
 def get_ord(chr_):
     """Retrieve an ordinal from the gen 4 character table.
 
     if row is not None:
         return row[0]
+    
+    return ''
 
 def get_growthrate(pokemon_id):
     """Retrieve the growth rate ID of a Pokémon by its Dex ID.

File pypkm/structs/gen1.py

Empty file removed.

File pypkm/structs/gen2.py

Empty file removed.

File pypkm/structs/gen3.py

Empty file removed.

File pypkm/structs/gen4.py

         ULInt8('tough'),
         ULInt8('sheen'),
     ),
-    BitStruct('sinnoh_ribbons_set11',
-        Flag('alert'),
-        Flag('world_ability'),
-        Flag('pair_ability'),
-        Flag('multi_ability'),
-        Flag('double_ability'),
-        Flag('great_ability'),
-        Flag('ability'),
-        Flag('sinnoh_champ'),
-    ),
-    BitStruct('sinnoh_ribbons_set12',
+    Swapped(BitStruct('sinnoh_ribbons_set1',
         Flag('royal'),
         Flag('gorgeous'),
         Flag('smile'),
         Flag('careless'),
         Flag('downcast'),
         Flag('shock'),
-    ),
-    BitStruct('sinnoh_ribbons_set21',
+        Flag('alert'),
+        Flag('world_ability'),
+        Flag('pair_ability'),
+        Flag('multi_ability'),
+        Flag('double_ability'),
+        Flag('great_ability'),
+        Flag('ability'),
+        Flag('sinnoh_champ'),
+    )),
+    Swapped(BitStruct('sinnoh_ribbons_set2',
+        Padding(4),
+        Flag('premiere'),
+        Flag('classic'),
+        Flag('carnival'),
+        Flag('festival'),
         Flag('blue'),
         Flag('green'),
         Flag('red'),
         Flag('record'),
         Flag('footprint'),
         Flag('gorgeous_royal'),
-    ),
-    BitStruct('sinnoh_ribbons_set22',
-        Padding(4),
-        Flag('premiere'),
-        Flag('classic'),
-        Flag('carnival'),
-        Flag('festival'),
-    ),
+    )),
 )
 
 _blockB = Struct('_blockB',
             BitField('hp', 5),
         ),
     ))),
-    BitStruct('hoenn_ribbons_set11',
-        Flag('beauty_master'),
-        Flag('beauty_hyper'),
-        Flag('beauty_super'),
-        Flag('beauty'),
-        Flag('cool_master'),
-        Flag('cool_hyper'),
-        Flag('cool_super'),
-        Flag('cool'),
-    ),
-    BitStruct('hoenn_ribbons_set12',
+    Swapped(BitStruct('hoenn_ribbons_set1',
         Flag('smart_master'),
         Flag('smart_hyper'),
         Flag('smart_super'),
         Flag('cute_hyper'),
         Flag('cute_super'),
         Flag('cute'),
-    ),
-    BitStruct('hoenn_ribbons_set21',
-        Flag('artist'),
-        Flag('victory'),
-        Flag('winning'),
-        Flag('champion'),
-        Flag('tough_master'),
-        Flag('tough_hyper'),
-        Flag('tough_super'),
-        Flag('tough'),
-    ),
-    BitStruct('hoenn_ribbons_set22',
+        Flag('beauty_master'),
+        Flag('beauty_hyper'),
+        Flag('beauty_super'),
+        Flag('beauty'),
+        Flag('cool_master'),
+        Flag('cool_hyper'),
+        Flag('cool_super'),
+        Flag('cool'),
+    )),
+    Swapped(BitStruct('hoenn_ribbons_set2',
         Flag('world'),
         Flag('earth'),
         Flag('national'),
         Flag('land'),
         Flag('marine'),
         Flag('effort'),
-    ),
+        Flag('artist'),
+        Flag('victory'),
+        Flag('winning'),
+        Flag('champion'),
+        Flag('tough_master'),
+        Flag('tough_hyper'),
+        Flag('tough_super'),
+        Flag('tough'),
+    )),
     Embed(BitStruct('x40',
         BitField('alt_form_unshifted', 5), # leftshift 3
         Flag('is_genderless'), # if both False, then it's male
     NicknameAdapter(StrictRepeater(11, ULInt16('nickname'))),
     Padding(1),
     ULInt8('hometown'),
-    BitStruct('sinnoh_ribbons_set31',
-        Flag('beauty_master'),
-        Flag('beauty_ultra'),
-        Flag('beauty_great'),
-        Flag('beauty'),
-        Flag('cool_master'),
-        Flag('cool_ultra'),
-        Flag('cool_great'),
-        Flag('cool'),
-    ),
-    BitStruct('sinnoh_ribbons_set32',
+    Swapped(BitStruct('sinnoh_ribbons_set3',
         Flag('smart_master'),
         Flag('smart_ultra'),
         Flag('smart_great'),
         Flag('cute_ultra'),
         Flag('cute_great'),
         Flag('cute'),
-    ),
-    BitStruct('sinnoh_ribbons_set41',
+        Flag('beauty_master'),
+        Flag('beauty_ultra'),
+        Flag('beauty_great'),
+        Flag('beauty'),
+        Flag('cool_master'),
+        Flag('cool_ultra'),
+        Flag('cool_great'),
+        Flag('cool'),
+    )),
+    BitStruct('sinnoh_ribbons_set4',
         Padding(4),
         Flag('tough_master'),
         Flag('tough_ultra'),
 pkm_party_struct = Struct('pkm_party_struct',
     Embed(pkm_struct),
     Embed(_blockE),
+)
+
+# Data sent from the GTS server to the client
+pkm_gtsserver_struct = Struct('pkm_gtsserver_struct',
+    Bytes('encrypted_pkm', 236),
+    ULInt16('id'),
+    ULInt8('gender'),
+    ULInt8('level'),
+    Struct('requested',
+        ULInt16('id'),
+        ULInt8('gender'),
+        ULInt8('min_level'),
+        ULInt8('max_level'),
+    ),
+    Padding(1),
+    ULInt8('ot_gender'),
+    Padding(1),
+    Struct('deposited_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    Struct('traded_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    ULInt32('pv'),
+    OTNameAdapter(StrictRepeater(8, ULInt16('ot_name'))),
+    ULInt16('ot_id'),
+    ULInt8('country'),
+    ULInt8('city'),
+    ULInt8('ot_sprite'),
+    Flag('is_exchanged'),
+    ULInt8('version'),
+    ULInt8('language'),
+)
+
+# Data sent from the GTS client to the server
+pkm_gtsclient_struct = Struct('pkm_gtsclient_struct',
+    ULInt32('pv'),
+    Bytes('encrypted_pkm', 236),
+    ULInt16('id'),
+    ULInt8('gender'),
+    ULInt8('level'),
+    Struct('requested',
+        ULInt16('id'),
+        ULInt8('gender'),
+        ULInt8('min_level'),
+        ULInt8('max_level'),
+    ),
+    Padding(1),
+    ULInt8('ot_gender'),
+    Padding(1),
+    Struct('deposited_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    Struct('traded_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    ULInt32('pv'),
+    OTNameAdapter(StrictRepeater(8, ULInt16('ot_name'))),
+    ULInt16('ot_id'),
+    ULInt8('country'),
+    ULInt8('city'),
+    ULInt8('ot_sprite'),
+    Flag('is_exchanged'),
+    ULInt8('version'),
+    ULInt8('language'),
 )

File pypkm/structs/gen5.py

         ULInt8('tough'),
         ULInt8('sheen'),
     ),
-    BitStruct('sinnoh_ribbons_set11',
-        Flag('alert'),
-        Flag('world_ability'),
-        Flag('pair_ability'),
-        Flag('multi_ability'),
-        Flag('double_ability'),
-        Flag('great_ability'),
-        Flag('ability'),
-        Flag('sinnoh_champ'),
-    ),
-    BitStruct('sinnoh_ribbons_set12',
+    Swapped(BitStruct('sinnoh_ribbons_set1',
         Flag('royal'),
         Flag('gorgeous'),
         Flag('smile'),
         Flag('careless'),
         Flag('downcast'),
         Flag('shock'),
-    ),
-    BitStruct('sinnoh_ribbons_set21',
+        Flag('alert'),
+        Flag('world_ability'),
+        Flag('pair_ability'),
+        Flag('multi_ability'),
+        Flag('double_ability'),
+        Flag('great_ability'),
+        Flag('ability'),
+        Flag('sinnoh_champ'),
+    )),
+    Swapped(BitStruct('sinnoh_ribbons_set2',
+        Padding(4),
+        Flag('premiere'),
+        Flag('classic'),
+        Flag('carnival'),
+        Flag('festival'),
         Flag('blue'),
         Flag('green'),
         Flag('red'),
         Flag('record'),
         Flag('footprint'),
         Flag('gorgeous_royal'),
-    ),
-    BitStruct('sinnoh_ribbons_set22',
-        Padding(4),
-        Flag('premiere'),
-        Flag('classic'),
-        Flag('carnival'),
-        Flag('festival'),
-    ),
+    )),
 )
 
 _blockB = Struct('_blockB',
             BitField('hp', 5),
         ),
     ))),
-    BitStruct('hoenn_ribbons_set11',
-        Flag('beauty_master'),
-        Flag('beauty_hyper'),
-        Flag('beauty_super'),
-        Flag('beauty'),
-        Flag('cool_master'),
-        Flag('cool_hyper'),
-        Flag('cool_super'),
-        Flag('cool'),
-    ),
-    BitStruct('hoenn_ribbons_set12',
+    Swapped(BitStruct('hoenn_ribbons_set1',
         Flag('smart_master'),
         Flag('smart_hyper'),
         Flag('smart_super'),
         Flag('cute_hyper'),
         Flag('cute_super'),
         Flag('cute'),
-    ),
-    BitStruct('hoenn_ribbons_set21',
-        Flag('artist'),
-        Flag('victory'),
-        Flag('winning'),
-        Flag('champion'),
-        Flag('tough_master'),
-        Flag('tough_hyper'),
-        Flag('tough_super'),
-        Flag('tough'),
-    ),
-    BitStruct('hoenn_ribbons_set22',
+        Flag('beauty_master'),
+        Flag('beauty_hyper'),
+        Flag('beauty_super'),
+        Flag('beauty'),
+        Flag('cool_master'),
+        Flag('cool_hyper'),
+        Flag('cool_super'),
+        Flag('cool'),
+    )),
+    Swapped(BitStruct('hoenn_ribbons_set2',
         Flag('world'),
         Flag('earth'),
         Flag('national'),
         Flag('land'),
         Flag('marine'),
         Flag('effort'),
-    ),
+        Flag('artist'),
+        Flag('victory'),
+        Flag('winning'),
+        Flag('champion'),
+        Flag('tough_master'),
+        Flag('tough_hyper'),
+        Flag('tough_super'),
+        Flag('tough'),
+    )),
     Embed(BitStruct('x40',
         BitField('alt_form_unshifted', 5), # leftshift 3
         Flag('is_genderless'), # if both False, then it's male
     NicknameAdapter(StrictRepeater(11, ULInt16('nickname'))),
     Padding(1),
     ULInt8('hometown'),
-    BitStruct('sinnoh_ribbons_set31',
-        Flag('beauty_master'),
-        Flag('beauty_ultra'),
-        Flag('beauty_great'),
-        Flag('beauty'),
-        Flag('cool_master'),
-        Flag('cool_ultra'),
-        Flag('cool_great'),
-        Flag('cool'),
-    ),
-    BitStruct('sinnoh_ribbons_set32',
+    Swapped(BitStruct('sinnoh_ribbons_set3',
         Flag('smart_master'),
         Flag('smart_ultra'),
         Flag('smart_great'),
         Flag('cute_ultra'),
         Flag('cute_great'),
         Flag('cute'),
-    ),
-    BitStruct('sinnoh_ribbons_set41',
+        Flag('beauty_master'),
+        Flag('beauty_ultra'),
+        Flag('beauty_great'),
+        Flag('beauty'),
+        Flag('cool_master'),
+        Flag('cool_ultra'),
+        Flag('cool_great'),
+        Flag('cool'),
+    )),
+    BitStruct('sinnoh_ribbons_set4',
         Padding(4),
         Flag('tough_master'),
         Flag('tough_ultra'),
 pkm_party_struct = Struct('pkm_party_struct',
     Embed(pkm_struct),
     Embed(_blockE),
+)
+
+# Data sent from the GTS server to the client
+pkm_gtsserver_struct = Struct('pkm_gtsserver_struct',
+    Bytes('encrypted_pkm', 220),
+    Padding(16),
+    ULInt16('id'),
+    ULInt8('gender'),
+    ULInt8('level'),
+    Struct('requested',
+        ULInt16('id'),
+        ULInt8('gender'),
+        ULInt8('min_level'),
+        ULInt8('max_level'),
+    ),
+    Padding(1),
+    ULInt8('ot_gender'),
+    Padding(1),
+    Struct('deposited_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    Struct('traded_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    ULInt32('pv'),
+    ULInt16('ot_id'),
+    ULInt16('ot_secret_id'),
+    OTNameAdapter(StrictRepeater(8, ULInt16('ot_name'))),
+    ULInt8('country'),
+    ULInt8('city'),
+    ULInt8('ot_sprite'),
+    Flag('is_exchanged'),
+    ULInt8('version'),
+    ULInt8('language'),
+    Padding(2),
+)
+
+# Data sent from the GTS client to the server
+pkm_gtsclient_struct = Struct('pkm_gtsclient_struct',
+    UBInt32('checksum'),
+    ULInt32('pv'),
+    ULInt16('length'),
+    Padding(2),
+    Bytes('encrypted_pkm', 220),
+    Padding(16),
+    ULInt16('id'),
+    ULInt8('gender'),
+    ULInt8('level'),
+    Struct('requested',
+        ULInt16('id'),
+        ULInt8('gender'),
+        ULInt8('min_level'),
+        ULInt8('max_level'),
+    ),
+    Padding(1),
+    ULInt8('ot_gender'),
+    ULInt8('ot_nature'),
+    Padding(4),
+    Struct('deposited_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    Struct('traded_time',
+        ULInt16('year'),
+        ULInt8('month'),
+        ULInt8('day'),
+        ULInt8('hour'),
+        ULInt8('minute'),
+        ULInt8('second'),
+        Padding(1),
+    ),
+    ULInt16('ot_id'),
+    ULInt16('ot_secret_id'),
+    OTNameAdapter(StrictRepeater(8, ULInt16('ot_name'))),
+    ULInt8('country'),
+    ULInt8('city'),
+    ULInt8('ot_sprite'),
+    Flag('is_exchanged'), # always False
+    ULInt8('version'),
+    ULInt8('language'),
+    ULInt8('unknown_0to8'),
+    ULInt8('tower_floors'),
+    Padding(4),
+
+    # according to maxg, the 128-byte signature is created by
+    # sending the party struct to pkvldtprod.nintendo.co.jp and
+    # using the token returned by the server (if it passes
+    # validation). because it would be a total PITA to either
+    # reverse engineer or include the legitimate validation, and
+    # that this library is made with fake GTS servers in mind, I'm
+    # going to default to NUL bytes. fake GTS servers can't even
+    # validate the signature, so it shouldn't matter.
+    Bytes('struct_signature', 128),
+    ULInt32('terminator'), # always 128
 )

File pypkm/util.py

         denom = 100
         stat = (num / denom) + 5
 
-        return int(floor(floor(stat) * nature_stat))
+        return int(floor(floor(stat) * nature_stat))
+
+class LengthError(Exception):
+
+    def __init__(self, expected_length, given_length, Errors):
+        message = 'expected {}, received {}'
+        message = message.format(expected_length, given_length)
+
+        # http://stackoverflow.com/a/1319675/374470
+        Exception.__init__(self, message)
+
+        self.Errors = Errors