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

Which tool

Find what you want to do in the first column, and use the tool beside it. Each row links the page that explains the tool. In your game’s folder, every play command runs as spawnite play <command>, and spawnite <command> --help prints each command’s options.

Stop and find the row here when you are about to write any of the following:

  • A global on window, such as window.__rig = rig, to read a view’s state from outside.
  • A loop of play step and play eval that waits for a moment.
  • A delay, such as sleep 3, before a read or a screenshot.
  • A Playwright script against a game’s page.
  • A play eval or an --until that imports the devtools store to stand the player somewhere or to turn the map camera on.
  • A script that renders or tiles pictures of a model, places a camera or a light round one, or sets a concept beside a render to judge it by eye.
  • A script that pulls a few fields out of a dump’s JSON.
  • A click worked out by hand from the box play see prints.
  • An afterEach that destroys a test’s worlds, or a loop of stepWorld in a test.
  • A vi.stubGlobal("fetch", …) in a test, with a Response built by hand.
  • A mkdtempSync in a test, with an rmSync in a hook to remove it.

Each of these has a tool below. When no row fits, tell your user which check had no tool, so it can be asked for.

You want to Use Page
Open the game in a browser you drive play start, and play stop when you finish, or the MCP’s director with action start and stop The director
See which playtests run, and who started each play list, or the MCP’s list_playtests The director
Read an error the engine made, by its code The code’s page, node_modules/@spawnite/engine/dist/wiki/engine/errors/<code>.md Errors and bug reports
Name the engine’s minified frames in a stack spawnite symbolicate <file> Errors and bug reports
Report a failure inside the engine spawnite bugreport [replay] Errors and bug reports
Run a scene with no browser and no GPU spawnite simulate, or the MCP’s simulate Devtools API
Step until something holds play step --until "<condition>", or --seconds and --steps; the MCP’s director with action step The director
Hold the game still, or let it run play pause, play resume, or the MCP’s director with action pause or resume The director
Start a run, turn damage off, or call a wave play actions, then play action "<name>" Devtools API
Press, hold or let go of a key, or move the mouse play press, play hold, play release, play move, step --keys The director
Send a command with no key, or one with a payload play press <plugin.key> --payload '{...}', play hold <plugin.key> --value <n> The director
Find why a button or a command did nothing play state, its Commands: line, then play dump --commands; the devtools’ Commands section Commands
Click a game’s button or link by its name send_input’s click with its name The director
Click an entity, such as an NPC to talk to play press MouseLeft --on "<subject>", or send_input’s click The director
Start playing at a point of a map play from-here --at <x,z>, --map <name> for another map’s preview Devtools API
Play the game on a slow connection to its room play start --latency 150 --jitter 20 --stall 500/10, or the MCP’s start_playtest with latency Multiplayer
You want to Use Page
Read every entity’s traits play dump, --where to filter Devtools API
Read only a few traits or fields play dump --fields, simulate --fields Devtools API
Change a trait’s value, such as coins or where the character stands play set <subject> <path> <value>, or the MCP’s set_trait, or in a room game director with action set Devtools API
Read what the player’s camera sees play see, or the MCP’s director with action see The director
Read where the crosshair points play aim, or the MCP’s director with action aim The director
Read what each entity is and does, in words play ai Metadata
Read a caster’s ability range, cost, cooldown, target reach and last refusal play eval "views", under abilities Devtools API

A view’s state lives outside the world: the pose it chose, a fade it keeps in a ref, a clip’s weight. Read it through useInspect and play eval, never through a global.

You want to Use Page
Read a view’s own state useInspect(name, read) in the view, then play eval "views..." or play dump Devtools API
Read the three.js scene: a bone, a material, the camera play eval "three.scene..." The director
Follow a value frame by frame play step --sample "<expression>" The director
Run a check of your own on the page play eval "<expression>", or --file for a script; the MCP’s director with action eval The director
Read the page’s errors and warnings play console Devtools API
Find a prediction mistake on a page in a room play state, each kind’s count and latest line, or play console Networking
Check a predicted mechanic at a player’s round trip play start --latency 150 --jitter 20 --stall 500/10, then play state Build a predicted mechanic
Read the room’s corrections of her character play state, on its Room: line, the last second’s and since the page loaded Networking
Read the room’s lead, round trip and bytes over a span play state --sample <seconds>, --every <ms>, --json for each sample Networking
Read what a room’s commands came to, refusals by reason The room’s log, its heartbeat line’s commands Development room
You want to Use Page
Shoot what the player sees play screenshot, --width to keep the file small Devtools API
Shoot one entity or place from a chosen side play screenshot --around "<subject>" --angle <side>, or --view Devtools API
Shoot the moment something happens play screenshot --until "<condition>", which reads views too Devtools API
See what a press does frame by frame, with its second play timeline <control>, --every and --seconds, or the MCP’s timeline The director
See a clip’s frames on the character, and read its release mark play timeline --clip <name>, or the MCP’s timeline with clip The director
Record a clip of what the game drew, for marketing The dev tools’ Record, or F9; from a held session, spawnite play record, or the MCP’s record_clip The director
Read a recorded video clip: its sheet, events and stills spawnite clip show <name>, --from and --to in seconds, or the MCP’s read_clip Dev server
Have a person scrub a clip to a mark’s frame in a browser /model?avatar=<name>&clip=<name>&marks=release:<s> on the tools app The model viewer
Read a clip’s motion in numbers: where the arms peak, each bone’s turn spawnite model check <clip.vrma>, or the MCP’s check_model Models
Check a change kept each shot’s colour and quality play compare, --accept to take a new set Performance on main
See a model’s size, facing, clips and bones spawnite model check <files...>, --angle once a view, --clip <name> --at <s>,<s> Models
Check a model against its concept, with the differences as numbers spawnite model check <file> --reference front=<concept>, --style silhouette,outline Models
Find the frames where a clip puts a limb through the head or torso spawnite model check <file> --clip <name>, or the MCP’s check_model with clip Models
See a model’s edges, topology or vertices spawnite model check <file> --style outline,wireframe,vertices Models
Compare several models side by side spawnite model check a.glb b.glb c.glb Models
Turn a Meshy character into a VRM avatar spawnite model avatar <file> Models
Turn a Meshy clip into a VRMA every avatar plays spawnite model clip <file> --clip <name> Models
Check a downloaded sound for a constant hiss spawnite sound check <files...>, or the MCP’s check_sound Assets
You want to Use Page
Count draw calls, triangles and textures play stats Devtools API
Find what a frame spends its time on, and its hitches play profile One frame
Measure a frame under the map editor’s camera play profile --map-camera <zoom> One frame
See which files a page requested play profile --har <path>, a HAR of every request, listed by URL Finding hitches
Weigh what a page downloads before the game starts spawnite build, the game’s own code and the engine’s, gzipped One frame
Go back to a moment a player hit spawnite replay list, then play start --replay <page> --at <moment>, and play seek within it; the MCP’s replay with action list, then director with action start and seek Replay
Read a replay’s log, or the shot it kept at a moment spawnite replay log, spawnite replay shot <replay> --at <moment>, or the MCP’s replay with action log or shot Replay
Continue a moment in a fresh room with the code as it stands spawnite replay rerun <replay> --at <moment>, or the MCP’s replay with action rerun Replay
Read the room’s whole world at a moment of a replay spawnite replay state <replay> --at <moment>, or the MCP’s replay with action state Replay
Check that a continue of a game’s runs comes back exactly spawnite replay check, or the MCP’s replay with action check Replay
Play a room with bots and no browser play room Runs
Find what a room’s heap holds, or what grows in it play room, its heap columns, and --heap-snapshot exit or a share such as 0.6 Runs
Time one engine call over worlds of several sizes Vitest’s bench in a .bench.ts file, vitest bench --run Testing
Join several pages to a room play join Joins

A fact that should stay true after the next change goes in a test, not only a playtest. A behaviour test runs under vitest in the project’s test/ folder: the rules step headless, and a scene mounts in Node through the engine’s headless mount, as Devtools API describes. A test of code that fetches stubs fetch with stubFetch from @spawnite/testing/fetch, as Devtools API shows. A test that renders the whole app in jsdom mocks fiber and drei with fakeFiber and fakeDrei from @spawnite/testing/fiber, as A whole game in jsdom shows. A rule that holds for every input, such as a round trip, goes in a property test, which draws the inputs for you. A test that writes files makes its folder with makeTempDir from @spawnite/testing/temp, as Devtools API shows. A test that runs a command, such as the cli or git, runs it with runProcess from @spawnite/testing/process, as Processes describes.

Import it from @spawnite/engine/testing rather than from vitest, and name the fixture the case needs. Each fixture tears down what it made after the case, so a test writes no afterEach:

You want to Use
A world on the file’s plugins, and its step ({ world, step }), then step(n) or step(n, input), where n counts fixed steps
A world on your plugins, or a second ({ createWorld }), then const { world, step } = createWorld(plugins)
A scene mounted headless ({ scene }), then await scene(Arena)
A headless game with the map’s ground ({ createGame }), then await createGame({ scene })
A player in control, ready to send a command addPlayer(world, { name: "Ada", controlled: entity })

