Recipes
Verified against Minecraft 26.3 · Part VII · Eight planks go around the empty centre of a crafting table, a chest appears in the result slot, and the client is never told which recipe it was.
You lay eight planks around the empty middle square of a crafting table and a
chest appears in the slot on the right. What the server did was take a trimmed
copy of the grid, scan every crafting recipe it has loaded in alphabetical
order until one matched, call a Recipe.assemble that ignored your planks
entirely, and push the answer down the wire in a packet written by hand,
outside the menu’s ordinary bookkeeping. What it never did was tell you which
recipe matched — and it could not have, because no Recipe ever crosses the
wire. Every RecipeSerializer is a record of a MapCodec and a
StreamCodec, and no game packet uses the stream half: the only reference to
Recipe.STREAM_CODEC anywhere in the tree is inside RecipeHolder.STREAM_CODEC,
which has no call sites at all. For the recipes it has unlocked the client
holds the whole contents — pattern, dimensions, ingredients, result — as a
RecipeDisplay. What it is denied is the identity, every recipe it has not
unlocked, and any authority whatever over the outcome.
The cast
| class | what it decides | thread |
|---|---|---|
Recipe | ten methods and no result getter — a result leaves through Recipe.assemble, Recipe.display, or a stonecutter’s StonecutterRecipe.resultDisplay | data, and a Recipe object never leaves the server |
RecipeManager | the loaded set, four indexes derived from it, and the server’s RecipeAccess | the worker pool for its constructor; the Server thread after, the first finalize on whichever thread builds the server |
RecipeMap | the immutable store, holding exactly two indexes: RecipeMap.byType and RecipeMap.byKey | built on the worker pool, swapped in and read on the Server thread |
Ingredient | whether one stack satisfies one slot — a HolderSet of items wearing a predicate face | both sides |
CraftingInput | the trimmed grid a crafting recipe is matched against, and the presence index a shapeless one is matched with | Server, and a result slot’s ResultSlot.onTake builds one on the Render thread too |
ResultSlot | the order of the endgame: award first, then look the recipe up again, then decrement | Server, mirrored on the Render thread’s menu |
ServerRecipeBook | which recipes this player has unlocked, and which of them still glow | Server |
ClientRecipeContainer | everything the client knows about recipes as recipes: nine RecipePropertySets and the stonecutter’s input set | Render, rebuilt wholesale from each packet |
Loading: one scan, one swap, and four indexes built later
RecipeManager is not a reload listener. RegistryDataLoader reads the recipe
files into a reloadable registry, Registries.RECIPE, and a fresh
RecipeManager builds its RecipeMap from that registry in its constructor,
both on the worker pool; on a reload the Server thread then swaps the new manager in.
That is the two-phase shape of
the resource system without a listener — and
the second phase does something unusual.
flowchart TD
W["RegistryDataLoader reads data/ns/recipe into Registries.RECIPE"]:::worker
W --> M["RecipeManager constructor builds its RecipeMap"]:::worker
M --> A["MinecraftServer.reloadResources swaps in the new manager"]:::server
A -. "the four indexes below are empty, not stale" .-> F
F["RecipeManager.finalizeRecipeLoading, called by MinecraftServer"]:::server
F --> P1["nine RecipePropertySet indexes"]:::server
F --> P2["SelectableRecipe.SingleInputSet"]:::server
F --> P3["RecipeManager.allDisplays, indexed by RecipeDisplayId"]:::server
P3 --> P4["RecipeManager.recipeToDisplay, key to displays"]:::server
The reload’s two phases — the worker pool’s two boxes above, the Server thread’s below — and the gap between them. The dotted edge is the one the section is about: the swap is not what builds the four indexes, and between those two boxes they hold nothing.
Each index applies its own feature-flag filter as it is built: an ingredient
is dropped from a RecipePropertySet unless every item in it is enabled, the
stonecutter’s set keeps an entry only if input and result display are both
enabled, and a display is kept only if its result and its crafting station
are. Three tests of three different things, all against one FeatureFlagSet, the
level’s, and the same filter ItemStack.isItemEnabled applies to an assembled
stack two sections below.
Three things in that picture are worth saying out loud.
The scan is sorted, and the sort is the whole ordering story.
ResourceManagerRegistryLoadTask.load registers the recipes sorted by their
files’ Identifiers, RecipeMap keeps that order, and Identifier.compareTo
compares the path first and the namespace only to break a tie. So
foo:acacia_boat sorts ahead of minecraft:zzz, matching is deterministic
across restarts, and the same order fixes the numbering of every display id
below.
The indexes are not built with the map.
RecipeManager.finalizeRecipeLoading has exactly two call sites, both of them in MinecraftServer, and neither is on the worker pool. Between the
swap and that call the four derived indexes are empty rather than stale: a
reload builds a fresh RecipeManager, and its constructor builds the
RecipeMap but leaves all four at their empty values, so the recipe book, the
property sets and the stonecutter index describe nothing at all rather than
describing the pack you just replaced. Nothing can catch the game in that
state — the swap and the call are five statements apart in one lambda on the Server thread — which is the only reason the gap is allowed to exist.
The last two indexes are a pair: RecipeManager.allDisplays is the flat
list, and RecipeManager.recipeToDisplay maps a recipe key to the entries it
produced in it.
A RecipeDisplayId is a list index, not an identifier. It is a record
wrapping a single int, and the int is the position the entry took in the flat
display list. Reload an unchanged pack and every id comes back identical; add
one recipe near the front and everything after it shifts. The server does not
try to work out which ids moved — ServerRecipeBook.sendInitialRecipeBook
re-sends the player’s whole book with the replace flag set. Nor is the list one
entry per recipe: Recipe.display returns a list, and TransmuteRecipe
returns one display per legal material count — up to
TransmuteRecipe.MAX_MATERIAL_COUNT, eight — so a single recipe can occupy
eight consecutive ids.
The same walk decides what a recipe is, and it forgives more than you would
expect. A non-special recipe whose PlacementInfo.isImpossibleToPlace — the cause being an ingredient tag that
resolved to nothing, which
Ingredient.CODEC cannot reject because only a literal empty list is illegal
— is logged as unplaceable and then kept. It stays in RecipeMap, where its empty ingredient matches no stack and so no
manual craft, and it still gets a RecipeDisplayEntry, one whose ingredient list
is present but empty; and RecipeDisplayEntry.canCraft on an empty ingredient
list is trivially true, so the book paints as craftable a recipe nobody can make. Only gate six of the auto-fill, below, stops the click.
It is not only shaped and shapeless
RecipeSerializers registers twenty-two serializers and fourteen of them are
crafting-table recipes. Nine of those fourteen are CustomRecipes — matching and assembly written in
Java, though eight of the nine read their ingredients and base result from JSON. CustomRecipe hard-codes Recipe.isSpecial true, Recipe.group
empty and PlacementInfo.NOT_PLACEABLE, and not one of the nine overrides
Recipe.display, so a special recipe contributes nothing to the display list in
the first place. Eight of the nine register under an id beginning crafting_special_, which is
where the name comes from; the ninth, DecoratedPotRecipe, registers as
crafting_decorated_pot and is a CustomRecipe all the same. The nine are the crafting recipes the book never shows: FireworkStarRecipe,
FireworkStarFadeRecipe, FireworkRocketRecipe, BookCloningRecipe,
BannerDuplicateRecipe, ShieldDecorationRecipe, MapExtendingRecipe,
DecoratedPotRecipe and RepairItemRecipe, which is the one this part comes
back for: it fuses two items of one kind that have durability and carries every curse from
both inputs onto the result (enchanting). Of
the five crafting-table serializers that remain, ShapedRecipe and
ShapelessRecipe are the pair everyone knows, and DyeRecipe, ImbueRecipe and TransmuteRecipe are genuine
NormalCraftingRecipes — data-driven, with hand-written matching, hand-built
placement info, and the exotic SlotDisplay variants to draw themselves with
(SlotDisplay.OnlyWithComponent and SlotDisplay.DyedSlotDemo for the dye
recipe, SlotDisplay.WithAnyPotion for the imbue). So special and neither shaped nor
shapeless are not the same set, and the difference matters, because it is
special that the book cannot show.
Eight planks: the trace
Putting the last plank into a crafting grid is two traces, the second on a later click, and only the second one reaches the recipe book.
sequenceDiagram
participant CraftM as CraftingMenu
participant TCC as TransientCrafting<br/>Container
participant RM as RecipeManager
participant ResultC as ResultContainer
participant Wire as the network
rect rgba(0, 0, 0, 0.04)
Note over CraftM,Wire: the tick the eighth plank lands
CraftM->>TCC: asCraftInput, trimming the empty rows and columns
CraftM->>RM: getRecipeFor CRAFTING, this input, this level, no hint
RM-->>CraftM: the first RecipeHolder that matches, in registry order, or nothing
CraftM->>ResultC: setRecipeUsed, refused under LIMITED_CRAFTING
CraftM->>ResultC: setItem 0, the assembled stack
CraftM->>Wire: ClientboundContainerSetSlotPacket, bumping the state id
end
The result slot is filled and announced in the tick the grid changed, and nothing has been taken yet. The packet at the foot is written by hand rather than left to the menu’s own diffing pass.
Nothing above happens on the client: the client’s CraftingMenu is handed
ContainerLevelAccess.NULL and matches nothing. What arrives is the last
arrow. The second half of the trace is a later click, and only then does a recipe reach
the book.
sequenceDiagram
participant ResultS as ResultSlot
participant ResultC as ResultContainer
participant SRB as ServerRecipeBook
participant RM as RecipeManager
participant TCC as TransientCrafting<br/>Container
participant Wire as the network
rect rgba(0, 0, 0, 0.04)
Note over ResultS,Wire: a later click, the player takes the result
ResultS->>ResultS: checkTakeAchievements runs before a cell is emptied
ResultS->>ResultC: awardUsedRecipes, which then nulls the stored holder
ResultC->>SRB: addRecipes, through ServerPlayer.awardRecipes
SRB->>Wire: ClientboundRecipeBookAddPacket
ResultS->>RM: getRecipeFor again, holder or no holder
ResultS->>TCC: Container.removeItem, one cell at a time, each followed by its remainder
Note over ResultS,TCC: every removal re-enters CraftingMenu.slotsChanged
end
Taking the stack is where the recipe reaches the book, and where the grid is emptied a cell at a time — each removal starting the first figure again.
The grid changes. Writing a plank into the menu’s
TransientCraftingContainer calls AbstractContainerMenu.slotsChanged, and both
crafting overrides of it end in the same static,
CraftingMenu.slotChangedCraftingGrid. They reach it differently:
CraftingMenu.slotsChanged goes through ContainerLevelAccess.execute, while
InventoryMenu.slotsChanged calls the static directly, gated only on having a
ServerLevel. The two-argument CraftingMenu constructor — the client’s — is
handed ContainerLevelAccess.NULL, which runs nothing at all (containers and
menus), so the
client never matches anything.
Trimming. Every recipe kind is matched against a RecipeInput, and there
are exactly four implementations of it in the game: CraftingInput for a
grid, SingleRecipeInput for the one-slot stations — the four AbstractCookingRecipe
kinds and the stonecutter’s, all of them SingleItemRecipes —
SmithingRecipeInput for the smithing table’s three, and BrewingInput for a
brewing stand’s bottle and reagent.
CraftingContainer.asPositionedCraftInput produces a
CraftingInput with the empty border rows and columns removed and the offset
beside it, which is why a shaped recipe works anywhere in the grid;
CraftingContainer.asCraftInput is the same call with the offset thrown away,
and it is what matching uses. Its constructor also fills a
StackedItemContents, and does it twice unusually: it accounts every stack as
one item, and it calls StackedItemContents.accountStack directly rather
than the filtered StackedItemContents.accountSimpleStack the recipe book
uses. Both choices point the same way — this index is a presence set for
shapeless matching, not the arithmetic the auto-fill does, and it deliberately
does not apply the auto-fill’s filter. What that filter is, and who it catches
out, is the last section of this page.
Matching is a linear scan. RecipeMap.getRecipesFor exits immediately on an
empty input, then streams the recipes of that RecipeType and filters them
through Recipe.matches, and RecipeManager.getRecipeFor takes the first.
ShapedRecipePattern.matches compares the ingredient count, then demands exactly
matching trimmed dimensions, then tries the mirrored layout before the
straight one — unless the pattern is symmetrical, which Util.isSymmetrical
settles once in the constructor. The chest’s ring of planks is symmetrical, so
only the straight pass ever runs, and a one-column pattern is always symmetrical
too. ShapelessRecipe.matches rejects on count, short-circuits the single-slot
case, and otherwise hands the presence index to StackedItemContents.canCraft,
which is a thin wrapper: the bipartite search itself lives in the generic
StackedContents that StackedItemContents holds and delegates to.
Three accelerations sit on top of that scan, and each belongs to a different
caller. RecipeManager.getRecipeFor takes an optional hint and tests it
before scanning; the auto-fill supplies one through
CraftingMenu.finishPlacingRecipe, so the recipe it has just laid out is
the first thing re-matched — and the base
AbstractCraftingMenu.finishPlacingRecipe is a no-op, so the player’s own
2×2 grid never gets one. RecipeManager.CachedCheck remembers the last
successful key and re-hints with it — AbstractFurnaceBlockEntity holds one per block entity,
BrewingStandBlockEntity another, and CampfireBlock hands one to
CampfireBlockEntity.cookTick. And
RecipeCache, ten entries held statically by CrafterBlock and keyed on the
grid contents, caches misses as well as hits, and invalidates on object
identity: it keeps a weak reference to the manager and wipes itself the moment
the level hands back a different one, which works because a reload builds a new
RecipeManager.
The gate, and then the result. RecipeCraftingHolder.setRecipeUsed returns
false — leaving the result slot empty — when GameRules.LIMITED_CRAFTING is on,
the recipe is not special, and ServerRecipeBook.contains says no. Limited
crafting is enforced here, in the result slot, not in matching. Past it, Recipe.assemble produces the stack — for every crafting recipe but the
hand-written ones it ignores its input entirely and materialises a stored
ItemStackTemplate (items and
stacks) —
ItemStack.isItemEnabled filters that against the level’s feature flags, and
ResultContainer.setItem stores it.
Pushing it. AbstractContainerMenu.setRemoteSlot forces the server’s belief
about slot zero and then a ClientboundContainerSetSlotPacket is sent by hand,
incrementing the state id on its own way past — outside the diffing that
containers and
menus
describes, and outside AbstractContainerMenu.suppressRemoteUpdates, which
guards only that diffing and so cannot reach a hand-written send. It is sent even when the
result is empty.
Taking it, in an order that surprises. ResultSlot.remove counts what was
taken, and then ResultSlot.onTake runs ResultSlot.checkTakeAchievements
before anything is consumed. That calls ItemStack.onCraftedBy, which awards
Stats.ITEM_CRAFTED and runs Item.onCraftedBy, and then
RecipeCraftingHolder.awardUsedRecipes, which fires
CriteriaTriggers.RECIPE_CRAFTED for every recipe, special ones included, and
then — for a non-special recipe only — calls ServerPlayer.awardRecipes and so
ServerRecipeBook.addRecipes, unlocking it, firing
CriteriaTriggers.RECIPE_UNLOCKED, sending ClientboundRecipeBookAddPacket, and
nulling the stored holder. The next thing ResultSlot.onTake does is look
the recipe up all over again — not because of that null, but because
ResultSlot.getRemainingItems never consults the stored holder on any path,
special recipes included. CraftingRecipe.getRemainingItems — whose
default implementation is spelled CraftingRecipe.defaultCraftingReminder, the
typo Mojang’s — maps each slot through Item.getCraftingRemainder, an
ItemStackTemplate or nothing. Then one item leaves each occupied cell and the
remainder goes back into the emptied slot, or merges with what is left there, or
goes to the inventory, or is dropped. Every one of those removals re-enters
AbstractContainerMenu.slotsChanged, so the result slot is recomputed eight
times on the way out.
A crafter block runs the same machinery through RecipeCache and fires its own
advancement trigger, CriteriaTriggers.CRAFTER_RECIPE_CRAFTED, not the player’s.
What the client actually gets
The client’s entire recipe API is two methods. RecipeAccess declares
RecipeAccess.propertySet and RecipeAccess.stonecutterRecipes and nothing
else, and exactly two classes implement it: RecipeManager on the server and
ClientRecipeContainer on the client, the latter holding a map and a set and
having no other content at all. Neither method’s type is a Recipe, but the type is not what keeps recipes off
the wire: on the server the stonecutter set carries a recipe in every entry, and
the codec below is what drops it.
ClientboundUpdateRecipesPacket goes out twice: once from PlayerList as a
player joins, once to everybody from PlayerList.reloadResources. It carries two
things. The RecipePropertySets are flat sets of items, and
RecipePropertySet.test is item identity only — the client’s slot predicate
ignores components entirely. The stonecutter’s
SelectableRecipe.SingleInputSet is written by
SelectableRecipe.SingleInputEntry.noRecipeCodec, which serialises the input
Ingredient’s contents and the option’s SlotDisplay and drops the recipe on
the floor: SelectableRecipe.noRecipeCodec decodes an empty optional in its
place.
Those sets exist so that menus can answer may this item go in this slot — and
route a shift-click on the strength of it — without knowing a single recipe.
The recipe classes behind those stations are as thin as the sets suggest:
SingleItemRecipe is the shape StonecutterRecipe and the cooking recipes extend, and SmithingTransformRecipe
and SmithingTrimRecipe are the smithing table’s two — one that replaces the
item, one that writes an ArmorTrim component onto it.
SmithingMenu builds its three input slots out of the three smithing sets,
AbstractFurnaceMenu holds whichever of RecipePropertySet.FURNACE_INPUT,
RecipePropertySet.BLAST_FURNACE_INPUT and RecipePropertySet.SMOKER_INPUT
its type was constructed with — the fourth set,
RecipePropertySet.CAMPFIRE_INPUT, reaches no menu at all and is read by
CampfireBlock on a right-click — BrewingStandMenu reads the two brewing sets,
and
StonecutterMenu asks the stonecutter set the same question through
SelectableRecipe.SingleInputSet.acceptsInput.
The book gets more, and still no names
The book gets something far richer and still anonymous: a RecipeDisplayEntry
per display, carrying the display id, the RecipeDisplay itself, a group index,
a RecipeBookCategory, and an optional ingredient list used for one thing only — deciding whether the entry
shows as craftable. RecipeDisplay and SlotDisplay are
dispatched registries of their own (Registries.RECIPE_DISPLAY,
Registries.SLOT_DISPLAY, and Registries.RECIPE_BOOK_CATEGORY for the
categories RecipeBookCategories fills), and their stream codecs are very much
used: five RecipeDisplay types, one per station shape, and eleven registered
SlotDisplay variants, which SlotDisplay.resolve turns into concrete stacks against a ContextMap that
SlotDisplayContext builds. The chest’s single ingredient reaches the client
as a SlotDisplay.TagSlotDisplay over minecraft:planks
(tags) — everything needed to draw the recipe, and
nothing at all about its name.
The recipe book: unlocked, glowing, and filled in for you
ServerRecipeBook stores four things: a display resolver, the settings,
ServerRecipeBook.known —
an identity set of recipe keys — and ServerRecipeBook.highlight, the subset
still new enough to glow. It is saved in the player NBT as
ServerRecipeBook.Packed and read back by ServerRecipeBook.loadUntrusted,
which validates every key against the live RecipeManager and logs and drops the
ones that no longer resolve (codecs).
A fresh player’s is genuinely empty, and stays empty until something fills it.
The ordinary route is not crafting at all: every recipe in vanilla that is not
special has an advancement whose reward names it, so unlocking the advancement is what fills
the book (advancements), and a new world’s
opening minutes are that machinery running. Crafting a recipe you had not
unlocked adds it too, which is the path this trace took.
ClientRecipeBook never sees any of that. It holds RecipeDisplayEntrys by
display id, and ClientRecipeBook.rebuildCollections groups them into
RecipeCollections by category and then by group index, which is why one button
in the book cycles through all thirteen kinds of plank.
The tabs are narrower than the recipe types. RecipeBookType has four values —
crafting, furnace, blast furnace and smoker — and exactly five concrete menus
extend RecipeBookMenu to claim them, under two abstract halves:
AbstractCraftingMenu gives CraftingMenu and InventoryMenu, both
answering RecipeBookType.CRAFTING, and AbstractFurnaceMenu gives
FurnaceMenu, BlastFurnaceMenu and SmokerMenu, one per remaining value. StonecutterMenu and SmithingMenu are not
RecipeBookMenus at all, so the stonecutter and the smithing table have no book
and no auto-fill, even though RecipeBookCategories.STONECUTTER and
RecipeBookCategories.SMITHING exist to categorise them — and the anvil and the
grindstone were never recipes at all (enchanting).
Craftability is decided twice
Craftability is decided on the client and then decided again on the server.
RecipeCollection.selectRecipes asks RecipeDisplayEntry.canCraft against a
StackedItemContents that RecipeBookComponent fills from the player’s
inventory, purely to choose which entries show as craftable. A lying client gains nothing by
it, because the placement re-checks and CraftingMenu.slotChangedCraftingGrid
runs a full Recipe.matches afterwards regardless.
Seven gates before a single plank moves
Auto-fill itself runs entirely on the server. Clicking an entry sends
ServerboundPlaceRecipePacket carrying nothing but a container id, a
RecipeDisplayId and a shift flag, and
ServerGamePacketListenerImpl.handlePlaceRecipe puts it through seven gates:
- the player is not a spectator, and the packet’s container id is the open menu’s;
AbstractContainerMenu.stillValidstill holds for the open menu;RecipeManager.getRecipeFromDisplayresolves the index to aRecipeManager.ServerDisplayInfo;ServerRecipeBook.containssays this player has unlocked the parent recipe;- the open menu really is a
RecipeBookMenu; - the recipe’s
PlacementInfo.isImpossibleToPlacesays no — which catchesPlacementInfo.NOT_PLACEABLEand any placement whose ingredients came out empty; - and only then does
RecipeBookMenu.handlePlacementrun.
AbstractCraftingMenu.handlePlacement calls AbstractCraftingMenu.beginPlacingRecipe,
which on a CraftingMenu raises a private flag that makes
CraftingMenu.slotsChanged a no-op while the shuffling happens — a
CraftingMenu override, so the 2×2 grid inside InventoryMenu re-matches on
every single write — and calls
ServerPlaceRecipe.placeRecipe, which counts before it clears: a dry run of
emptying the grid back into the inventory, a tally of what the player has, a
calculation of how many crafts that allows, clamped to the smallest maximum stack size among the items chosen, and only then the real clear and the layout through
PlaceRecipeHelper.placeRecipe. Whether it may drop items in order to clear the
grid is simply Player.isCreative, so for a survival player whose inventory cannot take back what the grid holds the
whole call returns RecipeBookMenu.PostPlaceAction.NOTHING — no
fill and no ghost. When the ingredients merely are not there it returns
RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE instead, and the server sends
ClientboundPlaceGhostRecipePacket carrying the RecipeDisplay itself.
The filter that greys out a recipe you could craft by hand
One filter runs underneath all of that and catches players out.
Inventory.isUsableForCrafting rejects any stack that is damaged, enchanted or
renamed, and it gates both halves of the auto-fill: the tally, through
StackedItemContents.accountSimpleStack, and the actual pull, through
Inventory.findSlotMatchingCraftingIngredient. Matching does not gate on it at
all — that is what CraftingInput’s direct call to
StackedItemContents.accountStack buys. So the book can grey out a recipe
that a manual craft with those very items would have accepted without
complaint, and the difference between the two is one method name.
Where to look
Read RecipeAccess first. It declares two methods, it is the whole of what a recipe
system has to offer a client, and everything else on this page is an argument
about why it is that short.
Then the server’s side in loading order: ResourceManagerRegistryLoadTask for
the scan, MinecraftServer.reloadResources for the swap and RecipeManager
for the four indexes, with RecipeMap beneath it for the two the store keeps. Recipe is the interface every kind implements, and
ShapedRecipePattern is the one piece of matching worth reading line by line,
for the mirrored pass. CustomRecipe is the nine the book never shows.
For the trace, CraftingMenu.slotChangedCraftingGrid is the whole of what
happens when a plank lands, and ResultSlot.onTake is the whole of what
happens when you take the chest — in that surprising order. ServerPlaceRecipe
is the auto-fill, and Inventory.isUsableForCrafting is the filter under it.
For the client’s half, RecipeDisplay and SlotDisplay are what a recipe
becomes when it may not be itself, and ClientRecipeBook is where they land.
Before this page: containers and
menus, for the
synchroniser that the result slot goes around, and items and
stacks
for ItemStackTemplate.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.