Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@etothepii/satisfactory-file-parser

etothepii4371MIT3.0.1TypeScript support: included

A file parser for satisfactory files. Includes save files and blueprint files.

satisfactory, save, parser, blueprint, unreal engine, typescript

readme

Satisfactory File Parser

This is a TypeScript github project to parse Satisfactory Saves/Blueprints. Satisfactory is a game released by Coffee Stain Studios. The reporitory is written entirely in TypeScript and is bundled on NPM. The examples listed here are Node.js. Should work in browser as well.

This parser can read, modify and write:

  • Save Files .sav
  • Blueprint Files .sbp, .sbpcfg

The parser is for deep save editing, since it just parses to JSON and back. The purpose to have an editable structure, game logic is not known. It is recommended that you look at the parsed save/blueprint to get an idea what you want to edit.

Supported Versions

The version support of the Game Updates is indicated below. The parser is pretty compatible with older saves and blueprints alike (for example from U8). If you still run into errors, let me know or raise an issue. Game Version Files of U5 and below are NOT supported.

Game Version Support
<= U5 ❌ not compatible
U6 ⚠️ mostly compatible
U7 ⚠️ mostly compatible
U8 ✅ compatible
U1.0 ✅ compatible
U1.1 ✅ compatible

Version Migration

The parser will NOT MIGRATE from one game version to another, for compatibility reasons. If you parse a U8 save/blueprint, the parser will also serialize it back to a U8 save/blueprint.

Installation

npm

npm install @etothepii/satisfactory-file-parser

yarn

yarn add @etothepii/satisfactory-file-parser

Bug Reports or Feedback

You can always raise an issue on the linked github project or hit me up on the satisfactory discord etothepii.

Mod Support ✅

By Default, most Mods just reuse Properties and Structs of the base game. If however a Mod should not be working or have just objects with a lot of trailing unparseable data, Raise an issue or contact me.

Some explicitly tested mods include: Ficsit-Cam, Structural Solutions, Linear Motion, Container Screens, Conveyor Wall Hole, X3-Signs, X3-Roads

Reading a Save

Reading a Save in Memory.

import * as fs from 'fs';
import { Parser } from '@etothepii/satisfactory-file-parser';

const file = new Uint8Array(fs.readFileSync('./MySave.sav')).buffer;
const save = Parser.ParseSave('MySave', file);

// write to json file
fs.writeFileSync('MySave.json', JSON.stringify(save));

You can also read a Save via stream, to save RAM. The binary data of the whole save will still be in memory, but the converted JSON can be streamed. (You can of course keep reading the stream in memory). The returned stream is a readable WHATWG stream of type string and represents a SatisfactorySave object. this object can be serialized again. WHATWG is used by default by browsers. Node js can use them using Writable.toWeb() and Writable.fromWeb() for example.

import * as fs from 'fs';
import { Writable } from 'stream';
import { WritableStream } from 'stream/web';
import { ReadableStreamParser } from '@etothepii/satisfactory-file-parser';

const file = fs.readFileSync('./MySave.sav');
const jsonFileStream = fs.createWriteStream('./MySave.json', { highWaterMark: 1024 * 1024 * 200 }); // your outgoing JSON stream. In this case directly to file.  
const whatwgWriteStream = Writable.toWeb(jsonFileStream) as WritableStream<string>;                  // convert the file stream to WHATWG-compliant stream

const { stream, startStreaming } = ReadableStreamParser.CreateReadableStreamFromSaveToJson('MySave', file);

stream.pipeTo(whatwgWriteStream);
jsonFileStream.on('close', () => {
    // write stream finished
});

startStreaming();

Writing a Save

Consequently, writing a parsed save file back is just as easy. The SaveParser has callbacks to assist during syncing on different occasions during the process. For example, when writing the header or when writing a chunk of the save body. The splitting in individual chunks enables you to more easily stream the binary data to somewhere else.

import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";

let fileHeader: Uint8Array;
const bodyChunks: Uint8Array[] = [];
Parser.WriteSave(save, header => {
    console.log('on save header.');
    fileHeader = header;
}, chunk => {
    console.log('on save body chunk.');
    bodyChunks.push(chunk);
});

