Crafting Behavior
Bukkit cannot match items that a provider such as CraftEngine builds with custom components, so AstralRecipes does not hand its recipes to the server and step back. It keeps its own matching engine and takes over the parts of crafting it needs, while registering backing vanilla recipes so the recipe book and the vanilla-driven stations still fire.
This page describes what actually happens at each station. For the YAML that produces these recipes, see Recipe files.
Item identity
Every stack — in a recipe file and in a grid — is wrapped as a RecipeItem carrying one identity Key:
Each registered
ItemStackSupplieris askedkeyOf(stack); the first one that answers wins, and the item is keyed by that provider's item id (for CraftEngine,ce's<pack>:<id>). The suppliers live in an unordered map, so an item that two providers both claim has no defined winner.If none answers, the item is vanilla and is keyed by its material.
An ingredient matches when the two keys are equal. Two consequences worth internalising:
Vanilla items are matched by material only. Name, lore, enchantments, durability, and components are ignored — a renamed, enchanted diamond satisfies a
diamondingredient. (This is a deliberate change from the old blueprint system, which used component-exactExactChoicematching everywhere.)Custom items are matched by their provider id, so a CraftEngine ruby is distinguished from the vanilla material it renders as.
Grid stacks are wrapped by reference, not cloned — that is how consuming a recipe mutates the real slots.
count
count on an ingredient is a stack-size gate on the matched slot: with count: 1 (the default) any stack size matches; with count: n the slot must hold at least n. It is checked at match time on every station, but what is actually removed depends on the station — see below.
Consumption
Station | Removed per slot |
|
|
|---|---|---|---|
Crafting table / 2×2 grid |
| Honoured — the item stays | Honoured |
Crafter |
| Recipe is refused | Honoured |
Furnace, blast furnace, smoker, campfire | 1, by vanilla | Ignored | Match-only; still just 1 consumed |
Stonecutter | 1, by vanilla | Ignored | Ignored |
Crafting table and the 2×2 grid
Two handlers cover player crafting.
Preview — on PrepareItemCraftEvent, the grid is trimmed of empty rows and columns and matched against the engine. On a hit the result slot is set to the recipe's assembled output. On a miss, if the grid still triggered one of AstralRecipes' own backing recipes (a look-alike shape, or a multi-count recipe with too few items in the slots), the vanilla result is cleared so it cannot be crafted for the wrong cost. A grid that matches nothing of ours is left completely alone and vanilla proceeds.
Craft — on InventoryClickEvent at EventPriority.HIGH, a click on the result slot is re-matched against the engine. If nothing of ours matches, vanilla handles the click. If something does, the event is cancelled and AstralRecipes performs the craft itself:
Click | Behavior |
|---|---|
Left/right mouse click | Crafts one result onto the cursor. If the cursor already holds a different item, or the merge would exceed the max stack size, the craft is aborted and nothing is consumed. |
Shift-click | Crafts repeatedly into the inventory while the grid still matches, capped at 64 iterations. Anything that does not fit is dropped at the player's feet, and crafting then stops. |
Number key, drop, anything else | Cancelled, nothing crafted — deliberately, to avoid duplication. |
The grid is written back and the player's inventory is refreshed afterwards.
Crafter
A crafter block only fires CrafterCraftEvent when its grid matches a server-registered recipe, which is why backing recipes exist at all. Which recipe vanilla picked is not trusted, though: a look-alike vanilla recipe can win the lookup. AstralRecipes handles the event at EventPriority.HIGHEST (last, to minimise the window in which another plugin could cancel after the ingredients are gone), reads the crafter's nine slots, and re-matches them against its own engine.
Our engine matches → the recipe's result is injected via
event.setResult(...)and the ingredients are consumed atcount − 1per slot, because vanilla shrinks every filled slot by one on its own after the event returns. This is what makes multi-count recipes cost the right amount in a crafter.Our engine matches, but the recipe keeps an ingredient (
consume: falseanywhere) → the event is cancelled. Vanilla always eats one of every filled slot, so a catalyst cannot survive a crafter; such recipes are crafting-table only.Nothing of ours matches, but one of our recipes won the vanilla lookup → the event is cancelled, so a look-alike shape or an under-filled multi-count grid cannot craft for the wrong cost.
Nothing of ours is involved → left alone; a genuine vanilla recipe crafts normally.
Cooking stations
Furnaces, blast furnaces, and smokers fire FurnaceSmeltEvent; campfires fire BlockCookEvent directly. AstralRecipes handles both at EventPriority.HIGH, resolves the station from the block material (campfire covers both CAMPFIRE and SOUL_CAMPFIRE), and looks for a cooking recipe of that station's type whose ingredient matches the source item. On a hit it overwrites the event's result.
The cook itself is entirely vanilla-driven. The backing cooking recipe is what lets the station start burning at all, including for items that have no vanilla recipe; matching in the event handler is what lets a custom item whose base material has its own vanilla smelt produce your result instead.
Because vanilla owns the consumption, exactly one input item is used per cook — count gates whether the recipe matches, and consume: false has no effect at a cooking station.
Stonecutter
There is no stonecutter listener. A stonecutter recipe is registered as a Bukkit StonecuttingRecipe whose input is a component-exact RecipeChoice.ExactChoice built from the resolved ingredient stacks, and vanilla does the rest. Practically:
Matching is component-exact, not material-only — the opposite of the engine's own rule.
Provider-backed custom items are transformed on their way to the client and generally will not match an
ExactChoice, so custom items are not a good fit for stonecutter recipes.countandconsumeare ignored.
Backing recipes and the recipe book
For every loaded recipe, RecipeService mirrors a Bukkit recipe under the key astralrecipes:<key-namespace>_<key-value> (any character outside [a-z0-9._-] replaced with _):
Custom type | Mirrored as | Ingredient choice |
|---|---|---|
|
|
|
|
|
|
cooking types | The station's |
|
|
|
|
These backing recipes are unlocked in every player's recipe book — for everyone online when recipes load, and for each player on join. That is what makes custom recipes discoverable; they are display and trigger entries, never the source of truth for matching or for the produced item.
If a backing recipe cannot be built (see the shaped symbol caveat), Skipping backing recipe for <key>: <error> is logged. The recipe still works at the crafting table through the engine, but it gets no recipe-book entry and no crafter support.
Crafter triggers
An ExactChoice built from a provider's custom item never matches the stack a crafter actually holds, so CrafterCraftEvent would never fire for those recipes. For each shaped/shapeless recipe that is crafter-compatible and uses at least one provider-backed custom item, AstralRecipes registers a second, hidden twin under astralrecipes:crafter_<key-namespace>_<key-value> using material-based RecipeChoice.MaterialChoice ingredients. Its only job is to make the crafter fire; the handler then re-validates by item id and either injects the real result or cancels. Triggers are never unlocked in the recipe book, and the load summary reports how many were registered.
Interaction with other plugins
Recipes are registered 10 ticks after onEnable, which is always after every other plugin's onEnable has run. AstralEssentials' disabled-crafts, which removes recipes by result material once at startup, therefore never sees them — see AstralEssentials configuration.