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.
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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
[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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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).
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.
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.
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.
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.
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.
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).
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/.
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).
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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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).
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.
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.
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)
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, 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.
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.
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).
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.
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.