RRTK RSB Specification
The official specification for the RRTK Stream Builder RSB file format.
Version 1.0.0-alpha.0
This work © 2025 by UxuginPython is licensed under CC BY 4.0. To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/
Reading the Specification
This specification assumes knowledge of Rust's basic number types (i32
, f64
, etc.) as well as a bit of familiarity with semantic versioning. Experience with tag-based formats (XML, HTML, etc.) will also be useful but is not necessary. This specification is fairly simple and there is not a lot of background knowledge required. However, there are a couple things that may not be immediately obvious.
"TAG
(value)" Syntax
As you will read later, the RSB format uses an XML-like tag system except binary. Every tag has an i8
value. Often, tags will be written as TAG
(#) where the number in parentheses is this i8
value, e.g., COORDINATES
(3).
Examples
The specification will occasionally provide examples of the layout of a concept. However, providing actual bytes or hex codes would prove quite inconvenient, so they are written like this:
SKIP_U8 15u8 x_f64 y_f64
This example is composed of a tag (an i8
), a u8
, and two f64
s concatenated. Elements of an example may vary in byte count, as is seen here. Whitespace may vary for readability, but all elements must be immediately concatenated.
Specification Versioning
Specification versioning for RSB roughly matches Semantic Versioning. An increase in minor version must not break backwards compatibility with the specification. In other words, a file generated based on specification version 1.1.0 must still be valid in version 1.0.0, although some features may not be present. A program based on 1.0.0 should be able to open a 1.1.0 file but should alert the user that not all features of the file may be usable and that they could be lost upon saving. How programs determine the specification version of a file will be explained in the next chapter.
Major version increases do not have this requirement. A 2.0.0 file does not need to be valid in 1.0.0, and programs based on 1.0.0 should refuse to open it.
Patch versions, of course, should be very minor in their scope and should not add any new features. These versions are only for clarification, and ideally the actual content of files should not change between patch versions. Guidelines for how programs should treat files of mismatched patch version depend on the version's content, but patch version should generally be able to be mostly ignored.
Prerelease version information should be mostly treated like the patch version but a bit more volatile.
Opening a file of an older major or minor version
If possible, programs should alert the user of the version difference and provide two options:
-
Convert the file to the latest version. If a difference in major version, programs should make it clear that this means that the program with which the user previously saved the file will no longer work. Having an interface for creating a backup is strongly recommended.
-
Run in a "compatibility mode" with features from newer versions disabled. Save in the file's existing version. This may be desired even with only a difference of minor version to prevent the following from occuring:
- The user creates the file in a program based on 1.0.0.
- The user opens the file in a program based on 1.1.0 and converts the file, assuming that the original program will still work correctly.
- The user opens the file in the original program again, only to discover that everything 1.1.0-specific has disappeared.
or, even worse,
- The user opens the file in the 1.0.0 program and doesn't see anything odd about newer features missing. The user saves the file and assumes that newer features in the file will be carried over.
- The user discovers in the newer program that previous work done there is lost.
Programs may also add a third option for the case of a major version difference:
- Convert the file to the latest minor version matching the file's existing major version, and then run in the compatibility mode for that version. Programs should make it clear that, although the previous program will still be able to read the file, it will likely not be able to save it without loss of more recently added features. A backup interface is not necessary here because the file can still be read by the older program.
Here's a summary of what these options mean:
Option | Reading Preserved | Writing Preserved |
---|---|---|
1 | No | No |
2 | Yes | Yes |
3 | Yes | No |
"Reading Preserved," obviously, means that programs based on the file's existing version will still be able to read it. "Writing Preserved" means that programs based on the file's existing version can additionally still write to it without loss of information added by a program of a newer version; the scenario described in option 2 cannot occur.
Opening a file of a newer minor version
Programs opening a file of a newer minor version can really only provide a single option: Offer to create a backup, open the file as though it were of an older version, and confirm that the user is OK potentially losing some information added by the newer version before saving. It is very important that programs do not save in an older version without either creating a backup or confirming with the user first.
Prefixed Information
The layout of an RSB file at its most basic level is "magic numbers - version - tag tree." The tag tree is discussed more thoroughly in the next chapter.
Magic Numbers
At the beginning of every RSB file, the UTF-8 string rrtkstrmbldr
appears.
Version
Immediately following the magic numbers, the RSB specification version appears. It is formatted as four u8
s, usually called 'major,' 'minor,' 'patch,' and 'pre.' The first three are identical to the major, minor, and patch components of the specification's semi-semantic version. The 'pre' strays from strict semantic versioning however: Instead of matching one of its numbers, it begins at 0 with the first prerelease and increments by 1 with each subsequent one, regardless of whether it's an alpha, beta, or something else. It is always equal to 255 in stable versions.
Tag Tree
RSB uses an XML-like tag system called the "tag tree" for internal organization. This system is designed for both extensibility and backwards compatibility. Unlike XML though, RSB is a binary format. Each tag has a constant i8
value.
Tags come in two types: matched and unmatched. Matched tags affect everything between an opening and a closing tag. They are generally used for things of varying length or things whose length is likely to change in future versions of the specification. Unmatched tags affect a certain fixed number of bytes after a single tag. They can save a bit of space in some places but are much less extensible, so they are used relatively sparingly.
With matched tags, there must be a 1:1 matchup between start and end tags. Additionally, before an outer block can be closed, all of its nested inner blocks must be fully closed, e.g., the following is not allowed:
X_START Y_START X_END Y_END
| |
| doesn't make sense outside of X block
|
X closed before inner Y
Skip Tags: Fixed and Variable
Skip tags are used to skip a certain number of bytes when searching for tags. This is useful when storing raw numerical information that may unintentionally contain tag bytes which should not be read.
Fixed Skip Tags
SKIP_N
instructs the interpreter to not search for tags within the following N bytes.
SKIP_2 N N Y
N
here means that a byte is skipped during tag searching and Y
means that it is checked for tags. SKIP_2
, of course, skips 2 bytes.
Name | Value |
---|---|
SKIP_1 | -128 |
SKIP_2 | -127 |
SKIP_4 | -126 |
SKIP_8 | -125 |
SKIP_16 | -124 |
Variable Skip Tags
SKIP_U8
(-123): Read the u8
value of the next byte. Add 1 and do not search for tags for that number of bytes after it. For example, one may see the following in a file:
SKIP_U8 3u8 N N N N Y
N
and Y
mean the same as above. SKIP_U8
can skip up to 256 bytes.
SKIP_U16
(-122): Read the u16
value of the next 2 bytes. Add 1 and do not search for tags for that number of bytes after them. For example, one may see the following in a file:
SKIP_U16 4u16 N N N N N Y
Note here that 4u16
takes up two bytes. SKIP_U16
can skip up to 64KiB.
Node Section
The node section is the most important part of the tag tree. There is exactly one node section in an RSB file. It is composed of a block created by NODE_SECTION_START
(1) and NODE_SECTION_END
(-1) tags containing an ordered list of node blocks. All information in a node section besides nodes must be ignored.
Node Blocks
A node block, created with NODE_START
(2) and NODE_END
(-2) tags, represents a Stream Builder node. It contains exactly one of each of three things in any order: the type of node, its coordinates, and a list of its inputs. The way these work is fairly intuitive. All information besides the node ID, coordinates, and its input list must be ignored.
NODE_ID
(0) Tag
NODE_ID SKIP_2 id_u16
Every node block contains a node ID, which tells what kind of node it is. This is represented by a NODE_ID
tag, a SKIP_2
tag, and a u16
node ID. NODE_ID
is an unmatched tag. An index of node IDs is provided at the end of the specification.
COORDINATES
(3) Tag
COORDINATES SKIP_16 x_f64 y_f64
Another element of every node block is its coordinates. The COORDINATES
tag simply holds two f64
s (for x and y) after a SKIP_16
. It too is an unmatched tag. The following is also allowed:
COORDINATES SKIP_8 x_f64 SKIP_8 y_f64
Input List
NODE_INPUT_LIST_START SKIP_U8 (input count * 2)u8
input_u16
input_u16
input_u16
NODE_INPUT_LIST_END
The final element of a node block is its input list. Unlike the node ID and coordinates, the input list is delimited by a pair matched tags, NODE_INPUT_LIST_START
(4) and NODE_INPUT_LIST_END
(-4). Between these tags is simply a list of escaped u16
s. These are zero-indexed indices of the broader node section, each representing an input for the node. Disconnected inputs have a special value of 65535. All inputs must be escaped with a skip tag. Usually, this will take the form of a SKIP_U8
just inside the node input list block. To skip all inputs, the value of the SKIP_U8
should be equal to the number of inputs multiplied by 2 as each input (u16
) takes 2 bytes. Alternatively, an implementor may simple insert a SKIP_2
tag before each input:
NODE_INPUT_LIST_START
SKIP_2 input_u16
SKIP_2 input_u16
SKIP_2 input_u16
NODE_INPUT_LIST_END
Tag Index
Every tag has an associated i8
value. See the Tag Tree chapter for information about matched and unmatched tags.
Unmatched
Name | Value |
---|---|
SKIP_1 | -128 |
SKIP_2 | -127 |
SKIP_4 | -126 |
SKIP_8 | -125 |
SKIP_16 | -124 |
SKIP_U8 | -123 |
SKIP_U16 | -122 |
NODE_ID | 0 |
COORDINATES | 3 |
Matched
Typically, the opening tag of a pair of matched tags is positive and the closing tag is negative, and they have the same absolute value.
Name | Value |
---|---|
NODE_SECTION_START | 1 |
NODE_SECTION_END | -1 |
NODE_START | 2 |
NODE_END | -2 |
NODE_INPUT_LIST_START | 4 |
NODE_INPUT_LIST_END | -4 |
Node Index
Every node has an associated u16
value. See the Node Section chapter for more information.
Name | Value |
---|---|
ConstantGetter | 0 |
NoneGetter | 1 |
Expirer | 2 |
Latest | 3 |
CommandPID | 4 |
EWMAStream | 5 |
MovingAverageStream | 6 |
PIDControllerStream | 7 |
PositionToState | 8 |
VelocityToState | 9 |
AccelerationToState | 10 |
NoneToError | 11 |
NoneToValue | 12 |
FloatToQuantity | 13 |
QuantityToFloat | 14 |
DimensionAdder | 15 |
DimensionRemover | 16 |
FreezeStream | 17 |
IfStream | 18 |
IfElseStream | 19 |
AndStream | 20 |
OrStream | 21 |
NotStream | 22 |
SumStream | 23 |
Sum2 | 24 |
DifferenceStream | 25 |
ProductStream | 26 |
Product2 | 27 |
QuotientStream | 28 |
ExponentStream | 29 |
DerivativeStream | 30 |
IntegralStream | 31 |
Example
Here is an example of a file containing two nodes, each with two inputs, with the first input of the second node connected to the output of the first node. All other inputs are disconnected.
rrtkstrmbldr 1u8 0u8 0u8 0u8
NODE_SECTION_START
NODE_START
NODE_ID SKIP_2 0u16
COORDINATES SKIP_16 0.0f64 0.0f64
NODE_INPUT_LIST_START SKIP_U8 3u8 //Remember that SKIP_U8 skips this number + 1.
65535u16
65535u16
NODE_INPUT_LIST_END
NODE_END
NODE_START
NODE_ID SKIP_2 0u16
COORDINATES SKIP_16 300.0f64 0.0f64
NODE_INPUT_LIST_START SKIP_U8 3u8
0u16
65535u16
NODE_INPUT_LIST_END
NODE_END
NODE_SECTION_END