The world is a dev world with Rapier loaded, as createHeadlessGame makes one. step(n) runs n fixed steps, 1 when left out, each 1/60 s, so step(60) is one second of game time; the devtools store’s step(n) on Headless tests counts seconds instead. createWorld returns a world and its own step, and takes the physics settings as its second argument. A game adds its own fixtures with it.extend("arena", async ({ world }) => spawnArena(world)).

A command that acts through an entity, such as a dash or a cast, needs a player who controls that entity. addPlayer(world, { name }), from the same entry, joins a player as a room does, with every plugin’s join hooks. Called before await scene(Meadow, world), it joins her as a page played alone does, and the scene’s <CharacterSpawn> hands her her character as it mounts. A case that spawns its own entity names it, addPlayer(world, { name: "Ada", controlled: kart }), and she controls it at once, predicted, so sendCommand(world, handle, payload, { entity: kart }) runs on the next step.

The plugins fixture is the list every world of a case is made from. It is the core alone unless a file sets it, as const it = test.extend("plugins", () => plugins) sets the game’s own. @spawnite/engine/testing needs vitest installed.

A trait declared predicted: true is written only in a predicted system. A test that needs a character’s predicted state in place between steps calls writePredictedForTest(character, trait, value), from the same entry, and reads it with readTrait(character, trait).

The engine’s own suite and Holdfast’s run without isolation: a worker runs one test file after another on the same modules, the same jsdom window and the same Koota worlds, which roughly halves the CPU a run spends outside the tests. A file therefore undoes what it changed before the next file starts:

  • Destroy each Koota world it makes, or take the world from a fixture above, which does. A process holds 16 worlds at most.
  • Unmount a React Three test renderer from create before you destroy its world. Testing Library’s render unmounts on its own after each case.
  • Undo each change outside the file: vi.unstubAllGlobals() after vi.stubGlobal, vi.useRealTimers() after fake timers, the page’s address after history.replaceState, a store’s state after setState, and each registration through the disposer it returned.
  • Test a module’s first state on a fresh copy: call vi.resetModules(), then import the module inside the case.

Vitest restores vi.spyOn spies between files on its own. A file that calls vi.mock, vi.doMock or vi.hoisted still runs in a fresh worker of its own, because a module loaded under its mock would keep the mock for every later file; the project’s vitest config finds those files by the call. A file that measures the heap goes in the config’s quiet list, whose files run one at a time after the rest; each is a *.extended.test.ts file, which only the full suite, nx test -c full, runs.

Each run takes the files in a new random order and prints its seed at the top, such as Running tests with seed "1790788700828". A file that fails in one order and passes in another reads what an earlier file left behind. Run the same order again with that seed, in the project’s folder:

Terminal window
pnpm exec vitest run --sequence.seed=1790788700828

One Playwright check per game proves the built bundle boots: test/e2e/boot.spec.ts calls bootSmoke from @spawnite/testing/e2e, which opens the page, waits for the canvas, runs the game’s own steps, and fails on any error the page reports. The package’s README shows the config and the spec.

@spawnite/engine/eslint is the flat config of the engine’s rules for a game’s source. It needs eslint and typescript-eslint installed, and it comes in the following two forms:

  • game reports each finding as an error, except the two replay rules, which warn.
  • gameWarnings reports every finding as a warning.

A game made with spawnite create has the following eslint.config.mjs. Its pnpm check runs tsc and ESLint, so a type error fails the check and a finding prints as a warning:

import tseslint from "typescript-eslint";
import { gameWarnings } from "@spawnite/engine/eslint";
export default [{ ignores: ["dist"] }, tseslint.configs.base, ...gameWarnings];

To make each finding fail the check, except the replay rules’, spread game in place of gameWarnings.

Each rule finds code that a game writes through the engine or in another place, and its message names the engine export to use or the place the code belongs:

  • A raw HTML element in JSX, where the engine’s UI primitives draw it.
  • zustand’s own persist, which writes past the player’s save.
  • motion from motion/react, which carries drag and layout into the first load.
  • Math.random and module state in a function that takes the world, as warnings.
  • engine/system-query: world.query in a system, which builds an array every tick, where updateEach, readEach and findEntity build nothing. A system is a function a definePlugin call in the same file names under systems, a variable typed System, or a function that takes a step or the step’s options.
  • engine/time-subscription: useTime read for seconds or steps, or with no selector, which re-renders the component on every frame that steps.
  • engine/view-outcome: gameplay decided in a component or a hook: an outcome the engine exports, such as addCoins, dealDamage or finishRound, or a trait written with set, add or remove. The step decides it, from a command the page sends with sendCommand. spawn and destroy stay open to a component, which spawns its scene’s own entity as it mounts.
  • engine/effect-trait: a trait added or removed in a useEffect, where defineBehaviour and useBehaviour do it.