// write complete sav file back to disk
fs.writeFileSync('./MyModifiedSave.sav', Buffer.concat([fileHeader!, ...bodyChunks]));

Reading Blueprints

Blueprints consist of 2 files. The .sbp main file and the config file .sbpcfg.

import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";

const file = new Uint8Array(fs.readFileSync('./MyBlueprint.sbp')).buffer;
const configFile = new Uint8Array(fs.readFileSync('./MyBlueprint.sbpcfg')).buffer;
const blueprint = Parser.ParseBlueprintFiles('Myblueprint', mainFile, configFile);

// write to json file
fs.writeFileSync('Myblueprint.json', JSON.stringify(blueprint));

Writing Blueprints

Consequently, writing a blueprint into binary data works the same way with getting callbacks in the same style as the save parsing.

import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";

let mainFileHeader: Uint8Array;
const mainFileBodyChunks: Uint8Array[] = [];
const summary = Parser.WriteBlueprintFiles(blueprint,
    header => {
        console.log('on main file header.');
        mainFileHeader = header;
    },
    chunk => {
        console.log('on main file body chunk.');
        mainFileBodyChunks.push(chunk);
    }
);

// write complete .sbp file back to disk
fs.writeFileSync('./MyBlueprint.sbp', Buffer.concat([mainFileHeader!, ...mainFileBodyChunks]));

// write .sbpcfg file back to disk, we get that data from the result of WriteBlueprintFiles
fs.writeFileSync('./MyBlueprint.sbpcfg', Buffer.from(summary.configFileBinary));

Additional Options on the Parser Methods

For every parser call, you can pass optional callbacks to receive additional info. Like a callback on the decompressed save body. Parsing saves provides a callback for reporting progress [0,1] and an occasional message.

const save = Parser.ParseSave('MySave', file.buffer, {
    onDecompressedSaveBody: (body) => console.log('on decompressed body', body.byteLength),
    onProgressCallback: (progress, msg) => console.log(progress, msg)
});
const { stream, startStreaming } = ReadableStreamParser.CreateReadableStreamFromSaveToJson(savename, file, {
    onProgress: (progress, msg) => console.log(`progress`, progress, msg);
});
const blueprint = Parser.ParseBlueprintFiles('Myblueprint', file, configFile, {
    onDecompressedBlueprintBody: (body) => console.log('on decompressed body', body.byteLength),
});

Save Editing Examples (in JS/TS)

import { SaveComponent, SaveEntity, StructArrayProperty, Int32Property, ObjectProperty, StrProperty, StructProperty, InventoryItemStructPropertyValue, DynamicStructPropertyValue } from '@etothepii/satisfactory-file-parser';

// method to overwrite save objects
// currently quite inefficient to loop through everything, so theres room to improve in a future version. Feel free to raise an issue.
const modifyObjects = (...modifiedObjects: (SaveEntity | SaveComponent)[]) => {
    for (const modifiedObject of modifiedObjects) {
        for (const level of Object.values(save.levels)) {
            for (let i = 0; i < level.objects.length; i++) {
                if (level.objects[i].instanceName === modifiedObject.instanceName) {
                    level.objects[i] = modifiedObject;
                }
            }
        }
    }
}

const objects = Object.values(save.levels).flatMap(level => level.objects);
const collectables = Object.values(save.levels).flatMap(level => level.collectables);

Example Print Hub Terminal Location

// get hub terminals. Beware that filter returns a COPIED array and not the original objects.
const hubTerminals = objects.filter(obj => obj.typePath === '/Game/FactoryGame/Buildable/Factory/HubTerminal/Build_HubTerminal.Build_HubTerminal_C') as SaveEntity[];
const firstHubPosition = hubTerminals[0].transform.translation;
console.log(`Hub terminal is located at ${firstHubPosition.x}, ${firstHubPosition.y}, ${firstHubPosition.z}`);

Example Modify Player Locations

