Skip to content

Host API: what you call ​

Every module of @inventery/host. The tables are generated from plot/schema/host-api.mjs by reference/gen.mjs (node docs/developers/reference/gen.mjs); the prose around them is hand-written. Status: Built (headless) unless noted. runtime marks calls only a runtime package may make (a mod is refused with runtimeOnly). Any call outside its context is refused with wrongContext; any call may be refused with badArguments or notSupported. Context and refusals: see the overview.

Type names (PlayerId, Vec3, Box, Visibility, Targets, ButtonDef, ...) are in plot/sdk/host.d.ts, with a doc comment each. The common ones:

ts
type PlayerId = string;          // per-plot, never the account id; stable across visits
type LobbyId = string;           // "main" when the plot has no lobbies; "draft" is the creator's
type Vec3 = [number, number, number];
type Box = { min: Vec3; max: Vec3 };               // block coordinates, max exclusive
type AssetRef = string;          // a bundle path ("models/bed.glb") or a content hash ("h:" + 32 hex)
type Color = string;             // "#rrggbb", "#rrggbbaa" or a palette name ("gold")
type Visibility = "public" | "lobby" | "serverOnly" | { players: PlayerId[] };
type Targets = PlayerId | PlayerId[] | "lobby";

scene ​

The replicated scene: what others see. Nothing replicates unless it's written here. Components: Components.

ts
const id = scene.create({ Transform: { pos: [8, 65, 8] }, Model: { asset: "models/chest.glb" } }, { visibility: "lobby" });
scene.setComponent(id, "Transform", { pos: [8, 66, 8] });
CallContextRefusesWhat
scene.create(components: Components, opts?: CreateOpts): EntityIdanywrongContext outsidePlotA new entity in the current lobby (or the shared base, with opts.shared at plot level)
scene.setComponent(id: EntityId, component: string, value: any)anywrongContext playerBodyReplace one component; null removes it.
scene.getComponent(id: EntityId, component: string): anyanyA component's value, or null.
scene.remove(id: EntityId)anywrongContext playerBody
scene.setParent(id: EntityId, parent: EntityId | null)anywrongContext
scene.setVisibility(id: EntityId, visibility: Visibility)any
scene.query(opts?: QueryOpts): EntityId[]anyThis lobby's entities (plus the shared base's)
scene.wakeProp(id: EntityId)lobbyTurn a resting prop back into a moving entity (protocol.md, resting props); onPropStartMoving runs.

input ​

This tick's inputs from this lobby's players. Call input.read() in tickLobby.

CallContextRefusesWhat
input.read(): InputEvent[]lobbyThe inputs since the last read (each is returned once)

network ​

Custom channels for game logic (never visuals: onlookers never receive them) and lag compensation. Channels are declared in plot.json ("channels": { "shop": { "reliable": true } }). Messages arrive in onMessage or network.receive.

ts
network.send("shop", p, JSON.stringify({ bought: "sword" }));
const hit = network.raycastAtTime(ev.renderTime ?? time.now(), ev.aimRay, { players: true, ignore: [ev.player] });
CallContextRefusesWhat
network.send(channel: string, to: Targets, data: Uint8Array | string)lobbyoverBudget wrongContext
network.receive(channel: string): Message[]lobby
network.pastPositions(time: number): PastPositionslobbyWhere the lobby's players were at a past server time (the last 500 ms are kept; older times are clamped)
network.raycastAtTime(time: number, ray: Ray, opts?: RaycastOpts): Hit | nulllobbyA raycast against the lobby as it was at a past server time (the last 500 ms are kept): hit tests that are fair to a lagging shooter.

assets ​

Bundle files, and assets your code makes at run time (meshes, materials, sounds), named by content hash.

CallContextRefusesWhat
assets.hashOf(ref: AssetRef): AssetRefanyA bundle path's content hash (and a check that the file exists)
assets.create(kind: "mesh" | "material" | "sound" | "texture" | "model", data: any): AssetRefanyoverBudgetAn asset made at runtime; returns its content hash (the same data gives the same hash). A mesh is {positions, indices?, colors? | color?}; a textured mesh (mesh terrain: world.replaceTerrainChunk) adds uvs (two a vertex) and texture {tiers: [bundle PNG or JPEG paths, finest first: the low graphics level gets the second], avg?: "#rrggbb" (shown until a tier arrives), mask?, doubleSided?}.

