Skip to content

Instantly share code, notes, and snippets.

@nolanlum
Created January 8, 2014 03:55
Show Gist options
  • Select an option

  • Save nolanlum/8311541 to your computer and use it in GitHub Desktop.

Select an option

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.
# 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))
@JanWerder
Copy link

print "parsing '%s% as a Payday 2 savefile." % filename
__________________________________^
SyntaxError: invalid syntax
Win8, python 3.4

Can you help me with that?

@Larrik
Copy link

Larrik commented Aug 8, 2014

Use Python 2.7 for this, not 3.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment