Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

State for an agent

An agent reads a running game as data. This page describes the calls an agent makes on useDevtools, the store that the Devtools API describes, and the state the engine keeps apart from the world.

These calls serve an agent that cannot look:

  • step(seconds) advances the bound world by that many seconds of fixed steps on its last input, with no clock. It is the harness’s stepSeconds, and an MCP call: it is never a control in the overlay.

  • dump() returns the world as JSON, the harness’s dumpState: every entity’s data traits keyed by id, a hash of them, and plugins, the plugins the world was made from, the engine’s first, each with its name, description, requires, options, systems, isEngine and omitted, and systems, every system of the step in its order, each with its name, plugin, phase, runsOn, predicted, isEngine and, for a replacement, replaces. The hash covers the entities alone, so two worlds whose entities match hash alike whatever their plugins, and a world on the building blocks alone lists none. spawnite play dump prints it, --where '[npc]' keeps the entities a selector picks, and --fields transform,npc.line keeps only the traits, or fields inside one, that it names, with the entities that hold one, and names each field no entity holds. spawnite simulate --fields and the MCP’s simulate fields narrow a headless run’s dump the same way. A trait name with dots, such as a state’s tag, reads as one key.

  • capture(view) returns a PNG of the scene from a bookmarked camera, and frameView(name) gives the camera for a name on the map of the World the player’s character is in, or of the first World mounted: a views entry, a region, a path, or map for the whole ground from above. The capture leaves out a gizmo, an object drawn for the play camera alone, such as the destination marker; a view marks one by setting gizmoUserData as its root object’s userData. It also leaves out a screen overlay, a quad a view draws in clip space over the whole frame, such as a hurt vignette, which would cover the view it frames: a view marks one with screenOverlayUserData, and capture(view, { overlays: true }) keeps them. The playtest’s screenshot takes the name as view, and a size for the viewport, so an agent shoots the same zone after every edit; the world page says what a view holds.

  • captureScreen() returns a PNG of what the player sees: one render from the camera the game draws with, and nothing the page draws over the canvas, the overlay and the HUD among it. Its screen overlays stay in, and captureScreen({ overlays: false }) leaves them out. Headless, it returns nothing. The overlay’s Screenshot button copies it to the clipboard. Send to agent saves it through the dev server’s /__shots into the game’s .spawnite/shots/, where the playtest’s screenshots land too; the dev server page has the folder.

  • readStats() draws 60 frames and returns what the page costs to draw: three.js’s renderer.info.render for the last of them (draw calls, triangles, points and lines, summed over every render call in the frame, so a composer’s passes count), its renderer.info.memory (geometries and textures), and the mean and worst frame time over the 60. A held world draws them without a step. Headless, it returns nothing. spawnite play stats prints it, --json as an object, and the MCP’s read_render_stats replies with it. Unity’s Stats panel, Unreal’s stat rhi and Godot’s monitors show the same counts.

  • frameShot(camera, aspect) gives the camera for a shot with no view written first, from the mounted map’s data: an angle, a subject and a zoom, or a raw eye and target. With no map mounted, as in a track game, all frames everything the scene draws. A subject the map has no name for may name an entity, by the key its dump files it under, by a selector of what it is such as [monster.kind=colossus], or by its row’s one name among the rows whose entity is still in the world: the shot frames what the page draws of it, whether or not the dump names its traits, or its body or its collider where it draws nothing, where it stands now, so the same camera after a step follows it. findEntities(subject) answers the keys a subject picks, which spawnite play dump --where prints. The camera page says what each takes. The playtest’s screenshot takes it as camera, and a map to shoot through the ?map= preview, and names the eye and the target it resolved to.

  • describe(id) returns one entity’s context bundle: its row in the tree, its behaviours with their props, the wiki page each behaviour declares, the source file a game’s own behaviour declares, the systems that touch it and its relations, the plugins its world was made from as the dump lists them, under panel the values its component declared through useDevtoolsPanel, where it stands on screen, and a PNG screenshot framed on the entity from the running renderer. Headless, there is no screenshot, no screen rect and no panel, since the hook does nothing there. A scattered instance’s id returns its placement in place of traits: its kind, where its foot stands, its height, and its yaw and tilt in degrees, with its family, its World’s map and, where it stands for one of the map’s props, the prop’s name. Its screenshot is framed on the instance, and the actions that move the camera or change the world, focus, hide, delete and add behaviour, are off for it, because only the map’s seed places it. describe takes a row’s id, its entity’s dump key, or its name where one entity row has it. An entity with no row, such as a bolt a system spawns, is found by its dump key and described from its traits, with no row and no behaviours or relations. An id that is one row’s id and another row’s dump key, as a room game’s replica can give, fails naming both rows.

  • setTraits(subject, traits) writes each trait, by the name the dump files it under and in the shape the dump writes it, onto every entity the subject names, as findEntities reads it, and returns their keys: a record’s fields over the ones it holds, null to take a trait off, true to add a tag. A walker whose transform it writes stands there at once, her body built again where the transform says. spawnite play set calls it on a game with no room; a room’s own write goes through the room, below.

  • aimCamera({ yaw, pitch, turn }) turns the player’s camera at once, to a yaw and a pitch in the degrees the engine’s state reads, or by turn degrees of yaw from where it looks, and returns where it looks. spawnite play look calls it.

  • readViews() returns what each view exposes of its own state through useInspect, below.

  • readScope() returns the scope a read of the page sees, which PlayScope types: the world as the dump files it, the engine’s state, the live world and scene, the game’s modules, see and aim, and the director’s input and time. compileRead(expression) reads the scope from an expression, as a browser’s console reads one, and stepUntil reads its condition that way. spawnite play eval runs over it, Playing a held session.

  • startSample(read) reads an expression, or a function of the scope, after each frame the page draws, and the call it returns stops and answers each frame’s value: spawnite play step --sample.

  • see(subject, { part }) returns what the player’s camera sees of each entity a subject picks, or of every entity it draws, each with point, where it shows on the page nearest the middle of what shows, which spawnite play press --on presses, and readAim() where the crosshair points beside each shot the page sent its room: spawnite play see and aim. Headless, both throw.

