Skip to content

Control plane: `spark`, JSON-RPC and MCP

Everything an agent can do with Sparklight is a command on one typed surface. The CLI spark, the JSON-RPC server, the MCP server, plugins and the editor all call the same commands, with the same JSON Schemas and the same error codes. Design background: docs/AGENT_FIRST.md sections 3.2 and 3.3. Code: crates/sparklight-control (the surface), crates/sparklight-cli (the binary spark). The runtime underneath is described in docs/agent/runtime.md; verification (scenarios, goldens) in docs/agent/testing.md.

You are Do this
An MCP client (Claude, an IDE agent) add the server below, then call scene_load, run_step, world_get …
A script or another process spark serve --listen 127.0.0.1:0 and send JSON-RPC lines, or spark serve --stdio
A shell spark headless <scene>, spark step, spark call <command> '<json>', spark test
Rust code Session::new(SessionConfig::default()).execute("world.get", &json!({"path": "/Hero"}))

MCP server entry (any client that launches stdio servers):

Build the binary with cargo build -p sparklight-cli (it is target/debug/spark).

A first session, as MCP tool calls (a command id world.get is the tool world_get):

scene_load {"path": "samples/platformer/scenes/main.scene.json"} -> {"entities": 10, "frame": 0, "hash": "..."}
run_step {"frames": 60, "input": {"move_x": 1}} -> {"frame": 60, "hash": "..."}
world_get {"path": "/Hero/Transform2D/position"} -> {"value": [-7.13, -6.6]}
world_patch {"path": "/Hero/Sprite", "patch": {"color": {"r": 0.2}}}
snapshot_take {"name": "a"} ... snapshot_restore {"name": "a"} -> hash equals the snapshot's hash
capture_frame {"inline": true} -> a PNG image block
assert_eq {"path": "/Hero/Transform2D/position/1", "expected": -6.6, "tolerance": 0.1}
  • Command: {id, summary, class, scope, input_schema, output_schema, errors[]}. system.commands returns the registry; the files schemas/commands/<id>.schema.json (plus index.json, errors.json) are generated from it and a test fails when they are stale (cargo run -p sparklight-control --example write_command_schemas).
  • Class: read changes nothing, write changes state, run advances simulated time.
  • Scope: what the command touches. world (entities, clock, RNG, input: recorded in the command log, undone by txn.rollback), session (snapshots, subscriptions, transactions, counters), files, platform (Gameway), editor, plugin.
  • Strict input: unknown parameters are an error (E_PARAMS_INVALID), so a misspelled name never silently does nothing. Numbers are JSON numbers; vectors are arrays (position/0 is x).
  • Paths are JSON Pointers into the world: /<entity>/<Component>/<field>/... (docs/agent/runtime.md). Entities are addressed by name or by id #7 when names repeat.
  • Errors are always {code, message, path?, hint?, data?}; match on code. Engine codes (E_COMPONENT_*, E_ENTITY_*, E_SCENE_*, …) pass through unchanged. hint says how to fix it; data carries details (the failing assertion’s expected / actual, the schema violations of a bad parameter, the failing step of a batch).
  • Determinism: a run is (scene, seed, commands). run.hash returns a 64-bit state hash; equal hashes after equal frames mean equal runs. Sessions are muted and headless; they never open an audio device or a window.
  • One thread, one world: a session owns one runtime. Servers handle one request at a time in arrival order, so connections see one consistent world.
Command Class Scope Parameters (* required) What it does
world.despawn write world entity* Delete an entity and its descendants.
world.find read world has, include, limit, name, where Find entities by name pattern, required components and one field predicate; optionally return component values.
world.get read world path Read the value at a path. Empty path lists entities, /Hero returns all components, /Hero/Transform2D/position/0 one number.
world.list read world component, limit, name List entities as {id, name, components}; filter by component or name pattern.
world.patch write world patch*, path* Apply an RFC 7396 merge patch to a component or field; null deletes an optional field; a missing component starts from its default.
world.remove write world path* Remove a component (/Hero/Sprite), an optional field (/Hero/BodySpec/friction) or a whole entity (/Hero).
world.reparent write world entity*, parent* Make an entity a child of another (local transform is kept), or a root with parent: null. Cycles are rejected.
world.set write world path*, value* Write the value at a path; the whole component is re-validated and a failed write changes nothing. /Hero with {component: value} sets several components, all or none.
world.spawn write world components, name* Create an entity from {component: value}; all components are validated, nothing is created when one is invalid.
Command Class Scope Parameters (* required) What it does
component.list read world - List every registered component: name, doc, access (read_write or read_only), persistence.
component.schema read world name JSON Schema and default value of one component, or the schema of a whole entity when no name is given.
Command Class Scope Parameters (* required) What it does
scene.diff read files a*, b* Structural diff of two scenes by object name. Each side is a scene file path or the word current (the live world).
scene.info read world - Summary of the loaded scene: file, assets, seed, entity and body counts, camera, frame, hash.
scene.load write world assets, path*, seed Replace the world with a scene file (clock to 0, RNG reseeded, assets loaded). Without assets the folder <scene dir>/../assets is used when it exists.
scene.new write world seed Replace the world with an empty scene.
scene.save write files path Write the world as a scene file (format version 1). Default path: the file the scene was loaded from. Must stay below the project root.
scene.validate read files path Check a scene file (or the live world) against schemas/scene.schema.json and try to load it; lists every violation with its JSON path.
Command Class Scope Parameters (* required) What it does
prefab.create write files entity*, name, path Build a prefab from an entity and its descendants; returns it, and writes it when path is given.
prefab.instantiate write world path, position, prefab Create a copy of a prefab (file or inline) in the world with its root at position. All or nothing.
Command Class Scope Parameters (* required) What it does
asset.import write files dest*, source* Copy a file into the assets folder under a relative destination and reload the assets (clips, sounds).
asset.list read files kind, root List the files under the assets folder with size and kind (texture, sound, script, clip, tilemap, palette, emitter, prefab, scene, input_map, json, other).
asset.validate read files path, root Validate assets: JSON assets against their schema (clip, tilemap, palette, emitter, prefab, scene, input_map), Lua scripts by compiling them. One file or all.
assets.import write files all, cache, force, path, paths, root Import assets with the settings in their .meta: textures to KTX2 (mips, BC7 / ASTC 4x4 / RGBA8, sRGB or linear), models to optimised meshes with LODs, skins, animations, textures and a prefab document, in a content-hash cache. See assets.md.
assets.reimport write files cache, path*, root, settings Import one asset again, optionally writing new import settings into its .meta first; the output is checked against the stored entry.
assets.report read files cache, root What the import cache knows about every asset: up to date, stale, not imported, invalid, unsupported; keys, outputs and summaries; cache size.
Command Class Scope Parameters (* required) What it does
schema.get read session kind* The JSON Schema of a file format (scene, prefab, …), components for the live registry (includes plugin components), commands for all command specs, errors for the error registry.
schema.list read session - List the file formats with a published JSON Schema (scene, prefab, clip, tilemap, palette, input_map, emitter, components).
schema.validate read files kind*, path, value Validate a JSON file or inline value against a published format; one line per violation with its JSON path.
Command Class Scope Parameters (* required) What it does
run.advance run world input, seconds* Advance simulated time by seconds: as many whole steps as fit, the remainder carries over.
run.hash read world - The state hash: equal hashes after equal frames mean equal runs.
run.pause write session - Mark the session paused: real-time hosts (editor, game window) stop advancing. Explicit run.step still works.
run.reset write world script_limits, seed Go back to the state at load time: clock 0, RNG reseeded, world as loaded. Snapshots taken with snapshot.take are kept. seed starts the run again under another seed (as if the scene had been loaded with it, no rebuild); script_limits changes the limits every script runs under (they stay for this session, also across scene.new and scene.load).
run.resume write session - Clear the paused mark.
run.seek run world frame* Move to a frame. Forward: step. Backward: restore the nearest keyframe at or before it and step forward, applying the input changes recorded since (input.*, run.step {input}) at the frames they happened, so the state equals the state the run had there (exact). The state is the one right after stepping to that frame: an input change made at that frame itself is not applied yet. A new command or step after a backward seek replaces the recorded future. Other edits (world.set, …) are not replayed.
run.status read world - Clock and run identity: frame, simulated seconds, step length, seed, run id, paused, hash.
run.step run world frames*, input Run exactly frames fixed steps (1/60 s each). Optional input sets action values first and keeps them.
run.until run world every, input, max_frames*, op*, path*, value Step until a world value meets a condition or max_frames pass. Checks the condition before the first step and after each every steps.
Command Class Scope Parameters (* required) What it does
input.axis write world action*, value* Set an action to any number, for example move_x to -1..1.
input.clear write world - Set every known action to 0.
input.pointer write world down, x*, y* Move the pointer (world units) and set the button: sets pointer_x, pointer_y, pointer_down.
input.press write world action*, frames Set an action to 1. With frames, hold it for that many steps and then release it (a tap).
input.release write world action* Set an action to 0.
input.state read world - All input action values.
Command Class Scope Parameters (* required) What it does
capture.diff write files a*, b*, max_ratio, out, tolerance Compare two PNG files of equal size. A pixel differs when a channel differs by more than tolerance. Optionally writes a diff image (differences red) below the output folder.
capture.frame write files height, inline, path, width Render the current frame off-screen to a PNG below the output folder. Uses the real (GPU) renderer when the host installed one, else the built-in fallback that draws sprites as flat rectangles.
capture.golden.check write files height, max_ratio, name*, tolerance, width Render the current frame and compare it with the golden image; a differing frame writes golden-<name>.png (the diff) below the output folder.
capture.golden.update write files height, name*, width Render the current frame and store it as the golden image goldens/<name>.png (the only images allowed in the repository).
Command Class Scope Parameters (* required) What it does
snapshot.drop write session name* Forget a snapshot.
snapshot.list read session - List the stored snapshots with frame and hash.
snapshot.load write world path* Rebuild the world from a snapshot.save file by replaying its recipe in a scratch session; the world is replaced only when the state hash matches (E_SNAPSHOT_MISMATCH otherwise, nothing changes). Replay time grows with the run length. The replay runs with a copy of the plugin host, so plugin commands in the recipe work; the plugins the recipe loaded are loaded afterwards.
snapshot.restore write world name* Go back to a snapshot bit for bit; the state hash afterwards equals the snapshot’s hash (checked). Later keyframes are dropped; events are not rewound.
snapshot.save write files path* Write the current state to a file as a replayable recipe: seed, the command log and the state hash of the 2D runtime, and (when the session has a 3D scene) the 3D command log with its frame and hash. snapshot.load rebuilds the world (2D and 3D) by replaying it and checks both hashes. Open transactions are not saved.
snapshot.take write session name Save the whole runtime (world, physics, scripts, animation, particles, input, clock, RNG) under a name. Snapshots live in memory for the session.
Command Class Scope Parameters (* required) What it does
txn.begin write session label Open a transaction: remember the whole world so every change after this can be undone with one txn.rollback. Transactions nest.
txn.commit write session txn Keep the changes and close the newest transaction (or the given one, which must be the newest).
txn.rollback write world txn Undo everything since the transaction began (world, clock, RNG, input) and close it with all newer ones. Events already emitted stay in the stream.
txn.status read session - Open transactions, oldest first.
Command Class Scope Parameters (* required) What it does
batch.run write session atomic, commands*, continue_on_error Run a list of commands in order. With atomic (default) a failure rolls the whole batch back; the error carries the failing index and the results so far.
Command Class Scope Parameters (* required) What it does
log.commands read session since The command log: every successful state-changing command with its frame, in batch.run format. Load the same scene and seed, run it with batch.run and the state hash is identical.
log.tail read session kinds, min_level, n, since, targets The newest events (or those since a seq) without a subscription: the structured log of the run.
Command Class Scope Parameters (* required) What it does
events.poll write session max, subscription* Events a subscription has not seen yet (advances it). missed counts events that left the ring before delivery.
events.status read session - Stream position and subscriptions: next_seq, first_seq (oldest event still held), dropped.
events.subscribe write session delivery, from_start, kinds, min_level, targets Subscribe to the event stream with a filter. Events: script logs and errors, contacts, animation events, finished particle effects, snapshot restores, warnings, Gameway events.
events.unsubscribe write session subscription* End a subscription.
Command Class Scope Parameters (* required) What it does
script.eval write world code*, entity* Evaluate a Lua snippet in the context of an entity’s script and return its value as JSON. The snippet is tried as an expression (return <code>), then as statements. It sees self (the object and its own fields), the script’s functions and globals and the sparklight API; changes to self and the world stay, global assignments do not. Same sandbox and limits as the script’s callbacks (run.reset {script_limits}). A script still waiting behind its ScriptPath is loaded first.
script.list read world - List scripted entities with script path, started and the script’s own state (self fields).
script.reload write world entity Re-read and re-attach the script file of one entity, or of all scripted entities (script state restarts). A script that fails to compile makes the call fail with E_SCRIPT_LOAD and the list of failures.
Command Class Scope Parameters (* required) What it does
plugin.list read plugin - Loaded plugins (id, version, components, commands). Empty when no plugin host is attached.
plugin.load write plugin path* Load a plugin folder (with plugin.json). Its components, commands and systems appear in the registry and the tool list.
plugin.reload write plugin id* Hot-reload a plugin. A failed reload leaves the old version running and returns a coded error.
plugin.unload write plugin id* Unload a plugin; its commands disappear.

