Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

classwhat it decidesthread
Recipeten methods and no result getter — a result leaves through Recipe.assemble, Recipe.display, or a stonecutter’s StonecutterRecipe.resultDisplaydata, and a Recipe object never leaves the server
RecipeManagerthe loaded set, four indexes derived from it, and the server’s RecipeAccessthe worker pool for its constructor; the Server thread after, the first finalize on whichever thread builds the server
RecipeMapthe immutable store, holding exactly two indexes: RecipeMap.byType and RecipeMap.byKeybuilt on the worker pool, swapped in and read on the Server thread
Ingredientwhether one stack satisfies one slot — a HolderSet of items wearing a predicate faceboth sides
CraftingInputthe trimmed grid a crafting recipe is matched against, and the presence index a shapeless one is matched withServer, and a result slot’s ResultSlot.onTake builds one on the Render thread too
ResultSlotthe order of the endgame: award first, then look the recipe up again, then decrementServer, mirrored on the Render thread’s menu
ServerRecipeBookwhich recipes this player has unlocked, and which of them still glowServer
ClientRecipeContainereverything the client knows about recipes as recipes: nine RecipePropertySets and the stonecutter’s input setRender, 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:

  1. the player is not a spectator, and the packet’s container id is the open menu’s;
  2. AbstractContainerMenu.stillValid still holds for the open menu;
  3. RecipeManager.getRecipeFromDisplay resolves the index to a RecipeManager.ServerDisplayInfo;
  4. ServerRecipeBook.contains says this player has unlocked the parent recipe;
  5. the open menu really is a RecipeBookMenu;
  6. the recipe’s PlacementInfo.isImpossibleToPlace says no — which catches PlacementInfo.NOT_PLACEABLE and any placement whose ingredients came out empty;
  7. and only then does RecipeBookMenu.handlePlacement run.

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.