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.
Signs you are about to hand-roll a tool
Section titled “Signs you are about to hand-roll a tool”Stop and find the row here when you are about to write any of the following:
- A global on
window, such aswindow.__rig = rig, to read a view’s state from outside. - A loop of
play stepandplay evalthat 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 evalor an--untilthat 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 seeprints. - An
afterEachthat destroys a test’s worlds, or a loop ofstepWorldin a test. - A
vi.stubGlobal("fetch", …)in a test, with aResponsebuilt by hand. - A
mkdtempSyncin a test, with anrmSyncin 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.
Run and step the game
Section titled “Run and step the game”| 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 |
Read the world
Section titled “Read the world”| 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 |
Read what a view draws
Section titled “Read what a view draws”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 |
See it
Section titled “See it”| 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 |
Measure and replay
Section titled “Measure and replay”| 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 |
Pin it in a test
Section titled “Pin it in a test”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).
Leave nothing for the next file
Section titled “Leave nothing for the next file”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
createbefore you destroy its world. Testing Library’srenderunmounts on its own after each case. - Undo each change outside the file:
vi.unstubAllGlobals()aftervi.stubGlobal,vi.useRealTimers()after fake timers, the page’s address afterhistory.replaceState, a store’s state aftersetState, 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:
pnpm exec vitest run --sequence.seed=1790788700828One 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.
Lint the game’s source
Section titled “Lint the game’s source”@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:
gamereports each finding as an error, except the two replay rules, which warn.gameWarningsreports 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. motionfrommotion/react, which carries drag and layout into the first load.Math.randomand module state in a function that takes the world, as warnings.engine/system-query:world.queryin a system, which builds an array every tick, whereupdateEach,readEachandfindEntitybuild nothing. A system is a function adefinePlugincall in the same file names undersystems, a variable typedSystem, or a function that takes a step or the step’s options.engine/time-subscription:useTimeread forsecondsorsteps, 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 asaddCoins,dealDamageorfinishRound, or a trait written withset,addorremove. The step decides it, from a command the page sends withsendCommand.spawnanddestroystay open to a component, which spawns its scene’s own entity as it mounts.engine/effect-trait: a trait added or removed in auseEffect, wheredefineBehaviouranduseBehaviourdo it.