The shader.* commands act on the shader set of the session: the built-in library plus every folder loaded with shader.load_dir (see shaders.md for the shader format, the parameters and the E_SHADER_* / E_MATERIAL_* codes). Shader failures keep those codes (path is file:line:column, data holds the full error with the snippet and the hint). Every E_SHADER_* and E_MATERIAL_* code is in the error registry (system.errors, source shader) with its hint, and every shader.* schema lists them under errors. A host that renders with Renderer3D shares the set it renders with through commands::shader::register_shared (the editor does: its 3D tab renders with the set the shader.* commands edit, see editor.md).

Command Class Scope Parameters (* required) What it does
shader.list read session kind, prefix Shaders and materials of the shader set: id, title, kind, shading, origin (built-in, memory, file), version and the error of a broken edit. Filter by kind or id prefix.
shader.describe read session id*, defines The machine-readable description of a shader: parameters (type, default, range, offsets), optional defines, bindings, entry points, what it requires of the renderer. defines selects a variant.
shader.compile read session id, source, name, defines, wgsl Compile a registered shader (id) or a WGSL text (source, not registered) as a variant and report the result as data: ok, the coded error with file, line, column, snippet and hint, a summary, optionally the composed WGSL.
shader.set_param write session material*, name*, value*, save Set a parameter of a material. The value is checked against the shader’s declaration with coded errors; the normalised value is returned. With save the material file is rewritten (below the project root).
shader.reload write session id Hot reload: with id re-read that shader’s file (a broken edit fails with the coded error and the previous version stays active); without it reload every watched file that changed and report one event per file.
shader.load_dir write session path* Load every shader and material below a folder into the set, add it to the import path and watch it for shader.reload. Files that fail to load are reported, not fatal.

The five commands below are declared by the control plane and answer E_EDITOR_NOT_ATTACHED in a headless session. A running editor (sparklight-editor --control) adds about forty more (editor.edit, editor.undo, editor.play, editor.drag …): see editor.md, which also has the window-action → command parity table.

Command Class Scope Parameters (* required) What it does
editor.capture write editor path, width, height, outlines, view, inline Capture the editor viewport (the scene through the editor camera, the selection outlined) to a PNG below the output folder. With view: "model3d" the 3D tab instead: the opened model rendered with the project shader set.
editor.select write editor entities, mode, all Change the selection in the editor: replace it (default), add to it, toggle or remove entities, or select everything.
editor.selection read editor - The entities selected in the editor, the primary (last chosen) one last.
editor.status read editor - Whether an editor is attached, and what it reports (mode, selection, tool).
editor.tool write editor name, snap, paint_tile Choose the editor tool (move, rotate, scale), grid snapping and the brush tile, or read them.
Command Class Scope Parameters (* required) What it does
assert.absent read world path* Nothing exists at path (a despawned entity, a removed component).
assert.count read world component, expected, max, min, name The number of entities (optionally with a component and/or a name pattern) equals expected or lies within min..max.
assert.eq read world expected*, path*, tolerance The value at path equals expected (numbers within tolerance, default 0, recursively inside arrays and objects).
assert.event read session code, contains, fields, kind, max_count, min_count, since, target The number of stream events matching the filter (since since) lies within min_count..max_count (default at least 1). fields must be a subset of the event’s fields.
assert.exists read world path* Something exists at path.
assert.frame read world expected* The frame counter equals expected.
assert.hash read world expected* The state hash equals expected (16 hex digits).
assert.perf read session max*, metric* A frame-time metric (ms per simulated frame, measured by this session) stays within max.
assert.range read world max, min, path* The number at path lies in [min, max] (either bound optional).

See ecs.md for what the answers mean.