The dump also holds commands, where the world declares any: timeline, its last 64 commands each with its result, its ticks and who settled it; pending, her page’s commands with no result; and, on a world that decides outcomes, a game played alone, simulate or a test’s room, queues, each handle’s commands waiting for their reader with their sender, lifetime, age and ticks left, and lineages, on a room, each page’s receipts the room keeps until her page confirms them. A page in a room dumps its own side alone. spawnite play dump --commands prints that section alone, as Commands shows.

The dump reads the engine’s traits and each trait a game defines. A Koota trait carries no name, so a game makes its own with defineTrait, which takes the name first, and the dump and describe show it under that name:

export const ScoreTrait = defineTrait("score", { points: 0 });

defineTrait’s options say who a room streams the trait to. Make a trait that holds what a dump cannot write with Koota’s trait: a physics world, a bake, or an object in a scene.

A behaviour declares its description, its page and its file beside its trait, and its row carries all three, so the inspector’s card shows the description. A behaviour whose component only adds the trait is made with defineBehaviour under a name, which puts the trait in the dump and the behaviour in the context menu’s Add behaviour list:

export const GlowBehaviour = defineBehaviour({
name: "glow",
trait: GlowTrait,
runsOn: RunContext.Client,
description: "Glows brighter as the character nears.",
wiki: "/behaviours/glow/",
source: "src/behaviours/glow.ts",
});

A view keeps state no trait holds: the pose it chose for a character lying on a slope, or the strength of a red it fades in a ref. useInspect(name, read) exposes it to the tools. spawnite play dump prints what read returns under views, by the key the dump files the view’s entity under, the nearest Entity’s or the one passed third, or under page for a view that draws none, and spawnite play eval reads the same as views. The read runs only when a tool asks, so it may read what the last frame wrote:

import { useRef } from "react";
import { useInspect } from "@spawnite/engine";
export function HurtVignette() {
// The red's strength, which each frame fades.
const strength = useRef(0);
useInspect("hurt vignette", () => ({ strength: strength.current }));
return null;
}
Terminal window
spawnite play eval "views.page['hurt vignette'].strength"

A --until condition on spawnite play step or spawnite play screenshot reads views too, so --until "views.page['hurt vignette'].strength > 0.5" stops on the frame the red passes half.

A read that throws stands as { "error": "..." } in its place. Unity’s inspector shows a component’s fields as the game plays, and Godot’s remote inspector a node’s; here a view names each value it wants a tool to read, since a function component keeps no fields a tool could find.

The engine keeps its own state apart from the world: where the camera stands and looks, how the frame loop runs, whether the cursor is captured, where the page stands with its room, the plugins its world was made from, and every system of its step. An agent reads it without devtools on a dev build and on a page opened with ?profile, a production build too, as window.__GAME_ENGINE_STATE__(). spawnite play state prints it on the running playtest, the MCP’s read_engine_state replies with the same lines, a --until condition reads it as camera, loop, cursor and room, spawnite play profile reads it as the recording starts and ends, and spawnite play join reads each page’s room. Godot’s remote scene tree and PlayCanvas’s pc.app hand a tool the same live values.

The read returns the following fields:

Field What it holds
camera.position Where the eye stands, in metres.
camera.yaw Degrees about the vertical the camera looks, from -180 to 180: 0 looks down -Z and 90 down -X, as three.js turns an object about Y.
camera.pitch Degrees above the horizon it looks; below it is negative.
camera.fov Degrees the lens sees top to bottom; null for an orthographic camera.
camera.turned Degrees the yaw has swept since the loop mounted, each drawn frame’s turn counted whichever way it went.
camera.locked Whether the camera’s lock is on: the mouse turns it, and the character faces its way.
camera.firstPerson Whether the eye stands in the character’s head.
camera.shake The shake the camera holds, from none at 0 to a whole shake at 1: a game’s big moments raise it, and it fades on its own.
loop.frameloop How React Three Fiber runs the canvas: always, demand while the devtools hold the world, or never while replay playback drives it.
loop.held Whether the devtools hold the world still.
loop.pausedBy The title of the open modal that pauses the world, or null.
loop.speed The devtools’ time scale: 1 is real time.
loop.running False once a frame has thrown and stopped the loop.
loop.steps Fixed steps of 1/60 s the world has taken since the loop mounted, and loop.seconds their seconds.
loop.frames Frames the page has drawn since the loop mounted.
loop.clock Seconds on the canvas’s clock, which a view animates on: it follows the world’s time, so it stands still while the world is held or a modal pauses it, and runs at the devtools’ speed.
cursor.captured Whether the cursor is captured, so the mouse turns the camera. In an automated browser, the engine grants the capture without a lock.
cursor.pointerLock Whether the browser itself holds a pointer lock on the canvas: always false in an automated browser.
room.status connecting until the room’s welcome, joined from then, reconnecting once a socket that opened closes, and closed for good once the room refused her, a minute passed with no socket open, or the game opened no room.
room.characterId Her character, by the id the room’s dump keys it under; null until the welcome.
room.build The page’s build, which each join names; null on a dev server’s page, which names none.
room.refusal The close code and reason the room refused her with, as { code, reason }, whatever the reason; null until it does.
room.full How full the room was when it refused her for being full, as { players, maxPlayers }; null otherwise.
room.buildMismatch The room’s build and the page’s when it refused her for another build, or its protocol and the page’s, as { room: "protocol 6", page: "protocol 5" }, when either refused the other; null otherwise.
room.players Players in the room, as its stream shows them to her.
room.roundTripMilliseconds A ping to the room and back, the mean of the last few; 0 before the first answer.
room.bytesPerSecond What the room’s messages to her weighed over the last second.
room.sentBytesPerSecond What her page’s messages to the room weighed over the last second: her input, a message a frame, her commands, her pings and the rest.
room.leadTicks Ticks her page’s clock runs ahead of the room’s, as the room’s answers to her pings estimate its tick, read once a second; null before the room’s first answer.
room.stepMilliseconds A step of the room’s, on average over the last second, as the room reports it.
room.replayedSteps The steps the room’s corrections of her character had her page replay over the last second.
room.takenSweeps The mover calls in those steps, each moveCharacter and moveAndSlide, that took the record their first run kept rather than sweeping again; none while sweepRecordSettings.check sweeps every one to compare.
room.tiedSweeps The mover calls in those steps that read what their kept record read and swept again, because one of its queries met two colliders at the same distance, which a replay cannot prove it breaks the same way.
room.correctionMilliseconds The milliseconds her page spent over the last second receiving the room’s messages that corrected her character, the restores and replays among them.
room.link The slow link a dev page plays the room over, as { latencyMilliseconds, jitterMilliseconds, stall }, the stall { milliseconds, everySeconds } or null; all zero on a direct link, and absent from a built game.
plugins The plugins the page’s world was made from, the engine’s first, as the dump lists them; empty for a world made from no list. spawnite play state prints each with its description.
systems Every system of the page’s step, in the order it runs them, the core’s among them, each with its name, plugin, phase, runsOn, predicted, isEngine and replaces, as the dump lists them. spawnite play state prints them one line per phase.

A page can run older engine code than its checkout: a tab left open across an edit, or a page from another worktree’s dev server. The engine’s plugin names the engine build in every page a dev server serves, as a spawnite-engine meta tag: a hash of the engine’s code and its package.json, twelve hex digits, alike for two copies of one engine. A game’s dev server answers at /__spawnite/engine with the build it serves now and the folder it reads the engine from.

spawnite play state and the MCP’s read_engine_state end with one line that reads both, such as Engine: build 3f9c02b41e7a, from /work/my-game/node_modules/@spawnite/engine/dist. When the server serves another build than the page loaded, the line says so and asks for a reload, since the page runs the older code until it reloads. A built page names its game’s build instead, and a page from an engine plugin older than the tag names none, so the line says the page names no engine build.