UNPKG

uesavetool

Version:

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

198 lines (162 loc) 8.01 kB
# UESaveTool A Node.js implementation for deserializing and converting GVAS/.sav files to JSON and vice-versa. [![npm version](https://badgen.net/npm/v/uesavetool)](https://www.npmjs.com/package/uesavetool) [![npm license](https://badgen.net/npm/license/uesavetool)](/LICENSE) ## Usage You must have [Node.js](www.nodejs.org) 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. ```json { "type": "module" } ``` #### Using `Gvas` class for deserialization ```js 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 ```js 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` ```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` ```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` ```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