Command Class Scope Parameters (* required) What it does
schedule.explain read world - The frame of the 2D scene as a schedule: systems in the order they run with what each reads and writes, and every pair that cannot run at the same time with the reason.
ecs.stats read world - How the 2D scene’s components are stored: per column layout (paged or sparse), values, pages, heap bytes, and advice for paged columns that are mostly empty.
Command Class Scope Parameters (* required) What it does
perf.budget read session assert, budget* Check frame-time limits (p50_ms, p95_ms, p99_ms, worst_ms, cpu_p95_ms, gpu_p95_ms, busy_p95_ms, max_frames_over_33_4, per-scope and gpu:<pass group> limits, skip_frames, min_frames) against the capture; a limit that cannot be measured counts as a failure; with assert a broken budget is E_ASSERT_FAILED.
perf.export write session path Write the capture as a Chrome trace JSON (Perfetto, chrome://tracing, Tracy via tracy-import-chrome) under the output folder.
perf.report read session hitch_factor, hitch_min_ms, max_hitches, skip_frames, top Report of the running or the last stopped capture: frame statistics, costliest CPU scopes, GPU time per pass group, hitches with causes and notes.
perf.reset write session - Forget the frame-time samples (start a clean measurement).
perf.start write session cpu_time, gpu, max_frames, scopes Start a profiler capture (frame times, CPU scopes, thread CPU time, GPU time per render-graph pass).
perf.stop write session - Stop the capture and keep it for report, budget and export.
perf.stats read session - Wall-clock cost of simulated frames measured by this session (ms per frame: mean, p50, p95, p99, max) against the 16.7 ms budget, plus world size.
Command Class Scope Parameters (* required) What it does
system.commands read session brief, id, prefix The command registry: id, summary, class (read/write/run), scope, input and output JSON Schema, error codes. Filter by prefix or id.
system.doctor read session - Environment checks: studio rules (mute, agent run, data root, output folder writable), renderer, Gameway backend, assets. ok is false when a check failed.
system.errors read session - The error code registry: engine, control and Gameway codes with one-line meaning and hint.
system.shutdown write session - Ask the server to stop after this reply (stdio, TCP and MCP servers end; in-process sessions only set a flag).
system.status read session - Session state: run id, seed, frame, hash, scene, assets, open transactions, snapshots, event stream position, backend of Gameway, output folder.
system.version read session - Versions: engine crate, control surface, supported MCP protocol versions.
Command Class Scope Parameters (* required) What it does
gameway.avatar_path write platform size_px*, user* [Gameway] Local avatar file of a user.
gameway.backend read platform - [Gameway] Which backend answers: “sdk” or “fake”.
gameway.chat_history write platform before_seq, conversation*, limit [Gameway] Request chat history.
gameway.chat_mark_read write platform conversation*, seq* [Gameway] Mark a conversation read.
gameway.chat_send write platform conversation*, text* [Gameway] Send a chat message.
gameway.clear_rich_presence write platform - [Gameway] Clear all rich presence.
gameway.client_online read platform - [Gameway] Whether the client reaches the platform.
gameway.connection_state read platform - [Gameway] Connection state of the Gameway client.
gameway.fake.achievements write platform names* Define the achievements the fake knows; unlocking an undefined one fails like on the platform.
gameway.fake.add_friend write platform connect, id, joinable, name* Add a friend to the fake (replaces one with the same id); a friends_changed event follows.
gameway.fake.connection write platform state* Set the fake’s connection state; a connection_state event follows.
gameway.fake.event write platform event* Queue any platform event for the game to poll, written as JSON: {"type": "friend_presence", ...} with the fields gameway.poll_events shows for that type (what the platform sends is what you can script). The event reaches gameway.poll_events, the event stream and the game’s callbacks like a real one.
gameway.fake.online write platform online* Set whether the fake client reaches the platform (offline mode when false).
gameway.fake.state read platform - What the in-memory platform recorded: unlocked achievements, own rich presence, joinable, overlay, call counts.
gameway.friend_rich_presence read platform friend*, key* [Gameway] One rich-presence value of a friend.
gameway.friends read platform - [Gameway] The friends list of this app.
gameway.granted_scopes read platform - [Gameway] Scopes the client granted this app.
gameway.invite_respond write platform accept*, invite* [Gameway] Accept or decline an invite.
gameway.invite_send write platform connect*, to* [Gameway] Invite a friend to a connect string.
gameway.join_friend write platform friend* [Gameway] Ask for a friend’s connect string.
gameway.launch_join_string read platform - [Gameway] The connect string the game was launched with.
gameway.open_client_page write platform arg, page* [Gameway] Ask the client to show a page.
gameway.open_conversation write platform friend* [Gameway] Open the direct conversation with a friend.
gameway.parse_connect read platform connect* [Gameway] Parse a connect string.
gameway.poll_events write platform - [Gameway] Drain the event queue; returns the events as JSON.
gameway.query_leaderboard write platform board*, limit, offset, period, scope [Gameway] Request a page of a board.
gameway.request_achievements write platform - [Gameway] Request the achievements list.
gameway.request_conversations write platform - [Gameway] Request the conversation list.
gameway.request_friend_requests write platform - [Gameway] Request pending friend requests.
gameway.request_leaderboards write platform - [Gameway] Request every board.
gameway.request_stats write platform - [Gameway] Request the stats.
gameway.request_ticket write platform aud, include_name, target_endpoint* [Gameway] Request a launch ticket for ip:port.
gameway.respond_friend_request write platform accept*, request* [Gameway] Accept or decline a friend request.
gameway.run_callbacks write platform - [Gameway] Pump the SDK; call once per frame.
gameway.set_joinable write platform connect, joinable* [Gameway] Tell friends whether they can join, with the connect string.
gameway.set_overlay_active write platform active* [Gameway] The overlay is open.
gameway.set_rich_presence write platform key*, value [Gameway] Set or (value null) clear one rich-presence key.
gameway.stat_add write platform delta*, name* [Gameway] Add to an integer sum stat.
gameway.stat_add_f write platform delta*, name* [Gameway] Add to a decimal sum stat.
gameway.stat_set write platform name*, value* [Gameway] Set an integer stat.
gameway.stat_set_f write platform name*, value* [Gameway] Set a decimal stat.
gameway.submit_score write platform board*, no_toast, score* [Gameway] Submit a score to a board.
gameway.unlock_achievement write platform api_name* [Gameway] Unlock an achievement by its portal id.
gameway.user read platform - [Gameway] The current user.

Parameter details, result shapes and error sets: system.commands {"id": "..."}, the files in schemas/commands/, or the MCP tool list (tools/list). spark commands prints this table from the live registry.

The 3D scene (Space3D) has its own command family: scenes, components with schemas, deterministic stepping, snapshots with a state hash, queries, the physics features (ragdoll, destruction, character, vehicle, rope, cloth), skeletal animation, headless capture through Renderer3D, goldens, assertions and scenario files. The 3D scene is independent of the 2D runtime (txn.*, batch.run rollback and snapshot.* do not cover it; space3d.snapshot.* and space3d.log do). Full reference: docs/agent/space3d.md.

Command Class Scope Parameters (* required) What it does
space3d.new write session assets, gravity Create an empty 3D scene (replacing the session’s current one): a physics world with gravity, no objects.
space3d.load write session assets, demo, json, path Load a 3D scene (replacing the session’s current one): a scene JSON file, inline scene JSON, or a built-in demo. Physics features are built; references to objects that do not exist are an error.
space3d.save write files path* Write the 3D scene to a JSON file below the project root or the output folder (poses are the current ones; velocities are not saved).
space3d.info read session - State of the 3D scene: object and body counts, physics frame, gravity, layers, state hash, snapshots.
space3d.schedule.explain read session - The frame of the 3D scene as a schedule: the passes of one fixed physics step in order with what each reads and writes, and every pair that cannot run at the same time (see ecs.md).
space3d.ecs.stats read session - How the 3D scene’s components are stored: per column layout, values, pages, heap bytes, advice for mostly empty paged columns.
Command Class Scope Parameters (* required) What it does
space3d.list read session component, limit, name List objects as {id, name, components}; filter by component or name pattern.
space3d.get read session path Read the value at a path. Empty path lists objects, /Crate returns all components, /Crate/Transform3D/position/1 one number.
space3d.set write session path*, value* Write a value at a path: a whole component (/Crate/Body) or one field (/Crate/Transform3D/position/1). The component is validated against its schema; physics features are built (or rebuilt) at once, and the call changes nothing when it fails.
space3d.patch write session component*, entity*, patch* Merge a JSON patch (RFC 7386: objects merge, null removes a key) into a component; creates it from its default when the object does not have it.
space3d.remove write session component*, entity* Remove one component from an object (its physics feature is torn down).
space3d.spawn write session components, name* Create an object from {component: value}; every value is validated first and nothing is created when one is invalid. Bodies and physics features are built at the object’s pose.
space3d.despawn write session entity* Delete an object with its descendants, their bodies and their physics features.
space3d.components read session - List every 3D component: name, doc, access (read_write or read_only), persistence.
space3d.component_schema read session name* JSON Schema and default value of one 3D component.
space3d.state read session entity* Live state of an object’s physics features and animator (ragdoll energy and joint error, destructible bond counts, grounded, gear, rope tension, animation state and pose hash) plus its transform and body.
space3d.camera.set write session entity* Make an object with a Camera component the active camera.
Command Class Scope Parameters (* required) What it does
space3d.step run session frames Advance the physics by exactly this many fixed steps (animators, ragdolls, characters, vehicles, destruction and cloth included). Deterministic: the same scene and commands give the same hash.
space3d.advance run session seconds* Advance the physics by simulated seconds (whole fixed steps; the remainder carries over like a real-time frame loop).
space3d.hash read session - The 64-bit state hash of the 3D scene (transforms, every physics body, features, animator poses) and the physics frame. Equal hashes mean equal states.
space3d.snapshot.take write session name Save the whole 3D scene (objects, physics world with contact caches, ragdolls, destructibles, cloth, characters, animators) under a name, with its state hash.
space3d.snapshot.restore write session name* Go back to a snapshot bit for bit; stepping on gives exactly the states the original run had. The state hash afterwards equals the snapshot’s (checked). Reported events are not rewound.
space3d.snapshot.list read session - List the 3D snapshots with frame and hash.
space3d.snapshot.drop write session name* Forget a 3D snapshot.
space3d.determinism.check run session frames, repeats Prove the scene is deterministic from here: snapshot, step N frames and note the state hash, restore, step N frames again and compare. The scene ends in the second run’s state.
space3d.log read session since The 3D command log: every successful state-changing space3d.* command with its physics frame, in batch.run order. Replaying it on a fresh session gives the same state hash.
space3d.events write session clear, entity, kind, limit, since_frame Events the physics features and animators reported: contact, joint_broken, bond_broken, pieces_detached, debris_expired, debris_recycled, anim_event, build_failed, and from Lua scripts script_error, script_log and sound.
Command Class Scope Parameters (* required) What it does
space3d.input write session action*, value Set an input action as scripts read it with sparklight.action(name) (move_x, move_y, jump, fire, …): the value stays until changed. This is how a headless scenario plays the game; the window sets the same actions from the keyboard.
space3d.script.eval write session code*, entity* Run a Lua snippet in the context of an object’s Script (its self, functions, globals and the sparklight API) under the same sandbox and limits as a callback; returns the value as JSON. Changes to self and to the world stay.
space3d.script.limits write session instructions, memory_bytes, wall_ms Read or change the sandbox limits every 3D script runs under: instructions per callback (default 2,000,000), memory per script state (default 16 MiB) and the wall-clock backstop (default 2000 ms). Without parameters it only reports them.

Sequences (*.sequence.json) that play in the 3D scene; reference docs/agent/cinema.md.

Command Class Scope Parameters (* required) What it does
cinema.validate read session json, path, schema Check a sequence file or text against the format: every problem is listed with its code (E_CINEMA_*) and JSON path. No scene needed.
cinema.load write session asset, autoplay, camera, entity, json, loop, path, speed Put a sequence on an object of the 3D scene as its Sequence component (the object is created, named after the sequence, when entity names none). A file or text is validated and embedded; asset keeps a reference to a file below the assets folder. Playback starts at the next step unless autoplay is false.
cinema.play write session entity, from, loop, speed Start or resume a sequence. from first moves the playhead (nothing fires for the skipped part); after the end it restarts from 0. Playback advances with the 3D steps (space3d.step, space3d.advance).
cinema.pause write session entity Pause a sequence at its current time; the scene keeps that state.
cinema.stop write session entity Stop a sequence and go back to time 0 (the scene shows the first frame at the next step).
cinema.seek write session entity, time* Scrub to a time: the scene takes the state of that time at once (objects moved, output camera set), also while paused, and no cue fires. Returns the evaluation that was applied.
cinema.status read session entity Playhead of a sequence (time, playing, looping, speed, finished, output camera, time scale); without entity every sequence of the scene.
cinema.evaluate read session entity, json, path, time* Evaluate a sequence at a time without changing anything: object transforms, the output camera (FOV, depth of field, shot, blend), audio clips sounding and the time scale. Pure: the same time always gives the same answer.
cinema.capture write files entity, height, inline, path, tier, time*, width Scrub to a time and render the frame through the output camera (with the sequence’s depth of field) to a PNG. Needs a GPU adapter. The scene stays at that time.
Command Class Scope Parameters (* required) What it does
space3d.physics.set write session gravity* Change the physics world’s gravity.
space3d.layers read session - The named collision layers (default is bit 0). Bodies, characters, ragdolls and queries refer to layers by name.
space3d.layer.define write session name* Add a collision layer (at most 32). Returns its bit; an existing name returns the same bit.
space3d.body.launch write session entity*, impulse, velocity Give an object’s body a velocity and/or an impulse (throw a ball at a wall).
space3d.raycast read session all, direction*, exclude, include_sensors, layers, max_distance, origin* Cast a ray through the 3D physics world. Each hit names the object and the part (body, ragdoll:<bone>, chunk:<n>, rope:<n>, vehicle, character), the distance, the point and the normal.
space3d.shape_cast read session direction*, exclude, include_sensors, layers, max_distance, position*, rotation, shape* Sweep a sphere, box or capsule through the physics world and return the first contact.
space3d.overlap read session exclude, include_sensors, layers, position*, rotation, shape* Which colliders overlap a sphere, box or capsule at a position.
space3d.closest read session exclude, include_sensors, layers, point* The collider closest to a point (distance, point on it, whether the point is inside).
Command Class Scope Parameters (* required) What it does
space3d.ragdoll.hit write session entity*, impulse*, point*, radius, recover_time Hit a ragdoll: an impulse on the bone nearest to point (a share to bones within radius); the bones lose their animation strength and recover over recover_time. Returns the bone hit.
space3d.ragdoll.set_mode write session entity*, mode* Switch a ragdoll between dynamic (pure physics), kinematic (follows the animation and pushes) and blended (chases the animation with a strength per bone). Leaving kinematic keeps the momentum.
space3d.ragdoll.set_strength write session bone, entity*, strength* Animation strength of a ragdoll bone (or all bones) in blended mode: 1 follows the animation, 0 is pure physics.
space3d.destructible.damage write session damage*, entity*, impulse, point*, radius* Damage a destructible around a point: bonds within radius take damage falling off with distance and along the bond graph; broken bonds release chunks as dynamic debris. Returns bonds damaged and broken and pieces detached.
space3d.destructible.shatter write session entity*, impulse, origin Break every bond of a destructible at once and push the pieces away from origin.
space3d.character.move write session entity*, jump, velocity* Set the wanted horizontal velocity (world space, m/s; y ignored) of a character controller until changed, and optionally press jump for the next step.
space3d.vehicle.input write session analog, brake, clutch, entity*, gear, handbrake, shift, steer, throttle Driver input of a vehicle, held until changed: throttle 0..1 (negative reverses), brake 0..1, steer -1 (left) .. 1 (right), handbrake, clutch 0..1, shift and gear one-shot gear changes for a manual gearbox, analog for sticks and triggers.
space3d.vehicle.telemetry read session entity* Speed, rpm, gear, clutch, pedals, steering, abs/tcs flags, airborne, strongest skid and surface, drift angle, accelerations in g, per wheel load, slip, skid, surface, compression, rpm and position, plus the damage state.
space3d.vehicle.damage write session detach_part, direction, entity*, point, speed Hit a vehicle by hand (speed m/s of speed change at a world point pushing along direction), or tear the named part off: dents the body, damages engine, wheels and suspension, loads the parts.
space3d.vehicle.repair write session entity* Repair a vehicle: systems back to full health, dents gone, every part back on the chassis.
space3d.damage.hit write session detach_part, direction, entity*, point, speed The same as space3d.vehicle.damage for any object with a damage model: a vehicle, or a dynamic body with a VehicleDamage component (crate, door, armour).
space3d.damage.repair write session entity* The same as space3d.vehicle.repair for any object with a damage model.
space3d.vehicle.tune write session entity*, patch* Change the driving description of a live vehicle with a merge patch (springs, tires, gears, differential, aids, steering feel); kept in the Vehicle component.
space3d.vehicle.reset write session entity*, position*, repair, rotation, yaw Put a vehicle back on the road: at a pose, upright, not moving; the damage stays unless repair.
space3d.rope.cut write session entity*, link* Cut a rope or chain after link link (0-based).
Command Class Scope Parameters (* required) What it does
space3d.anim.set_param write session entity*, name*, value* Set an animator parameter: a number for float and int, a boolean for bool, true to fire a trigger (false resets it).
space3d.anim.trigger write session entity*, name* Fire a trigger parameter (cleared when a transition that tests it fires).
space3d.anim.play write session entity*, layer, normalized, state* Force a state of an animator layer at a normalised time (0..1), skipping transitions.
space3d.anim.validate_graph read session entity, graph* Validate an animation graph (a path below the assets folder, or inline JSON text) with coded errors (E_ANIM_GRAPH_* with JSON pointers); with entity the graph is also compiled against that object’s rig and clips.
space3d.ik.set_target write session entity*, position* Set the world-space target of an object’s IkTarget (replacing a target object), e.g. a foot position from a ground ray.
space3d.skeleton.pose read session entity*, final, space Bone transforms of an object’s skeleton in local, model or world space, with a hash of the pose; final includes the ragdoll blend, otherwise the animation (and IK) alone.

Capture, goldens, assertions and scenarios

Section titled “Capture, goldens, assertions and scenarios”
Command Class Scope Parameters (* required) What it does
space3d.capture write files eye, fov_degrees, height, inline, path, target, tier, width Render the 3D scene headless with the PBR renderer (clustered lights, cascaded shadows, IBL, post-processing) to a PNG below the output folder. Needs a GPU adapter (E_SPACE3D_RENDERER otherwise). Generated pieces (ragdoll bones, debris chunks, ropes, cloth, vehicle) are drawn too.
space3d.render.stats read session - Statistics of the last frame the headless renderer drew (space3d.capture, the golden commands): size, draws, draw calls, local lights, lights dropped by the scene-wide limit (lights.max_lights), and cluster_cap: the lights the per-cluster cap (lights.max_per_cluster) left out in that frame (clusters over the cap, light/cluster pairs dropped, lights dropped, the most lights one cluster wanted) with the running maxima since the renderer was created and the number of measured frames that dropped something (the renderer logs one warning at its first overflow and keeps counting). null before the first capture.
space3d.golden.update write files eye, fov_degrees, height, name*, target, tier, width Render the 3D scene and store it as the golden image goldens/<name>.png (the only images allowed in the repository). Inspect it before committing.
space3d.golden.check write files eye, fov_degrees, height, max_ratio, name*, target, tier, tolerance, width Render the 3D scene and compare it with the golden image (default: 6/255 per channel on at most 0.4 percent of the pixels); a differing frame writes golden3d-<name>.png (the diff) and golden3d-<name>.actual.png below the output folder. equal is false on a mismatch; use space3d.assert with kind golden to fail a scenario.
space3d.assert read session code, component, entity, event, expected, eye, field, height, kind*, max, max_count, max_ratio, min, min_count, name, op, path, since_frame, target, tier, tolerance, value, width Check something about the 3D scene; a failing check is E_ASSERT_FAILED with data.expected and data.actual. Kinds: exists/absent (path), eq (path, value, tolerance), range (path, min, max), count (component?, name?, op, value), hash (expected), feature (entity, field like Destructible.bonds_alive, op/value or min/max), event (event kind, entity?, min_count, max_count, since_frame; max_count 0 asserts that none happened), script (entity, code, value, tolerance: the value of a Lua expression in the object’s script), golden (name, …as space3d.golden.check).
space3d.scenario.run run session json, path, update_goldens Run a 3D scenario file: {version: 1, name, scene | demo, assets?, steps: [{"do": "step", "frames": 120}, ...]}. A step do names a space3d.* command without the prefix (step, set, spawn, ragdoll.hit, destructible.damage, body.launch, assert, golden.check, snapshot.take, determinism.check, …) and the other fields of the step are its parameters. Stops at the first failing step (E_SCENARIO_FAILED with the step, its error and the results so far) and writes report.json below the output folder.

Characters (a rig with its face, hair, cloth and foot placement) live in the session next to the 2D runtime and the 3D scene; a character has its own clock that advances only with character.step, and its own renderer for character.capture. Everything about them, the data formats and the sample is in characters.md.

Command Class Scope Parameters (* required) What it does
character.animate write session clip, locomotion_speed, looping, name, speed Choose what the character plays: a clip by name (restarted, looping or not, at a speed) or locomotion (idle blended with walk by ground speed in m/s).
character.capture write files eye, fov_degrees, frames, height, light_direction, light_intensity, name, path, target, width Render the loaded characters headless (ground, key light, shadows, temporal anti-aliasing) to a PNG below the output folder. Needs a GPU adapter (E_CHAR_RENDERER otherwise).
character.face write session blink_enabled, blink_now, clear, expression, look_at, look_weight, morph, name, value, weight Drive the face: set an expression weight (it fades in at the definition’s speed; clear fades all out), switch blinking or blink now, aim the eyes at a model-space point (look_at, null to release) or force one morph target (morph with value, null to release).
character.golden write files eye, fov_degrees, frames, golden*, height, light_direction, light_intensity, name, target, update, width Render like character.capture and compare with the golden image (6/255 per channel on at most 0.4 percent of the pixels), or write it with update. A differing frame writes character-golden-<name>.actual.png below the output folder.
character.ground write session degrees, height, kind*, x_below Set the ground foot placement works against (session-wide): none (foot placement off), flat at a height, a step (ground at height where world x is below x_below) or a slope rising towards +z. Foot IK then places the feet and moves the pelvis on every character.step.
character.hair write session lod, name Choose which hair is drawn by character.capture: the simulated strands, the authored cards or off; returns the groom statistics (guides, rendered strands, points).
character.info read session name Everything about one character: bones, meshes with their morph targets, clips, the face’s expressions and visemes, hair grooms (guides, strands) and cloth pieces.
character.list read session The loaded characters with their step counts, clocks and state hashes, and the session’s ground and wind.
character.load write session name, path*, position, yaw_degrees Load a character from its definition file (*.character.json: the glTF with the armature, skinned meshes, morph targets and animations, plus a face definition, hair grooms, cloth definitions and foot placement). Everything is checked against each other and every problem is named at once (E_CHAR_BUILD). Returns what the character has.
character.morphs read session name The final morph target weights of the last step (animation clips plus the face), the targets each mesh has and the active-target limit (32 per mesh: the strongest weights win and every dropped target is reported once).
character.pose read session bone, name World positions of bones after the last step (all, or one bone), with the cloth and hair summary: cloth stretch and the hair’s root and tip points.
character.retarget read files clip, fps, map, require_complete, source*, target* Retarget animation between two skeletons (loaded characters or glTF files): the bone map (map file, or built by name when absent), rest-pose correction and scaling by hip height; reports mapped and unmapped bones and the scale, and with clip bakes that clip of the source onto the target and returns its duration and tracks. Use require_complete to fail on unmapped bones.
character.say write session intensity, name, phonemes* Lip sync from phoneme timings (data only, nothing is played): ARPABET phonemes with start and end seconds become a viseme track the face plays from now. Unknown phonemes are an error that names all of them (E_ANIM_FACE_PHONEME).
character.scenario.run run files path*, update_goldens Run a character scenario (*.charscenario.json, version 1): it loads a character and runs steps (step, animate, transform, ground, face, say, hash, assert on morph weights, bone heights, cloth stretch, time or a state hash, golden against an image) and then repeats everything except the goldens from scratch: every recorded hash must come out the same. passed is false with the failures listed otherwise.
character.step run session dt, frames, morph_limit, name, strict_limits, velocity, wind Advance a character by frames steps of dt seconds: animation (idle and walk blended by locomotion speed), morph clips, face (expressions, visemes, blinks, eye look-at), foot placement on the session ground, skin palette, cloth, then hair. Deterministic: the same commands give the same state_hash. With strict_limits a step that drops morph targets over the active limit is an error (E_CHAR_MORPH_LIMIT), as a batch or test run wants.
character.transform write session name, position, yaw_degrees Place the character in the world (position and heading). A jump of more than a few metres is a teleport: cloth and hair move with it instead of stretching.
character.unload write session name* Remove a character (its GPU resources are freed with the capture renderer’s next use).
character.validate read files json, kind*, path Check one of the character data files without loading a model: face, groom, cloth, foot_ik, chain, retarget or character (the JSON of the file, or its path). Errors carry the format’s code (E_ANIM_FACE, E_CHAR_HAIR_DEF, E_ANIM_CLOTH, E_ANIM_FOOT_IK, E_ANIM_RETARGET, E_CHAR_DEF) and a JSON pointer; a groom is also built, so a limit is reported (E_CHAR_HAIR_LIMIT).

The character codes are listed in characters.md; the animation-side codes (E_ANIM_FACE*, E_ANIM_RETARGET, E_ANIM_FOOT_IK, E_ANIM_CLOTH, …) are in animation3d.md.

Open worlds: the terrain.* and world.* commands

Section titled “Open worlds: the terrain.* and world.* commands”

A 3D scene can hold an open world: heightfield terrain in streamed cells with continuous LOD, splat-map materials, roads and lakes, foliage and scatter, a physical sky with time of day and fog, and the colliders that make bodies stand on it. terrain.create gives the scene one (a definition object or a world file); the physics steps of space3d.step then stream the cells around the camera. The world saves with the scene (world field of the scene file: the definition plus the edit log), is part of space3d.snapshot.* and of the state hash. world.list, world.get and the other plain world.* commands stay the 2D entity world’s. Full reference: docs/agent/world.md.

Command Class Scope Parameters (* required) What it does
terrain.create write session definition, path, threads Give the 3D scene an open world (replacing any earlier one): heightfield terrain in streamed cells with continuous LOD, splat materials, roads, lakes, scatter rules for grass, trees, bushes and rocks, a physical sky with time of day, fog and wind. Give a definition object, or a world file (JSON). Bodies of the scene stand on the terrain colliders as cells stream in.
terrain.remove write session - Remove the open world from the 3D scene (its colliders go with it at the next restore or step).
terrain.info read session - Describe the open world: size, cells, LOD levels, layers, number of edits, generation and a hash of the edits and loaded cells.
terrain.height read session x*, z* Ground at a point: height of the terrain surface (what bodies stand on), the smooth height, the surface normal, slope in degrees, splat layer weights, hole flag, nearest road and the water level of a lake there.
terrain.raycast read session direction*, max_distance, origin* Cast a ray against the terrain surface (marched in steps no longer than the ground spacing and refined by bisection); returns the first hit.
terrain.layers read session - The material layers of the terrain with their colours, generator rules and tile sizes.
terrain.edit write session edit* Apply one edit given as data: {“op”: “raise”
terrain.raise write session amount, falloff, radius*, x*, z* Raise the ground under a round brush by up to amount metres at the centre.
terrain.lower write session amount, falloff, radius*, x*, z* Lower the ground under a round brush by up to amount metres at the centre.
terrain.smooth write session falloff, radius*, strength, x*, z* Smooth the ground under a round brush: heights move towards the average of their neighbours by strength.
terrain.flatten write session falloff, height, radius*, strength, x*, z* Flatten the ground under a round brush towards height (the height at the centre when absent) by strength.
terrain.paint write session erase, falloff, layer*, radius*, strength, x*, z* Paint a splat layer under a round brush (or erase it). Painted texels keep their paint when the ground under them is edited.
terrain.hole write session fill, radius*, x*, z* Cut a round hole in the terrain (the collider has it too; bodies fall through), or fill one with fill.
terrain.undo write session - Undo the last terrain edit.
terrain.redo write session - Redo the last undone terrain edit.
terrain.edits read session - The edit log: every applied edit in order as data. Replaying it onto a fresh world reproduces the terrain bit for bit.
terrain.save write session path* Write the open world (definition plus edit log) to a JSON file below the project root or the output folder.
world.stream.step run session frames, position Advance the world streaming by whole frames at a position (without rendering): cells load within the per-frame budget, far ones unload, colliders follow. Deterministic with streaming.threads 0 or deterministic true: the same positions give the same cells and hash.
world.stream.drive run session fps, height, path*, speed A headless scenario: move the stream focus along a path at a speed and step the streaming every frame, as a vehicle driving across the world would. Reports cells loaded and unloaded, the most cells installed in one frame, the final hash and a hash of the whole event sequence, so two runs can be compared.
world.stream.focus write session position Set the point the world streams around (a position), or null to follow the active camera again.
world.stream.status read session - Streaming statistics of the open world: cells loaded and in flight, memory, colliders, instances, and (after a render) LOD nodes drawn per level, grass patches and foliage counts.
world.stream.configure write session cells_per_frame, collider_radius, deterministic, load_radius, unload_radius Change streaming settings of the open world: radii (metres), cells installed per frame, uploads per frame, deterministic waiting. Takes effect at the next frame; threads need terrain.create again.
world.cells read session events The loaded cells (sorted) with their instance counts and content hashes, and the events (loaded, unloaded, invalidated, edited) since the last call.
world.hash read session - A hash of the terrain edits and the content of every loaded cell: two runs that streamed the same way agree.
world.scatter.count read session - Count the instances of the scatter rules of the loaded cells (trees, bushes, rocks; grass is generated per patch near the camera), per rule.
world.scatter.list read session area*, limit, rule List the scatter instances (trees, bushes, rocks) of one rule or all of them inside a disc, as the world places them now with the removed ones left out, whether or not their cells are loaded. Each has a stable id {rule, gx, gz} for world.scatter.remove; an id changes when its rule’s density, spacing or seed changes.
world.scatter.remove write session ids, near, rule Remove scatter instances (trees, bushes, rocks): by ids ({rule, gx, gz}, as world.scatter.list gives them) or every instance of the rules inside a disc near. One edit in the log, so terrain.undo puts them back and a saved world replays it. Grass has no instances to remove.
world.atmosphere.get read session - The sky of the open world: the atmosphere definition (Rayleigh and Mie coefficients, fog, volumetric tier), the clock, and the sun now (direction, elevation, light colour).
world.atmosphere.set write session definition, time_of_day Change the sky: any field of the atmosphere definition (given as definition fields to merge: fog, volumetric, mie_g, sun_intensity, latitude, …). time_of_day is a shortcut for the clock.
world.time write session advance, hours Set the time of day (hours), or advance it by advance hours. Moves the sun, the sky and the lighting.

In scenarios (space3d.scenario.run) these steps are named in full ({"do": "terrain.create", "path": "..."}, {"do": "world.stream.drive", ...}) and space3d.assert has the kind world (a field of world.stream.status, terrain.info, terrain.height or world.atmosphere.get). Errors raised here: E_WORLD_NONE, E_WORLD_JSON, E_WORLD_SIZE, E_WORLD_SPACING, E_WORLD_NODE, E_WORLD_CELL, E_WORLD_CONTROL, E_WORLD_LAYERS, E_WORLD_ROAD, E_WORLD_LAKE, E_WORLD_STREAMING, E_WORLD_SCATTER, E_WORLD_MODEL, E_WORLD_ATMOSPHERE, E_WORLD_RENDER, E_TERRAIN_RANGE.

Weather and sky: the weather.* and sky.* commands

Section titled “Weather and sky: the weather.* and sky.* commands”

The open world has volumetric clouds and a weather system: clear, cloudy, overcast, rain, storm, snow and fog, transitions over simulated time, wet ground, puddles, snow cover, lightning, wind. It is data and a seeded clock: the same commands give the same state and hash, and the weather is in space3d.snapshot.*, the saved scene and the state hash while it is enabled. Full reference: docs/agent/weather.md.

Command Class Scope Parameters (* required) What it does
weather.get read session - State and parameters now, the transition in progress, wetness, puddles, snow cover, wind, flash, hash.
weather.presets read session - The named states with their parameters.
weather.set write session kind, params, enabled Jump to a named state with optional parameter fields; enabled switches the weather system on or off.
weather.transition write session kind*, seconds*, params Blend from the weather now to a state over simulated seconds.
weather.step write session seconds* Advance the weather alone; returns the thunder that happened.
weather.configure write session definition* Change the weather definition (enabled, initial, seed, wetting and melting rates).
weather.ground write session wetness, puddles, snow_cover, clear Set or clear the wet ground, puddles and snow.
weather.events write session - Thunder since the last call: time, distance, seconds until it is heard.
sky.get read session - Atmosphere and cloud definition, the clock, the sun, the cloud settings now and the overcast.
sky.set write session time_of_day, definition Change the sky (same as world.atmosphere.set; clouds holds the cloud layer).
sky.clouds write session enabled, definition Switch the volumetric clouds on or off and change the layer.
sky.cloud_shadow read session x*, z*, height Share of the sun’s light that reaches a ground point (CPU, same model as the renderer).

Errors raised here: E_WORLD_NONE, E_WEATHER_KIND, E_WEATHER_PARAM, E_WEATHER_DEF, E_WORLD_ATMOSPHERE.

3D positional sound, buses with ducking, blend sounds (engine layers by RPM), one-shot pools and music crossfades, all rendered offline and measured – the control plane never opens an output device and never plays sound. The state belongs to the session’s command registry; it is not part of snapshots and never changes the 3D state hash. Components AudioSource3D, AudioListener and ImpactSound put sounds on space3d objects. Full reference: docs/agent/audio.md. Error codes: E_AUDIO_UNKNOWN_CLIP, E_AUDIO_UNKNOWN_NAME, E_AUDIO_UNKNOWN_VOICE, E_AUDIO_INVALID, E_AUDIO_LOAD, E_AUDIO_TOO_MANY_VOICES.

Command Class Scope Parameters (* required) What it does
audio.analyze read session channel, end, start, tone Measure a part of the last audio.render_offline buffer: peak, RMS, dominant frequency (FFT), frequency from zero crossings, spectral centroid (brightness: a low-pass lowers it), the largest step between samples, and the amplitude of a chosen tone.
audio.blend.define write session blend* Register a blend sound: layers (looping clips) whose volume and pitch follow named parameters through piecewise curves. A car engine: three loops crossfaded and pitched by rpm, one layer only under load. See docs/agent/audio.md for the format.
audio.bus.set write session bus*, fade, muted, volume Set a bus: volume (0..1), mute, and an optional fade time. The master bus scales everything. Changes are ramped inside a 3 ms block, never stepped.
audio.clip.define write session name*, path, synth Register a named clip: either generated (synth: waveform, frequency, seconds, amplitude; deterministic, loopable) or loaded from a file (path, WAV or OGG, mono or stereo). Sound names can also be used directly as synth:sine:440:1 or a file below <assets>/sounds/.
audio.duck.set write session attack_s, gain_db, hold_s, name*, release_s, target*, threshold, trigger Add or replace a ducking rule: while the trigger bus is sounding above threshold (or while audio.duck.trigger holds it), the target bus is lowered by gain_db with an attack, a hold and a release. Music under the announcer: trigger ui, target music.
audio.duck.trigger write session hold, name* Fire a ducking rule for hold seconds without a sounding trigger bus (a crash lowers the music: audio.duck.trigger {name: "music_under_crash", hold: 1.5}).
audio.listener.set write session forward, position, up, velocity Place the listener by hand. A 3D scene with an AudioListener object, or an active camera, overrides this at every step; use it for a scene-less test.
audio.load write files assets, path* Apply an audio setup file (bus volumes, ducking rules, clips, blend sounds, pools) – the same format as <assets>/audio.json, which is applied automatically once per assets folder.
audio.music.play write session fade, looping, name*, path, synth, volume Start a music track on the music bus. Whatever plays fades out over the same fade seconds while the new track fades in: an equal-power crossfade. The track is a file (path, streamed) or a generated clip (synth).
audio.music.stop write session fade Fade the music out.
audio.play write session blend, bus, doppler, fade_in, forward, loop_crossfade_ms, looping, max_distance, min_distance, model, params, pitch, pool, position, priority, random_start, rolloff, smoothing_ms, sound, velocity, volume Start a sound: a clip (sound), a blend sound (blend) or a pool (pool). With position it is placed in the world (distance attenuation, pan with interaural delay, doppler from velocity); without, it plays flat. Returns the voice id; steer it with audio.voice.set, end it with audio.stop.
audio.pool.define write session pool* Register a one-shot pool: clips picked at random (never the same twice in a row) with random volume and pitch ranges and a limit of simultaneous instances. Play it with audio.play {pool}.
audio.render_offline run session format, path, scene, seconds*, tones, window Render seconds of the mix to a buffer – no device, no sound – and measure it: peak, RMS, a hash (equal hashes are bit-identical audio), the largest step between samples (a click detector), the RMS envelope per window, the level of chosen frequencies per window (tones), and the dominant frequency. With scene, the 3D scene is stepped in lockstep (one fixed step, then the audio is brought in step with the scene and one step of sound is rendered), so a car driving past a listener is heard with its real velocity; the steps are logged like space3d.step. path also writes a WAV file. The buffer is kept for audio.analyze.
audio.reset write session - Forget everything: voices, clips, blend sounds, pools, ducking rules, buses back to full volume, the listener and the last render.
audio.status read session - State of the 3D audio stage: sample rate, time rendered, voice counts (real and virtual), buses (volume, mute, gain, peak), ducking rules, music tracks, the listener, registered clips, blend sounds and pools, and recent notes. The control plane never opens an output device; sound is verified by rendering offline.
audio.stop write session fade, voice Fade a voice out (default 5 ms, so a sound in mid-wave does not click) and remove it; without voice, every voice. Sounds attached to scene objects are not stopped for good: set the component’s play to false instead.
audio.voice.set write session forward, occlusion, params, pitch, position, priority, velocity, voice*, volume Steer a voice: volume and pitch (smoothed, so a continuously driven value does not click), position, velocity and facing of a placed voice, occlusion (0 clear .. 1 behind a wall: a low-pass and a volume loss), priority, and the parameters of a blend sound.
audio.voices read session - List the sounding voices: bus, priority, real or virtual, gain, distance to the listener, pitch (with doppler), play position.
Section titled “Navigation and AI: the nav.* and ai.* commands”

Navigation meshes baked from the 3D scene or the 2D scene, path queries, dynamic obstacles, off-mesh links, crowd steering, behaviour trees and utility AI as JSON, all with schemas and coded errors (E_NAV_*, E_AI_*). With a 3D scene the navigation and AI state is part of the scene: space3d.step runs the AI, space3d.snapshot.* and snapshot.save / snapshot.load carry it and space3d.hash covers it. Full reference: docs/agent/navigation.md.

Command Class Scope Parameters (* required) What it does
nav.bake write session agent_height, agent_radius, areas, bounds, cell_height, cell_size, exclude, include_destructibles, include_kinds, include_sensors, max_edge_len, max_simplification_error, max_slope_deg, min_region_cells, name, obstacles, solid_rects, source, step_height, walkable Bake a navigation mesh.
nav.drop write session mesh* Delete a navmesh together with its obstacles and agents.
nav.export read files mesh, path The baked mesh as JSON (sparklight-navmesh, version 1), inline or written to a file below the output folder.
nav.import write files json, name, path Load a navmesh document written by nav.export (from json or a path).
nav.info read session mesh, polygons Describe a navmesh: counts, bounds, the agent it was baked for, the bake statistics, and (with polygons) every polygon.
nav.list read session - List the navigation meshes with polygon, obstacle and agent counts.
Command Class Scope Parameters (* required) What it does
nav.nearest read session area_costs, excluded_areas, extents, mesh, point* The nearest point of the navmesh to a point (projected onto the polygon below or beside it), with its polygon and distance.
nav.path read session area_costs, excluded_areas, extents, from*, max_points, mesh, to* The shortest path between two points: A* over polygons weighted by area costs, then string pulling to the corner points.
nav.random_point read session count, mesh, seed Random points on the navmesh, uniform by area.
nav.raycast read session extents, from*, mesh, to* Walk a straight line along the navmesh and stop at the first wall: hit is false when the line stays on the mesh.
Command Class Scope Parameters (* required) What it does
nav.obstacle.add write session follow, height, mesh, position, shape*, yaw Add a dynamic obstacle that carves the navmesh: its footprint, grown by the agent radius, is cut out of the polygons it covers and paths flow around it at once.
nav.obstacle.list read session mesh List the obstacles of a mesh.
nav.obstacle.move write session id*, mesh, position*, yaw Move (and optionally turn) an obstacle; the mesh is carved again.
nav.obstacle.remove write session id*, mesh Remove an obstacle; the polygons it cut are whole again.
nav.offmesh.add write session arc_height, bidirectional, cost, end*, kind, mesh, radius, speed, start* Add an off-mesh link: a jump or a drop between two places the walkable mesh does not join.
nav.offmesh.list read session mesh List the off-mesh links of a mesh with the polygons their ends joined.
nav.offmesh.remove write session id*, mesh Remove an off-mesh link.
Command Class Scope Parameters (* required) What it does
nav.agent.add write session drive, entity, eye_height, height, max_accel, max_neighbors, max_speed, mesh, name, neighbor_dist, position, radius, repath_distance, replan_period, slow_radius, stop_distance, time_horizon, time_horizon_wall Create a navigation agent on a mesh, optionally bound to a 3D object.
nav.agent.get read session agent* One agent: position, velocity, heading, state (idle, moving, arrived, no_path, off_mesh), target and the corners of its path.
nav.agent.list read session mesh List the navigation agents with position, state and target.
nav.agent.remove write session agent* Remove a navigation agent (and its brain).
nav.agent.set write session agent*, heading, height, max_accel, max_neighbors, max_speed, neighbor_dist, radius, repath_distance, replan_period, slow_radius, stop_distance, time_horizon, time_horizon_wall Change the numbers of an agent (speed, radius, avoidance horizons, …) or turn it.
nav.agent.set_target write session agent*, target* Send an agent to a point or to where a 3D object stands now.
nav.agent.stop write session agent* Cancel an agent’s move; it stands still.
nav.step run session dt, frames Advance only the navigation: every crowd steps frames times (default dt 1/60 s).
Command Class Scope Parameters (* required) What it does
nav.debug.capture write files bounds, height, inline, mesh, path, portals, width Draw the navmesh from above to a PNG: polygons shaded by height and cost class, edges, walls, carving obstacles, agent paths (yellow with white corners), agents (disc and heading).
nav.golden.check write files bounds, height, max_ratio, mesh, name*, portals, tolerance, width Draw the navmesh picture and compare it with goldens/nav/<name>.png.
nav.golden.update write files bounds, height, mesh, name*, portals, width Draw the navmesh picture and store it as goldens/nav/<name>.png.
nav.hash read session - A hash of all navigation state (meshes with their obstacles, crowds, brains, event count).
Command Class Scope Parameters (* required) What it does
ai.leaves read session - The actions and conditions behaviour trees can call, with their parameters.
ai.schema read session kind* The JSON Schema (draft 2020-12) of a behaviour tree document or a utility AI document, with the built-in action and condition names as examples.
ai.tree.drop write session name* Unload a behaviour tree; brains using it stop thinking until another tree is attached.
ai.tree.get read session name* A loaded tree: its source document and the flat node list after subtrees were inlined (id, type, label, children, path).
ai.tree.list read session - List the loaded behaviour trees.
ai.tree.load write session doc, name, path Load a behaviour tree from JSON (doc) or a file (path).
ai.utility.drop write session name* Unload a utility document.
ai.utility.evaluate read session inputs*, last, name*, now Score the actions of a utility document for given inputs, best first, with the evidence of every consideration (input, raw value, normalised value, curve score).
ai.utility.list read session - List the loaded utility documents.
ai.utility.load write session doc, name, path Load a utility AI document from JSON (doc) or a file (path): actions scored by consideration response curves.
ai.validate read session doc, kind*, path Check a behaviour tree or utility document without loading it.
Command Class Scope Parameters (* required) What it does
ai.assert read session agent, distance, event, key, kind*, max_count, min_count, op, state, status, target, value Check something about the AI and fail with E_ASSERT_FAILED (data: expected, actual) when it does not hold.
ai.blackboard.get read session agent*, key Read one entry (or, without key, the whole blackboard) of a brain.
ai.blackboard.set write session agent*, key*, value* Write an entry of a brain’s blackboard (JSON value); null deletes it.
ai.brain.attach write session agent*, blackboard, period, tree, utility Give a navigation agent a mind: a behaviour tree (tree) or a utility document (utility) ticked every period frames, with an initial blackboard.
ai.brain.detach write session agent* Remove an agent’s brain (the agent stops where it is).
ai.brain.get read session agent* One brain: tree, last status, blackboard, the running nodes (id, label, path in the document) and action memory.
ai.brain.list read session - List the brains with their tree, last status and tick count.
ai.events read session agent, kind, limit, since The AI event log: spotted, lost_sight, waypoint, arrived, tree_status, utility and the events trees emit.
ai.hash read session - A hash of the navigation and AI state (meshes, crowds, brains, blackboards, event count).
ai.scenario.run run session json, path, update_goldens Run a navigation/AI scenario file: {"version": 1, "name": "guard", "steps": [{"do": "space3d.load", "path": "..."}, {"do": "nav.bake"}, {"do": "ai.step", "frames": 120}, {"do": "ai.assert", "kind": "event", "event": "spotted"}, {"do": "nav.golden.check", "name": "guard"}]}.
ai.step run session frames Advance the 3D scene frames fixed steps (like space3d.step: the AI runs inside every step of the scene): for each fixed step, update obstacles that follow objects, read where the agents’ objects are, tick every brain, steer every crowd (path following, arrive, ORCA avoidance of agents and walls), feed the velocities to the characters (or move plain objects) and step the 3D scene.
ai.until run session max_frames*, that* Step the world with AI (like ai.step) one frame at a time until an ai.assert condition holds, then stop.

A retained UI (*.ui.json documents: layout, themes, widgets, focus navigation, data binding, localisation) runs headless inside the session, so an agent can load a menu, press buttons like a gamepad, click widgets by id, read texts and values, switch theme and language, and capture the screen as a PNG without a GPU. Coded document errors are UI-E001 … UI-E019 (warnings UI-W001, UI-W004); command errors are E_UI_NOT_LOADED, E_UI_NO_WIDGET, E_UI_CLICK, E_UI_BUTTON, E_UI_VALUE, E_UI_DATA, E_UI_FONT, E_UI_LOCALE. Full reference: docs/agent/ui.md.

Command Class Scope Parameters (* required) What it does
ui.load write session path, text, document, width, height, language, locale_dir, theme, fonts, fresh, data Load a UI document: validate (coded errors), build the tree, lay it out. Loading again hot-reloads and keeps widget state, focus and screen.
ui.validate read session path, text, document, theme Every error and warning of a document with code, JSON pointer and message; loads nothing.
ui.tree read session id, depth, visible_only The widget tree as nested JSON with rect, text, focus and values.
ui.query read session type, text, class, focusable, visible, id Find widgets.
ui.get read session id* One widget with its resolved style.
ui.click write session id* Click a widget at its centre with real pointer events; returns the events.
ui.press write session button*, times Press up/down/left/right/confirm/cancel/tab_prev/tab_next/… like a gamepad or keyboard.
ui.type write session text* Type into the focused input.
ui.stick write session x*, y*, seconds Hold an analog stick; repeats navigation steps.
ui.focus write session id* Move focus to a widget.
ui.read_text read session id* The displayed text of a widget.
ui.set_value write session id*, value* Set a slider, toggle, input, dropdown or tabs value like a player.
ui.data write session path, value, merge Read or change the data the UI binds to.
ui.step write session seconds, frames Advance UI time (animations, toasts, bindings).
ui.events write session peek Events raised since the last read.
ui.screen write session action*, name Show, push or leave a screen.
ui.theme write session theme, language Switch the theme (built-in or registered) or the language.
ui.register_theme write session name, path, text, definition, activate Register a custom theme under a name (UI-E004 for a built-in name).
ui.toast write session text*, level, ttl Show a notification.
ui.tween write session id, property, to, duration, delay, ease, clear Animate opacity, scale or position of a widget.
ui.scenario write session path, scenario, width, height, language, theme Play a *.uitest.json scenario (press, click, focus, type, set_value, data, step, screen, theme, toast, tween, expect) and fail at the first failing step.
ui.assert read session expect* Check focus, screen, texts, values, visibility; fails with the list of mismatches.
ui.stats read session Widget counts, layouts, paint ops, warnings.
ui.capture write session path, inline, background, images Render the UI to a PNG with the CPU rasteriser.
Transport Start Notes
In process Session::execute(id, &params) the base of all others
JSON-RPC over stdio spark serve --stdio (also spark serve without options) one JSON object per line; stdout carries only protocol messages
JSON-RPC over TCP lines spark serve --listen 127.0.0.1:7777 port 0 picks a free port; the first stdout line is {"listening":"127.0.0.1:PORT",...}
JSON-RPC over WebSocket the same port the server detects the HTTP upgrade; one JSON-RPC message per text frame
MCP over stdio spark mcp see below
MCP over TCP lines spark mcp --listen 127.0.0.1:7778 the same messages, one per line; the first stdout line is {"listening":"127.0.0.1:PORT",...}; each connection initialises on its own
MCP over WebSocket the same port one MCP message per text frame

A command can answer later (Session::defer returns a token, Session::complete_deferred finishes it; the editor’s editor.screenshot does, see editor.md): the transport sends no reply when the request arrives, serves other requests meanwhile (also on the same connection), and sends the reply, with the id of the original request, when the session completes the token (TCP, WebSocket, MCP tools/call). A connection that closed in between loses the reply. Only a command called directly can defer, not one inside a batch; an in-process caller gets the command’s placeholder result and collects the final one with Session::take_deferred_result. In a JSON-RPC batch the reply of a deferred item arrives as a separate response.

Servers bind only to loopback (127.0.0.1, ::1, localhost); any other address is E_BIND_REFUSED (exit code 5). There is no authentication and no TLS: loopback is the boundary. Messages longer than 16 MiB are refused.

The method is the command id (run.step) or its tool name (run_step); params is the parameters object.

-> {"jsonrpc":"2.0","id":1,"method":"run.step","params":{"frames":10}}
<- {"jsonrpc":"2.0","id":1,"result":{"steps":10,"frame":10,"elapsed":0.1666,"hash":"..."}}
-> {"jsonrpc":"2.0","id":2,"method":"world.get","params":{"path":"/Nobody"}}
<- {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"...","data":{"code":"E_ENTITY_UNKNOWN","message":"...","hint":"...","path":"/Nobody"}}}

Extra methods: rpc.discover (all command specs), rpc.ping. Batches (arrays) and notifications (no id: executed, no reply) work as in the spec. JSON-RPC error codes: -32700 parse, -32600 invalid request, -32601 unknown command, -32602 invalid params, -32000 every other command error. Always match on error.data.code.

Events are pushed as notifications to connections that subscribed with delivery: "push": {"jsonrpc":"2.0","method":"event","params":{"subscription":3,"event":{...}}}, sent after the reply of the request that produced them.

spark mcp [--scene f] [--assets d] [--seed N] [--project dir] speaks the Model Context Protocol on stdio (newline-delimited JSON-RPC). Protocol versions 2025-06-18, 2025-03-26 and 2024-11-05; the server answers with the client’s version when it knows it.

  • Tools: every command. Name = id with . as _ (capture.golden.update is capture_golden_update). inputSchema is the command’s input schema; outputSchema is included for protocol 2025-06-18 and later. Annotations: readOnlyHint for class read, destructiveHint for world/file writes, openWorldHint for gameway_*. The description carries the command id, class, scope and error codes.
  • Tool results: structuredContent is the result and a text block repeats it as JSON. A command error is a tool error (isError: true) whose text is the {code, message, path?, hint?, data?} object, so the model can correct itself; an unknown tool name is a JSON-RPC error (-32602). A result with png_base64 (capture_frame with inline: true) becomes an image block.
  • Tool list changes: initialize announces tools.listChanged: true. When the set of commands changes (plugin_load, plugin_unload, plugin_reload, a snapshot_load whose recipe loads plugins) every initialised connection receives {"jsonrpc":"2.0","method":"notifications/tools/list_changed"} after the reply of the command that caused it; call tools/list again. Over the socket the notification also reaches a connection that did not make the change (it is sent after the next message of any connection).
  • Resources: spark://status, spark://commands, spark://errors, spark://components, spark://scene/tree (all entities with all component values), spark://logs/tail, spark://frame/last (PNG), spark://snapshots, spark://command-log, spark://schemas/<kind> for scene prefab clip tilemap palette input_map emitter components; template spark://entity/{name}.
  • Prompts: author_scene, playtest, debug_divergence, integrate_gameway: short recipes naming the tools to use.
  • Events: call events_subscribe with delivery: "push"; matching events arrive as notifications/message (logger: "sparklight", data: {subscription, event}).

Cluster DAG meshes drawn with GPU culling and continuous level of detail (needs space3d.new / space3d.load; a capture draws them with the scene). Assets come from a procedural source or a .vgeo file; errors carry E_VGEO_* codes. Full reference: docs/agent/virtual-geometry.md.

Command Class Scope Parameters Summary
vgeo.add write session material, position, rotation, scale, source* Register a virtual geometry asset (built from source, cached per source) and place one instance of it in the scene.
vgeo.build write files path, source* Build a .vgeo file from a procedural source (the same bytes every time).
vgeo.capture write files debug, eye, fov_degrees, frames, height, inline, path, target, tier, width Render the scene to a PNG, optionally with a diagnostic view: clusters, lod or overdraw.
vgeo.clear write session - Remove every virtual geometry instance and asset of this session.
vgeo.info read files path* Read and validate the header of a .vgeo file (counts, levels, bytes per source triangle) without a GPU.
vgeo.list read session - The registered assets and how many instances of each are placed.
vgeo.scatter write session count*, field, material, max_scale, min_scale, seed, source* Place count instances of an asset at random spots of a square field (stress and scale tests).
vgeo.settings write session backend, cone_culling, debug, enabled, max_page_loads_per_frame, occlusion, pixel_error, pool_mib, queue_capacity, shadow_pixel_error, shadows, visible_capacity Read or change the virtual geometry settings of the session.
vgeo.stats write files eye, fov_degrees, frames, height, target, tier, width Render for frames frames and report what culling did: clusters and triangles drawn of the source triangles, what each stage removed, page residency, over-budget pages, overflows, GPU time.

Particles, ribbons, mesh particles, skid marks and decals for the 3D scene (needs space3d.new / space3d.load). Effects are JSON with a schema and coded errors (E_VFX_*); the state-changing commands go to the 3D command log, so snapshot files replay them. Full reference: docs/agent/vfx.md.

Command Class Scope Parameters (* required) What it does
vfx.decal write session color, direction, fade, lifetime, position*, size, sprite, yaw Project a decal onto the geometry at a point for a while (scrapes, stains, burn marks). For permanent decals use the Decal component.
vfx.define write session config*, name* Define (or replace) a named effect in this scene; ParticleEmitter3D.effect, vfx.spawn and sub-emitters can then name it. Validates first; nothing changes on error.
vfx.list read session - The running effects: component emitters, spawned effects and trails with their id, name, live particles, particles born so far and whether they still emit.
vfx.preset read session name* The JSON of a built-in or defined effect: a starting point to edit and pass to vfx.define or vfx.spawn’s patch.
vfx.presets read session - List the built-in 3D effects (sparks, tyre_smoke, dust, exhaust, fire, explosion, debris_chips, rain, …) and the effects defined in the scene, plus the trail presets.
vfx.quality write session scale, tier Set the particle quality of the scene: a tier name (low 0.35, medium 0.6, high/ultra 1.0 of the authored emission) or a scale from 0 to 1. Effects marked essential keep at least 0.75. It scales particles born from now on; the render tier of space3d.capture (tier) switches soft particles and lighting.
vfx.schema read session kind JSON Schema of an effect (kind: effect) or a trail (kind: trail).
vfx.spawn write session burst, effect*, entity, patch, position, rotation, seed Start an effect at a world point, or on an object (entity: it follows the object, position is then local to it). Returns an id for vfx.stop. Looping effects run until stopped; one-shot ones (explosion, debris_chips) fire their bursts and disappear. burst adds that many particles at once.
vfx.stats read session - Totals of the scene’s effects (alive and born particles, trails, decals, quality) and, after a capture, what the renderer’s particles pass drew (batches, particles, ribbon vertices, GPU slots, upload bytes, CPU ms).
vfx.stop write session clear, id* Stop an effect started by vfx.spawn: it stops emitting and disappears once its particles are gone (clear removes them at once).
vfx.validate read session config, trail Check an effect (config) or a trail (trail) without starting it. Returns valid and, if not, the first problem as {code, path, message} (codes: vfx.presets → error_codes).

Indirect diffuse light of the 3D scene (needs space3d.new / space3d.load): the settings every space3d.capture renders with, the statistics of the last frame and baked lightmaps. Errors are E_GI_*. Full reference: docs/agent/gi.md.

Command Class Scope Parameters (* required) What it does
gi.bake write session path, settings Bake lightmaps for the items of the 3D scene as it is now: every mesh gets a second UV set (planar charts, packed into one atlas), a GPU path tracer collects the indirect light (sky, emitters, lit surfaces; direct light stays real time), the result is denoised and stored as irradiance plus a dominant direction. Placements that move later lose their lightmap. Skinned and see-through items are skipped (listed in the report).
gi.get read session - The GI settings of this session (mode off, ambient, screen_space or probes; the probe grid, screen-space and ambient parameters; lightmap use) and what lightmaps are loaded. space3d.capture renders with them.
gi.lightmaps.clear write session - Stop using lightmaps and forget the loaded set.
gi.lightmaps.info read session - The loaded lightmaps: instances, meshes, atlas size, bytes, the bake settings and the report of the last bake in this session.
gi.lightmaps.load write session path* Load lightmaps from a .slm file. They apply to the placements they were baked for (same meshes at the same transforms).
gi.lightmaps.save write session path* Save the loaded lightmaps to a .slm file (below the project folder).
gi.schema read session kind JSON Schema of the GI settings (kind: settings, default) or of the bake settings of gi.bake (kind: bake).
gi.set write session settings* Change GI settings: settings is a JSON merge patch onto the current ones (send only what changes; null resets a key). Unknown keys and values out of range are errors and nothing changes.
gi.stats read session - What GI did in the last captured frame: mode, probe grid (dimensions, spacing, probes updated, rays), traced scene (instances, triangles, nodes), atlas size and bytes, items skipped and content that did not fit a limit (named).
gi.tier write session tier* Set the GI preset of a quality tier: low = analytic ambient bounce, medium = screen-space GI, high = probe grid (64 rays), ultra = denser grid (128 rays). Measured costs: docs/agent/gi.md.

Everything the runtime and the platform report is an event {seq, frame, level, kind, target, code?, message, fields}. seq is the position in the stream and is never reused, so a gap shows dropped events; the ring holds the last 20000.

kind target fields
script_log, script_error script object (errors carry code: E_SCRIPT_RUNTIME, E_SCRIPT_BUDGET, E_SCRIPT_LOAD)
contact physics a, b, started (level debug: ask for min_level: "debug")
animation animation object, event, frame
particle_finished particles object
snapshot runtime frame (a restore happened)
run runtime the world was replaced (scene.load, scene.new, snapshot.load)
log any warnings, for example a missing asset or render when the GPU renderer is unavailable
gameway gameway event (name), data (payload), request (id of the request it answers)

Three ways to read it: log.tail (the newest events, or everything since a seq, no subscription needed), events.subscribe + events.poll, or events.subscribe with delivery: "push" on a transport that pushes (stdio, TCP, WebSocket, MCP). A filter has kinds, targets and min_level. assert.event counts matching events; events.status shows next_seq to use as since. Events are an output stream: restores and rollbacks do not rewind them.

Tool Use it for Cost
snapshot.take / snapshot.restore go back to a named point in the same session; restoring is bit for bit and checked against the hash memory
txn.begin … txn.commit / txn.rollback make a multi-step edit atomic; transactions nest; rollback undoes everything since begin (world, clock, RNG, input) one snapshot per begin
batch.run run a list of commands; atomic (default) rolls everything back on the first failure and returns data.batch_index and the results so far one transaction
snapshot.save / snapshot.load keep a state across sessions and processes replay time on load
run.seek jump to a frame; backward uses automatic keyframes (about every 120 frames) memory

scene.load, scene.new, snapshot.load and plugin.* replace the world and are refused inside a transaction (E_TXN_BLOCKED). run.seek backward restores the keyframe and steps forward again, applying the input changes the session recorded (input.*, run.step {input}) at the frames they happened: the state equals the one the run had (exact: true). A change made at the target frame itself belongs to the next step. Doing something new after a backward seek (an input, a step, any write command) replaces the recorded future; a forward seek replays it. Other edits (world.set, script.eval) are not replayed by a seek: use a snapshot or the command log for those.

Every successful state-changing command of the world scope (plus snapshot.take, snapshot.drop, txn.begin, txn.commit, plugin.load, plugin.unload, plugin.reload) is recorded with its frame: log.commands returns them in batch.run format. A rolled-back transaction removes its own entries. Load the same project and seed in a fresh session, run batch.run with atomic: false and the state hash is identical. snapshot.save writes {format, version: 2, seed, frame, hash, scene, log, space3d}; space3d is null without a 3D scene, else {frame, hash, log} with the 3D command log (space3d.log). snapshot.load replays the 2D log and then the 3D log in a scratch session and adopts the result only when both frames and hashes match (E_SNAPSHOT_MISMATCH otherwise, data.scene is space3d when the 3D scene is the one that differs; nothing changes). A version 2 file describes the whole world, so loading one without a 3D scene removes the session’s 3D scene; a version 1 file (2D only) leaves it alone. Save at frame 100, load in a new process and step on: the hash at frame 300 equals the uninterrupted run’s (tested for 2D and for 3D through spark serve --stdio). The scratch session gets a copy of the plugin host (PluginHost::fork), so plugin commands in the recipe run; afterwards the plugins the recipe loaded are the loaded ones and their commands are in the registry. A host that cannot fork makes the replay fail at the first plugin command. Open transactions are not saved. (The scenario crate has its own snapshot file, sparklight_scenario::SnapshotFile, with a state dump; the two are not interchangeable.)

paths are E_OUTPUT_REFUSED). The spark binary installs the GPU renderer of sparklight-scenario and uses it for the first capture (result field renderer, for example gpu: Apple M5 Max (Metal)); without an adapter, or with --no-gpu / SPARKLIGHT_NO_GPU=1, the built-in flat renderer is used: every sprite as a flat rectangle, textured sprites in a colour derived from the texture key. It needs no GPU and is deterministic, but it is not what players see. Pixel hashes of the two differ. capture.diff compares two PNGs (channel tolerance, share of differing pixels). capture.golden.update / capture.golden.check store and compare goldens/<name>.png below the project root. For perceptual (ΔE) goldens with tolerance and diff images use the scenario runner: spark test samples/platformer/scenarios (docs/agent/testing.md).

