# Inventery plots: a cheat sheet for agents writing mods > A plot is part of a shared 3D block world that runs your code in a wasm sandbox on the room server (the > referee). People see it from outside, walk in, and play. Write TypeScript against "@inventery/host" (or any > language compiled to wasm against the raw ABI). Full reference: plot/sdk/AGENTS.md and plot/sdk/host.d.ts > (generated from plot/schema/host-api.mjs). Human docs: docs/developers/README.md. API names: plot/NAMING.md. ## Status Runtime, SDK, harness and protocol are built and run headless (branch plot-runtime). Plots also run in a real room with plot hosting, part of the core game (the local plots dev stack: browser and the iOS app). Not in the live world, staging or production. In progress: client code (onFrame/predictStep in a Worker, the rollback relay). Planned: inventery-mod dev, the creator API, publishing from the app. ## A package - folder: plot.json + logic.ts (+ assets/). Build: node plot/sdk/build.mjs (writes dist/logic.js). - Lint: node plot/sdk/lint.mjs . Run headless: node docs/developers/examples/run.mjs [--players N]. - plot.json: kind ("runtime" | "mod"), namespace, id ".", version (semver), name, apiVersion: 1, code: "dist/logic.js". Runtime only: stashItems (bool), lobbies ({size, area?} | null), worldgen ({generator: flat|hills|void|world, seed?, features?}), baseRules ({blocks, pvp, drops, pickup, hunger, mobs}, each true by default), rollback. Optional: depends, tickRate 20..60, movement "server"|"client", entry, channels {name: {reliable, rate?}}, assets, registries, rating, tags. Unknown fields are flagged. Coordinates in plot.json are relative to the plot's min corner; host calls take world coordinates. - A plot = exactly one runtime + any number of mods, each its own package with its own plot.json. No runtime = the default Inventery runtime (survival and build: no lobbies, no stash, every base rule on). The runtime decides lobbies, stashItems, entry handling, game rules (deaths, win, reset), camera and movement, world gen, which buttons get screen slots, which base-rule categories stay on, and extension points for mods (mods.sendEvent -> onModEvent). It never replaces the base game's mechanics, the guarantees (Leave, entry protection, items given back, the trusted layer) or anything outside the plot. Mods add content and world-event reactions. A mod calling a runtime-only call gets HostError rule "runtimeOnly". Reusable runtimes let games be mods: Bedwars and Spleef are mods on Team Minigame (plot/samples/team-minigame, README lists its events). ## The rules 1. Visible = in the scene. Onlookers run no plot code; they draw only the replicated scene. Use scene.create/setComponent (Model, Light, Emitter, AudioSource, Billboard...) or audio.play for anything others should see or hear. 2. Channels (network.send) are game logic only; onlookers never get them. 3. Base rules run inside the plot (on the player's lobby layer and in-plot inventory): breaking and placing with tools, two-part beds and doors, base drops, PvP with weapons/knockback/armour, items on the ground picked up by walking over them, hunger, plot entities' ai and the melee on them. Don't rebuild them. Filter with onBlockBreak/onBlockPlace/onUse/onPlayerDamage/onEntityDamage (return false to refuse; runtime first, then mods in load order; any refusal wins); listen with onBlockBroken/onBlockPlaced (ev.changes: every cell changed). world.setDrops(block, stacks|null) (plot level), world.dropItem(pos, item, n), world.placeBlock(pos, block, {facing}) (both halves), world.rotateBlock. A runtime sets baseRules. false to own that category itself (a game that is its own server). Plot entities have no base AI and no base melee: a plot moves them and calls content.damageEntity itself (an "attack" input action, or onUse on the entity). 4. Contexts. Lobby hooks (onPlayerEnter, tickLobby, onBlockBreak, onUse, ...) act on their lobby. Plot level (top-level code, init, chooseLobby, tickPlot, tickAllLobbies, onUpgrade) has no lobby; use lobbies.runWithin(id, fn) (runtime), or lobbies.switchTo(id) inside tickAllLobbies. Wrong context: refused "wrongContext". Closures defined inside init count as plot level for the lint: add handlers at top level. 5. Lobbies are siloed rooms of up to lobbies.size players: players, entities, block edits, storage, timers, UI, audio, chat. A plot declares only the size. The platform puts arrivals in the fullest lobby that isn't full or locked, makes new lobbies and suspends empty ones. lobbies.lock() closes one that has room (a round in progress, a fixed party); unlock() reopens it. Full = players.length >= size. Without lobbies the plot is one lobby "main". At plot level, world.setBlock/fillBox and scene.create({..},{shared:true}) write the shared base. 6. Entry protection (R4): for 10 s after entering you can't teleport, setMovement, setCamera, playCameraPath, damage, setHealth, respawn, give, take or clearInventory that player (refused "playerProtected"). Do it in onPlayerProtectionEnd. player.protectionSecondsLeft = seconds left. 7. Start countdown (optional, runtime): lobbies.startCountdown(opts?) runs until the newest member's entry protection ends (or opts.minSeconds, if later); the base draws it; then onCountdownEnd(). By default only those still in their entry protection are protected; {protectEveryone: true} protects every member until it ends (an equal start). onCountdownPlayerLeft(p) if someone leaves meanwhile: startCountdown() again, cancelCountdown(), or let it run. A room anyone can walk into (a 4-player puzzle) needs no countdown. 8. Stashed items (stashItems: true, R6): the player's real items, body and flight are set aside on entry and given back on leaving; give/take act on the plot's own kit. Not stashed (R7): players try things with their real items, and it's undone on leaving, unless they choose to keep their items: players.askToKeepItems -> onKeepItemsAnswer. Admin creative is off in any plot. 9. Deaths: the runtime's onPlayerDeath returns true to handle it (players.respawn); else a soft death outside. 10. Memory is never saved. Restarts rerun top-level code, init, onLobbyLoad. Keep state in storage (get/set per lobby, getPlayer/setPlayer per player in the lobby, getPlot/setPlot plot-wide, getPlayerProfile/ setPlayerProfile per player plot-wide) or use defineLobbyGame. 11. Determinism: time.now(), time.tickNumber(), time.random(); timers: time.startTimer(seconds, name, data) -> onTimer. 12. Budgets: 2 ms CPU a tick (hard 8), 20 ms a rolling second, 32 MB memory, 1 MB storage. Over: refused "overBudget" or "storageFull"; a crash restarts the sandbox, which rebuilds from storage. 13. Never: reach outside the plot, network, device, accounts, real money; draw over the settings button (top left; its menu's first item is "Leave {Plot}", never an on-screen button), chat (top right), the protection or countdown ring (top centre) or prompts; draw real-style nametags. ## Ids "namespace:name". Add content with the local name (blocks.add("bed") -> "bedwars:bed"). Base: "inventery:stone" (world.blockId("stone") also works). Base blocks are the engine's real list (plot/runtime/base-blocks.json: "grass_block", "red_wool", "bed" and "bed_head" with _south/_west/_east facings...); older names ("grass", "wool_red") still resolve. Every base block is also an item ("inventery:white_wool"), plus the base items ("inventery:iron_sword"). ## Common calls scene.create(components, {visibility?, parent?, shared?}) -> EntityId; scene.setComponent(id, "Light", v | null); scene.remove(id); scene.setVisibility(id, "public" | "lobby" | "serverOnly" | {players: [...]}) world.blockId(name); world.getBlock(pos); world.setBlock(pos, id); world.fillBox(box, id); world.raycast(ray, opts) world.placeBlock(pos, id, {facing?}); world.setDrops(id, stacks | null); world.dropItem(pos, item, count?) players.list(); players.get(p) -> {id, name, pos, health, itemsStashed, keepsItems, protectionSecondsLeft, entity, ...}; players.teleport/setCamera/setMovement/respawn (runtime); players.damage/heal/give/take/inventory ui.chat(to, text, {color?}); ui.showTitle(to, title, {subtitle?}); ui.setScoreboard(to, title, rows); ui.showToast(to, {text}); ui.setBar(to, {id, kind, fill}); ui.setClock(to, {mode, seconds}) (all drawn as panels); ui.setPanel(to, id, {kind: "box", anchor?, dir?, children: [{kind: "text", text, color?}, {kind: "bar", fill}, {kind: "space"}, ...]}) (Minecraft-style: dimmed rects, the chat's font; only buttons are native); ui.setButton(to, {id, icon, label}) (runtime) -> onButtonPress; ui.drawOverlay(to, ops); ui.showDialogue(to, speakerEntity | null, text, {name?, color?, voice?, choices?}) -> onDialogueChoice (the base NPC dialogue, the tutorial's Map Keeper's: subtitles; with choices its plank box; the speaker speaks in the scene); ui.askForText(p, prompt) -> onTextSubmit. to = PlayerId | PlayerId[] | "lobby". input.read() -> InputEvent[] {player, action?, move?, look?, aimRay?, renderTime?} (in tickLobby) network.send(channel, to, data) -> onMessage; network.raycastAtTime(time, ray, {ignore: [shooter]}) (lag compensation, 500 ms) lobbies.current(); lobbies.lock()/unlock(); lobbies.startCountdown({protectEveryone?}); lobbies.list/create/ runWithin/movePlayers/regenerateTerrain/setFeatured content.addBlock/addItem/addRecipe/addEntityType/spawnEntity/addStructurePattern/addWorldgenFeature; content.damageEntity(id, amount, {by?}) -> onEntityDeath; content.getEntity(id); EntityTypeDef.ai "hostile" | "ranged" | "neutral" | "passive" | "none" runs the base mob AI (baseRules.mobs), players' swings are the base melee (onEntityDamage may refuse), a mob's hit is onPlayerDamage(player, amount, null, entity); mods.sendEvent(event, data) -> others' onModEvent audio.play(sound, {at?, to?}); audio.playMusic(track); log.info(text) ## Every call (generated from the schema) scene: create, setComponent, getComponent, remove, setParent, setVisibility, query, wakeProp input: read network: send, receive, pastPositions, raycastAtTime assets: hashOf, create storage: get, set, delete, keys, getPlayer, setPlayer, getPlot, setPlot, getPlayerProfile, setPlayerProfile lobbies: current, lock*, unlock*, startCountdown*, cancelCountdown*, switchTo, list, get, create*, regenerateTerrain*, suspend*, resume*, delete*, movePlayers*, announce, runWithin*, exportData*, setFeatured* world: bounds, plotChunks, isInsidePlot, monument, blockId, getBlock, setBlock, fillBox, raycast, groundHeight, replaceTerrainChunk, placeBlock, rotateBlock, setDrops, dropItem, setArrival*, setChestItems, getChestItems, takeFromChest, putInChest, getChestSlots players: list, positions, get, teleport*, setMovement*, setCamera*, playCameraPath*, damage, heal, setHealth, respawn*, give, take, inventory, clearInventory, askToKeepItems*, setAvatar*, setRealNametagsVisible* ui: setButton*, removeButton*, chat, showTitle, setScoreboard, removeScoreboard, setClock, removeClock, setBar, removeBar, showToast, drawOverlay, showDialogue, askForText, setPanel, removePanel content: addBlock, addItem, addRecipe, addEntityType, spawnEntity, addStructurePattern, addWorldgenFeature, damageEntity, getEntity mods: sendEvent, list audio: play, stop, playMusic time: now, tickNumber, random, startTimer, cancelTimer, setTickRate* log: info, warn subareas: enter, leave sky: setForPlayer* portal: set* (* = runtime only) ## Every hook (generated) init, chooseLobby*, onLobbyLoad, onPlayerEnter, onPlayerProtectionEnd, onCountdownEnd*, onCountdownPlayerLeft*, onPlayerLeave, tickLobby, tickPlot, tickAllLobbies, onTimer, onMessage, onBlockBreak, onBlockPlace, onBlockBroken, onBlockPlaced, onUse, onButtonPress*, onTextSubmit, onPlayerDamage, onPlayerDeath*, onKeepItemsAnswer*, onPropStartMoving, onPropStopMoving, onUpgrade, onDialogueChoice, onStructureComplete, onEntityDeath, onPlotChunksChanged, onModEvent, onEntityDamage client: onFrame, predictStep, onInput, onServerMessage ## Minimal mod import { scene, ui } from "@inventery/host"; import { blocks, events } from "@inventery/minecraft"; export { onBlockBroken } from "@inventery/minecraft"; const LANTERN = blocks.add("lantern", { texture: "textures/lantern.png", hardness: 0.5 }); events.blockBroken.on(LANTERN, (ev) => { // after the base game broke it (events.blockBreak: before, false refuses) scene.create({ Transform: { pos: ev.pos }, Light: { color: "gold", intensity: 3, range: 6 }, Lifetime: { seconds: 0.5 } }); if (ev.by) ui.chat(ev.by, "The lantern flickers out."); }); ## Minimal round with a start countdown (runtime) import { lobbies, players, ui } from "@inventery/host"; import { defineLobbyGame } from "@inventery/lobby"; const game = defineLobbyGame({ setup: () => ({ joined: [] as string[] }), onPlayerEnter(s, p) { s.joined.push(p); const l = lobbies.current(); if (l.players.length >= l.size && !l.locked) { lobbies.lock(); lobbies.startCountdown({ protectEveryone: true }); } }, onCountdownEnd(s) { for (const p of s.joined) players.teleport(p, [8, 40, 8]); ui.showTitle("lobby", "Go!"); }, }); export const { onLobbyLoad, onPlayerEnter, onCountdownEnd } = game; // unlock() when the round is over, so the next players can join. ## Examples (build, lint and run clean) docs/developers/examples/{lanterns, bedwars, dungeon, arena}. The full samples: plot/samples/ (team-minigame + bedwars + spleef, story-dungeon, gltf-level, hidden-roles, frontier).