NOT from the devs
aka Loupau's understanding of savegame files
Last updated : 1.0.0-alpha4-rc1
In the 'Play' menu, click the 'Show Folder' button. The folder that opens contains all of your savegames.
Locate the UID at the top of your savegame in the ingame list.
In the folder that opened previously, open the folder whose name is that UID. This contains all backups of your savegame.
A savegame backup is a file whose name has the format backup-v{backup_number}-{timestamp}.spz2. Note that this file name format is only required when the file is in the savegame's folder, the file can have any name when you use the 'Import' button in the 'Play' menu. .spz2 files are zip archives, so to open one, you can open it like you would open a .zip file.
The archive contains multiple files and folders. The files are either .json or .bin.
The files are padded with null bytes to have a file size which is a power of two. This padding can be ignored and is also not required for the game to load the save.
Due to a language like C# being used, when an object is said to have a specific type, it can be either that specific type or null. When the game allows that object to be null, the encoding format will reflect that by having special cases for null objects. If the format doesn't specify how an object should be encoded if it's null, then that object shouldn't be able to be null in normal circumstances.
When a type's name begins with I, it means the type is a C# interface. An interface doesn't fully represent a data type, but instead is more like a blueprint for other types to inherit from and build upon. This means that when an object's type is an interface, in reality the object will have a type that inherits from that interface. As for the serialization of interfaces, two cases exist:
- either the interface doesn't provide special serialization code, and the actual object's type's serialization code will be called directly
- or the interface does provide serialization code, in that case it will be called, then that code will decide if it calls the actual type's code or not.
The names of data types in this documentation use the same names as the class names in the game's code, except when name changes or simplifications of the data structures help understandability.
The custom C# objects that make up the contents of a savegame are broken down into basic types to be serialized into the .bin files. The format of these basic types is described below.
A single byte is simply written as is. It can be used to represent an integer between 0 and 255 or a single ASCII character.
All integers are encoded as little endian.
| Type | Encoded on |
|---|---|
| (u)short | 2 bytes |
| (u)int | 4 bytes |
| (u)long | 8 bytes |
Booleans are encoded on a single byte, 0 for false and 1 for true.
Strings are encoded using a lookup table. What actually gets written is the string's index in the table as an int. However, if the string is null, it isn't added to the table and instead an index of -2^31 is written. The lookup table is then written to strings.bin.
Exception : if you ended up here from the blueprint codes specifications, since blueprints don't have a lookup table, the string is directly written in the data :
| Type | Description |
|---|---|
| short | The string's length, or -1 if it's null |
| bytes | The string encoded in UTF-8 |
Checkpoints are markers along the data stream that ensure a reader is still reading the correct data to avoid creating dummy data if a format mismatch happens.
They are only used if enabled for the savegame, with that info being found in savegame.json. If they aren't enabled, nothing is written when a format specifies using a checkpoint.
They are initially defined with a string ID, that is then hashed using the below algorithm, with s the checkpoint ID :
uint hash = 523423;
for (int i = 0; i < s.Length; i++)
{
hash += s[i];
hash += hash << 10;
hash ^= hash >> 6;
}
hash += hash << 3;
hash ^= hash >> 11;
hash += hash << 15;
return hash;That algorithm produces a uint that is then written to represent the checkpoint.
The checkpoints currently used by the game and their hash can be found below for convenience.
| ID | hash (uint) |
hash (bytes) |
|---|---|---|
| blob:start | 3295808852 | 54 0D 72 C4 |
| blob:end | 2225352737 | 21 30 A4 84 |
| island | 2971730586 | 9A 02 21 B1 |
| buildings | 2756275580 | 7C 6D 49 A4 |
| building | 899714533 | E5 8D A0 35 |
Blobs are used to wrap pieces of data. Since they store the length of their content, a reader can easily skip a blob if needed, in case of outdated data for example.
| Type | Description |
|---|---|
| Checkpoint | blob:start |
| int | The content's length in bytes |
| Elem | The blob's content |
| Checkpoint | blob:end |
Arrays don't have special formatting, but they are referenced here for completeness.
| Type | Description |
|---|---|
| Elem | Array element 0 |
| Elem | Array element 1 |
| ... | ... |
| Elem | Array element n |
todo
todo
todo
The string lookup table for the savegame.
| Type | Description |
|---|---|
| int | The number of entries in the table |
| Array<StringEntry> | The strings in the table |
| Type | Description |
|---|---|
| int | The length of the string in bytes |
| bytes | The string encoded in UTF-8 |
todo
todo
The files in this folder encode the type, position and configuration of buildings and islands. Each file is an 'island bundle', i.e. a group of islands, with the maximum number of islands per bundle when generated by the game being determined by the formula ceil(4^log10(island_count)). When the game loads a save however, there is no restrictions to how many islands can be in each bundle nor in which order the islands are inside a bundle nor the order of bundles themselves.
Each file has the following format :
| Type | Description |
|---|---|
| int | The number of islands in the bundle |
| Array<PlacedIsland> | The islands inside the bundle |
| Type | Description |
|---|---|
| Checkpoint | island |
| GlobalChunkCoordinate | The island's position |
| string | The island's ID |
| Rotation | The island's rotation |
| Blob<PlacedIslandData> | The island's configuration and placed buildings |
| Type | Description |
|---|---|
| bool | Whether the island has configuration data |
| Blob<IIslandConfiguration> | The island's configuration data, only there if the previous value was true |
| Blob<BuildingsData> | The buildings placed on the island |
Represents an island configuration object, the type of which should be deduced from the previously decoded island ID. Can be RailConfig or TrainUnloaderConfig.
Applies to all rail types (regular, splitter, merger).
| Type | Description |
|---|---|
| int | The number of connection color filters encoded |
| Array<int> | The color filter for each connection of the rail (a connection being one of the possible input -> output paths), stored as a bit mask, where each color is 1 << colorIndex, with the colorIndex being determined by the RailColorsConfig key of the current scenario |
Applies to shape and fluid train unloaders and transfer stations.
| Type | Description |
|---|---|
| int | The lanes disabled for unloading, stored as a bit mask, where each lane is 1 << layerIndex |
| Type | Description |
|---|---|
| Checkpoint | buildings |
| int | The number of buildings placed on the island |
| Array<PlacedBuilding> | The buildings placed on the island |
| Type | Description |
|---|---|
| Checkpoint | building |
| IslandTileCoordinate | The building's position |
| Rotation | The building's rotation |
| string | The building's ID |
| bool | Whether the building has configuration data |
| Blob<IBuildingConfiguration> | The building's configuration data, only there if the previous value was true |
Represents a building configuration object, the type of which should be deduced from the previously decoded building ID. Can be LabelConfig, SignalProducerConfig, ItemProducerConfig, FluidProducerConfig, ButtonConfig, CompareGateConfig or GlobalSignalReceiverConfig.
Applies to labels.
| Type | Description |
|---|---|
| string | The label's text |
Applies to signal producers.
| Type | Description |
|---|---|
| ISignal | The signal produced |
Applies to item producers.
| Type | Description |
|---|---|
| IBeltItem | The item produced |
Applies to fluid producers.
| Type | Description |
|---|---|
| IFluid | The fluid produced |
Applies to buttons.
| Type | Description |
|---|---|
| bool | Whether the button is activated |
Applies to comparison gates.
| Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| byte |
The gate's compare mode :
|
Applies to global signal receivers and operator signal receivers.
| Type | Description |
|---|---|
| SignalChannelId | For operator signal receivers, this represents the ROS line selected. TODO : is this representative for global signal receivers |
todo
todo
todo
todo
Some objects are not specific to one file in the save file, their format is described below.
Represents an island level position.
| Type | Description |
|---|---|
| int | The X coordinate |
| int | The Y coordinate |
| short | The Z coordinate |
| Type | Description |
|---|---|
| byte | 0 : East, 1 : South, 2 : West, 3 : North |
Represents a building level positon relative the island center (see the 'Note' in this section).
| Type | Description |
|---|---|
| short | The X coordinate |
| short | The Y coordinate |
| byte | The Z coordinate |
The encoded data starts with a byte representing the type of signal :
| Byte value | Type of signal | Data encoded after | Notes |
|---|---|---|---|
| 0 | null |
none | This means a null object, which produces errors if actually loaded ingame |
| 1 | Null | none | This means a Null Signal, supported by the game |
| 2 | Conflict | none | |
| 3 | Integer | int | |
| 4 | Integer 0 | none | This produces the same type of Integer Signal as with a 3 byte |
| 5 | Integer 1 | none | This produces the same type of Integer Signal as with a 3 byte |
| 6 | Belt Item | IBeltItem | Ingame, this returns a Null Signal if the IBeltItem object is null |
| 7 | Fluid | IFluid | Ingame, this returns a Null Signal if the IFluid object is null |
The encoded data starts with a byte representing the type of belt item :
| Byte value | Data encoded after |
|---|---|
| 0 | null (nothing encoded) |
| 1 | ShapeItem |
| 2 | FluidPackageItem |
| 3 | FluidPackageOnTrack |
| 4 | ShapePackageOnTrack |
| Type | Description |
|---|---|
| bool | false if the object is null, true otherwise |
| string | The shape code, only there if the previous value was true |
| Type | Description |
|---|---|
| IFluid | The type of fluid contained |
| FluidUnit | The amount of fluid contained |
| Type | Description |
|---|---|
| short | The amount contained |
| IFluid | The fluid contained, only present if the amount isn't 0 |
| Type | Description |
|---|---|
| short | The amount contained |
| ShapeItem | The shape contained, only present if the amount isn't 0 |
The encoded data starts with a byte representing the type of fluid :
| Byte value | Data encoded after |
|---|---|
| 0 | null (nothing encoded) |
| 1 | ColorFluid |
| Type | Description |
|---|---|
| byte | The color's color code (a single character) |
| Type | Description |
|---|---|
| long | The amount of fluid, divide this number by 38419920000 to get the amount in liters |
| Type | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| int |
The channel ID. The upper byte represents the type of channel and the channel value is an int bitwise-OR'ed with the channel type (unexpected behavior can happen if the channel value needs more than 3 bytes to be encoded) :
|