spark test <file|dir>... takes two kinds of input:

  • *.scenario.json files and folders of them: the scenario runner (--update-goldens rewrites goldens, --no-render skips screenshot assertions and reports the run as incomplete). Load errors exit with 2.
  • any other .json: a command script {"name", "scene", "assets"?, "seed"?, "steps": [{"command", "params", "expect"?, "name"?, "continue"?}]}. A step passes when the command succeeds (and expect.result is a subset of the result) or, with expect.error, fails with that code. Use assert.* commands for checks. Exit 3 on a failed script.

The JSON report is written to test-report.json below the output folder (--report name changes the name).

spark <verb> [options]. With --json every verb prints one document {ok, verb, result | error} on stdout; without it a short human rendering (errors go to stderr as error[CODE]: message, then hint: ...).

Verb What it does
run <scene> [--assets d] play with a window (for humans; muted when GAMEWAY_MUTE=1)
headless <scene> [--frames N] [--seed N] [--input name=value]... load, step (default 600 frames), print hash, frames, p95_ms
step <scene> --frames N [--input ...] / step --frames N --connect ADDR load and step, or step a running server
serve [--stdio | --listen ADDR] [--scene f] [--assets d] [--seed N] JSON-RPC server
mcp [--listen ADDR] [--scene f] [--assets d] [--seed N] MCP server on stdio, or on a loopback socket with --listen
call <command> [json | @file] [--scene f] [--connect ADDR] run one command
commands [--prefix p] [--brief] the command table
schema [list | <kind> | commands | errors] [--write dir] JSON Schema of a format, the commands or the errors; --write writes <dir>/*.schema.json and <dir>/commands/
validate <file | dir> [--kind k] validate a scene, prefab, clip, tilemap, palette, input map, emitter, Lua script, or an assets folder
capture <scene> [--frames N] [--out name.png] [--width W --height H] render a PNG below the output folder
doctor studio rules, output folder, renderer (probes the GPU), Gameway backend, assets
test <file | dir>... scenarios and command scripts, see above

Common options: --json, --project dir (the folder files may be written to; default the current directory), --seed N, --assets d, --scene f, --input name=value (repeatable), --no-gpu, --connect ADDR (talk to spark serve --listen).

Exit codes: 0 success, 1 a command failed (coded error), 2 usage error (or scenario load errors), 3 a test or assertion failed, 5 environment problem (cannot bind, output folder not writable, failed doctor check).

Studio rules: sessions are always muted and headless; GAMEWAY_MUTE=1, --gw-mute and an output-folder override are honoured by the studio context (agent run: data only under the agent data root, system.doctor shows it); outputs go below .gwdev/out or folder (E_OUTPUT_REFUSED otherwise); servers bind only to loopback; nothing prints secrets.

The gameway.* commands are generated from the one function table of sparklight_gameway::script::functions() (the same table the Lua bridge sparklight.gameway.* uses), so a function added there appears here and in the MCP tool list. They run against the session’s GameWayPlatform: the in-memory FakePlatform (connected, user “Fake Player”) unless a host attached the real client with Session::attach_platform. gameway.backend says which. Results are the bridge’s JSON; platform errors keep their stable names (GW_E_*, SPARK_GW_*) as the error code. Answers to requests arrive as events: after every run.* and gameway.* command the session calls the platform’s frame callback and moves its events into the stream (kind: "gameway"); gameway.poll_events returns the ones not yet fetched. gameway.fake.* scripts the fake: connection, online, add_friend, achievements, event (queue any platform event written as JSON, for example {"type":"invite_received","invite_id":"<uuid>","from":"<uuid>","expires_at":9,"kind":"invite","from_name":"Ann"}; the fields are the ones gameway.poll_events shows) and state shows what it recorded (unlocked achievements, rich presence, call counts). The start-up gate (exit codes 20/21/22) is not a control command: it runs before a game starts (docs/gameway.md).

session.registry_mut().try_add(Def { id: "health.damage", summary: "...", class: Class::Write, scope: Scope::World,
input: sc::object(&[("target", sc::string("Entity")), ("amount", sc::number("HP"))], &["target", "amount"]),
output: sc::out(&[("hp", sc::number("HP left"))], &["hp"]), errors: &["E_ENTITY_UNKNOWN"] },
|session, params| { /* use session.runtime */ Ok(json!({"hp": 0})) })?;

