Home

Awesome

UESaveTool

A Node.js implementation for deserializing and converting GVAS/.sav files to JSON and vice-versa.

npm version npm license

Usage

You must have Node.js Version 16.7.0+ installed.
Download/Clone this entire repo by clicking on the Code button above.

Convert Between .sav and .json

node ./usavetool.js [mode: -sav-to-json, -json-to-sav] [input path] [output path?]

Pipe output to another script

node ./uesavetool.js [mode] [input] | my-script

Adding to your project

First things first:

npm install uesavetool

Also set type as module in package.json. Only ESModules are supported currently.

{
  "type": "module"
}

Using Gvas class for deserialization

import * as fs from 'fs'
import { Gvas, Serializer } from 'uesavetool'

fs.readFile(sav-path, (err, buf) => {
    if(err) throw err;

    const gvas = new Gvas();
    const serial = new Serializer(buf);
    gvas.deserialize(serial);
    
    // manipulate gvas here

    fs.writeFile(json-path, JSON.stringify(gvas), (err) => {
        if(err) throw err;
    })
})

Using Gvas class for serialization

import * as fs from 'fs';
import { Gvas } from 'uesavetool';

fs.readFile(json-path, 'utf8', (err, data) => {
    if(err) throw err;

    const gvas = Gvas.from(JSON.parse(data));

    // manipulate gvas here

    fs.writeFile(sav-path, gvas.serialize(), (err) => {
        if(err) throw err;
    })
})

Implementation Notes

If you want to expand functionality of this tool, you should follow the design patterns implemented within:

Adding a new Property type

AnotherPropery.js

import { 
    Property,
    PropertyFactory,
    Serializer
} from 'uesavetool'

export class AnotherProperty extends Property {
    constructor() {
        super();
        // Attributes specific to this property type
        // Use this.Property for it's value(s)
    }
    get Size() {
        // Calculate number of bytes for serialization. Including this.Name and this.Type
        // Each string attribute will have a 4-byte size followed by that actual string
        let size = this.Name + 4;
        size += this.Type + 4;
        // this.Property may be another `Property` with it's own `Size` getter
        return size;
    }
    deserialize(serial, size) {
        // Serial is a `Serialzer` to make this easier
        // If `size` is passed, use `serial.tell < (start_offset + size)` as a loop condition
        // Do not deserialize this.Name, this.Type or the serialized Size here.
        // This function is called from the parent Property which already deserializes them
        // The Size that is deserialized here is not the same as this.Size
        return this;
    }
    serialize() {
        let serial = Serializer.alloc(this.Size);
        serial.writeString(this.Name);
        serial.writeString(this.Type);
        serial.writeInt32(/* this.Property size in bytes */)
        serial.seek(/* padding length */)
        // serialize this.Property
        return serial.Data;
    }
    static from(obj) {
        let prop = new AnotherProperty();
        prop.Name = obj.Name
        prop.Type = obj.Type
        if(obj.Property) {
            // If this.Property is a value
            prop.Property = obj.Property

            // If this.Property is a `Property`
            prop.Property = PropertyFactory.create(obj.Property);
        }
        return prop;
    }
}

index.js

import { PropertyFactory } from 'uesavetool'
import { AnotherProperty } from './AnotherProperty.js'

PropertyFactory.Properties['AnotherProperty'] = AnotherProperty;

export { AnotherProperty }

Adding a new Array type

Essentially the same as adding a new Property type, but since ArrayProperty.StoredPropertyType will be a normal Property name, adding to PropertyFactory is different. Also Size will ONLY include the size of it's properties.

index.js

import { PropertyFactory } from 'uesavetool'
import { AnotherProperty } from './AnotherProperty.js'
import { AnotherPropertyArray } from './AnotherPropertyArray.js'

PropertyFactory.Properties['AnotherPropertyArray'] = AnotherPropertyArray //Needed if `Type` string ends with "Array" after being stored
PropertyFactory.Arrays['AnotherProperty'] = AnotherPropertyArray //

export { AnotherPropertyArray }

Notes on PropertyFactory.js

PropertyFactory will automatically trim null-terminating characters from strings. The name of the Property type will be in the .sav in utf8

Anotomy of a Property in a GVAS

Sizes are in Little-Endian, so the first byte read is the least significant. Strings are null-terminating. The following example is an StrProperty type.

BytesValue
Name Size0D 00 00 0013 bytes
Name53 61 76 65 53 6C 6F 74 4E 61 6D 65 00"SaveSlotName\0"
Type Size0C 00 00 0012 bytes
Type53 74 72 50 72 6F 70 65 72 74 79 00"StrProperty\0"
Property Size0E 00 00 0014 bytes
Padding00 00 00 00 005 null characters
Property Value Size0A 00 00 0010 bytes
Property Value47 61 6D 65 53 74 61 74 65 00"GameState\0"

Padding is different for most properties. Some properties contain a StoredPropertyType value, such as the StructProperty and ArrayProperty. The Tuple does not exist in a GVAS, instead, Tuple was written to encapsulate lists of properties within the StructProperty and Gvas.Properties. A Tuple is used whenever the string None appears within the file and marks the end of a list of properties.

Disclaimer

THIS SCRIPT WAS BUILT FOR THE BPM: BULLETS PER MINUTE COMMUNITY, BUT THIS MAY PROVE USEFUL FOR OTHER UE-BASED GAMES. IT IS NOT GUARANTEED TO WORK FOR ALL UE GAME SAVES.

Verification Tools

HxD Freeware Hex Editor

https://mh-nexus.de/en/hxd/

Notepad++

https://notepad-plus-plus.org/

Credits

GVAS Converter

https://github.com/13xforever/gvas-converter

UeSaveSerializer

https://gist.github.com/Rob7045713/2f838ad66237f87c86d5396af573b71c