storage ​

The plot's own saved data, as JSON values. Four scopes: this lobby's (get, set), one player's in this lobby (getPlayer, setPlayer), the whole plot's (getPlot, setPlot), and one player's across the whole plot (getPlayerProfile, setPlayerProfile: a profile that follows them into any lobby). 1 MB per plot plus blocks.

ts
storage.set("round", 3);
storage.setPlayer(p, "kills", (storage.getPlayer<number>(p, "kills") ?? 0) + 1);
storage.setPlayerProfile(p, "chapter", 2);
storage.setPlot("season", 4);
CallContextRefusesWhat
storage.get(key: string): anylobby
storage.set(key: string, value: any)lobbystorageFull
storage.delete(key: string)lobby
storage.keys(prefix?: string): string[]lobby
storage.getPlayer(player: PlayerId, key: string): anylobby
storage.setPlayer(player: PlayerId, key: string, value: any)lobbystorageFull
storage.getPlot(key: string): anyanyThe whole plot's, shared by every lobby.
storage.setPlot(key: string, value: any)anystorageFull
storage.getPlayerProfile(player: PlayerId, key: string): anyanyOne player's across the whole plot (a profile that follows them into any lobby: story progress, ranked stats)
storage.setPlayerProfile(player: PlayerId, key: string, value: any)anystorageFull

lobbies ​

A lobby is a siloed room of up to lobbies.size players. The platform puts arrivals in the fullest lobby that isn't full or locked, creates lobbies as needed and suspends empty ones; lock() and unlock() close and reopen one; the start countdown is optional. See Lobbies. Most of these are the runtime's.

