The .ssp format

The exact text of a .ssp project file (shotsystem 1), line by line: enough to write your own reader or writer that agrees with shot byte for byte.

The file as a whole

UTF-8 text: the header, one empty line, then the body, one line per instance, member or source line.

  • Line ends: shot writes LF, the last line too. Reading drops a CR at the end of a header or body line; source lines keep theirs (SOURCE BLOCKS).
  • BOM: never written; a file starting with one (EF BB BF) reads as if it didn't.
  • Indentation: tabs only, one per level. A space in it is refused.
  • Empty lines, or lines of only spaces and tabs, are skipped in the body, except inside a source block. shot writes none outside one.
  • No comments.
  • Line numbers in shot's messages count from the file's first line, 1.

A whole file

Whole.ssp, a System project, beside Units.ssp (a Generic project whose module has CelsiusOf(fahrenheit : number) : number). It reads with no message, shot save leaves every byte, and shot run Whole.ssp prints hi:

shotsystem 1
kind System
export Main
external "Units.ssp" as Units interface 0342ff2ff560
    .CelsiusOf(fahrenheit : number) -> number
    module Units

id 3e9b7d1f0c5a48e2b6f4a2d8c1e07b95
attribute Version = 3
tag Demo
Folder Data #0a1b2c3d
    BoolValue Enabled #1b2c3d4e
        Value = true
    StringValue "File name" #4e5f6071
        Value = "readings.json"
        tag "two words"
    Vector3Value Offset #5a6b7c8d
        Value = Vector3(0.1, 2, -3.5)
    ObjectValue Target #3d4e5f60
        Value = #4e5f6071
    Color3Value Tint #2c3d4e5f
        Value = Color3(1, 0.5, 0)
        attribute Note = "warm \"orange\"\n"
Luau Helper #5f607182
    source (no newline at end)
        return 1
Script Main #60718293
    source
        use RunService

        print("hi")
scenario Smoke #71829304
    source
        print(root.Data.Enabled.Value)

The header

Every line before the first empty line (empty once a CR at its end is dropped).

  • Line 1 is exactly shotsystem 1, or the file is refused.
  • shot keeps the rest as text with LF ends (CRs at line ends and empty lines at its end dropped), and writes it back so.
  • The reader looks only at kind Impostor (IMPOSTOR FILES). The project tools read the rest; shot check reports a line they can't:
Demo.ssp/.shotsystem:4   a header line is export <Folder.Name> or external "<path>" as <Alias> interface <hash>; got `note hello`

The lines:

kind Generic                                   at most one: Generic, System, Roblox or Impostor (none: System)
export Main                                    at most one: an instance path from the root, names joined by .
external "Units.ssp" as Units                  a project this one uses, and the name it goes by
external "Units.ssp" as Units interface 0342ff2ff560
                                               the same with its interface recorded: 12 hex digits
    .CelsiusOf(fahrenheit : number) -> number  the recorded interface: a tab deeper, under its external line
  • A new header is written in that order; an edited one is kept as it is.
  • A header holds no empty line: that would end it. Exactly one empty line follows.
  • A file with an empty body is the header and that line (shotsystem 1, LF, LF).

The body

A line's leading tabs are its depth; the rest is trimmed. Its first words decide what it is, in this order:

sourcethe start of a source block
source (no newline at end)the same, for a text that doesn't end with a newline
id <project id>the project's id, at depth 0 only
attribute <Name> = <value>an attribute
tag <Name>a tag
<Name> = <value>a property: Name is a letter or _, then letters, digits and _
scenario <Name> #<id>a scenario, at depth 0 only
<Class> <Name> #<id>an instance: Class starts with a capital letter, then letters, digits, _ and .
  • A depth-0 instance is a child of the project root.
  • A member line (source, id, attribute, tag, property) belongs to the instance one level up; at depth 0, to the root.
  • A line is at most one tab deeper than an instance line above it, and no deeper than a member line above it. The first body line is at depth 0.
  • An instance's member lines come before its first child. The root's (id, attribute, tag) come before the first instance; the root has no properties or source.

