Astral Realms Documentation Help

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:

  1. Each registered ItemStackSupplier is asked keyOf(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.

  2. 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 diamond ingredient. (This is a deliberate change from the old blueprint system, which used component-exact ExactChoice matching 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

consume: false

count > 1

Crafting table / 2×2 grid

count

Honoured — the item stays

Honoured

Crafter

count − 1 by AstralRecipes, plus the 1 vanilla shrinks after the event

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 at count − 1 per 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: false anywhere) → 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.

  • count and consume are 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

shaped

ShapedRecipe with the same shape

RecipeChoice.ExactChoice over the resolved stacks

shapeless

ShapelessRecipe, one slot per ingredient

RecipeChoice.ExactChoice

cooking types

The station's CookingRecipe with the recipe's experience and cooking time

RecipeChoice.ExactChoice

stonecutter

StonecuttingRecipe

RecipeChoice.ExactChoice

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.

Last modified: 03 September 2026