Skip to content

The control channel

Protocol 11. Generated from the channel's declaration (tools/lusine/sdk/channel.py) at build, as the clients are: this page and every client come from the one declaration, so none can disagree with another. A client speaks JSON lines: one request, {"id": n, "op": name, ...arguments}, and one reply, the envelope and the operation's own fields.

Each example below is rendered from the declaration: the JSON line a client sends, and the call in each generated client: Python (lusine_channel.py, shipped in sdk/clients/python/), JavaScript (lusine_channel.js, in sdk/clients/javascript/, for Node) and Lua (lusine_channel.lua, in sdk/clients/lua/). An operation's reply is its table's fields beside the envelope.

A session compiles Trait and calls procedures. compile takes Trait source (functions, procedures and the session's own output quantities) and compiles it against the running game; call runs a procedure by name, the session's, the game's, or step, one step of the game. An action is a compiled procedure that inserts; a read is an output a procedure writes, answered by the call that wrote it, a float in its declared unit and an integer as its count of quanta. The Text tab shows each example as the console takes it (lusine.sdk.wire): NAME(); is a call, anything else Trait source.

The envelope

Field Type Unit
id int the request's id, echoed
ok bool true, or false for a refusal
step int step the step the engine is at
refused text a refusal's code, one of the closed set (optional)
message text a refusal's reason, for a person (optional)
line int a refusal of compiled text: the line in the declaration it names (declaration), else in the text sent (optional)
column int a refusal of compiled text: the column in the line (optional)
declaration text a refusal of compiled text: the declaration its line counts in (optional)

Refusals

A refusal is an answer: the world is unchanged and the session carries on. The codes are a closed set; a code outside it is a protocol failure, and a new code is a row of tools/lusine/errors.py first.

Code When
bad-request the request's shape: a missing or ill-typed argument, a field outside its declared range
name a name the definition does not declare, refused where it is written
stamp an action stamped with a step already passed, or a readout past the engine's history
action an action the game's own rule refuses: out of turn, out of balls
size a load whose extents the running scene cannot take
io a file that cannot be read or written
not-in-this-build an operation or a readout the release does not carry (ship=False), in a release build: an instrument, refused by code rather than absent
script-timeout a script's done for a step that came after the step's budget: the engine went on without its operations
declare a declare whose text is not one line of Trait's runtime subset (readout, event, band, say, sound, setting, panel, on), or is not well formed
unresolved a name in declared text that resolves to no declaration the session can reach
ambiguous a name declared twice under one owner
owner a declaration under another owner's name, or a reach into another session's
unit an expression whose dimensions disagree, or a readout whose expression is not in its declared unit
slot a line's slot with no band and no event field to fill it, or a key no declaration names
budget a declaration past the session's budget of live reduce instances
device a Vulkan object or call the engine needs failed
unsupported an operation this device or this build cannot do: GPU timestamps a run needs, a second compute queue a measurement needs, a developer build a ship=false pass needs
ir a game's compiled program refused at load: one this engine cannot run, or a tuning table beside it of another shape
save a save refused for what it holds, its size and its file aside: another game's, a quantity missing, or one of another kind, element, unit or quantum
capacity a list's appends in one step past its capacity: the step keeps the first in item order and drops the rest
command-line a launch argument the engine does not take
compile a session's Trait source the compiler refuses: its refusals in trait's words, session:LINE:COLUMN: code: why, the line and column of the first in the text sent
token a listener's connection whose first request does not carry the token its token setting asks: the connection is closed

Events

Lines the engine sends unasked, {"event": name, ...fields}.

ready

the first line: the protocol and what ready answers, before any request

Field Type Unit
protocol int the protocol's version; a client speaking another refuses the session
game text the game
version text its version
unit text its unit's id (UNIT_ID): what a session compiles against, and a transcript replays on (optional)
step int step the step the world is at
dt real s the step's duration
spaces list of SpaceExtent each space and its extent
seed int the world's seed (optional)

Operations

describe

The game's spaces and the unit's catalog (what the program declares: its spaces, quantities, kernels and procedures), what the session has compiled so far, and the ready record again (the game, the step, each space's extent) with the protocol's and Trait's versions, for a client attaching to a running engine. What a session may name.

It answers, beside the envelope:

Field Type Unit
describe Definition the game's spaces and the unit's catalog
protocol int the channel protocol's version
trait text the Trait version the engine compiles a session's source under
session Session what this session has compiled: its procedures and outputs
mode text this session's mode: driving (it steps the engine by calling its step)
game text the game
version text its version
unit text its unit's id (UNIT_ID): what a session compiles against, and a transcript replays on (optional)
step int step the step the world is at
dt real s the step's duration
spaces list of SpaceExtent each space and its extent
seed int the world's seed (optional)

May refuse: bad-request.

{"id":1,"op":"describe"}
client.describe()
await client.describe();
client:describe()
> describe

compile

Compile Trait source into this session: functions, procedures and the session's own output quantities, against the running game, its shaders made and loaded; nothing runs. Its names are the session's: an unqualified name in it is looked up in the session's declarations, then the game's root namespace; a name the game declares too is refused. The session keeps its declarations by name: one the session declared before is replaced where it stands, whatever its kind, and a new one is added last. A recompile that leaves the outputs' layout as it was keeps their values (kept); otherwise they start over. A refusal is the compiler's: every refusal in its words, session[NAME]:LINE:COLUMN: code: why with LINE in the declaration NAME's own text (a syntax refusal session:LINE:COLUMN, LINE in the text sent), the first's line, column and declaration in the envelope, and the session is as it was.

Argument Type Unit
text text the Trait source

It answers, beside the envelope:

Field Type Unit
procedures list of text the session's procedures now, by their qualified names
outputs list of text the session's outputs now, by their qualified names
declarations list of text the session's declarations now, by their names, in order
kept bool whether the outputs kept their values (trait: their layout unchanged)
ms int ms the compiler's time for it
refusals list of text a refusal's: every refusal of trait's, in its words (optional)

May refuse: bad-request, compile, unsupported.

{"id":2,"op":"compile","text":"output struct { integer highest; } seen;\nvoid track() { seen.highest = max(seen.highest, max(board.level)); }\n"}
client.compile('output struct { integer highest; } seen;\nvoid track() { seen.highest = max(seen.highest, max(board.level)); }\n')
await client.compile("output struct { integer highest; } seen;\nvoid track() { seen.highest = max(seen.highest, max(board.level)); }\n");
client:compile("output struct { integer highest; } seen;\nvoid track() { seen.highest = max(seen.highest, max(board.level)); }\n")
> output struct { integer highest; } seen;
void track() { seen.highest = max(seen.highest, max(board.level)); }

call

Run a procedure by name, now, between steps: the session's own, the game's (a render among them), or a step. A call takes arguments, values for host quantities stored right before it runs: a session's procedure the session's own it reads (a host struct), a game's procedure or a step the game's, by their qualified names; a name that is none of them is refused name, a value of the wrong kind bad-request. step (or the game's main) is one step of the game, answered once it ran; any other runs outside the steps, with nothing of the game's in flight. Its answer is the procedure run, the step its values are as of, the session's outputs it wrote (each in its declared unit, units; an integer as its count of quanta, quanta; a session's table as its live rows) and the errors it raised. A session changes the game only through what its procedures write.

Argument Type Unit
procedure text the procedure: its name in the session or the game, qualified or not; step for a step
arguments map of any values for host quantities, stored right before it runs: a session's procedure's by their names in the session (args.speed) or their last part where it is one's alone (speed); a game's procedure's or a step's by their qualified names; a float in its declared unit, an integer as its count of quanta, a column of many items the list of them (optional)

It answers, beside the envelope:

Field Type Unit
procedure text the procedure run, by its qualified name
as_of int step the step the values are as of: the steps run so far
outputs map of any each session output the call wrote, by its name in the session
units map of text each float output's declared unit, where it declares one (optional)
quanta map of list of int each integer output's quantum, [numerator, denominator], where it is not 1 (optional)
errors list of text the errors the call raised, as the engine reports them
appended list of any a game's launch's: each row append queued for it, in the order queued, as its receipt gave it: its relation [slot, generation], or the refusal's text (its table full) (optional)
removed list of any

May refuse: bad-request, name, unsupported.

{"id":3,"op":"call","procedure":"throw_red","arguments":{"speed":6.8}}
client.call('throw_red', arguments={'speed': 6.8})
await client.call("throw_red", {arguments: {"speed": 6.8}});
client:call("throw_red", {arguments = {speed = 6.8}})
> throw_red(speed = 6.8);

read

Read the game's columns and tables whole, now, between steps, in one answer: each name a space or a stored column of the game's, qualified or in the game's root namespace. A table is its live rows in slot order, each {"row": slot, FIELD: value, ...} (a table's column its rows with that field alone); a lattice's column its items in the unit's order, with its extents in extents; a single's column its value; a single's or a lattice's space an object of its columns. Each value as a call's outputs are: a float in its declared unit (units), an integer as its count of quanta (quanta), a relation [slot, generation] or null, a float that is no number null. A name the game has no space or stored column of is refused name.

Argument Type Unit
names list of text the spaces and columns to read, by their names

It answers, beside the envelope:

Field Type Unit
as_of int step the step the values are as of: the steps run so far
values map of any each name's value, by the name as given
extents map of list of int each lattice's extents, by the name as given (optional)
units map of text each float column's declared unit, where it declares one, by the name as given (a space's column's as NAME.FIELD) (optional)
quanta map of list of int each integer column's quantum, [numerator, denominator], where it is not 1, named as units names (optional)

May refuse: bad-request, name, unsupported.

{"id":4,"op":"read","names":["board.level","contacts"]}
client.read(['board.level', 'contacts'])
await client.read(["board.level", "contacts"]);
client:read({"board.level", "contacts"})
> read

load

Set a game's stored column whole, now, between steps: its every item in the layout's order (a lattice's in the unit's order, a table's by slot), each given as a call's argument is. Any stored column, not only a host quantity: a starting state the source does not state (a board's pattern). Only the column's own bits are written; a record's other fields keep theirs. A name the game stores no column of is refused name; a count or a value that does not fit, bad-request.

Argument Type Unit
name text the stored column, by its name
items list of any its every item, in the layout's order

It answers, beside the envelope:

Field Type Unit
as_of int step the step the column is set at: the steps run so far

May refuse: bad-request, name, unsupported.

{"id":5,"op":"load","name":"board.alive","items":[true,false,false,true]}
client.load('board.alive', [True, False, False, True])
await client.load("board.alive", [true, false, false, true]);
client:load("board.alive", {true, false, false, true})
> load

append

Add rows to one of the game's input tables as the host appends them: queued for the game's next launch (a step, or a call of a game's procedure), applied by its host operations before it runs. Each row is {FIELD: value, ...} by its fields' names within the row, every stored field given, each value as a call's argument is. That launch's answer carries each row's relation, or the refusal its receipt gave (appended). A name that is no input table of the game's, or a field its row has not, is refused name; a value of the wrong kind, or a row lacking a field, bad-request; an append with a launch in flight, unsupported.

Argument Type Unit
table text the input table, by its name
rows list of map of any the rows, each by its fields' names

It answers, beside the envelope:

Field Type Unit
queued int the rows queued for the next launch so far, these among them
lands int step the step the next launch is numbered: the step they land in

May refuse: bad-request, name, unsupported.

{"id":6,"op":"append","table":"throws","rows":[{"owner":[0,1],"speed":6.8,"aim":0.02}]}
client.append('throws', [{'owner': [0, 1], 'speed': 6.8, 'aim': 0.02}])
await client.append("throws", [{"owner": [0, 1], "speed": 6.8, "aim": 0.02}]);
client:append("throws", {{owner = {0, 1}, speed = 6.8, aim = 0.02}})
> append

remove

Remove rows from one of the game's tables the host may remove from (the unit's removable: a table no binding writes), each by its relation [slot, generation]: queued for the game's next launch (a step, or a call of a game's procedure) as append is, applied by its host operations before it runs, in the order queued with the host's appends. A removed row's slot is freed and its generation moves on, so every relation to it reads none and a later removal by it is refused. A name that is no table the host may remove from is refused name; a relation that is not two whole numbers, bad-request; a removal with a launch in flight, unsupported.

Argument Type Unit
table text the table, by its name
relations list of list of int the rows, each its relation [slot, generation]

It answers, beside the envelope:

Field Type Unit
queued int the removals queued for the next launch so far, these among them
lands int step the step the next launch is numbered: the step they apply in

May refuse: bad-request, name, unsupported.

{"id":7,"op":"remove","table":"pucks","relations":[[0,0]]}
client.remove('pucks', [[0, 0]])
await client.remove("pucks", [[0, 0]]);
client:remove("pucks", {{0, 0}})
> remove

save

Save the whole world by name, at the step boundary, with nothing of the game's in flight: every value the game stores, what its next step reads of the steps before, the values the host keeps, the step and the clock, so the world restored from it steps bit for bit as this one does. A name already saved is written over. The C++ engine writes user/saves/<name>.fsw beside its executable; the browser host keeps it in the page's IndexedDB. A name a save may not take is refused name; a file that cannot be written, io.

Argument Type Unit
name text the save's name: / separates folders, each part a file name

It answers, beside the envelope:

Field Type Unit
as_of int step the step the world was saved at: the steps run so far

May refuse: bad-request, name, io, unsupported.

{"id":8,"op":"save","name":"end3"}
client.save('end3')
await client.save("end3");
client:save("end3")
> save

restore

Put back the world saved by name, at the step boundary, with nothing of the game's in flight and no record queued: the steps are counted on from the save's, so the next step is the save's next and runs bit for bit as it did after the save. A save is the game's while what it holds is the same (the same quantities, alike on every target of the game), whichever build wrote it; another is refused save, by name, as old saves are not kept. No save of that name is refused name; one taken at other extents size; a file that cannot be read io; a restore with records queued (append, remove, a call's stores not yet launched) unsupported. The session's own outputs are untouched.

Argument Type Unit
name text the save's name, as save was given it

It answers, beside the envelope:

Field Type Unit
as_of int step the step the world is at now: the save's

May refuse: bad-request, name, save, size, io, unsupported.

{"id":9,"op":"restore","name":"end3"}
client.restore('end3')
await client.restore("end3");
client:restore("end3")
> restore

drop

Remove declarations from this session by name (its procedures, functions and outputs, as describe's session and a compile's declarations name them), and compile the rest again, answered as a compile is. A name the session does not hold is refused name; a declaration the rest still names refuses the drop compile, and the session is as it was.

Argument Type Unit
names list of text the declarations to remove, by their names

It answers, beside the envelope:

Field Type Unit
procedures list of text the session's procedures now, by their qualified names
outputs list of text the session's outputs now, by their qualified names
declarations list of text the session's declarations now, by their names, in order
kept bool whether the outputs kept their values (trait: their layout unchanged)
ms int ms the compiler's time for it
refusals list of text a refusal's: every refusal of trait's, in its words (optional)

May refuse: bad-request, name, compile, unsupported.

{"id":10,"op":"drop","names":["track"]}
client.drop(['track'])
await client.drop(["track"]);
client:drop({"track"})
> del track;

quit

End the session; the engine exits.

May refuse: bad-request.

{"id":11,"op":"quit"}
client.quit()
await client.quit();
client:quit()
> quit

Records

The named records the operations carry.

SpaceExtent

A space and its extent, as ready and describe give it.

Field Type Unit
name text the space's name
extent list of int 1 its count on each axis (a list's count; a level's extent per axis)

CatalogSpace

A space the program declares.

Field Type Unit
name text its qualified name
kind text single, table or lattice
size int 1 a table's capacity, a lattice's rank, 1 for a single item
element text its struct or type
package text the package declaring it: lusine, or the game's root namespace

CatalogQuantity

A quantity a space holds, as the unit states it.

Field Type Unit
name text its qualified name
space text its space
path text its name within the space
type text its type, in base dimensions
declared text the unit it is declared in, as written; empty for none
scale real a value v in the declared unit is (v + offset) * scale in the base unit
offset real see scale
range_min real its declared range's least, in the base unit (optional)
range_max real its declared range's greatest, in the base unit (optional)
overflow text none, wrap, saturate or refuse
writer text program, host, lusine, or empty for a constant
qualifier text host, input, output, const, or empty
place text device, host, dropped or release-absent
element text bool, float, integer, bits, code, relation, text, ref or struct
lanes int 1 a vector's count, else 1

CatalogKernel

A kernel the program reaches.

Field Type Unit
name text its qualified name
signature text its parameters and result, in base dimensions

CatalogProcedure

A procedure the program runs by its switch.

Field Type Unit
name text its qualified name

Catalog

The unit's catalog: what the program declares, for the clients to be generated from.

Field Type Unit
bases list of text the base dimensions, by name
spaces list of CatalogSpace the spaces
quantities list of CatalogQuantity the quantities, a table's own [live], [generation] and [born] among them
kernels list of CatalogKernel the kernels the program reaches
procedures list of CatalogProcedure the procedures the program runs

Definition

What a session may name: the game's spaces and the unit's catalog.

Field Type Unit
spaces list of SpaceExtent each space and its extent
catalog Catalog the unit's catalog: what the program declares

Session

What the session has compiled so far.

Field Type Unit
procedures list of text its procedures, by their qualified names, each a call may name
outputs list of text its outputs, by their qualified names: what its calls answer
declarations list of text its declarations, by their names, in order: what a drop may name
hosts map of text its own host quantities, by their qualified names, each with the kind a call's argument for it takes: bool, float (a number in its declared unit), integer (a whole count of quanta), code or relation