uesavetool

1.1.7 • Public • Published

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:

  • All Properties extend from the Property class which has 4 functions: get Size(),deserialize(), serialize(), and static from()
  • Properties are in charge of serializing/deserializing themselves due to the unique formatting for each property
  • Use the Serializer class to read/write data on it's internal Buffer
  • Never instantiate a Property with new outside of a constructor or it's own from function, instead, you should use the PropertyFactory.create() static function, which passes an object argument to the from() function implemented within the specified property type
  • For integration in other projects, you should use the Gvas class, which has 4 functions: get Size(), deserialize(), serialize(), and static from()
  • deserialize() accepts a Serializer as an argument, and returns the Gvas instance
  • Properties' names must be exactly the same as in the .sav
  • Array properties are implementation dependent due to unique serialization
  • Size calcualated by get Size() is NOT written to the serialized data buffer. That value is usually, but not always, the total size of the property/properties within the current Property.

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.

Bytes Value
Name Size 0D 00 00 00 13 bytes
Name 53 61 76 65 53 6C 6F 74 4E 61 6D 65 00 "SaveSlotName\0"
Type Size 0C 00 00 00 12 bytes
Type 53 74 72 50 72 6F 70 65 72 74 79 00 "StrProperty\0"
Property Size 0E 00 00 00 14 bytes
Padding 00 00 00 00 00 5 null characters
Property Value Size 0A 00 00 00 10 bytes
Property Value 47 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

Package Sidebar

Install

npm i uesavetool

Weekly Downloads

14

Version

1.1.7

License

MIT

Unpacked Size

45 kB

Total Files

31

Last publish

Collaborators

  • ch1pset