Instance lines

Folder Data #0a1b2c3d
StringValue "File name" #4e5f6071

The class word, the name, then #id, one space between them as shot writes it (any spaces or tabs as it reads).

  • Names: only ASCII letters, digits and _ . - /: bare. Any other (a space, a sign, a letter outside ASCII, empty): in double quotes, with the escapes of VALUES. A bare name ends at a space, tab, #, " or =.
  • Ids: # and 8 lowercase hex digits. shot writes every one; a line without one gets a new id (IDS AND REFERENCES).
  • Class words: shot's own below, or a Roblox class; any other is refused.
Foldera folder: no properties, no source
Scripta ShotScript file (.shot): its source block is the ShotScript text
Luaua plain Luau module (.luau): its source block is the Luau text
StringValueValue: a string, default ""
NumberValueValue: a number, default 0
IntValueValue: a whole number, default 0
BoolValueValue: true or false, default false
ObjectValueValue: a reference (#id, root or nil), default nil
Color3ValueValue: a Color3, default Color3(0, 0, 0)
Vector3ValueValue: a Vector3, default Vector3(0, 0, 0)
DiskFolderPath: a string, default "": a real folder on disk; the file keeps only its path
ProjectLinkFingerprint, Path, ProjectKind and Root: strings, default "": a link to another project file

Roblox script classes have a source block too, their Luau text as it is:

Roblox.Scripta Roblox Script holding plain Luau (Script is the ShotScript file)
LocalScripta Roblox LocalScript
ModuleScriptread as a Luau; written Script when it holds ShotScript text, Folder when it is a folder module, Luau otherwise (never ModuleScript)

Member lines

Value = "readings.json"
attribute Note = "warm"
tag Demo
tag "two words"
  • Properties: Name = value, written with one space each side of = (read with any). A class takes its own from the table above. Roblox properties only in an Impostor file (IMPOSTOR FILES); elsewhere refused. A value equal to the default is never written.
  • Attributes: attribute Name = value.
    • Name: 1 to 100 ASCII letters, digits and _ . - /; never starting with RBX in any case; never ShotId, ShotHeader or ShotStore, which the file keeps in its own lines. Written bare, also read quoted.
    • Value: a string, number, boolean, Vector2, Vector3, Color3, UDim, UDim2, CFrame, NumberRange, Rect or BrickColor; never nil or a reference.
    • A ProjectLink's four fields are properties, never attributes.
  • Tags: tag Name, bare when ASCII letters, digits and _ . - /, else quoted. One starting with " reads as a quoted string with nothing after it; any other is the rest of the line, spaces included.
  • Each property and attribute at most once per instance.

Values

Every form shot writes, each read back to itself:

"text"                                     a string, between double quotes
"say \"hi\"\n\t1 \\ 2"                     escapes: \" \\ \n \r \t \b \f
"\u0000 \u001b \u007f"                     any other control byte (0-31, 127): \u and 4 lowercase hex digits
"é"                                        every other byte as it is: the file is UTF-8
12                                         numbers: NUMBERS
-0.25
0.000001
1e-07
123000000000000000000
1e+21
-0
inf
-inf
nan
true
false
nil                                        no instance (a reference)
root                                       the project root (a reference)
#3f9a1c2e                                  the instance with that id (a reference)
Enum.Material.Plastic                      an enum item: Enum.<Enum>.<Item>
BrickColor(21)                             a BrickColor, by its number
Content("rbxassetid://123")                a Content with a URI
Content()                                  no Content
Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.Bold, Enum.FontStyle.Italic)
                                           a Font: its family, weight and style, always all three
Vector2(1.5, -2)                           x, y
Vector3(0.1, 2, -3.5)                      x, y, z
Color3(1, 0.5, 0)                          r, g, b
UDim(0.5, 10)                              scale, offset
UDim2(0.5, 10, -0.25, -3)                  x scale, x offset, y scale, y offset
CFrame(1, 2, 3)                            a position with no rotation
CFrame(1, 2, 3, 0, 0, 1, 0, 1, 0, -1, 0, 0)
                                           x, y, z, then the rotation's 9 numbers, row by row (r00, r01, ...)
