Created
January 8, 2014 03:55
-
-
Save nolanlum/8311541 to your computer and use it in GitHub Desktop.
Save parsing/modification library for Payday 2: The Heist. Saves and loads directly from the steam cloud file in the payday userdata folder. After making modifications via the dict structure, supports saving back into flatfile form that the client loads and will validate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # coding=utf-8 | |
| import hashlib | |
| import sys | |
| from binascii import b2a_hex | |
| from collections import OrderedDict, namedtuple | |
| from struct import pack, unpack_from | |
| SizedInt = namedtuple('SizedInt', ['size', 'value']) | |
| size_to_c_name = {1: 'byte', 2: 'short', 4: 'int'} | |
| SizedInt.__str__ = lambda self: "%s(%s)" % (size_to_c_name[self.size], self.value) | |
| class ParseException(Exception): | |
| pass | |
| class SerializeException(Exception): | |
| pass | |
| class PaydaySave(OrderedDict): | |
| magic = '\x0A\x00\x00\x00' | |
| def __init__(self, filename="save098.sav", *args, **kwargs): | |
| super(PaydaySave, self).__init__(*args, **kwargs) | |
| self.prologue = None | |
| self.epilogue = None | |
| self.payload = None | |
| self.treehash = None | |
| self.filehash = None | |
| self.filename = filename | |
| print "Parsing '%s' as a Payday 2 savefile." % filename | |
| with open(filename, "rb") as file: | |
| self.__from_file(file.read()) | |
| def regen_payload(self): | |
| self.payload = PaydaySave.__gen_tree(self) | |
| self.__verify_and_update_hashes() | |
| def save(self, filename=None): | |
| if filename is None: | |
| filename = self.filename | |
| print "Saving to '%s'." % filename | |
| self.regen_payload() | |
| bytes = self.__bytes() + self.filehash | |
| bytes = PaydaySave.__xor_stream(bytes) | |
| with open(filename, "wb") as file: | |
| file.write(bytes) | |
| print "Wrote magic identifier." | |
| print "Wrote 0x%x bytes of prologue." % len(self.prologue) | |
| print "Wrote 0x%x bytes of payload+hash." % (len(self.payload) + 0x14) | |
| print "Wrote 0x%x bytes of epilogue." % len(self.epilogue) | |
| print "Wrote file hash." | |
| def __from_file(self, file): | |
| file = PaydaySave.__xor_stream(file) | |
| if file[0:4] != PaydaySave.magic: | |
| raise ParseException("unsupported format (0x%08x) or decode error!" % b2a_hex(file[0:4])) | |
| prologue_length = unpack_from("<I", file, 4)[0] | |
| prologue_end = 8 + prologue_length | |
| self.prologue = file[8:prologue_end] | |
| print "Prologue length: 0x%04x" % prologue_length | |
| tree_length_from_file = unpack_from("<I", file, prologue_end)[0] | |
| if file[prologue_end+4:prologue_end+8] != PaydaySave.magic: | |
| raise ParseException("unsupported format (0x%08x) or decode error!" | |
| % b2a_hex(file[prologue_end+4:prologue_end+8])) | |
| print "TreeStruct begin offset: 0x%04x" % (prologue_end + 8) | |
| print "TreeStruct length: 0x%04x" % tree_length_from_file | |
| print "" | |
| tree_length, root = PaydaySave.__parse_tree(file, prologue_end + 9) | |
| if tree_length + 0x15 != tree_length_from_file: | |
| raise ParseException("Expecting 0x%04x bytes of TreeStruct data but got 0x%02x!" % | |
| tree_length_from_file, tree_length) | |
| tree_end = prologue_end + 8 + tree_length + 1 | |
| super(PaydaySave, self).update(root) | |
| print "" | |
| print "TreeStruct end offset: 0x%04x" % tree_end | |
| self.payload = file[prologue_end+8:tree_end] | |
| self.treehash = file[tree_end:tree_end+0x10] | |
| self.epilogue = file[tree_end+0x10:-0x10] | |
| self.filehash = file[-0x10:] | |
| self.__verify_and_update_hashes(False) | |
| def __verify_and_update_hashes(self, silent=True): | |
| calc_treehash = PaydaySave.__hash_main(self.payload) | |
| if self.treehash != calc_treehash: | |
| if not silent: | |
| print "WARNING: TreeStruct hash was invalid! Will fix on save." | |
| print "WARNING: from file: %s" % b2a_hex(self.treehash) | |
| print "WARNING: was: %s" % b2a_hex(calc_treehash) | |
| self.treehash = calc_treehash | |
| elif not silent: | |
| print "TreeStruct hash: %s (valid)" % b2a_hex(self.treehash) | |
| calc_filehash = PaydaySave.__hash_final(self.__bytes()) | |
| if self.filehash != calc_filehash: | |
| if not silent: | |
| print "WARNING: File hash was invalid! Will fix on save." | |
| print "WARNING: from file: %s" % b2a_hex(self.filehash) | |
| print "WARNING: was: %s" % b2a_hex(calc_filehash) | |
| self.filehash = calc_filehash | |
| elif not silent: | |
| print "File hash: %s (valid)" % b2a_hex(self.filehash) | |
| def __bytes(self): | |
| return (PaydaySave.magic + pack("<I", len(self.prologue)) + self.prologue | |
| + pack("<I", len(self.payload) + 0x14) + PaydaySave.magic | |
| + self.payload + self.treehash + self.epilogue) | |
| @staticmethod | |
| def __parse_tree(data, at=0, level=0): | |
| def parse_string(pos): | |
| end = pos | |
| while data[end] != '\x00': | |
| end += 1 | |
| return (end - pos + 1, data[pos:end]) | |
| type_parsers = { | |
| '\x01' : parse_string, | |
| '\x02' : lambda pos: (4, unpack_from("<f", data, pos)[0]), | |
| '\x03' : lambda pos: (0, None), | |
| '\x04' : lambda pos: (1, SizedInt(1, ord(data[pos]))), | |
| '\x05' : lambda pos: (2, SizedInt(2, unpack_from("<H", data, pos)[0])), | |
| '\x06' : lambda pos: (1, data[pos] == '\x01'), | |
| '\x07' : lambda pos: PaydaySave.__parse_tree(data, pos, level + 1) | |
| } | |
| def try_get_parser(offset): | |
| if not data[offset] in type_parsers: | |
| raise ParseException("unknown data type (0x%02x) @ 0x%04x! next 4 bytes: %s" | |
| % (ord(data[offset]), offset, b2a_hex(data[offset + 1:offset + 5]))) | |
| return type_parsers[data[offset]] | |
| count = unpack_from("<I", data, at)[0] | |
| root = OrderedDict() | |
| indent = "".join([" " for _ in range(level)]) | |
| offset = 4 | |
| print "TreeStruct (%u elements):" % count | |
| for _ in xrange(count): | |
| data_len, key = try_get_parser(at + offset)(at + offset + 1) | |
| offset += data_len + 1 | |
| print "%s%s :" % (indent, str(key)), | |
| data_len, value = try_get_parser(at + offset)(at + offset + 1) | |
| offset += data_len + 1 | |
| if not isinstance(value, OrderedDict): | |
| print str(value) | |
| root[key] = value | |
| return (offset, root) | |
| @staticmethod | |
| def __gen_tree(tree): | |
| import types | |
| serializers = { | |
| str : lambda data: "\x01" + data + "\x00", | |
| float : lambda data: "\x02" + pack("<f", data), | |
| types.NoneType : lambda data: "\x03", | |
| SizedInt : lambda data: { | |
| 1 : lambda x: "\x04" + chr(x), | |
| 2 : lambda x: "\x05" + pack("<H", x), | |
| }[data.size](data.value), | |
| bool : lambda data: "\x06\x01" if data else "\x06\x00", | |
| OrderedDict : PaydaySave.__gen_tree | |
| } | |
| def try_get_serializer(data): | |
| if not type(data) in serializers: | |
| raise SerializeException("unserializable type '%s'!" % type(data)) | |
| return serializers[type(data)] | |
| payload = "\x07" + pack("<I", len(tree)) | |
| for key, value in tree.items(): | |
| payload += try_get_serializer(key)(key) | |
| payload += try_get_serializer(value)(value) | |
| return payload | |
| @staticmethod | |
| def __xor_stream(data): | |
| xor_key = [ord(x) for x in "t>?\xA42C&.#67jm:HG=S-cAk)8jh_MJh<nf\xF6"] | |
| xor_key_len = len(xor_key) | |
| data_len = len(data) | |
| data = (ord(x) for x in data) | |
| def key_idx(i): | |
| return ((data_len + i) * 7) % xor_key_len | |
| data = ((byte ^ (xor_key[key_idx(i)] * (data_len - i))) % 256 for i, byte in enumerate(data)) | |
| return "".join((chr(x) for x in data)) | |
| @staticmethod | |
| def __hash_main(data): | |
| return hashlib.md5(data).digest() | |
| @staticmethod | |
| def __hash_final(data): | |
| key = [ord(x) for x in "\x1A\x1F2,"] | |
| data = (ord(x) for x in data) | |
| data = (i % 7 if ((x + key[i % 4]) % 2) else x for i, x in enumerate(data)) | |
| data = "".join((chr(x) for x in data)) | |
| return hashlib.md5(data).digest() | |
| def __main(argc, argv): | |
| if argc < 2: | |
| print "Usage: %s [file]" % argv[0] | |
| return 0 | |
| save = PaydaySave(argv[1]) | |
| if __name__ == '__main__': | |
| sys.exit(__main(len(sys.argv), sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
print "parsing '%s% as a Payday 2 savefile." % filename
__________________________________^
SyntaxError: invalid syntax
Win8, python 3.4
Can you help me with that?