CallContextRefusesWhat
lobbies.current(): Lobbylobby
lobbies.lock() runtimelobbyTake no newcomers, even with room (a round in progress, a fixed party)
lobbies.unlock() runtimelobbyTake newcomers again (for the next round)
lobbies.startCountdown(opts?: CountdownOpts): number runtimelobbyOptional: a base-drawn countdown that ends when the newest member's 10 s entry protection ends (or after opts.minSeconds, whichever is later), then onCountdownEnd. Members past their protection stay unprotected unless opts.protectEveryone (R4). Returns countdownEndsAt.
lobbies.cancelCountdown() runtimelobby
lobbies.switchTo(id: LobbyId | null): booleananyInside tickAllLobbies only: carry on in lobby id's context (null: back to plot level). The previous lobby's writes are saved here; false means skip this lobby (it stopped, or the tick's CPU budget is spent: false from then on)
lobbies.list(): Lobby[]any
lobbies.get(id: LobbyId): Lobby | nullany
lobbies.create(opts?: LobbyCreateOpts): LobbyId runtimeplot
lobbies.regenerateTerrain(id: LobbyId, seed?: number) runtimeplotlobbyNotEmptyDrop the lobby's block edits and generate its terrain again (a new seed if given)
lobbies.suspend(id: LobbyId) runtimeplot
lobbies.resume(id: LobbyId) runtimeplot
lobbies.delete(id: LobbyId) runtimeplotlobbyNotEmpty
lobbies.movePlayers(players: PlayerId[], to: LobbyId) runtimeplotplayerProtected
lobbies.announce(text: string, opts?: ChatOpts)plotA chat line to every player in every lobby.
lobbies.runWithin(id: LobbyId, fn: () => any): any runtimeplotRun fn in that lobby's context (read and write its storage, entities, edits)
lobbies.exportData(id: LobbyId): LobbyData runtimeplot
lobbies.setFeatured(id: LobbyId) runtimeplotThe lobby distant viewers, the map and the feed see.

A Lobby is { id, players, size, locked, countdownEndsAt?, countdownProtectsEveryone?, suspended, region?, neverSuspend, featured }. It's full when players.length >= size.

world ​

Blocks and mesh terrain, inside the plot only (reads may reach one chunk outside). Edits in a lobby hook go to that lobby's layer; at plot level they go to the shared base.

What players break and place is done by the base game's rules (unless baseRules.blocks is off), on their lobby's layer; your hooks filter it. These calls work with those rules: placeBlock places a block as the base game does (both halves of a bed or door, in a facing), rotateBlock turns a directional block, setDrops replaces a block's base drops in this plot, and dropItem leaves an item lying in the lobby for the base pickup.

ts
const STONE = world.blockId("stone");
world.fillBox({ min: [0, 64, 0], max: [16, 65, 16] }, STONE);
const hit = world.raycast({ origin: [0, 70, 0], dir: [0, -1, 0] }, { maxDistance: 20 });
world.placeBlock([2, 65, 8], world.blockId("bed"), { facing: "east" });   // foot here, head at x 3
CallContextRefusesWhat
world.bounds(): BoxanyThe box around the plot's chunks (full height). A plot is a monument's chunks (register #102), which needn't fill the box: use isInsidePlot. Its min corner moves when the plot grows west or down; manifest coordinates stay relative to the corner at load.
world.plotChunks(): number[]anyThe plot's chunks as flat [cx, cz, cx, cz, ...] (16×16 columns, full height)
world.isInsidePlot(pos: Vec3): booleanany
world.monument(): MonumentInfo | nullanyThe ASDFGHJKL this plot belongs to (its two blocks are the base game's: build around them, never over them); null for a dev plot.
world.blockId(name: string): BlockIdanyA block's runtime id by name ("stone", "oak_planks", "air", or "<namespace>:<name>")
world.getBlock(pos: Vec3): BlockIdany
world.setBlock(pos: Vec3, block: BlockId)anyoutsidePlot overBudgetIn a lobby: its edit layer. At plot level: the shared base (published to every lobby)
world.fillBox(box: Box, block: BlockId): numberanyoutsidePlot overBudget
world.raycast(ray: Ray, opts?: RaycastOpts): Hit | nullany
world.groundHeight(x: number, z: number): number | nullanyThe y a player would stand at in this column (blocks and mesh terrain, in this context's layer)
world.replaceTerrainChunk(chunk: TerrainChunk)anyoutsidePlot overBudgetMesh terrain: replace a chunk's mesh (budgeted; the latest wins)
world.placeBlock(pos: Vec3, block: BlockId, opts?: BlockStateOpts): numberanyoutsidePlot overBudgetA block as the base game places it, in the state opts asks for (its directional variant: bed_east for bed facing east): a bed's foot here and its head one step the way it faces, a door's lower half here and its upper half on top; any other block as setBlock. Returns the cells written.
world.rotateBlock(block: BlockId, turns: number): BlockIdanyThe block turned quarter turns clockwise seen from above (its directional variant: beds, doors, stairs, gates, ladders, furnaces, chests, wall torches); itself if it has none.
world.setDrops(block: BlockId, drops: ItemStack[] | null)plotWhat a block drops when broken in this plot, instead of the base game's drop rules ([] for nothing: beds in a bed game); null goes back to the base rules.
world.dropItem(pos: Vec3, item: string, count?: number): EntityIdlobbyoutsidePlotAn item lying in the lobby (the lobby and its onlookers see it), picked up the base way by whoever walks over it (a keeper never picks up a new base item); a generator's ingots.
world.setArrival(pos: Vec3, yaw?: number) runtimeplotoutsidePlotWhere a portal's arrival puts you in this plot (its lobby area, the start), facing yaw (radians, atan2(dx, dz); default 0). The room then does as when you walk in: your lobby is chosen, your items are set aside, your 10 s entry protection starts. Without it, a portal puts you just inside the plot by its monument. Runtime only.
world.setChestItems(pos: Vec3, items: ItemStack[] | null)lobbyoutsidePlotA chest's contents in this lobby (a lobby's chests are its own, never the world's: each lobby has its own box at pos, saved with its edits and gone with its terrain, lobbies.regenerateTerrain). Up to 27 base-item stacks ("inventery:<name>", 1 to 64 each; never Nimgots), in slot order; null empties it. It replaces what's there: what plot code puts in is the plot's (loot), so a player just trying the plot takes it as theirs until they leave, a keeper never (R7: keepers take out only real items keepers put in); in a stashing plot it's part of the kit. Set it whenever the chest should be refilled (a lobby's load, a new round): a box can be set before its chest block is there.
world.getChestItems(pos: Vec3): ItemStack[]lobbyoutsidePlotWhat's in the chest at pos in this lobby now, in slot order (players open chests and move stacks in and out with the base chest screen, manifest baseRules.chests)
world.takeFromChest(player: PlayerId, pos: Vec3, slot: number): numberlobbyoutsidePlot wrongContextA base transaction: the whole stack in the chest's slot (0 to 63) into the player's inventory, as the base chest screen's tap moves it (what doesn't fit stays; "Inventory full"), under the item rules (stashed kit, keepers' ledger, never Nimgots). Real stacks, wear and enchantments kept; nothing is made or lost, and it's undone with the hook if the hook fails. The chest is this lobby's (in a sub-area, its parent lobby's). Returns how many moved.
world.putInChest(player: PlayerId, pos: Vec3, slot: number): numberlobbyoutsidePlot wrongContextA base transaction: the stack in the player's hand into the chest's slot (0 to 63; onto the same item up to a full stack, into an empty slot whole), as the base chest screen moves it, under the item rules. Atomic with the hook, like takeFromChest. Returns how many moved.
world.getChestSlots(pos: Vec3): (ItemStack | null)[]lobbyoutsidePlotThe chest's box slot by slot (null for an empty slot; 27 long, or up to 64 once takeFromChest or putInChest used a slot past 26; [] for a chest never opened). The chest is this lobby's (in a sub-area, its parent lobby's)

players ​

The players in the current lobby. Calls that move, change the movement of, hurt or change the items of a player are refused with playerProtected during their entry protection (and during a start countdown with protectEveryone).

CallContextRefusesWhat
players.list(): PlayerId[]lobby
players.positions(): number[]lobbyX, y, z for each player of players.list(), in order, flat: the cheap read for per-tick checks.
players.get(id: PlayerId): Player | nullany
players.teleport(id: PlayerId, pos: Vec3) runtimelobbyplayerProtected outsidePlot
players.setMovement(id: PlayerId, def: MovementDef) runtimelobbyplayerProtectedHow the player moves: walk, swim, fly, a vehicle...
players.setCamera(id: PlayerId, def: CameraDef | null) runtimelobbyplayerProtected
players.playCameraPath(id: PlayerId, path: CameraPath) runtimelobbyplayerProtectedPlay a cinematic camera path for this player.
players.damage(id: PlayerId, amount: number, opts?: DamageOpts)lobbyplayerProtected
players.heal(id: PlayerId, amount: number)lobby
players.setHealth(id: PlayerId, health: number, max?: number)lobbyplayerProtected itemsStashed
players.respawn(id: PlayerId, pos: Vec3) runtimelobbyplayerProtected outsidePlotThe plot's own respawn (inside a plot, death is the plot's; R7)
players.give(id: PlayerId, item: string, count?: number)lobbyplayerProtected itemsStashedInto the plot's own inventory when the plot stashes items (R6); the real inventory otherwise (taken back on leaving unless they keep their items, R7)
players.take(id: PlayerId, item: string, count?: number): numberlobbyplayerProtected itemsStashed
players.inventory(id: PlayerId): ItemStack[]lobbyWhat the plot can see: its own kit when it stashes items (R6); the real items otherwise (R7)
players.clearInventory(id: PlayerId)lobbyplayerProtected
players.askToKeepItems(id: PlayerId) runtimelobbyitemsStashedShow the base "keep your items?" prompt (plots that don't stash); the answer arrives in onKeepItemsAnswer.
players.setAvatar(id: PlayerId, model: AssetRef | null) runtimelobby
players.setRealNametagsVisible(visible: boolean) runtimelobbyShow or hide real players' trusted nametags (anonymity); a plot can never add a real-style tag.

ui ​

Native UI, drawn by the app in each platform's style, per player, never seen by onlookers. show* calls go away on their own; set* calls stay until removed. Targets is a player id, a list, or "lobby". Buttons are the runtime's (ui.setButton; a mod hands its buttons to the runtime, as Bedwars does on Team Minigame). Nothing here can cover the settings button (its menu holds Leave), chat or the other reserved controls.

ts
ui.showTitle("lobby", "Round 2", { subtitle: "Fight!", color: "gold", stay: 2 });
ui.setBar(p, { id: "hp", kind: "resource", label: "Shield", fill: 0.6, color: "aqua" });
ui.drawOverlay(p, [{ op: "rect", x: 20, y: 80, w: 120, h: 40, color: "#000000aa" }, { op: "text", x: 28, y: 92, text: "Ammo 12" }]);
CallContextRefusesWhat
ui.setButton(to: Targets, def: ButtonDef) runtimelobbyAdd or update an on-screen button; presses arrive in onButtonPress.
ui.removeButton(to: Targets, id: string) runtimelobby
ui.chat(to: Targets, text: string, opts?: ChatOpts)lobby
ui.showTitle(to: Targets, title: string, opts?: TitleOpts)lobby
ui.setScoreboard(to: Targets, title: string, rows: ScoreboardRow[])lobby
ui.removeScoreboard(to: Targets)lobby
ui.setClock(to: Targets, def: ClockDef)lobbyA countdown or stopwatch on screen (time.startTimer is the plot's own timer)
ui.removeClock(to: Targets)lobby
ui.setBar(to: Targets, def: BarDef)lobby
ui.removeBar(to: Targets, id: string)lobby
ui.showToast(to: Targets, def: ToastDef)lobby
ui.drawOverlay(to: Targets, ops: DrawOp[])lobbyA raw 2D draw list (rects, images, text, hit regions) in screen points.
ui.showDialogue(to: Targets, speaker: EntityId | null, text: string, opts?: DialogueOpts)lobbyAn NPC line in the base game's NPC dialogue, the tutorial's Map Keeper's: subtitles over play, a word at a time, or with choices its conversation box. From a speaker entity (it speaks in the scene for everyone who sees it: its Speech component) or the plot (null)
ui.askForText(to: PlayerId, prompt: string)lobbyA text box, labelled "Typing to {Plot}" by the base; the answer arrives in onTextSubmit.
ui.setPanel(to: Targets, id: string, node: PanelNode)lobbyAdd or replace a Minecraft-style panel: dimmed rects with text in the chat's font, laid out as a tree of boxes (PanelNode). The scoreboard, clock, bars, titles and toasts are drawn as panels too.
ui.removePanel(to: Targets, id: string)lobby

content ​

The registries. Ids get your package's namespace. Most people use these through @inventery/minecraft.

CallContextRefusesWhat
content.addBlock(def: BlockDef): BlockIdplot
content.addItem(def: ItemDef): stringplot
content.addRecipe(def: RecipeDef)plot
content.addEntityType(def: EntityTypeDef): stringplot
content.spawnEntity(type: string, pos: Vec3, opts?: CreateOpts): EntityIdlobbyoutsidePlotAn entity of an added type, with its model, collider and health.
content.addStructurePattern(pattern: StructurePattern)plot
content.addWorldgenFeature(feature: WorldgenFeature)plotA world-gen feature, applied when lobbies are generated.
content.damageEntity(entity: EntityId, amount: number, opts?: DamageOpts)lobbyHurt a spawned entity; at 0 health it dies (onEntityDeath, its drops)
content.getEntity(entity: EntityId): EntityInfo | nullanyA spawned entity's type and health (for a boss bar)

mods ​

Extension points a runtime offers its mods.

CallContextRefusesWhat
mods.sendEvent(event: string, data?: any)anyCalls onModEvent(event, data) in every other package that exports it.
mods.list(): string[]anyThe installed packages' ids, in load order.

audio ​

Sounds are scene-level: heard by whoever their to visibility allows (default everyone in range, onlookers quieter). Music is heard inside and close by only.

CallContextRefusesWhat
audio.play(sound: AssetRef, opts?: PlayOpts): numberlobby
audio.stop(handle: number)lobby
audio.playMusic(track: AssetRef | null)lobbyHeard inside and close by only (decided); null stops it.

time ​

Determinism: no wall clock in game code. Math.random and Date.now in JS packages are wired to these.

CallContextRefusesWhat
time.now(): numberanyServer time in seconds (the tick clock)
time.tickNumber(): numberany
time.random(): numberanySeeded per plot, lobby and tick; recorded for replay.
time.startTimer(seconds: number, name: string, data?: any): numberanyoverBudgetA timer kept by the host (it survives restarts); calls onTimer(name, data) in the same context.
time.cancelTimer(timer: number)any
time.setTickRate(hz: number) runtimeplotoverBudget20 by default, up to 60 within the CPU quota (decided)

log ​

Debug lines, shown in the dev tools and the runner (run.mjs prints them).

CallContextRefusesWhat
log.info(text: string)any
log.warn(text: string)any

subareas ​

A sub-area is a lobby with its own region: a chest's room, a shop's interior. It's the one way a mod moves a player between lobbies. It's scoped to your package's own sub-areas, entered at the player's own action (call it from their onUse), and always has a way back: subareas.leave, Leave (which leaves the sub-area first), death, a disconnect, a room restart, or leaving its region all bring them back to where they stood. Only your package's hooks run in it. Its blocks are its own layer, and its chests are its parent lobby's (world.getChestSlots, world.takeFromChest, world.putInChest). Example: plot/samples/chest-rooms.

CallContextRefusesWhat
subareas.enter(player: PlayerId, opts: SubAreaOpts): LobbyIdlobbywrongContext outsidePlotMove a player of this lobby into this package's sub-area for opts.key (made on first use: its onLobbyLoad runs, to build it), standing at opts.at; where they stood is where they go back to. Call it from their own action (onUse, onMessage); from a sub-area, it's left first.
subareas.leave(player: PlayerId)lobbywrongContextA player of this sub-area back to the lobby and the spot they came from (their look and camera as before)

sky: the sky inside your plot ​

What your players see overhead, and the light they see by, while they're inside the plot (its lobbies and draft), over the manifest's sky; onlookers outside always keep the world's. By default it's the world's lighting; light changes the light model's settings (a fixed time of day, the sun's colour and strength, the ambient floor), and every surface in the plot (blocks, models, mesh terrain) lights through what you set, so nothing looks pasted in (register #110, #116). It crossfades over fadeSeconds on crossing in and out, and on every change. Example: plot/samples/tropical-island.

CallContextRefusesWhat
sky.setForPlayer(id: PlayerId, def: SkyDef | null) runtimelobbyThis player's sky and light while inside, over the manifest's sky (crossfaded over def.fadeSeconds); null goes back to the plot's own (the manifest's sky, else the world's). It ends when they leave. Runtime only.

portal: your plot's portal ​

How your plot looks on Explore's map, in the feed and at the spawn: its plot portal's title (also its name), subtitle and the feed card's line, in the game's title fonts and palette (register #124). The manifest's portal is the plot's own; portal.set changes it while the plot runs (set it again in init). A published creator plot with one gets its destination portal built at its arrival spot (world.setArrival) once it has run; a portal you lit inside the plot already is restyled instead. A travel portal to it at the spawn is an admin's choice (/plotfeature). /plotportal in game shows the published look, the draft's (what publishing would make it) and the portal standing now.

CallContextRefusesWhat
portal.set(def: PortalDef | null) runtimeanyThe plot portal's title, subtitle and feed line from now on, for the whole plot (from any lobby's hook too), over the manifest's portal (null goes back to it). The portal is the plot's destination portal, built at world.setArrival's spot once the plot has run (a published creator plot whose runtime has a portal; samples get theirs from /plotsamples); a portal the owner lit inside the plot already is restyled instead. It holds while the plot runs; set it again in init. Runtime only.