NumberRange(1, 5)                          min, max
Rect(1, 2, 3, 4)                           min x, min y, max x, max y
NumberSequence([0, 0.5], [1, 1, 0.25])     keypoints: [time, value] or [time, value, envelope]
ColorSequence([0, 1, 0, 0], [1, 0, 0, 1])  keypoints: [time, r, g, b]

A string whose bytes aren't UTF-8 text (an encoded surrogate counts) is written BinaryString("//4="), its bytes in Base64. Today's shot can't open a file holding one: it refuses that line.

Strings:

  • Written: ", \ and control bytes escaped, every other byte as it is.
  • Read: \" \\ \/ \b \f \n \r \t, and \u with 4 hex digits in either case. A high surrogate \u (D800-DBFF) right before a low one (DC00-DFFF) is one character; any other \u is that code point. Any other \ is refused.
  • Other bytes between the quotes, a raw tab too, are taken as they are. Nothing may follow the closing quote.

Typed values: the type word, ( at once, the parts (NUMBERS) separated by commas, written , and read with any spaces. Written exactly as above:

  • a CFrame whose rotation would be 1, 0, 0, 0, 1, 0, 0, 0, 1: its position alone;
  • a NumberSequence keypoint: no envelope when it is 0 ([0, 0.5]); a ColorSequence keypoint: always four parts;
  • a sequence: 2 to 20 keypoints, the first at time 0, the last at 1, times never going down;
  • a Font: always its weight and style; a BrickColor: always its number.

Numbers

A plain number (NumberValue, IntValue, a number attribute) is the shortest decimal that reads back to the same 64-bit value:

  • nan, inf, -inf; -0 for negative zero, 0 for zero;
  • otherwise, with the digits d1 d2 ... dk and the exponent n (d1.d2...dk times 10 to the n): for n from -6 to 20, no exponent (12, -0.25, 0.000001, 123000000000000000000, 9223372036854776000); else d1, . and the other digits if any, e, the sign and n in at least two digits (1e-07, 9.5367431640625e-07, 1e+21, 1.5e+21, 5e-324).

A typed value's part is the shortest text that reads back to the same stored part: shot tries 1 to 17 significant digits (C's %.Ng), takes the first that does, and writes it as above.

  • 32-bit floats: the parts of Vector2, Vector3, Color3, CFrame, NumberRange, Rect and the sequences, and UDim scales. A UDim offset is a 32-bit whole number. So Color3(0.2, 0.5, 1) and UDim2(0.33333334, 5, 0.1, 0).
  • Vector3 and CFrame values on shot's own instances are rounded to multiples of 1/65536 (a Vector3's -0 to 0): shot set V Value 0.3333333333333333 2 -0 writes Vector3(0.33333, 2, 0). A file's Vector3(0.3333333333, 1, 1) is written back Vector3(0.33333, 1, 1); Vector3(0.1, 2, -3.5) stays.

Read: inf, -inf, nan, or an optional -, digits, a . and an exponent (e or E, an optional sign, digits), when the whole is one number. A leading +, hex and _ are refused.

Other forms shot reads

Each is written in the form beside it:

"\/ é"reads as "/ é"
0.50reads as 0.5
.5reads as 0.5
5.reads as 5
-1E-7reads as -1e-07
Vector3( 1,2 ,3 )reads as Vector3(1, 2, 3)
CFrame(1, 2, 3, 1, 0, 0, 0, 1, 0, 0, 0, 1)reads as CFrame(1, 2, 3)
NumberRange(4)reads as NumberRange(4, 4)
NumberSequence([0, 1, 0], [1, 0, 0])reads as NumberSequence([0, 1], [1, 0])
BrickColor("Bright red")reads as BrickColor(21)
Font("rbxasset://fonts/families/GothamSSm.json")reads as Font("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)

Also:

  • a Font with its weight alone (style Normal);
  • a quoted attribute name;
  • Roblox.<Class> for any Roblox class (Roblox.Folder is written Folder);
  • a script class with no source line: empty, written source (no newline at end).

Source blocks

Script Main #60718293
    source
        use RunService

        print("hi")

The line source one level under the instance, then each line of the text one level deeper. Nothing is escaped.

  • Written: a text ending with LF gets source, its last LF left out; any other gets source (no newline at end). Each line gets the block's tabs, except an empty one. An empty text is source (no newline at end) alone.
  • Read: after the source line, each line starting with the block's tabs is a line of the text, those tabs off. An empty line or a lone CR is one too (a lone CR is written back with the tabs). The block ends at any other line.
  • The text is those lines joined by LF, plus one LF, except after source (no newline at end) or with no lines. A CR at a line's end is part of the text.
  • So an empty line right after a block belongs to it.
  • The text isn't read here: shot check checks it later.
  • At most one source block per script. Its other member lines may follow it, but not its first child.

Scenarios

scenario Smoke #71829304
    source
        print(root.Data.Enabled.Value)

A plain Luau script beside the tree (Scenarios), at depth 0 only. It takes a source block, attributes and tags, no properties. Written after the whole tree.

Impostor files

With the header line kind Impostor (nothing else on it but spaces), a Roblox class's line also holds its Roblox properties:

keptits own and its superclasses' in the Roblox API shot carries, of a type with a form in VALUES, or a reference
neverName, Parent, ClassName, UniqueId, SourceAssetId, HistoryId, Capabilities, Sandboxed, DefinesCapabilities, Tags, AttributesSerialize, read-only ones (but Players' MaxPlayers and PreferredPlayers), types with no form (PhysicalProperties, Faces, Axes)
writtenwhen its text differs from a new instance's; a service at depth 0 every one it has a value for
servicesWorkspace, ReplicatedStorage, ReplicatedFirst, ServerScriptService, ServerStorage, StarterGui, StarterPack, StarterPlayer, Lighting, SoundService, Teams, Players, HttpService, Chat, TextChatService, VoiceChatService, AssetService
  • shot's own classes and the script classes keep only their own properties.
  • A reference is #id or root, left out when nil or outside the file. Reading, a value must be the property's own type (an item of its own enum).
  • A number property is the shortest text that reads back the same through it: Lighting's ShadowSoftness is 0.2.
  • A BinaryString or SharedString property is always written BinaryString(...), and also reads from a string.
  • The Workspace's world (<Name>.world/ beside the file) is never in the file. A script placed in the world is written as a child of the Workspace.

Part of an Impostor project. It reads with no message and shot save leaves every byte:

shotsystem 1
kind Impostor

id 7bff6706e9b80909828ae3db85c572a2
ReplicatedStorage ReplicatedStorage #0a0b0c0d
    Archivable = true
    Model Tower #3a3b3c3d
        PrimaryPart = #1a1b1c1d
        Part Brick #1a1b1c1d
            Anchored = true
            Material = Enum.Material.Neon
            Size = Vector3(4, 1, 2)
            attribute Speed = 12
ServerScriptService ServerScriptService #4a4b4c4d
    Archivable = true
    LoadStringEnabled = false
    Roblox.Script Spin #2a2b2c2d
        source
            print(script.Parent.Name)

Writing byte for byte

shot writes in one order, so a writer that follows it makes the same bytes:

the header, its lines ending LF
one empty line
id <project id>                  when the project has one (bare or quoted, as names are)
the root's attributes            sorted by name
the root's tags                  in the order they were added (a repeated one stays)
the root's children              sorted, each as below
the scenarios                    sorted, each as below

and for each instance, depth first:

<Class> <Name> #<id>
its own properties               in the order of the class table (a ProjectLink's: Fingerprint, Path,
                                 ProjectKind, Root), only those not at their default
its Roblox properties            Impostor files only, sorted by name
its attributes                   sorted by name
its tags                         in the order they were added
its source block                 for a script class, always (an empty one too)
its children                     sorted
  • Sorted: by name, then class word, then id, comparing bytes, the shorter first when one starts the other: 10, 9, B, Z, _x, a, "a b", b, é.
  • Values in their forms from VALUES and NUMBERS; the separator = ; every line ending LF.
  • shot rewrites a file only after a change or a fix on reading. shot save of an unchanged project leaves its bytes, other forms included.
  • A file with no id line gets one, written with the next change.

Ids and references

shot fixes a file as it reads it:

  • an instance with no id gets a new one (8 random lowercase hex digits);
  • of two instances with one id, the first keeps it;
  • a reference to an id not in the file becomes nil. A reference may point at any instance, scenarios included, or root.

The fixes are written at once:

Folder Dup1 #40000001
Folder Dup2 #40000001
Folder NoId
ObjectValue Ptr #40000003
    Value = #deadbeef

became

Folder Dup1 #40000001
Folder Dup2 #6dbb5aee
Folder NoId #81af32bb
ObjectValue Ptr #40000003

What shot refuses

A refused file gives one message, line <N>: <why>, and nothing is read:

a ShotScript project file starts with the line `shotsystem 1`                                       (line 1)
lines are indented with tabs, not spaces
indented <T> tabs, but this line can be indented at most <D> (one more than the instance it belongs to)
expected an instance (Class Name #id), a property (Name = value), attribute, tag, source or scenario line
<Word> is not a class: a ShotScript class (Folder, Script, Luau, StringValue, NumberValue, IntValue, BoolValue, ObjectValue, Color3Value, Vector3Value) or a Roblox class
expected a name after <Word> (a name with spaces or signs is written in quotes, like "My name")
expected #id after the name (# and 8 lowercase hex digits, like #3f9a1c2e; a name with spaces is written in quotes)
a scenario line is not indented: scenarios are top-level, beside the tree
the root's id, attribute and tag lines go before the first instance (to give this to an instance, indent it under that instance)
<Class> <Name> (line <L>) has children above this line; an instance's properties, attributes, tags and source go before its children
the project root has no properties, only attribute and tag lines (to give this to an instance, indent it under that instance)
<Class> <Name> has no source (only Script, Luau, scenarios and Roblox script classes do)
<Class> <Name> has a second source
a second id line (the first is line <L>)
expected attribute Name = value
"<name>" can't be an attribute name (letters, digits and _ . - /, at most 100, not starting with RBX in any case)
<name> is kept by the file itself (the id line, the header, the #ids), never as an attribute line
<name> is a <Class> property: write <name> = ...
attribute <name> takes <what>
attribute <name> holds one of string, number, boolean, Vector2, Vector3, Color3, UDim, UDim2, CFrame, NumberRange, Rect, BrickColor (not nil or an instance)
attribute <name> is set twice (also on line <L>)
<name> is set twice (also on line <L>)
expected tag Name or tag "any text"
expected a property (Name = value), attribute Name = value or tag Name
a <Class> has no property <name> (it has <names>)
<Class> has no properties in a ShotScript project file (only Value on the value classes); write it as an attribute
a <Class> has no property <name> a project file holds (...)                                          (Impostor)
<Class>.<name> takes <what>[; expected <what>]
  • A scenario is named scenario <Name> in them, the root "the project root".
  • <what> is the form wanted, or for a value that didn't read, what went wrong:
line 7: IntValue.Value takes a whole number
line 7: StringValue.Value takes a string ("text"); expected a string that doesn't end, or a bad \ escape in it
line 7: StringValue.Value takes a string ("text"); expected something after the string's closing quote
line 7: ObjectValue.Value takes #id, root or nil; expected an id: # and 8 lowercase hex digits, like #3f9a1c2e
line 7: Part.Material takes an item of Enum.Material, like Enum.Material.Plastic; expected a value: "text", a number, true, false, Vector3(1, 2, 3) and the like
line 7: Vector3Value.Value takes a Vector3(x, y, z); expected Vector3(x, y, z), numbers only
  • An instance, property, attribute or tag Roblox itself refuses is refused at its line, with Roblox's words.