The command appears in system.commands, rpc.discover, schema.get commands and the MCP tool list immediately, its input is validated against its schema, and registry_mut().remove_prefix("health.") removes it. Two seams exist for the other tasks: PluginHost (plugin.list/load/unload/reload delegate to it; without a host plugin.list is empty and the rest are E_PLUGIN_HOST_MISSING) and EditorBridge (editor.* delegate to it, with the session as an argument so the editor runs commands through it; without an editor editor.status says attached: false and the rest are E_EDITOR_NOT_ATTACHED; the editor hosts a transport::TcpServer it polls once per frame, see editor.md). Session::set_renderer_factory lets a host install its own renderer.

Code Meaning Hint
E_CMD_UNKNOWN No command with this id is registered. List commands with system.commands (or spark schema commands) and use an id verbatim.
E_PARAMS_INVALID The command parameters do not match its input schema. Read the command’s input schema (system.commands) and send parameters that validate against it.
E_PARSE The message is not valid JSON. Send one JSON-RPC 2.0 object per line.
E_PROTOCOL The message is valid JSON but not a valid JSON-RPC 2.0 or MCP request. Send {“jsonrpc”:“2.0”,“id”:1,“method”:“”,“params”:{…}}.
E_TXN_NONE There is no open transaction. Open one with txn.begin first.
E_TXN_UNKNOWN No open transaction has this id. List open transactions with txn.status; ids are returned by txn.begin.
E_TXN_ORDER Transactions must be closed innermost first. Commit or roll back the most recent transaction before the outer one.
E_TXN_BLOCKED The command cannot run while a transaction is open. Commit or roll back the transaction first; commands that replace the whole world are not transactional.
E_SUBSCRIPTION_UNKNOWN No event subscription has this id. List subscriptions with events.status.
E_ASSERT_FAILED An assertion did not hold. Compare data.expected with data.actual in the error.
E_NOT_IMPLEMENTED The command exists in the surface but its backend is not available in this build. See docs/agent/commands.md for what the backend needs.
E_EDITOR_NOT_ATTACHED No editor is attached to this session. Attach to a running editor’s control socket, or start the editor with a control server.
E_PLUGIN_HOST_MISSING No plugin host is attached to this session. Plugins are available once the plugin host is attached (task plugins).
E_PLUGIN_ERROR A plugin operation failed. Read the message for the plugin’s own error; the previous version keeps running.
E_BIND_REFUSED The listen address is not a loopback address. Servers bind only to 127.0.0.1 (or ::1); pass –listen 127.0.0.1:.
E_SEEK_UNAVAILABLE No keyframe at or before the requested frame exists. Seek forward, restore a named snapshot first, or reset and step.
E_IMAGE_INVALID An image file could not be decoded or the sizes do not match. Pass PNG files of the same size.
E_IMPORT_UNSUPPORTED The file is not an asset kind the import pipeline processes. Import a PNG, JPEG, glTF or GLB file; assets.report lists what the pipeline handles.
E_IMPORT_SETTINGS The import settings in the .meta are not valid. Fix the import object of the .meta; the message names the field.
E_IMPORT_FAILED The asset could not be processed. Read the message: the source file is damaged or uses a feature the importer does not support.
E_SNAPSHOT_FILE A snapshot file could not be read, written or replayed. Check the path; the file must come from snapshot.save of the same project.
E_SNAPSHOT_MISMATCH Replaying a snapshot file did not reproduce its state hash. The scene, assets or engine changed since the file was saved; save it again.
E_SCENARIO_FAILED A test script step did not meet its expectation. Read the report: the failing step, the expectation and the actual result are listed.

