Recipes
Drive crafting resolution from a plugin — the value-returning matcher seam
Crafting in Sulfur is resolved through the plugin API. The event bus is fire-and-forget dispatch; the recipe seam is its value-returning twin: the host calls a plugin callable and reads the result back into Go.
Even a default server crafts through this seam — the bundled crafting plugin is a literal port
of the 26.2 jar’s matching algorithms (CraftingInput.ofPositioned, ShapedRecipePattern.matches,
ShapelessRecipe.matches, SingleItemRecipe.matches) registered via set_recipe_matcher.
The builtins
There is a single matcher slot: set_recipe_matcher captures one callable, and the
last-loaded plugin wins (plugins load in directory order). A custom-recipe plugin should therefore
check its own recipes first and fall through to the vanilla algorithm over recipes() — see
the pattern below. Unloading the owning plugin clears the matcher.
The grid payload and result
The matcher receives one argument, a frozen dict:
It returns either None (no match) or a dict {"id": result_item_id, "count": result_count}.
The host validates the result: a non-dict, missing keys, or id <= 0 / count <= 0 is treated as
no-match — a plugin cannot forge a bogus stack.
The recipe table returned by recipes() is a list of dicts:
where each ingredient set is a dict used for membership (id in ing); an empty dict is the
empty-optional pattern cell.
Isolation
Matcher calls follow the same isolation rules as event hooks: each Match runs on a fresh
step-budgeted thread inside a recover. A matcher that errors, panics, or exceeds the step budget
logs and returns “no match” — the tick never hangs on crafting.
Example: adding a custom recipe
The bundled customrecipe operator plugin adds a non-vanilla recipe (1 dirt → 1 diamond) while
keeping every vanilla recipe working, via the custom-first + vanilla-fallthrough pattern:
The full vanilla match helpers (_of_positioned, _matches_shaped, _matches_shapeless,
_matches_single) are in plugins/crafting/main.star — Starlark plugins cannot import one
another, so a custom matcher includes its own copy of the fallthrough.