const players = objects.filter(obj => obj.typePath === '/Game/FactoryGame/Character/Player/Char_Player.Char_Player_C') as SaveEntity[];
for (const player of players) {
    const name = (player.properties.mCachedPlayerName as StrProperty).value;
    player.transform.translation = {
        x: player.transform.translation.x + 5000,
        y: player.transform.translation.y + 5000,
        z: player.transform.translation.z,
    }
    console.log(`Player ${name} is now located at ${player.transform.translation.x}, ${player.transform.translation.y}, ${player.transform.translation.z}`);
}

// modify original save objects
modifyObjects(...players);

Example Overwrite Item Stack in a Storage Container

// get the first storage container, either mk1 or mk2.
const storageContainers = objects.filter(obj =>
    obj.typePath === '/Game/FactoryGame/Buildable/Factory/StorageContainerMk1/Build_StorageContainerMk1.Build_StorageContainerMk1_C'
    || obj.typePath === '/Game/FactoryGame/Buildable/Factory/StorageContainerMk2/Build_StorageContainerMk2.Build_StorageContainerMk2_C'
);
const firstContainer = storageContainers[0];

// the container has a reference name to an inventory component.
const inventoryReference = firstContainer.properties.mStorageInventory as ObjectProperty;
const inventory = objects.find(obj => obj.instanceName === inventoryReference.value.pathName) as SaveComponent;
const inventoryStacks = inventory.properties.mInventoryStacks as StructArrayProperty;
const firstStack = inventoryStacks.values[0];

// Items within ItemStacks are quite nested. And StructProperties can basically be anything.
// overwrite first item stack with 5 Rotors.
(((firstStack.value as DynamicStructPropertyValue).properties.Item as StructProperty).value as InventoryItemStructPropertyValue).itemReference = {
    levelName: '',
    pathName: '/Game/FactoryGame/Resource/Parts/Rotor/Desc_Rotor.Desc_Rotor_C'
};
((firstStack.value as DynamicStructPropertyValue).properties.NumItems as Int32Property).value = 5;

// modify original save object
modifyObjects(firstContainer);

Auto-Generated TypeDoc Reference.

Basic Guide.

More detailed explanation of some basic things in the parser.

Changelog

Licence

changelog

Changelog

[3.0.1] (2025-04-20)

Breaking Changes in Save Structure for 1.1

  • The save header in 1.1 has now the save file name. Which gets used over the passed name when the file is from 1.1.
  • Save objects have 1-2 new fields.
  • BuildableSubsystemSpecialProperties have now a slightly different structure. Their buildables typePath gets replaced with an object reference typeReference.
  • SaveObject's field unknownType2 is now called shouldMigrateObjectRefsToPersistent.
  • SaveObject's field objectVersion is now called saveCustomVersion.
  • Levels also have a field saveCustomVersion.
  • BuildableSubsystemSpecialProperties have one more field (currentLightweightVersion) and their buildables can now be also a different type.
  • The parentObjectRoot and parentObjectName got merged to parentObject as a Reference struct, instead of two strings.
  • The objects of VehicleSpecialProperties are now of type VehiclePhysicsData instead of just listing unknownBytes.
  • Levels within a Save are not an array, but an object with level name as key now.
  • Blueprint Configs have now a configVersion
  • InventoryItems' naming of fields changed. itemStateRaw is now better resolved into individual properties
  • InventoryItems have now a ObjectReference itemReference instead of the single string itemName, since that is more correct. InventoryItems' fields are also mostly optional due to compatibility.

Internal Updates

  • Some internal changes like making Reader and Writer have context. To support different save versions.
  • SatisfactorySaveHeader and BlueprintHeader have their own namespace now.

Changelog

[2.1.3] (2024-11-24)

Update README

  • fixed link to auto-generated typedoc.

[2.1.2] (2024-11-24)

Update README

  • fixed link to auto-generated typedoc.

[2.1.1] (2024-11-24)

Blueprint structure

  • Item costs and recipes in a blueprint are now correctly treated as ObjectReference, instead a single path string.

    Internal Updates

  • Migrated the rest of the generic properties towards namespaces

    Updated README examples

    Provided auto-generated typedoc

