Contributing content
Plugins own their data too. Declaring a blueprint or a weapon is the one place a whole structure crosses the boundary β and it's fine, because it's static config copied once at load, never live state.
spec.components is a table of component defaults. engine.spawn(name, β¦) then builds it.label, behavior, cooldown, projectile, ammo, and so on. It joins the arsenal like any built-in.the ember a fire-weapon lobsengine.defineBlueprint("fine-ember", { components = { pos = { x = 0, y = 0 }, vel = { x = 0, y = 0 }, gravity = { g = 1400, grounded = false }, lifetime = { ttl = 5 }, ember = { owner = 0, team = "", fire = "fire-zone" }, }, })
grounded = false). Remember that in Lua 0 is truthy β if grounded then is only correct because grounded is a real boolean. For a numeric 0/1 flag, always test == 1, never rely on truthiness.
Services & dependencies
Plugins build on plugins. An infrastructure plugin exposes an API; a content plugin depends on it and calls in. The dependency always points down β core never names a plugin, and a plugin never names its dependents.
engine.provide(name, table) publishes a table of functions. engine.require(name) fetches it. Both stay inside the VM, so a call like items.register("fine", β¦) is just a Lua call β no boundary crossing, no cost.
a content plugin standing on the items infrastructurelocal items = engine.require("items") items.register("fine", { give = "weapon", weapon = "fine", ammo = 3 })
You declare which plugins yours needs; the host loads them in dependency order, so a provider always runs before anyone who requires it. A missing dependency or a cycle fails loudly at load, not mysteriously at runtime.
items.give. You don't write that side; it's the seam that lets a plugin quietly take over a built-in job.
Guardrails
Three things the host guarantees, so a plugin can't quietly break a match.
The server sim must be reproducible, so randomness comes only from engine.rng() (the seeded stream). math.random is off-limits; a plugin that reached for it would desync clients from the server.
If your plugin throws at runtime, the host disables that plugin β its callbacks no-op from then on β and keeps the match alive. One bad plugin degrades to nothing; it never takes the tick down. Load-time errors, by contrast, are loud and immediate.
The VM sees engine plus string, table, math β and nothing else. No io, no os, no require, no load, no filesystem. A plugin can only reach what the engine hands it.
Every plugin runs in its own environment. Your top-level locals and your bare globals are private β another plugin can't see or clobber them, even by accident. You also get a unique plugin symbol: plugin.name is your plugin's name, at load and at runtime. To share across plugins, use provide/require β the one deliberate channel.
A whole plugin, end to end
This is This Is Fine β a complete weapon, shipping in the game, in one file. It declares the ember it lobs, the weapon that lobs it, the fire behaviour, and its pickup item. It depends only on items. Core names none of it.
content/plugins/this-is-fine.lua β abridged-- the ember it lobs (a gravity-bound orb that lights a fire where it lands) engine.defineBlueprint("fine-ember", { components = { pos = { x = 0, y = 0 }, vel = { x = 0, y = 0 }, gravity = { g = 1400 }, lifetime = { ttl = 5 }, ember = { owner = 0, team = "", fire = "fire-zone" }, }, }) -- the weapon: lob a fine-ember on a long cooldown engine.defineWeapon("fine", { label = "This Is Fine", behavior = "drop", cooldown = 4.5, projectile = "fine-ember", ammo = { max = 6, start = 0 }, }) -- the fire behaviour: spawn the ember at the aim, stamped with the shooter's team + id engine.behavior("drop", function(fire) local id = engine.spawn(fire.projectile, fire.aim_x, fire.aim_y) engine.set(id, "ember", { team = engine.get(fire.shooter, "body", "team"), owner = fire.shooter, }) end) -- its pickup item, registered into the items infrastructure local items = engine.require("items") items.register("fine", { give = "weapon", weapon = "fine", ammo = 3 })
Every mechanism in this guide appears here: a blueprint, a weapon, a fire behaviour that spawns-and-stamps by handle, and a dependency on an infrastructure plugin. Nothing else in the codebase knows this weapon exists.
Is it a primitive, infrastructure, or content?
When you're not sure where something belongs, the deciding question isn't importance β it's frequency and shared-ness.
- Content-agnostic and needed by everything? β a primitive in the JS engine. The ECS, spawn, query, events, terrain, rng. You use these; you don't write them.
- A game system others build on, that a world might toggle? β an infrastructure plugin (Lua). Items, lighting, day/night, hunger. It exposes an API via
provide. - An individual piece of content? β a content plugin (Lua). A weapon, an enemy, a hazard. It depends on infrastructure and names itself to no one.
The litmus for the hard cases: would a sandbox reasonably run without it? A world with no fire weapons is sensible β so fire is a plugin. A world with no ability to spawn entities at all is not β so spawn is a primitive.
Meme Wars plugin system Β· runs on a sandboxed Fengari (Lua 5.3) host, server-side, one per world. Companion documents: docs/plugin-architecture.md (the boundary & migration) and docs/plugin-viability.md (how far plugins can go).