uesavetool
v1.1.7
Published
A Node.js implementation for deserializing and converting GVAS/.sav files to JSON and vice-versa.
Downloads
47
Maintainers
Readme
UESaveTool
A Node.js implementation for deserializing and converting GVAS/.sav files to JSON and vice-versa.
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()
, andstatic 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 internalBuffer
- Never instantiate a
Property
withnew
outside of a constructor or it's ownfrom
function, instead, you should use thePropertyFactory.create()
static function, which passes an object argument to thefrom()
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()
, andstatic from()
deserialize()
accepts aSerializer
as an argument, and returns theGvas
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 currentProperty
.
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