UeSaveSerializer.py

February 27, 2021 ยท View on GitHub

import argparse import json import os import struct import sys from io import BytesIO

HEADER = b'GVAS' HEADER_LEN = 1073

def read(filename, output=sys.stdout): data = None with open(filename, 'rb') as stream: data = stream.read()

if data[:4] != HEADER:
    print("Bad Header")

save = deserialize_save(BytesIO(data[HEADER_LEN:]))

print(json.dumps(save, indent=2), file=output)

def deserialize_save(stream): save = {} save['script'] = read_str(stream) save['properties'] = read_properties(stream)

return save

def read_properties(stream): try: data = [] while True: next = read_str(stream) if next == 'None': break data.append(read_property(stream, next)) return data

except Exception as e:
    print(e)
    print(data)

def read_str(stream): length = read_int(stream) ret = '' for i in range(length - 1): char = stream.read(1).decode() ret += char

if stream.read(1).decode() != '\x00':
    raise Exception(f"Bad String ({length}): '{ret}'")

return ret

def read_property(stream, name): #name = read_str(stream) type = read_str(stream)

if type not in deserializers:
    raise Exception(f"No deserializer for {type}")


if type == 'StructProperty':
    length = read_int(stream)
    read_padding(stream, 4)
    struct_type = read_str(stream)
    read_padding(stream, 17)
    data = stream.read(length)
    value = read_struct(BytesIO(data), struct_type)
elif type == 'ArrayProperty':
    length = read_int(stream)
    read_padding(stream, 4)
    data = stream.read(length + 20)
    value = deserializers[type](BytesIO(data))
elif type == 'BoolProperty':
    read_padding(stream, 8)
    data = stream.read(2)
    value = deserializers[type](BytesIO(data))
else:
    length = read_int(stream)
    stream.read(1)
    read_padding(stream, 4)
    data = stream.read(length)
    value = deserializers[type](BytesIO(data))

return { 'name': name, 'type': type, 'value': value }

def read_padding(stream, count): ret = '' for i in range(count): byte = stream.read(1) if byte != b'\x00': print(f"Bad Padding: {byte}") ret += '.'

return ret

def read_int(stream): return int.from_bytes(stream.read(4), byteorder='little')

def read_float(stream): return struct.unpack('f', stream.read(4))[0]

def read_bool(stream): return struct.unpack('?', stream.read(1))[0]

def read_struct(stream, type): return { 'type': type, 'properties': read_properties(stream) }

def read_array(stream): read_str(stream) read_int(stream) read_padding(stream, 1)

read_str(stream)
read_str(stream)
length = read_int(stream)
read_padding(stream, 4)

type = read_str(stream)
read_padding(stream, 17)

data_stream = BytesIO(stream.read())

data = []

while data_stream.tell() < length:
    props = read_properties(data_stream)
    if props:
        data.append(props)
    else:
        break

return { 'type': type, 'data': data }

deserializers = {} deserializers['FloatProperty'] = read_float deserializers['IntProperty'] = read_int deserializers['StrProperty'] = read_str deserializers['ObjectProperty'] = read_str deserializers['SoftObjectProperty'] = read_str deserializers['StructProperty'] = read_struct deserializers['ArrayProperty'] = read_array deserializers['BoolProperty'] = read_bool

if name == "main": parser = argparse.ArgumentParser(description="UE4 Save file serializer") parser.add_argument(metavar='mode', dest='mode', choices=['sav_to_json'], help='Conversion to perform') parser.add_argument(metavar='input', dest='input', help='File to read') parser.add_argument('-o', '--output', dest='output', default=None, help='File to write to') args = parser.parse_args()

output = sys.stdout

if args.output:
    output = open(args.output, 'w')

if args.mode == 'sav_to_json':
    read(args.input, output=output)