The 3D commands add E_SPACE3D_* codes (no scene loaded, unknown component, unresolved reference, bad layer, renderer unavailable, …; the list is in docs/agent/space3d.md) and pass through the E_FX_* codes of the physics features (docs/agent/physics.md) and the E_ANIM_* codes of skeletal animation (docs/agent/animation3d.md) and the E_CHAR_* codes of characters (docs/agent/characters.md).

All other codes are the engine’s (docs/agent/runtime.md) and Gameway’s (GW_E_*, SPARK_GW_*). system.errors returns the whole registry.

A session can host one game server and join any number of games as clients, over UDP with a simulated link (latency, jitter, loss, duplicates) for testing. Nothing advances between commands: net.pump lets real time pass, the other commands deliver the packets that arrived. Coded errors are E_NET_* (E_NET_FULL, E_NET_VERSION, E_NET_SCENE_MISMATCH, E_NET_TIMEOUT, …; the table is in docs/agent/netcode.md, the registry in system.errors). A server listens on 127.0.0.1 unless lan: true and bind ask otherwise. Full reference: docs/agent/netcode.md.

Command Class Scope Parameters (* required) What it does
net.host write session assets, bind, demo, duplicate, fill_bots, jitter_ms, lan, latency_ms, loss, max_players, min_players, name, net, port, rtt_ms, scene, seed, start, toy_props, toy_slots Start a game server in this session on a UDP port (loopback unless lan). The scene and its net game file define players, replication, inputs and bots. Clients join with net.join (also from other processes or spark join-game).
net.join write session address*, assets, demo, duplicate, jitter_ms, label, latency_ms, loss, name, net, predict, rtt_ms, scene, seed, toy_props, toy_slots, wait_ms Join a game server as a client of this session (several clients may live in one session, each with a label). Waits for the welcome; a refused join is a coded error (E_NET_VERSION, E_NET_FULL, E_NET_SCENE_MISMATCH, E_NET_TIMEOUT).
net.stop write session target Stop the server (every client is told it is closing), leave a game with a client, or both. The state is gone afterwards.
net.pump run session ms, ticks Let real time pass (up to 60 s): the server runs its ticks, clients send inputs and predict, packets flow. Nothing in a session advances between commands.
net.status read session target The server and the clients of this session: phase, tick, players (slots, bots, ready, host, rtt, loss), state hashes, the link.
net.stats read session target Counters: server ticks, snapshot sizes (average and last), bytes per second per client, packets lost, retransmissions, starved/dropped inputs; client snapshots, predictions checked, mispredictions with the worst field, replayed ticks, rtt and loss.
net.converged read session logs Proof of convergence: compares every authoritative snapshot each client received (tick and state hash) with the server’s state at the same tick. mismatched must be 0. Clients that run in other processes (spark join-game) are checked by passing their hash_log in logs.
net.view read session client*, entity What a client would draw now: the local player predicted (with the fading correction), everybody else interpolated between snapshots. Values are in world units, by component and field. Also writes the remote entities into the client’s own scene copy.
net.events write session keep, source Events since the last call (and clears them): players joining and leaving, phase changes, refusals, game events, mispredictions, closes. Server events come from host, client events from their label.
net.simulate_link write session duplicate, jitter_ms, latency_ms, loss, rtt_ms, target Make the network worse on purpose: delay, jitter, loss and duplicates for what a side sends. Apply to both sides for a symmetric link (rtt_ms: 80 on all is an 80 ms round trip). All zero turns it off.
net.input write session client*, inputs* Set a client’s inputs (-1..1, held until changed). The names are the inputs of the net game file. Takes effect on the client’s next tick (use net.pump).
net.ready write session client*, ready A client sets its ready flag in the lobby (with start: auto the game begins when every human is ready).
net.start write session client Start the game: with client that client asks (only the host may; E_NET_NOT_HOST arrives as a notice event otherwise); without it the server of this session starts directly (a dedicated server has no host).
net.end write session - End the running game and go back to the lobby: the world is reset, bots leave, players keep their slots and must get ready again.
net.kick write session player*, reason Remove a human player from the server; they are told why (E_NET_KICKED). Their slot goes to a bot in a running game.
net.action write session client*, kind*, payload A client reports something it did (a shot, a pickup) together with the server tick it was looking at; the server validates it against the world as that client saw it (lag compensation) and may broadcast game events.
  • script.eval runs under the script limits (2 million instructions, 16 MiB, 2 s by default; run.reset {script_limits} changes them). Global assignments of a snippet are dropped; only changes to self and the world stay.
  • run.pause / run.resume set a flag for real-time hosts (editor, game window); explicit stepping always works.
  • The pointer is three input actions (pointer_x, pointer_y, pointer_down); gamepads and keyboards are the actions scripts read.
  • spark serve --listen speaks JSON-RPC; MCP over a socket is spark mcp --listen. A socket carries one protocol.
  • snapshot.load costs the replay time of the recorded run.
  • run.seek replays recorded input changes only (up to 200000 per timeline, then exact: false); other edits between the keyframe and the target are not replayed.
  • A pedantic-lint exception list for casts and must_use is at the top of crates/sparklight-control/src/lib.rs.