Blotter File Format/v7: Difference between revisions
m Protected "Blotter File Format/v7" ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)) |
m add anchors and links |
||
| Line 16: | Line 16: | ||
== File structure == | == File structure == | ||
* Header (16 bytes) | * [[#Header|Header]] (16 bytes) | ||
* '''Save info''' | * '''Save info''' | ||
** Save Format Version (1 byte) | ** [[#Save_Format_Version|Save Format Version]] (1 byte) | ||
** Game version (16 bytes) | ** [[#Game_version|Game version]] (16 bytes) | ||
** Save type (1 byte) | ** [[#Save_type|Save type]] (1 byte) | ||
** Number of components and wires (8 bytes) | ** [[#Component_and_wire_counts|Number of components and wires]] (8 bytes) | ||
** Mod versions (4+ bytes, dependent on the number of mods in the save) | ** [[#Mod_versions|Mod versions]] (4+ bytes, dependent on the number of mods in the save) | ||
** Component IDs Map (4+ bytes, dependent on the number of loaded component IDs) | ** [[#Component_IDs_Map|Component IDs Map]] (4+ bytes, dependent on the number of loaded component IDs) | ||
* '''Object data''' | * '''Object data''' | ||
** Components data (0+ bytes, dependent on the number of components) | ** [[#Components_data|Components data]] (0+ bytes, dependent on the number of components) | ||
** Wires data (0+ bytes, dependent on the number of wires) | ** [[#Wires_data|Wires data]] (0+ bytes, dependent on the number of wires) | ||
** Circuit states (4+ bytes, variable, has a different format for Worlds and PartialWorlds) | ** [[#Circuit_states|Circuit states]] (4+ bytes, variable, has a different format for Worlds and PartialWorlds) | ||
* Footer (16 bytes) | * [[#Footer|Footer]] (16 bytes) | ||
The "Save Info" data is at the beginning of the file so that it is easy to extract as metadata. This is done by the game itself in a few places, as well as logicworld.net when uploading a save there. | The "Save Info" data is at the beginning of the file so that it is easy to extract as metadata. This is done by the game itself in a few places, as well as logicworld.net when uploading a save there. | ||
| Line 46: | Line 46: | ||
== Game version == | == Game version == | ||
16 bytes store the game version that this save was created in. | 16 bytes store the game [[#version|version]] that this save was created in. | ||
== Save type == | == Save type == | ||
| Line 68: | Line 68: | ||
== Component and wire counts == | == Component and wire counts == | ||
Here there are two ints. The first specifies the number of components in the save, the second specifies the number of wires. | Here there are two [[#int|ints]]. The first specifies the number of components in the save, the second specifies the number of wires. | ||
== Mod versions == | == Mod versions == | ||
Mods will be included if components they provide are used in this save. This is so that if a later mod version has changes, older save files can be converted. | Mods will be included if components they provide are used in this save. This is so that if a later mod version has changes, older save files can be converted. | ||
* An int for the number of mod versions stored in this file | * An [[#int|int]] for the number of mod versions stored in this file | ||
* For each mod version: | * For each mod version: | ||
** A string for the text ID of the mod | ** A [[#string|string]] for the text ID of the mod | ||
** A version for the version of the mod that this save was created with | ** A [[#version|version]] for the version of the mod that this save was created with | ||
== Component IDs Map == | == Component IDs Map == | ||
When humans add component types to Logic World, they use text IDs like <code>MHG.CircuitBoard</code>. To make save files and network packets small in size, the game internally uses numeric IDs to reference component types. The relationship between text IDs and numeric IDs is not deterministic or consistent between different save files, so the map from text IDs to numeric IDs must be stored with the save. | When humans add component types to Logic World, they use text IDs like <code>MHG.CircuitBoard</code>. To make save files and network packets small in size, the game internally uses numeric IDs to reference component types. The relationship between text IDs and numeric IDs is not deterministic or consistent between different save files, so the map from text IDs to numeric IDs must be stored with the save. | ||
First we write an int for the number of component IDs in this file. | First we write an [[#int|int]] for the number of component IDs in this file. | ||
One by one the mappings are written to the file: | One by one the mappings are written to the file: | ||
* A 2-byte unsigned integer for the numeric ID of the component | * A 2-byte unsigned integer for the numeric ID of the component | ||
* A string for the text ID of the component | * A [[#string|string]] for the text ID of the component | ||
== Components data == | == Components data == | ||
| Line 95: | Line 95: | ||
Each component is written like so: | Each component is written like so: | ||
* The component address of this component | * The [[#component_address|component address]] of this component | ||
* The component address of the component's parent | * The [[#component_address|component address]] of the component's parent | ||
* 2 bytes for the numeric ID of the component's type (see Component IDs Map) | * 2 bytes for the numeric ID of the component's type (see [[#Component_IDs_Map|Component IDs Map]]) | ||
* Three ints for the local fixed position of the component; <code>x y z</code> | * Three [[#int|ints]] for the local fixed position of the component; <code>x y z</code> | ||
** One unit in the fixed position corresponds to a distance 0.001 game units, or 1mm | ** One unit in the fixed position corresponds to a distance 0.001 game units, or 1mm | ||
* Four floats for the local rotation of the component as a quaternion; <code>x y z w</code> | * Four [[#float|floats]] for the local rotation of the component as a quaternion; <code>x y z w</code> | ||
* Information about the component's inputs: | * Information about the component's inputs: | ||
** An int for the number of inputs the component has | ** An [[#int|int]] for the number of inputs the component has | ||
** For each input: | ** For each input: | ||
*** An int for the input's circuit state ID | *** An [[#int|int]] for the input's circuit state ID | ||
* Information about the component's outputs: | * Information about the component's outputs: | ||
** An int for the number of outputs the component has | ** An [[#int|int]] for the number of outputs the component has | ||
** For each output: | ** For each output: | ||
*** An int for the output's circuit state ID | *** An [[#int|int]] for the output's circuit state ID | ||
* The component's custom data: | * The component's custom data: | ||
** An int for the number of bytes in the custom data | ** An [[#int|int]] for the number of bytes in the custom data | ||
*** If the component has no custom data, <code>-1</code> may be written instead of <code>0</code> | *** If the component has no custom data, <code>-1</code> may be written instead of <code>0</code> | ||
** All the bytes of the custom data, in order | ** All the bytes of the custom data, in order | ||
| Line 117: | Line 117: | ||
One by one, every wire is listed in the file. | One by one, every wire is listed in the file. | ||
* The peg address of the wire's first point | * The [[#peg_address|peg address]] of the wire's first point | ||
* The peg address of the wire's second point | * The [[#peg_address|peg address]] of the wire's second point | ||
* An int for the wire's circuit state ID | * An [[#int|int]] for the wire's circuit state ID | ||
* A float for the wire's rotation, relative to the default rotation it would have | * A [[#float|float]] for the wire's rotation, relative to the default rotation it would have | ||
We do not need to save wire addresses; nothing in the save file needs to reference them, and there is no need for them to be consistent between save loads. Instead, wire addresses are dynamically assigned when the save is loaded. | We do not need to save wire addresses; nothing in the save file needs to reference them, and there is no need for them to be consistent between save loads. Instead, wire addresses are dynamically assigned when the save is loaded. | ||
| Line 132: | Line 132: | ||
Worlds are large, and generally use most circuit states, starting from state 0. All circuit states are listed. | Worlds are large, and generally use most circuit states, starting from state 0. All circuit states are listed. | ||
* First an int for the number of bytes in this sequence | * First an [[#int|int]] for the number of bytes in this sequence | ||
* Then, that number of bytes. Each bit represents the value of the next circuit state; each byte contains 8 states. | * Then, that number of bytes. Each bit represents the value of the next circuit state; each byte contains 8 states. | ||
** If, when saving the game, the number of circuit states is not divisible by 8, the last byte will be padded. | ** If, when saving the game, the number of circuit states is not divisible by 8, the last byte will be padded. | ||
| Line 140: | Line 140: | ||
Instead, we save a list of circuit state IDs which are both used in this PartialWorld and have a state of on. All unlisted state IDs are assumed to be in the off state. | Instead, we save a list of circuit state IDs which are both used in this PartialWorld and have a state of on. All unlisted state IDs are assumed to be in the off state. | ||
* First an int for the number of ints in this sequence | * First an [[#int|int]] for the number of ints in this sequence | ||
* Then, that number of ints. Each int signifies a state ID with a value of on. | * Then, that number of ints. Each int signifies a state ID with a value of on. | ||
== Sub-standards == | == Sub-standards == | ||
==== '''Int''' (4 bytes) ==== | ==== '''Int''' (4 bytes) <span id="int"></span> ==== | ||
32 bit signed integers, little endian. | 32 bit signed integers, little endian. | ||
==== '''Float''' (4 bytes) ==== | ==== '''Float''' (4 bytes) <span id="float"></span> ==== | ||
32 bit floating point values, little endian. | 32 bit floating point values, little endian. | ||
==== '''Boolean''' (1 byte) ==== | ==== '''Boolean''' (1 byte) <span id="boolean"></span> ==== | ||
An entire byte that is either <code>00</code> for false or <code>01</code> for true. | An entire byte that is either <code>00</code> for false or <code>01</code> for true. | ||
==== '''String''' (4+ bytes) ==== | ==== '''String''' (4+ bytes) <span id="string"></span> ==== | ||
An int for the number of bytes in the string, followed by that number of bytes, representing that string in UTF-8. | An int for the number of bytes in the string, followed by that number of bytes, representing that string in UTF-8. | ||
==== '''Version''' (16 bytes) ==== | ==== '''Version''' (16 bytes) <span id="version"></span> ==== | ||
Versions contain four numbers (i.e. 1.0.3.1069). These four numbers are written to the file in order, each as a 4-byte int. Thus, 4x4 bytes = 16 bytes for this. Note: if any of these four numbers are missing, they will be saved as <code>-1</code>. So if your mod defines its version as <code>1.2</code>, the four numbers will be <code>1</code>, <code>2</code>, <code>-1</code>, <code>-1</code>. This is not our fault, it's just how System.Version works. | Versions contain four numbers (i.e. 1.0.3.1069). These four numbers are written to the file in order, each as a 4-byte int. Thus, 4x4 bytes = 16 bytes for this. Note: if any of these four numbers are missing, they will be saved as <code>-1</code>. So if your mod defines its version as <code>1.2</code>, the four numbers will be <code>1</code>, <code>2</code>, <code>-1</code>, <code>-1</code>. This is not our fault, it's just how System.Version works. | ||
==== '''Component Address''' (4 bytes) ==== | ==== '''Component Address''' <span id="component_address"></span> (4 bytes) ==== | ||
Component Addresses are written as 32 bit unsigned integers, little endian. | Component Addresses are written as 32 bit unsigned integers, little endian. | ||
==== '''Peg Address''' (9 bytes) ==== | ==== '''Peg Address''' (9 bytes) <span id="peg_address"></span> ==== | ||
* One byte to indicate the peg type: | * One byte to indicate the peg type: | ||
Revision as of 00:52, 13 October 2025
The Blotter File Format, successor to the format known as ".tung files", is a system for storing the components and wires of a Logic World world. It is used for both full world saves as well as partial worlds (the data structure used by Subassemblies), but there are slight differences between the two.
File types
- World files:
- Represent a full, playable world space
- May have zero or more root components at different positions/rotations in the world
- Use
.logicworldfile extension - All circuit states are stored contiguously, from circuit state 0 through the highest state.
- PartialWorld files:
- Represent a substructure within a playable world space; can be loaded and added to a world with each root at an arbitrary position/rotation
- Must have at least one root component
- Use
.partialworldfile extension - Has a list of circuit state indexes that are On. All indexes not listed here are inferred as being Off.
File structure
- Header (16 bytes)
- Save info
- Save Format Version (1 byte)
- Game version (16 bytes)
- Save type (1 byte)
- Number of components and wires (8 bytes)
- Mod versions (4+ bytes, dependent on the number of mods in the save)
- Component IDs Map (4+ bytes, dependent on the number of loaded component IDs)
- Object data
- Components data (0+ bytes, dependent on the number of components)
- Wires data (0+ bytes, dependent on the number of wires)
- Circuit states (4+ bytes, variable, has a different format for Worlds and PartialWorlds)
- Footer (16 bytes)
The "Save Info" data is at the beginning of the file so that it is easy to extract as metadata. This is done by the game itself in a few places, as well as logicworld.net when uploading a save there.
Header and footer
Header
The first 16 bytes of every Blotter file are 4C-6F-67-69-63-20-57-6F-72-6C-64-20-73-61-76-65, which is UTF-8 for “Logic World save”.
Footer
The final 16 bytes of every Blotter file are 72-65-64-73-74-6F-6E-65-20-73-75-78-20-6C-6F-6C , which is UTF-8 for “redstone sux lol”.
The header and footer exist to protect against save corruption. If the file does not have them, we know that something is wrong with the file.
Save Format Version
One byte specifies the save format version. Currently this is 07.
Game version
16 bytes store the game version that this save was created in.
Save type
There are two types of file that use the Blotter format, Worlds and PartialWorlds. One byte specifies which type of file this is.
| Byte | Meaning |
|---|---|
00
|
Unknown/error |
01
|
World |
02
|
PartialWorld |
03 - FF
|
Reserved for future use |
Component and wire counts
Here there are two ints. The first specifies the number of components in the save, the second specifies the number of wires.
Mod versions
Mods will be included if components they provide are used in this save. This is so that if a later mod version has changes, older save files can be converted.
- An int for the number of mod versions stored in this file
- For each mod version:
Component IDs Map
When humans add component types to Logic World, they use text IDs like MHG.CircuitBoard. To make save files and network packets small in size, the game internally uses numeric IDs to reference component types. The relationship between text IDs and numeric IDs is not deterministic or consistent between different save files, so the map from text IDs to numeric IDs must be stored with the save.
First we write an int for the number of component IDs in this file.
One by one the mappings are written to the file:
- A 2-byte unsigned integer for the numeric ID of the component
- A string for the text ID of the component
Components data
One by one, every component is listed in the file. The first component must be a root component -- i.e. have an address of C-0 for its parent -- and every subsequent component must have a parent that was listed before it.
It is explicitly okay to have a World file with no components in it (i.e. an empty world). However, a PartialWorld file must have at least one component.
Each component is written like so:
- The component address of this component
- The component address of the component's parent
- 2 bytes for the numeric ID of the component's type (see Component IDs Map)
- Three ints for the local fixed position of the component;
x y z- One unit in the fixed position corresponds to a distance 0.001 game units, or 1mm
- Four floats for the local rotation of the component as a quaternion;
x y z w - Information about the component's inputs:
- Information about the component's outputs:
- The component's custom data:
- An int for the number of bytes in the custom data
- If the component has no custom data,
-1may be written instead of0
- If the component has no custom data,
- All the bytes of the custom data, in order
- An int for the number of bytes in the custom data
Wires data
One by one, every wire is listed in the file.
- The peg address of the wire's first point
- The peg address of the wire's second point
- An int for the wire's circuit state ID
- A float for the wire's rotation, relative to the default rotation it would have
We do not need to save wire addresses; nothing in the save file needs to reference them, and there is no need for them to be consistent between save loads. Instead, wire addresses are dynamically assigned when the save is loaded.
Circuit states
We've saved the circuit state IDs of all the pegs and wires in the save. Now we need to save the values of those state IDs, so states can be looked up.
A state is either on or off. At runtime, the game stores all circuit states from zero through the highest state in a contiguous block of memory.
World files
Worlds are large, and generally use most circuit states, starting from state 0. All circuit states are listed.
- First an int for the number of bytes in this sequence
- Then, that number of bytes. Each bit represents the value of the next circuit state; each byte contains 8 states.
- If, when saving the game, the number of circuit states is not divisible by 8, the last byte will be padded.
PartialWorld files
PartialWorlds represent a subset of a World. Most of the circuit states in a world will be irrelevant to a particular PartialWorld, so it would be silly and wasteful to save all of them with every PartialWorld. Instead, we save a list of circuit state IDs which are both used in this PartialWorld and have a state of on. All unlisted state IDs are assumed to be in the off state.
- First an int for the number of ints in this sequence
- Then, that number of ints. Each int signifies a state ID with a value of on.
Sub-standards
Int (4 bytes)
32 bit signed integers, little endian.
Float (4 bytes)
32 bit floating point values, little endian.
Boolean (1 byte)
An entire byte that is either 00 for false or 01 for true.
String (4+ bytes)
An int for the number of bytes in the string, followed by that number of bytes, representing that string in UTF-8.
Version (16 bytes)
Versions contain four numbers (i.e. 1.0.3.1069). These four numbers are written to the file in order, each as a 4-byte int. Thus, 4x4 bytes = 16 bytes for this. Note: if any of these four numbers are missing, they will be saved as -1. So if your mod defines its version as 1.2, the four numbers will be 1, 2, -1, -1. This is not our fault, it's just how System.Version works.
Component Address (4 bytes)
Component Addresses are written as 32 bit unsigned integers, little endian.
Peg Address (9 bytes)
- One byte to indicate the peg type:
| Byte | Meaning |
|---|---|
00
|
Undefined |
01
|
Input peg |
02
|
Output peg |
03 - FF
|
Reserved for future use |
Any value other than 01 or 02 is invalid.
- A component address for the component of the peg
- An int for the zero-based index of the peg (i.e. is it the 0th input, the 1st input, etc)