Skip to content

Tooling: build, lint, run, render, pack ​

You'll learn: the tools that exist today, and the ones coming. Commands run from servers/inventery/.

ToolStatusDoes
node plot/sdk/lint.mjs <dir>Builtchecks the platform's rules, with a fix for each finding
node plot/sdk/build.mjs <dir>Builtvalidates plot.json, bundles logic.ts to dist/logic.js, hashes the artifacts
node plot/sdk/build.mjs pack <dir> [out.inv]Builtbuilds, then writes an .inv bundle
node docs/developers/examples/run.mjs <dir>Builtruns a built plot in the headless room (with the base game's rules) with players walking in
npm run test:plotBuiltthe runtime's own tests, including onlooker and player-view renders
the harness (createHarness, plot/harness/harness.mjs)Builta deterministic room with scripted clients that speak the real protocol, assertions, and renderOnlooker / renderPlayerView PNGs
node docs/developers/reference/gen.mjsBuiltrefills 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 devPlannedyour own package hot-reloaded in a local room, with budget meters and an outside camera
the creator APIPlannedupload 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)
RuleCatches
visible-in-scenegeometry, lights or sounds made locally in client code: onlookers would never see them
determinismperformance.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-levela lobby call at plot level (top-level code, init, chooseLobby, tickPlot...)
plot-call-in-lobbya plot-level call inside a lobby hook
runtime-onlya mod using the runtime's calls or hooks
channel-visualsvisual state sent on a custom channel
invisible-modela model with visibility "serverOnly" (nobody sees it)
costly-call-every-ticka JSON-heavy host call made every tick, outside any if (warning)
manifesta missing or bad plot.json field (a runtime-only field in a mod, an unknown baseRules category...)
registriesa 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's example onlooker render

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>.inv

An .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.)