[2.0.1] (2024-10-31)

Normal Properties Update

  • Most Normal Properties classes got refactored to namespaces as well. More will come. Please refrain from using instances of them. Background being, that its anyway only static methods and types.
  • Since normal properties of an object are effectively always of type AbstractBaseProperty | AbstractBaseProperty[], the AbstractBaseProperty has now all fields and BasicProperty got removed as there is no difference anymore between the two. They would be the same now.
  • type guards of "normal" properties like isObjectProperty() accept now any as parameter and should work now as expected
  • Since ArrayProperties and SetProperties in the save format dont necessarily always have the same structure as their subtype, I introduced own types like StrArrayProperty and Int32SetProperty with corresponding type guards (e.g. isStrArrayProperty()). Means more overhead in code, but hence its more correct in usage.

    Bugfix

  • The total conveyor length in the special properties of a ConveyorChainActor got serialized as int32, but correctly now serializes as float32.

[1.1.1] (2024-10-21)

Improved Special Properties

  • Improved on SpecialProperties of BuildableSubsystem and ConveyorChainActor as the meaning became more clear.
  • Special Properties are refactored into their own namespaces and exported.
  • The union type SpecialAnyProperties is now automatically derived and more concisely named SpecialProperties.AvailableSpecialPropertiesTypes instead, in case you need it.

[1.0.3] (2024-10-17)

Hotfix

  • fix being forced to use callbacks when writing saves or blueprints.

[1.0.2] (2024-10-17)

Internal renaming

  • ...SpecialProperty got all renamed to ...SpecialProperties.

[1.0.1] (2024-10-17)

Major breaking changes on Parser usage

  • Cleaned Usage methods of Save / Blueprint Parsing. The callbacks are an optional additional parameter object now instead of required.
  • Re-Added a method to parse Saves in memory again. (sorry for the inconvenience)

    Internal structure changes

  • SatisfactorySave structure changed, the grids and gridHash fields are slightly different now, since their meaning became more clear. Not relevant for normal save editing.
  • Level is a namespace instead of a class now, since the classes had only static methods anyway.

    module build now includes source maps

    module build should now include a docs folder for auto-generated documentation

[0.5.1] (2024-10-15)

Added Mod Support

Internal Renamings

  • DynamicStructPropertyValue extracted to own file.
  • Parsing of object data partially moved to SaveObject.
  • Renamed DataFields class to PropertiesList.
  • Moved parsing of class-specific properties into own namespace.
  • ObjectProperty and SoftObjectProperty now reuse the correct method for parsing/serializing the reference value.

[0.4.22] (2024-10-07)

compatibility fix

  • referenced icon libraries in blueprints are now optional when being parsed.

[0.4.21] (2024-10-07)

internal package restructuring

  • restructured some internal packages.
  • provides now typeguards for every property.

[0.4.20] (2024-10-06)

bugfix

  • added parsing of icon library reference to parsing blueprints.

[0.4.19] (2024-10-06)

Migrated repo to public github

[0.4.18] (2024-10-05)

updated README

[0.4.17] (2024-10-05)

updated README

bugfix

  • ClientIdentityInfo field names and structure got changed, since the meaning is now more clear.
  • removed trailing object list from satisfactory save object.
  • deleted entities references get serialized again, just based on collectables list.

[0.4.16] (2024-10-03)

bugfix

  • exporting isSaveEntity and isSaveComponent again.

[0.4.15] (2024-10-02)

updated README

  • changelog document doesn't seem linkable, so it is in the readme for now.

[0.4.14] (2024-10-02)

updated README

  • updated the code examples with more context

    Internal renamings (won't affect you if you stick to the code examples)

  • improved the interface for abstract parser classes
  • extended some error log messages
  • added an additional check when parsing struct property InventoryItem, since ported saves often have a few more bytes.
  • changed function name writeFloat() to writeFloat32() of the save writer.
  • changed variable name saveOrBlueprintIndicator to objectVersion for objects, since the meaning of that number became now more clear.