simulatoraddon
Pages against a server with no time passing, every frame, step, packet and message written down.
Using it
The simulator plays pages against a server with no time passing: one timeline in milliseconds with an event queue on it, and the real code on both ends, the engine, the scene manager, the loopback wire, the player and the game's own rules and actors. Every frame, step, packet and message is written down as it happens, one JSON object a line (JSONL). Two runs of the same config with the same seed give the same bytes. It has no settings in a scene.
From node, a config is a Run (or a RealRun) as JSON, with game, the path of the manager scene file from where node runs, in place of file, sim and base. The sim is the <game>.sim.ts beside the scene file.
{
"game": "games/mygame/mygame.scene.json",
"seed": 7,
"ms": 10000,
"server": { "late": 2, "pause": { "every": 20, "ms": 80 } },
"pages": [
{
"join": 100,
"display": { "hz": 60, "wobble": 3, "drop": 0.02 },
"up": { "delay": 40 },
"down": { "delay": 40 },
"presses": [{ "at": 1000, "key": "ArrowRight" }, { "at": 1800, "key": "ArrowRight", "up": true }]
}
],
"watch": { "kinds": ["player"], "fields": ["pos"], "own": [] }
}
node --experimental-strip-types addons/simulator/simulator.addon.ts run config.json out.jsonl
node --experimental-strip-types addons/simulator/simulator.addon.ts real config.json out.jsonl
node --experimental-strip-types addons/simulator/simulator.addon.ts summary out.jsonl
run plays the config simulated, real plays it on real clocks over a real socket, and summary prints a summary of a recording. The output file is optional for the first two. From code, import the functions below from #addons/simulator/simulator.addon.ts.
A simulated run
The argument of simulate( run ), and the JSON config above. All times are milliseconds on the timeline unless said otherwise.
| Key | Meaning |
|---|---|
ms | How many milliseconds to play. They take as long as the code takes to run them. |
seed | Every roll, the network's, the displays' and the server's, comes from it. |
game or file, sim, base | The manager scene, its sim, and the url its scene files are loaded from. |
server | The server's timing, below. |
pages | One entry per page, below. |
watch | What the records say about rows, below. |
ready | ( truth ), called with the manager's truth once its scenes are loaded and before anything runs, for what the run sets up on the server first, such as a playtester's seat or spawned rows. |
turns | [{ at, turn }]: calls at milliseconds of the timeline, for a knob the run turns as it plays, such as a page's up or down leg, which its link reads live. |
cost | true to write each frame's and step's cost, the processor milliseconds it took. Left out, no cost is written, since no two machines take the same time. |
write | ( line ), called with each record as a JSON line as it is written. |
mount | ( client, scene ), handed to each page's view as it makes its client. |
server
The server is the manager stepped as every game's server steps it, on a clock of the timeline's.
| Key | Meaning |
|---|---|
ppm | Parts in a million the server's clock runs fast. Default 0. |
grain | Milliseconds the server's timer rounds each wait down to, never to less than one grain. Default 1, as in node. With 0 a timer fires on the very millisecond asked for, a nanosecond at the least. |
late | Milliseconds a timer fires late on the mean, an exponential. Default 0. |
pause | { every, ms }: a pause of ms milliseconds falls every every seconds on the mean. Nothing fires while the process is paused, and every timer due inside a pause fires as it ends. |
loop | 'interval' steps once for each firing of a timer asked for again every step's milliseconds, as setInterval does, which is how a server that loses steps is played. 'owed' is the default, the loop every game server runs. |
steps | Times of the timeline: the manager steps at each of them and at no other, as a recording's server did (see timing). |
pages
Each page opens its link at join and runs a view on a loopback wire, or with predict a page that runs its scene ahead.
| Key | Meaning |
|---|---|
join | Milliseconds at which the page opens its link. Default 0. |
open | Milliseconds later that the wire opens, as a socket's handshake does. |
up, down | The wire's legs to the server and from it (see loopback), which hand each envelope on the millisecond it arrives. |
predict | true for a page that runs its scene ahead of the server (predict), as client.html makes it. Its frame is handed the page's clock. |
clock | { offset, ppm }: the page's clock, which its code reads as performance.now, is offset milliseconds plus the timeline's, ppm parts in a million fast. |
display | The page's display, below. |
presses | The page's input, below. |
think | ( rows ), asked before every frame with the rows the page holds. It returns a press, [name, ...args] of the player plugin, or nothing. |
display
| Key | Meaning |
|---|---|
hz | Frames a second. Required. |
phase | Milliseconds past each multiple of a frame's time that vsyncs fall at. Left out, a phase of its own comes from the seed. |
wobble | A frame is called up to this many milliseconds after its vsync. |
drop | The fraction of vsyncs that draw nothing. |
long | { every, ms }: a long frame of ms milliseconds comes every every seconds on the mean and draws nothing on the vsyncs it covers. |
hidden | [[from, to], …]: nothing is drawn while a span is on, as a hidden tab's requestAnimationFrame stops. |
stall | Milliseconds after the page joins that the first frame waits, as a page that is still loading does. |
at | Times of the timeline: a frame is drawn at each of them and at no other, as a recording's page did (see timing). |
presses
A list of inputs handed to the page between frames, as a real keyboard's are.
| Key | Meaning |
|---|---|
at | Milliseconds of the timeline. |
act | [name, ...args], sent through the player plugin as a bound key's message is. |
key, up | The KeyboardEvent.code of a key going down, or going up when up is true, handed to the listeners the page's code bound. |
watch
| Key | Meaning |
|---|---|
kinds | The kinds of row to write down. Every row when left out. |
fields | The fields of each row to write down. Every field when left out. |
own | Which row is the page's own. An empty list means the page's own player row. A field name means the id that field of the player row holds. A list of fields follows the chain: each row reached holds the id in the next field, as ['held', 'chassis'] reaches the body of the car a player row holds. A field names a row by its id, a number, or by { id }. |
drawn | true to record where the page drew things on each frame, between the steps and after a page that runs ahead eased them. The first plugin of the scene that exports drew( f ) supplies it; render3d does, with or without a canvas. Smoothness is read from these, never from the rows, which hold a step's places and not what was drawn. |
A real run
real( run ) plays the same pages with the same records on real clocks over the real socket. Each page dials url, or the manager is booted here behind socket's server on port (the manager's own port when unset, a free one when 0) and stepped on the machine's clock. A real run takes ms, watch, ready, turns, cost and write as above, and pages of { join?, hz, presses?, think?, predict? }, drawing hz frames a second on the machine's timers. A server it does not hold writes no steps and no send times. A real run and a simulated one can be put side by side.
Recording a browser
A page in a real browser is recorded the same way when its address has ?tap=<name>. The name is letters, digits, _, . and - only. The lines go to the simulator's routes, which keep every recording in addons/simulator/runs/<name>.jsonl, the run line first, then a batch a second and the last as the page is hidden. The watch comes from the address too: kinds and fields as comma-separated lists, own as fields separated by dots, and drawn, present or not. Without ?tap the page runs untouched. In a browser the envelopes' send times are unknown and are null.
tap( wire, make, options? ) : Tapped
Wraps the page's view for recording. The page's startup hands it the wire it dials and the function that makes its view on a wire, and uses what it returns as it would the view: input() before the page's steps, frame( now? ) with the time requestAnimationFrame gave, client and close(). options.search is the address's query, the page's own by default, and options.send( body, last ) is how lines leave the page, a fetch to the route by default.
Records
One JSON object a line, each with a type.
| Type | Fields |
|---|---|
run | What was played: mode ('sim' or 'real'), seed, ms, server, each page's knobs in pages, watch, and clocks, each page's clock at the timeline's 0. |
frame | One page's frame. page, t (the timeline's milliseconds it was called at), clock (the page's clock as the frame began) and n (the step of the packet it drew, null before the first). took lists every envelope that arrived since the frame before, each { k, n, sent, at }: its number, its step, when it was sent (null if unknown) and when it arrived. rows has each watched row the page drew, by id, as { kind, …fields }. own is the id of the page's own row. eye is the camera's { pos, dir, target? }, taken from the actor that exports eye(), or else from the first plugin of the scene that does, as chasecamera3d does. drawn, with watch.drawn, is { camera, rows, meshes }: the camera's place, each row's drawn place by id, and every mesh tied to no row by its three.js id. With it comes between, how far past the step the page's clock stood when it drew. cost only with run.cost. |
step | One manager step. t, n (the manager's step after it), scenes (each scene's step), applied (each page's message a scene answered this step, { page, scene, seq, action, n } with n the scene's step that answered it, as heard lists it), rows (each scene's watched rows) and cost when asked for. |
press | An input handed to a page: page, t, act or key (and up), and taken, whether the page had a client or listener to take it. |
packet, message | Each envelope down and each message up: page, scene, seat, k and n (a packet) or seq and action (a message), when it was sent, and at, each time it arrived: none when lost, two when duplicated. A packet also has got, each time the page's word that it got it reached the server. Written once the run is over. |
Methods
simulate( run ) : Promise<Record[]>
Plays a simulated run and returns its records.
real( run ) : Promise<Record[]>
Plays a real run and returns its records.
timing( records ) : { ms, server, pages }
The timing of a recorded run, a real one's above all, for a simulation to play again: when its server stepped (server.steps); when each page's code saw each frame (display.at); how long each page's wire took to open; and each page's network, item by item. Each envelope, message and word that an envelope was got arrives when it arrived, however long the frame that sent it took in the machine's time, and one that never arrived is lost again. A simulation of the same game given it draws what the recording drew, so what a real run shows can be told from what the machine's timers did to it. Whatever the recording does not know, such as a server it did not hold, is left to each leg's middle wait.
running() : { page } | 'server' | undefined
What the run is running at the moment it is asked: a page's frame or input, { page }; the server's step, 'server'; or neither. Whatever times the code can say whose time it is.
Reading a recording
A test uses these to state a fact such as "every frame moves one step" about a run. They read any run's records, a real one's too.
parse( text ) : Record[]
Reads a JSONL file's records.
histogram( records, page ) : { first, counts }
How many of the page's frames moved the step it drew by 0, 1, 2 and so on steps, counted from the frame after its first packet's. first is the timeline's milliseconds of that first frame, or null when the page drew no packet.
standoff( records, page, field = 'pos' ) : { frames, jitter }
The page's own row (watch.own) against its camera, frame by frame from its first packet. frames is { t, d }, d the distance from the camera's eye to the row's field, a point. jitter is { max, rms } of how much d changed from one frame to the next: a camera following smoothly changes it little, one that judders changes it every frame.
packets( records, page ) : Record<number, number>
How many of the page's frames took 0, 1, 2 and so on envelopes, from the frame after its first packet's.
summary( records ) : Summary
{ mode, ms, steps, rate, pages }: the milliseconds run (the last frame's time for a browser recording), the server's steps and their rate a second, and for each page its number, its frame count, and its histogram, standoff jitter and packets.
report( summary ) : string
A summary as lines to print.
In the editor
The simulator's Runs panel shows the recordings kept in addons/simulator/runs/.
Works with
- scenemanagerThe manager scene it boots and steps, and the link each page opens.
- loopbackThe simulated wire between each page and the server.
- socketThe real wire of a real run.
- predictA page that runs its scene ahead.
- playerThe seat's presses, and the
thinkthat asks for one. - heardLists what each scene's step answered.