Appearance
Runtimes and mods
You'll learn: the two kinds of package, what a runtime decides and what it never replaces, how the base game's rules run inside a plot, and how mods compose. You'll need this when: you decide whether you're writing a game, adding to one, or writing a game as a mod on someone's runtime.
Status: Built (headless, and in the plots dev stack): both kinds, the runtime-only rule, dependency and event order, data packs, the base rules with their category switches, and a reusable runtime (Team Minigame) with Bedwars and Spleef as mods on it.
A plot is one runtime plus any number of mods
Each is its own package, with its own plot.json and its own namespace.
| Runtime | Mod | |
|---|---|---|
| How many per plot | exactly one; none means the default Inventery runtime (survival and build: no lobbies, no stash, every base rule on) | any number |
| Decides | lobbies (none, or rooms of lobbies.size; chooseLobby, lock/unlock, the optional start countdown); whether items are stashed, and causality; entry handling; game rules (what a death means, winning, resetting); the camera and custom movement; the world-gen recipe; which buttons get screen slots; which base-rule categories stay on | nothing exclusive |
| Adds | its own game, plus extension points for mods | content and behaviour: blocks, items, recipes, entity types, structure patterns, world-gen features, world-event reactions |
| Without code | a data pack (registries JSON, no code) |
"Runtime" is the developer name. What players will see it called is still open (maybe "Mode").
Think of it like a web app: the runtime is the app's router and session; mods are the components and content that plug into it.
What a runtime never replaces
A runtime controls how its plot is run. It doesn't replace:
- the base game's mechanics, which run inside the plot (next section): your hooks filter them and react to them;
- the guarantees: "Leave {plot}" in the settings menu, the 10 s entry protection, real items given back, trusted nametags and the rest of the trusted layer, visibility to onlookers, budgets, Nimgots moving only through base transactions;
- anything outside the plot: the world, other plots, players' accounts.
The base game runs inside
Inside a plot, the base game's own rules run on the player's lobby layer and their in-plot inventory, as they do in the world:
Category (baseRules) | What runs |
|---|---|
blocks | breaking and placing with tools, two-part blocks (a bed's foot and head, a door's halves) |
pvp | a player's hit on another player (falls, creatures, explosions and starving always hurt as the base does: armour, cooldown, knockback) |
drops | broken blocks' drops |
pickup | items lying on the ground, picked up by the base rules (reach, delay, stack limits, sound) |
hunger | food, effects, breath |
mobs | plot entities: an entity type's ai (the base mob behaviours) and the base melee on them |
Your hooks are wrapped round them, as Bukkit events wrap vanilla:
- Filters run first and can refuse:
onBlockBreak,onBlockPlace,onUse,onPlayerDamagereturnfalse. - Listeners hear what the base game did:
onBlockBrokenandonBlockPlaced, with every cell it changed (both halves of a bed). - Helpers change what it does:
world.setDrops(block, stacks | null)(nothing from beds in a bed game),world.dropItem(pos, item)(a generator's ingot, picked up the base way),world.placeBlock(pos, block, { facing })(a whole bed or door),world.rotateBlock.
So a Minecraft-style plot gets Minecraft for free, and never re-implements melee, drops, pickup or bed halves.
A runtime that wants a category to itself turns it off; then that category is the plot's alone (the raw layer: a game that is its own server):
json
{ "kind": "runtime", "baseRules": { "blocks": false, "pvp": false } }With blocks off, a break or place your hooks allow writes just that one cell, and the rest is your code.
Plot entities are base rules too ("mobs"). An entity type's ai runs the base game's mob behaviour for it: "hostile" (a zombie's: hunts the nearest player within 16 blocks), "ranged" (a skeleton's: keeps its distance and shoots), "neutral" (a wolf's: fights back when hit) or "passive" (a pig's: flees when hit), on the lobby's blocks, inside the plot, after only that lobby's players and never one still protected. Its hits reach onPlayerDamage(player, amount, by, entity) with the entity's id. A player's swing at any entity with health is the base melee (weapon damage, crits, knockback, the half second of invulnerability): onEntityDamage can refuse it, and at 0 health it dies (onEntityDeath, its drops). With "mobs": false entities are yours alone: move them and hurt them with content.damageEntity, and read the page's swings as the entity:hit input action.
What only the runtime may do
These calls and hooks are the runtime's. A mod that calls one gets HostError rule runtimeOnly; a mod's export of a runtime-only hook is never called.
| Calls | Hooks |
|---|---|
lobbies.lock, unlock, startCountdown, cancelCountdown, create, suspend, resume, delete, movePlayers, runWithin, exportData, setFeatured, regenerateTerrain | chooseLobby |
players.teleport, setMovement, setCamera, playCameraPath, respawn, askToKeepItems, setAvatar, setRealNametagsVisible | onCountdownEnd, onCountdownPlayerLeft |
ui.setButton, ui.removeButton | onButtonPress |
time.setTickRate | onPlayerDeath, onKeepItemsAnswer |
Plus the runtime-only plot.json fields: stashItems, lobbies, worldgen, baseRules, rollback.
How mods meet: through the world, not each other's code
Mods don't call each other. They meet through the shared content model:
- Namespaced registries. A furniture mod's
furniture:oak_tablecan be an ingredient in your recipe. - World events. A block break, a place, a use, damage, an entity's death, a structure being built.
Refusable events (break, place, use, damage) go to the runtime first, then to mods in load order. Any refusal wins. So a runtime that refuses breaking outside a round stays in charge, whatever mods do, and a game mod adds its own refusals on top.
Extension points
A runtime can invite mods in with its own events, as data (each package is its own component, so no function crosses):
ts
// in the runtime (Team Minigame)
import { mods } from "@inventery/host";
mods.sendEvent("minigame:roundStart", { round: 3, teams });
// in a mod
export function onModEvent(event: string, data: any) {
if (event === "minigame:roundStart") { /* start the generators */ }
}A runtime can ship a typed wrapper for its events that mods bundle (Team Minigame's api.ts). mods.list() gives the installed packages' ids in load order.
Reusable runtimes: games as mods
A runtime can be general enough that games are mods on it. Team Minigame (plot/samples/team-minigame) runs the round: lobbies of N, stashed items, balanced teams, a locked lobby with one start countdown, spawns and kits, respawn or out, the last team standing wins, then the round's edits undone and the next countdown. Bedwars and Spleef are mods on it that bring only their rules: a map, beds, generators, a shop. The plot's packages are that list:
json
{ "dir": "plot/samples/team-minigame", "mods": ["plot/samples/bedwars"] }See the Bedwars guide.
Isolation (you don't have to think about it)
Each package with code is its own wasm component: its own memory, namespace, storage and slice of the plot's budget, all inside the plot's one sandbox. A crashing mod doesn't corrupt another. Dependencies (depends in plot.json) load first.
Choosing
- Adding blocks, items, mob types, a structure that summons a boss? A mod.
- A team game with rounds? A game mod on a reusable runtime such as Team Minigame, if it fits.
- A game whose flow is its own (a puzzle room for four, a custom camera, a shooter)? A runtime.
- Only blocks, items and recipes, no behaviour? A data pack: a mod with
"registries": "registries"and JSON files, no code. - Both? A runtime for the game, and your content as mods others can reuse.