Skip to content

Quickstart: your first mod ​

You'll learn: the shape of a package, how to add a block, an item and a recipe, how to react to a world event, and how to build, lint and run it. Time: about five minutes.

Status: Built (headless; a plot also runs in the local plots dev stack, see Tooling). Running it in the live world, and inventery-mod dev (your own package hot-reloaded), are Planned.

You need Node 22.18 or newer and a checkout of the plot-runtime branch. Commands run from servers/inventery/, after npm install there.

1. Make a package ​

A package is a folder with a plot.json and some code. Ours adds lanterns.

lanterns/
  plot.json
  logic.ts
  assets/textures/lantern.png     a 16×16 PNG
json
{
  "kind": "mod",
  "namespace": "lanterns",
  "id": "ana.lanterns",
  "version": "0.1.0",
  "name": "Lanterns",
  "apiVersion": 1,
  "code": "dist/logic.js",
  "assets": "assets"
}
  • kind: "mod": composable content. (The other kind, a runtime, owns a plot's game flow. See Runtimes and mods.)
  • namespace: "lanterns": every id you add becomes lanterns:<name>.
  • id: <owner>.<name>, the package's own id.

2. Add a block, an item and a recipe ​

ts
// logic.ts
import { scene, ui } from "@inventery/host";
import { blocks, items, recipes, events } from "@inventery/minecraft";

// Hooks the Minecraft layer's events need: re-export the ones you use.
export { onBlockBroken } from "@inventery/minecraft";

// 1. Content. Top-level code runs at plot level when the plot's code starts (and after every restart).
const LANTERN = blocks.add("lantern", {               // id "lanterns:lantern"
  texture: "textures/lantern.png",
  hardness: 0.5,
  drops: [{ item: "lanterns:lantern", count: 1 }],
});
items.add("lantern", { texture: "textures/lantern.png", places: "lanterns:lantern" });
recipes.add({
  pattern: ["G", "T"],
  keys: { G: "inventery:glass", T: "inventery:torch" },
  result: { item: "lanterns:lantern", count: 1 },
});

That's already a working mod: a content pack. Content is data in registries, so other mods can use your lantern in their own recipes by its id. You don't write any breaking or placing code: the base game's rules run inside the plot, so players place and break lanterns like any block, and a broken one drops its drops.

A pure data pack, with no code at all, is a mod whose plot.json has "registries": "registries" and no code: the folder's blocks.json, items.json, recipes.json (and entities, structures, world-gen features) are added before any code runs (Built). build.mjs and lint.mjs check every entry. Example: plot/test/fixtures/sdk/lanterns/.

3. React to a world event ​

Add this below the content:

ts
// 2. Behaviour: after a lantern is broken, flash a light and tell whoever broke it.
events.blockBroken.on(LANTERN, (ev) => {
  scene.create({
    Transform: { pos: ev.pos },
    Light: { color: "gold", intensity: 3, range: 6 },
    Lifetime: { seconds: 0.5 },                        // the room removes it after half a second
  });
  if (ev.by) ui.chat(ev.by, "The lantern flickers out.", { color: "gold" });
});

World events come in two kinds. Filters run before the base game acts and can refuse it (events.blockBreak.on(...): return false to refuse); listeners hear what it did (events.blockBroken: every cell it changed). Any package's refusal wins.

Two things worth noticing:

  • The light goes in the scene (scene.create). That's how everyone, including people standing outside the plot, sees it. If it should be seen, it's in the scene. (Why.)
  • The handler runs in the lobby where the block broke. Chat, the scene and storage all act on that lobby only. (Lobbies.)

Prefer no library? The same thing as a plain hook:

ts
import type { BlockChangeEvent } from "@inventery/host";

export function onBlockBroken(ev: BlockChangeEvent) {
  if (ev.block !== LANTERN) return;                    // not ours
  // ...the same body...
}

4. Lint, build and run ​

sh
node plot/sdk/lint.mjs path/to/lanterns      # the platform's rules, with a fix for each finding
node plot/sdk/build.mjs path/to/lanterns     # bundles logic.ts to dist/logic.js, hashes the artifacts
node docs/developers/examples/run.mjs path/to/lanterns --seconds 1 \
  --place lanterns:lantern@5,0,4 --break 5,0,4

The last line walks a player into the plot in the headless room (with the base game's rules), steps them up to the spot, places a lantern, breaks it, and prints what the player received:

place lanterns:lantern at 5,0,4: true
break 5,0,4: true
lobby draft: suspended
lobby main: 1 player

ana: at 4,0,6, inside in lobby main, plot health 20
  inventory: []
  CHAT {"scope":"lobby","from":"ana.lanterns","text":"The lantern flickers out.","opts":{"color":"gold"}}

(The lantern it dropped lies on the ground a step away; walking over it would pick it up.)

This exact mod lives in examples/lanterns.

5. What you didn't have to do ​

  • No networking. Writing the scene replicates it to every viewer.
  • No block rules. Placing, breaking, drops and pickup are the base game's, inside your plot too.
  • No lobby bookkeeping. A plot with no runtime of its own runs the default Inventery runtime: one lobby ("main"), shared with the world. A runtime that sets a lobby size gets lobbies the platform makes and fills.
  • No save code for the block. Block edits are kept by the host, per lobby.

Next ​

Coming soon: inventery-mod dev, your own package hot-reloaded in a local room (Tooling). Today: the harness (plot/harness/harness.mjs) for scripted players and renders, and the plots dev stack for the samples in a browser.