mirror of
https://github.com/quackerd/bdeh.git
synced 2024-11-22 03:06:45 +00:00
281 lines
8.0 KiB
Python
281 lines
8.0 KiB
Python
import os
|
|
import sys
|
|
import OakSave_pb2
|
|
|
|
dk1 = bytearray([0x71, 0x34, 0x36, 0xB3, 0x56, 0x63, 0x25, 0x5F, 0xEA, 0xE2, 0x83, 0x73, 0xF4, 0x98, 0xB8, 0x18, 0x2E, 0xE5, 0x42, 0x2E, 0x50, 0xA2, 0x0F, 0x49, 0x87, 0x24, 0xE6, 0x65, 0x9A, 0xF0, 0x7C, 0xD7])
|
|
dk2 = bytearray([0x7C, 0x07, 0x69, 0x83, 0x31, 0x7E, 0x0C, 0x82, 0x5F, 0x2E, 0x36, 0x7F, 0x76, 0xB4, 0xA2, 0x71, 0x38, 0x2B, 0x6E, 0x87, 0x39, 0x05, 0x02, 0xC6, 0xCD, 0xD8, 0xB1, 0xCC, 0xA1, 0x33, 0xF9, 0xB6])
|
|
|
|
|
|
def write_u32_le(sc : bytearray, offset : int, val: int):
|
|
sc[offset] = val & 0xFF
|
|
sc[offset + 1] = (val >> 8) & 0xFF
|
|
sc[offset + 2] = (val >> 16) & 0xFF
|
|
sc[offset + 3] = (val >> 24) & 0xFF
|
|
|
|
def read_u32_le(sc : bytearray, offset : int):
|
|
ret = sc[offset]
|
|
ret |= sc[offset + 1] << 8
|
|
ret |= sc[offset + 2] << 16
|
|
ret |= sc[offset + 3] << 24
|
|
return ret
|
|
|
|
def find_save_core_offset(sc : bytearray):
|
|
#core offset is always "OakSaveGame\0" + 4 bytes (<- this is the payload size)
|
|
sc_str = "OakSaveGame"
|
|
ret = -1
|
|
for offset in range(0, len(sc) - len(sc_str)):
|
|
match = True
|
|
for i in range(0, len(sc_str)):
|
|
if sc[offset + i] != ord(sc_str[i]):
|
|
match = False
|
|
break
|
|
if match:
|
|
ret = offset
|
|
break
|
|
|
|
if ret == -1:
|
|
print("Cannot locate core offset")
|
|
exit(-1)
|
|
|
|
# skip the OakSaveGame string
|
|
ret = ret + len(sc_str) + 1
|
|
core_sz = read_u32_le(sc, ret)
|
|
return ret, core_sz
|
|
|
|
def usage():
|
|
print("Usage: python bdeh.py <save file>")
|
|
|
|
|
|
def decrypt(src : bytearray):
|
|
offset = len(src) - 1
|
|
while offset >= 0:
|
|
k1 = 0
|
|
if offset >= 0x20:
|
|
k1 = src[offset - 0x20]
|
|
else:
|
|
k1 = dk1[offset]
|
|
|
|
k2 = offset
|
|
k2 = k2 % 0x20
|
|
k2 = dk2[k2]
|
|
k2 = k2 ^ k1
|
|
|
|
src[offset] = int(src[offset]) ^ k2
|
|
offset = offset - 1
|
|
return src
|
|
|
|
def encrypt(src : bytearray):
|
|
offset = 0
|
|
while offset < len(src):
|
|
if offset >= 0x20:
|
|
k1 = src[offset - 0x20]
|
|
else:
|
|
k1 = dk1[offset]
|
|
|
|
k2 = offset
|
|
k2 = k2 % 0x20
|
|
k2 = dk2[k2]
|
|
k2 = k2 ^ k1
|
|
|
|
src[offset] = int(src[offset]) ^ k2
|
|
offset = offset + 1
|
|
return src
|
|
|
|
def editor(save_obj):
|
|
TYPE_INT = 0
|
|
TYPE_FLOAT = 1
|
|
TYPE_STRING = 2
|
|
TYPE_DICT = 3
|
|
TYPE_BOOL = 4
|
|
TYPE_MS = 5 #MS_Complete or MS_Active
|
|
|
|
OP_READ = 0
|
|
OP_WRITE = 1
|
|
|
|
#only works for write since primitives will be copied
|
|
vMap = {
|
|
'save_game_id': {
|
|
'func': save_obj.save_game_id,
|
|
'type': TYPE_INT
|
|
},
|
|
'last_save_timestamp': {
|
|
'func': save_obj.last_save_timestamp,
|
|
'type': TYPE_INT
|
|
},
|
|
'time_played_seconds': {
|
|
'func': save_obj.time_played_seconds,
|
|
'type': TYPE_INT
|
|
},
|
|
'player_class_data': {
|
|
'func': save_obj.player_class_data,
|
|
'type': TYPE_DICT
|
|
},
|
|
'resource_pools': {
|
|
'func': save_obj.resource_pools,
|
|
'type': TYPE_DICT
|
|
},
|
|
'saved_regions': {
|
|
'func': save_obj.saved_regions,
|
|
'type': TYPE_DICT
|
|
},
|
|
'experience_points': {
|
|
'func': save_obj.experience_points,
|
|
'type': TYPE_INT
|
|
},
|
|
'game_stats_data': {
|
|
'func': save_obj.game_stats_data,
|
|
'type': TYPE_DICT
|
|
},
|
|
'inventory_category_list': {
|
|
'func': save_obj.inventory_category_list,
|
|
'type': TYPE_DICT
|
|
},
|
|
'inventory_items': {
|
|
'func': save_obj.inventory_items,
|
|
'type': TYPE_DICT
|
|
}
|
|
}
|
|
|
|
def show(vType):
|
|
if vType in vMap:
|
|
print("==========================\n")
|
|
print(str(vType) + ": " + str(vMap[vType]['func']))
|
|
print("==========================\n")
|
|
return
|
|
print("Error: " + str(vType) + " is not a valid value.")
|
|
|
|
def cash(op, cType, val): #op 0 read 1 write cType 0 cash 1 eridium
|
|
hashVal = 0
|
|
if cType == 0:
|
|
hashVal = 618814354
|
|
elif cType == 1:
|
|
hashVal = 3679636065
|
|
|
|
for i in save_obj.inventory_category_list:
|
|
if i.base_category_definition_hash == hashVal:
|
|
if cType == 0:
|
|
print("Current cash: " + str(i.quantity))
|
|
elif cType == 1:
|
|
print("Current eridium: " + str(i.quantity))
|
|
if op == OP_WRITE:
|
|
i.quantity = int(val)
|
|
print("Success.\n")
|
|
return
|
|
print("Error: Cash category hash is not existing.")
|
|
|
|
def experience_points(op, val):
|
|
print("Current experience points: " + str(save_obj.experience_points))
|
|
if op == OP_WRITE:
|
|
save_obj.experience_points = int(val)
|
|
print("Success.\n")
|
|
|
|
def print_menu():
|
|
print("BdEH: Save Editor for Borderlands 3\n")
|
|
print("Commands:\nhelp\nget\nset\nsaveexit\nexit\n")
|
|
|
|
def command(input):
|
|
#try:
|
|
ele = input.split()
|
|
cmd = ele[0]
|
|
stat = ele[1]
|
|
if cmd == 'get':
|
|
if stat == 'cash':
|
|
cash(OP_READ, 0, 0)
|
|
elif stat == 'eridium':
|
|
cash(OP_READ, 1, 0)
|
|
elif stat == 'experience_points':
|
|
experience_points(OP_READ, 0)
|
|
else:
|
|
show(stat)
|
|
elif cmd == 'set':
|
|
if stat == 'cash':
|
|
cash(OP_WRITE, 0, ele[2])
|
|
elif stat == 'eridium':
|
|
cash(OP_WRITE, 1, ele[2])
|
|
elif stat == 'experience_points':
|
|
experience_points(OP_WRITE, ele[2])
|
|
else:
|
|
print("Error: Invalid command")
|
|
#except:
|
|
# print("Error: Invalid command.")
|
|
|
|
print_menu()
|
|
while True:
|
|
uin = input('Please input a command: ')
|
|
if uin == 'saveexit':
|
|
break
|
|
elif uin == 'exit':
|
|
exit()
|
|
command(uin)
|
|
|
|
f = open('obj.txt', 'w')
|
|
f.write(str(save_obj))
|
|
f.close()
|
|
return
|
|
|
|
def main():
|
|
if len(sys.argv) != 2:
|
|
usage()
|
|
exit()
|
|
|
|
print("Reading " + sys.argv[1])
|
|
with open(sys.argv[1], "rb") as f:
|
|
savfile = f.read()
|
|
|
|
core_offset, core_sz = find_save_core_offset(savfile)
|
|
|
|
if core_offset + core_sz + 4 != len(savfile):
|
|
print("Incorrect file size!")
|
|
exit(1)
|
|
|
|
print("Save file loaded - size: " + hex(len(savfile)) + " core offset: " + hex(core_offset) + " core size: " + hex(core_sz))
|
|
|
|
# copy the payload
|
|
payload = bytearray(core_sz)
|
|
|
|
for i in range(0, core_sz):
|
|
payload[i] = savfile[core_offset + 4 + i]
|
|
|
|
# copy the header
|
|
header = bytearray(core_offset)
|
|
|
|
for i in range(0, core_offset):
|
|
header[i] = savfile[i]
|
|
|
|
# garbage collect the savfile buffer
|
|
savfile = None
|
|
|
|
# decrypt the payload
|
|
payload = decrypt(payload)
|
|
|
|
# deserialize the payload
|
|
save_obj = OakSave_pb2.Character()
|
|
save_obj.ParseFromString(payload)
|
|
|
|
# editor loop
|
|
editor(save_obj)
|
|
|
|
# serialize the payload
|
|
payload_ro = save_obj.SerializeToString()
|
|
|
|
payload = bytearray(len(payload_ro))
|
|
for i in range(0, len(payload_ro)):
|
|
payload[i] = payload_ro[i]
|
|
|
|
# encrypt the payload
|
|
payload = encrypt(payload)
|
|
|
|
# build the full save
|
|
full_save = bytearray(core_offset + 4 + len(payload))
|
|
for i in range(0, core_offset):
|
|
full_save[i] = header[i]
|
|
write_u32_le(full_save, core_offset, len(payload))
|
|
for i in range(0, len(payload)):
|
|
full_save[core_offset + 4 + i] = payload[i]
|
|
|
|
# Write
|
|
with open(sys.argv[1] + ".bdeh", "wb") as f:
|
|
f.write(full_save)
|
|
|
|
main()
|