Modules / Infrastructure

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.

KeyMeaning
msHow many milliseconds to play. They take as long as the code takes to run them.
seedEvery roll, the network's, the displays' and the server's, comes from it.
game or file, sim, baseThe manager scene, its sim, and the url its scene files are loaded from.
serverThe server's timing, below.
pagesOne entry per page, below.
watchWhat 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.
costtrue 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.

KeyMeaning
ppmParts in a million the server's clock runs fast. Default 0.
grainMilliseconds 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.
lateMilliseconds 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.
stepsTimes 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.

KeyMeaning
joinMilliseconds at which the page opens its link. Default 0.
openMilliseconds later that the wire opens, as a socket's handshake does.
up, downThe wire's legs to the server and from it (see loopback), which hand each envelope on the millisecond it arrives.
predicttrue 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.
displayThe page's display, below.
pressesThe 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

KeyMeaning
hzFrames a second. Required.
phaseMilliseconds past each multiple of a frame's time that vsyncs fall at. Left out, a phase of its own comes from the seed.
wobbleA frame is called up to this many milliseconds after its vsync.
dropThe 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.
stallMilliseconds after the page joins that the first frame waits, as a page that is still loading does.
atTimes 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.

KeyMeaning
atMilliseconds of the timeline.
act[name, ...args], sent through the player plugin as a bound key's message is.
key, upThe KeyboardEvent.code of a key going down, or going up when up is true, handed to the listeners the page's code bound.

watch

KeyMeaning
kindsThe kinds of row to write down. Every row when left out.
fieldsThe fields of each row to write down. Every field when left out.
ownWhich 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 }.
drawntrue 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.

TypeFields
runWhat 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.
frameOne 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.
stepOne 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.
pressAn 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, messageEach 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

  • scenemanager
    The manager scene it boots and steps, and the link each page opens.
  • loopback
    The simulated wire between each page and the server.
  • socket
    The real wire of a real run.
  • predict
    A page that runs its scene ahead.
  • player
    The seat's presses, and the think that asks for one.
  • heard
    Lists what each scene's step answered.

In the guide