Getting started

Write, load, and hot-reload your first Sulfur plugin

View as Markdown

Plugin layout

A plugin is a directory inside the server’s plugins/ folder containing a plugin.toml manifest and an entrypoint script:

plugins/
my_plugin/
plugin.toml # the manifest (required)
main.star # the entrypoint (referenced by the manifest)

Directories without a plugin.toml are treated as containers and scanned recursively (up to 8 levels deep), so you can group plugins into folders:

plugins/
mobs/
vanilla_pig/
plugin.toml
main.star
vanilla_cat/
plugin.toml
main.star
gate_events/
plugin.toml
main.star

A directory with a plugin.toml is loaded as a plugin and never recursed into.

A minimal plugin

1name = "hello"
2version = "0.1.0"
3entrypoint = "main.star"
4runtime = "starlark"
5capabilities = []

Drop the directory into plugins/ and start the server (or just save the file while the server is running — the hot-reload watcher picks it up). The server logs:

plugin host: loaded 1 plugin(s) from plugins/ (hot-reload watcher armed)

The runtime selector

The runtime manifest field routes the plugin to its execution lane:

  • runtime = "starlark" — the entrypoint .star file is executed once by the embedded Starlark interpreter. Hooks run inline on the tick goroutine (fast, sandboxed, deterministic).
  • runtime = "python" — the entrypoint .py file is executed once by embedded CPython if the server was built with -tags python. Hooks are dispatched off-tick on a bounded worker pool. On a default build the plugin is skipped with a log line: plugin "name": python runtime not built in this binary; skipping.
  • Any other value is rejected loudly: unknown runtime "..." (want "starlark" or "python").

Load semantics

  • The module body runs exactly once. Everything you want the server to know must be captured during that run (via register, declare_mob, set_recipe_matcher, …). After load, module globals are frozen.
  • Loading is tolerant per plugin. The operator scan (plugins/) skips a plugin that fails to load — bad manifest, Starlark error, unknown capability — logging the reason loudly, and continues with the rest. One broken plugin never takes down the others.
  • Errors are loud, never silent. An unknown event name, capability string, goal flag, base type, skill trigger, mechanic kind, or a typo’d entrypoint fails at load with a precise message. Sulfur’s plugin API has a strict “load loudly or skip” discipline: nothing silently no-ops.

Builtins available to every Starlark plugin

BuiltinSignatureWhat it does
registerregister(event_name, fn)Subscribe fn to a discrete event. Unknown event = load error.
loglog(msg)Write a line to the server log, prefixed plugin:.
chatchat(msg)Broadcast a system chat message to every player. Falls back to log() when no chat sink is wired (e.g. unit tests).
echoecho(s)Returns its string argument (sandbox demo builtin).
mathmoduleStarlark’s standard math module (math.cos, math.sin, math.pi, math.sqrt, …). Pure and deterministic.
set_recipe_matcherset_recipe_matcher(fn)Register the crafting matcher — see Recipes.
set_recipe_remainingset_recipe_remaining(fn)Register the remaining-items callable — see Recipes.
recipesrecipes()Returns the host-injected vanilla recipe table (or None).

Mob plugins additionally receive the declaration builtins declare_mob, goal, skill, mechanic, targeter, and condition — see declare_mob and Skills.

Starlark has no open, no network, no import, and no way to reach the filesystem. If a plugin needs data (like the recipe table), the host parses it in Go and hands it in as a frozen value.

Hot reload

While the server runs, an fsnotify watcher observes plugins/ and every plugin subdirectory. On any .star/.toml write, create, remove, or rename:

  1. Events are coalesced behind a 150 ms debounce (one editor save = one rebuild).
  2. A fresh plugin manager is built off the tick goroutine by re-scanning the whole directory.
  3. On success, the new manager is handed to the tick loop over a swap channel and installed between ticks — dispatch never observes a half-updated hook table.
  4. On failure (e.g. a file saved mid-edit with a syntax error), the rebuild is discarded and the last-good plugin set keeps running.

New plugin directories created at runtime are picked up automatically.