Appearance
Tooling: build, lint, run, render, pack
You'll learn: the tools that exist today, and the ones coming. Commands run from servers/inventery/.
| Tool | Status | Does |
|---|---|---|
node plot/sdk/lint.mjs <dir> | Built | checks the platform's rules, with a fix for each finding |
node plot/sdk/build.mjs <dir> | Built | validates plot.json, bundles logic.ts to dist/logic.js, hashes the artifacts |
node plot/sdk/build.mjs pack <dir> [out.inv] | Built | builds, then writes an .inv bundle |
node docs/developers/examples/run.mjs <dir> | Built | runs a built plot in the headless room (with the base game's rules) with players walking in |
npm run test:plot | Built | the runtime's own tests, including onlooker and player-view renders |
the harness (createHarness, plot/harness/harness.mjs) | Built | a deterministic room with scripted clients that speak the real protocol, assertions, and renderOnlooker / renderPlayerView PNGs |
node docs/developers/reference/gen.mjs | Built | refills these docs' reference tables from the schema (--check to see if they're current) |
the plots dev stack (node servers/inventery/scripts/plot-dev.mjs) | Built (dev only) | a real room and page on local ports with plot hosting on: the samples on ASDFGHJKL monuments, in a browser at http://localhost:8866/play/ (and the iOS app pointed at it). Never staging or production. Setup: src/plots/README.md |
inventery-mod dev | Planned | your own package hot-reloaded in a local room, with budget meters and an outside camera |
| the creator API | Planned | upload builds, edit the draft, publish (used by the agent and future CLIs) |
The lint
$ node plot/sdk/lint.mjs my-plot
logic.ts:4: error [lobby-call-at-plot-level] ui.chat needs a lobby, but init runs at plot level: refused with "wrongContext"
fix: wrap it in lobbies.runWithin(id, () => { ... }), or move it into a lobby hook (onLobbyLoad, onPlayerEnter, tickLobby, ...)
1 error(s), 0 warning(s)| Rule | Catches |
|---|---|
visible-in-scene | geometry, lights or sounds made locally in client code: onlookers would never see them |
determinism | performance.now, timers, crypto randomness and the like in sim code (Math.random and Date are already seeded in a JS package) |
lobby-call-at-plot-level | a lobby call at plot level (top-level code, init, chooseLobby, tickPlot...) |
plot-call-in-lobby | a plot-level call inside a lobby hook |
runtime-only | a mod using the runtime's calls or hooks |
channel-visuals | visual state sent on a custom channel |
invisible-model | a model with visibility "serverOnly" (nobody sees it) |
costly-call-every-tick | a JSON-heavy host call made every tick, outside any if (warning) |
manifest | a missing or bad plot.json field (a runtime-only field in a mod, an unknown baseRules category...) |
registries | a bad entry in the registries JSON files |
It reads calls in the hook they're written in, and doesn't follow helper functions, so it favours few false alarms over catching everything. A closure written inside init counts as plot level, even if it runs later in a lobby: add event handlers at the top level instead.
The build
$ node plot/sdk/build.mjs my-plot
built ana.lanterns@0.1.0: dist/logic.js (3 artifacts)It writes dist/logic.js (an esbuild bundle that runs in the sandbox) and dist/manifest.json (each artifact's content hash). @inventery/host, @inventery/minecraft and @inventery/lobby resolve to the SDK.
Running headless
run.mjs loads your built plot into the same headless room the runtime's tests use (real sandboxes, lobbies, entry protection, stashed items, and the base game's rules: breaking and placing within reach, two-part beds and doors, drops, PvP), walks players in, and prints each lobby and what each player received. For --place and --break the first player steps up next to the block first:
sh
node docs/developers/examples/run.mjs my-plot --players 2 --seconds 12 \
[--place <block>@x,y,z] [--break x,y,z] [--fire]Your plot's log.info and log.warn, and every refusal or crash, print as [plot <id>] ....
Seeing what onlookers see

The runtime has a headless software renderer (plot/render/) that draws from what a client actually decoded, so the picture is exactly what onlookers receive. The tests write examples to plot/test/out/ (gltf-sample-level.png and others). The harness's renderOnlooker({ eye, target, w, h }) and renderPlayerView(client, { w, h }) wrap it for your own plots:
js
import { writeFileSync } from "node:fs";
import { createHarness } from "./plot/harness/harness.mjs"; // from servers/inventery/
const h = await createHarness({ plots: [{ dir: "my-plot", origin: [0, -16, 0] }], seed: 1 });
const ana = h.client("ana", { at: [-8, 0, 4] });
await ana.walkTo([3, 0, 4]);
await h.run(12); // entry protection, a start countdown if your plot runs one
ana.pressButton("shop"); ana.useEntity(keeperId); ana.chooseDialogue(0); ana.answerKeepItems(true);
writeFileSync("view.png", await h.renderPlayerView(ana, { w: 640, h: 400 }));.inv bundles
sh
node plot/sdk/build.mjs pack my-plot # dist/<namespace>-<version>.invAn .inv bundle (.inventery where .inv clashes) holds one package: its manifest, code, assets and (planned) a signature. It's for sharing outside the store: a download, a repo, a forum post.
The Create tab and the marketplace (Planned)
In the app, the Create tab becomes the hub: your plots, your mods and runtimes, and the marketplace where you install other people's packages into your plot.
Where your builds live (you don't need to export)
Every built artifact is content-addressed: named by a hash of its bytes, in our own store (R2). A plot's draft and its published version are each a list of hashes. Upload only what's new, share packages across plots for free, and never manage files: publishing is "point the plot at these hashes". (build.mjs hashes with SHA-256 truncated to 128 bits today; the protocol's eventual hash is BLAKE3.)