Crafting and stations
How recipes, bot.craft and the station APIs (bot.openFurnace, bot.openEnchantmentTable, bot.openAnvil,
bot.openVillager, bot.trade) work on Bedrock. The station APIs keep mineflayer's signatures, window members, events
and error messages; underneath, every crafting and station operation is a server-authoritative item stack request
(lib/bedrock/stations.js, see clickWindow). Observed on BDS 1.26.51.1 (protocol 2193,
bedrock-protocol 3.60.1).
Recipes come from the server's crafting_data
- Affects:
bot.recipesFor,bot.recipesAll,bot.registry.recipes,require('prismarine-recipe')(bot.registry) - Java: mineflayer reads minecraft-data's Java recipes (
registry.recipes[resultId]={ result, inShape | ingredients, outShape? }, tags already expanded to one recipe per item) through prismarine-recipe'sRecipe.find. - Bedrock: minecraft-data's Bedrock
recipeshave another shape (keyed by index, Bedrock names) and are not what the server uses. BDS sends its recipes at join incrafting_data(1.26.51: typed arraysshaped_recipes,shapeless_recipes,shulker_box_recipes, …): each with arecipe_id, anetwork_id(what crafting requests name), ablocktag, apriority, ingredient descriptors (name+ metadata, 32767 = any;item_tag) and one or more outputs (network-id items).- Every crafting-grid recipe — 2×2 included — has
block: 'crafting_table'; whether it fits the 2×2 grid follows from its size. deprecatedrecipes are hidden legacy duplicates (BDS still accepts them).- Furnace, smoker, blast furnace, campfire, stonecutter, cartography, smithing and potion recipes are not
crafting-grid recipes;
multi_recipes(map cloning, fireworks, banner / book copies, …) have no fixed inputs or outputs. - Tag ingredients name the tag only; see item tags.
- Every crafting-grid recipe — 2×2 included — has
- bedboo:
lib/bedrock/recipes.jsturns thecrafting_tableshaped / shapeless / shulker-box recipes into prismarine-recipeRecipes with the same fields (result {id, count, metadata},inShape,outShape,ingredients,delta,requiresTable) plus the BedrocknetworkId.- Tag ingredients are expanded like minecraft-data's Java recipes: every cell of one tag gets the same member (13
stick recipes, one per plank type); two tags in one recipe give the cartesian product (13 planks × 13 wooden slabs
= 169 barrels); structurally identical expansions are kept once (lower Bedrock
priorityfirst: the genericminecraft:stickbeatsstick_from_crimson_planks; BDS accepts either id, and the other ids are kept for limited crafting). Like Java's expanded recipes, one recipe never mixes members of a tag (2 oak + 2 birch planks give no crafting table recipe, although BDS would accept that grid) — upstream's behaviour, kept. BDS 1.26.51'scrafting_datayields 4874 recipes from 4128 Bedrock recipes, none skipped. bot.registry.recipesis replaced per bot by the same recipes in minecraft-data's Java shape (filled in place on everycrafting_data), sorequire('prismarine-recipe')(bot.registry).Recipe.find(id)works as on Java. Those Recipes lacknetworkId(prismarine-recipe drops unknown fields), sobot.craftmatches them to the server recipe by result and ingredients (also for Recipes built by hand). A Recipe kept from an oldercrafting_datais matched again the same way.recipesFor/recipesAllare upstream's code over these recipes, with one fix: themetadataargument filters on the result metadata (upstream'sRecipe.findlooks for ametakey and never filters). It matters on Bedrock, where e.g. the bed colour is thebeditem's metadata (white 0, red 14).- With the gamerule
doLimitedCraftingon, only the recipes the server unlocked (unlocked_recipes) are returned (a grid is available when any of its Bedrock recipes is unlocked, and crafted with that one). - Netherite gear is a smithing-table upgrade, not crafting (and has no mineflayer API).
- Tag ingredients are expanded like minecraft-data's Java recipes: every cell of one tag gets the same member (13
stick recipes, one per plank type); two tags in one recipe give the cartesian product (13 planks × 13 wooden slabs
= 169 barrels); structurally identical expansions are kept once (lower Bedrock
Crafting is one item stack request per batch
-
Affects:
bot.craft -
Java: mineflayer clicks the ingredients into the grid (
window_clickper click), takes the result from slot 0 and puts it away, once per craft. -
Bedrock: there is no click packet; the server crafts from an
item_stack_request, and only from the crafting grid: consuming straight from the hotbar is refused (craft_recipe: status 35 "mismatched recipe for grid items";craft_recipe_auto: status 27). The 2×2 grid iscrafting_inputui 28-31 and needs the player's inventory screen (status 7 without it); the crafting table's grid is ui 32-40 (row-major; shapes may sit anywhere and mixed tag members are accepted). Verified request shape (one request, any number of crafts):place× n (inventory → grid cell),craft_recipe { recipe_network_id, times_crafted }(legacy id 12),results_deprecated { result_items: [{ type: 'name', legacy_type: 1, name, metadata, count (per craft), block_runtime_id, extra }], times_crafted }(19),consume(5) × each grid cell (count = times),- per result,
place(1) fromcreative_output50 (stack id = the request id) into the inventory — ortaketo the cursor.
The created output holds at most one stack per request (16 logs → 64 planks in one request; 20 logs → status 14), every result must be taken (status 17 otherwise), and a result may also be dropped from the created output (
drop). Bulk crafting =times_crafted> 1. -
bedboo:
bot.craft(recipe, count, craftingTable)keeps upstream's checks, errors and window handling (Recipe requires craftingTable, but one was not supplied: …,crafting: non craftingTable used as craftingTable, errors re-thrown asnew Error(err)so a missing ingredient readsError: missing ingredient, the table window is opened once and closed at the end or on error).- Each request crafts as many of the
countcrafts as the inventory, one stack per grid cell and one stack of each result allow (count= 25 sticks → 2 requests of 16 and 9); when the ingredients run out after some crafts it throwsmissing ingredientlike upstream's nextcraftOnce. - Ingredients are taken in upstream's
findInventoryItemorder (main inventory, then hotbar; one kind of stack per cell); results go where upstream'sputSelectedItemRangeputs them (stacks with room, then the first empty slot, main inventory first). - The request goes through the inventory model (prediction, rollback on rejection); items found in the grid are returned first.
- A container window open when crafting in the 2×2 grid, or another window open when crafting at a table, is closed
first: the 2×2 grid only exists in the player's inventory screen (upstream would click into the open container
instead) and BDS ignores a container activation while another window is open
(window switching). A table window already open on the same block is reused and, like
the one upstream's
activateBlockwould have opened in its place, closed at the end.
- Each request crafts as many of the
Several results: buckets and bottles go to the inventory
- Affects:
bot.craft/recipe.delta/recipe.outShapeof the cake, honey block and honey bottle → sugar - Java: the empty buckets / bottles stay in the crafting grid (minecraft-data's old
outShape), and mineflayer puts them away after the craft. - Bedrock: the recipe lists them as extra outputs (cake:
output: [cake, bucket × 3]); the client selects each one withcreate { result_slot_id: k }(legacy id 6) and takes it from the created output like the main result (the vanilla client's request, confirmed on BDS: withoutcreatethe take fails with status 49). - bedboo: the extra outputs are added to
recipe.delta(positive counts);outShapestays null since nothing remains in the grid.bot.craftemitscreatebefore taking each result when there are several.
A result that does not fit is carried, then tossed
- Affects:
bot.craftwith a full inventory - Java: upstream's
putSelectedItemRangetosses what it cannot put away (tossLeftover). - Bedrock: the result must be taken in the craft request itself.
- bedboo: what does not fit is taken to the cursor in the same request, then
returnCursorputs it back (Java's rule, as in closing a window) and drops the rest — the same outcome as upstream (anitemDrop). Only one stack can be carried, so a batch whose results overflow by more than that crafts fewer times (the rest in the next request); a single craft that cannot place its results even so fails withdestination full(upstream's message for a full range).
No furnace recipes from the server
- Affects: any code that wants to know what smelts into what
- Java: minecraft-data / the client's recipe book include the furnace recipes.
- Bedrock: BDS 1.26.51's
crafting_datacarries no furnace recipes (see recipes); minecraft-data's Bedrock recipes are not what the server uses. - bedboo: the library has no smelting data. The agent package (
packages/agent) vendors a vetted furnace table from CloudburstMC/Data (Apache-2.0)recipes.json@73bda57f9afa (1.21.40, the last dump with furnace recipes):packages/agent/src/data/smelting.json, regenerated bypackages/agent/scripts/gen-smelting.jsand checked against the 1.26.51 registry.
Furnace properties come from container_set_data
- Affects:
Furnace.fuel,.progress,.totalFuel,.fuelSeconds,.totalProgress,.progressSeconds, theFurnaceupdateevent,bot.openFurnace(furnace, blast furnace, smoker) - Java:
craft_progress_bar(window property) carries property 0 (fuel left), 1 (burn time of the fuel), 2 (cook progress) and 3 (total cook time). - Bedrock:
container_set_data { window_id, property, value }. Property 0 is the tick count (progress), 1 the lit time (fuel left) and 2 the lit duration (burn time). 3 (stored XP) and 4 (fuel aux) have no Java counterpart. There is no total cook time: it is the block's constant, 200 ticks for a furnace and 100 for a blast furnace or smoker (a blast furnace or smoker also burns one coal for 800 ticks, not 1600). BDS sends the block's state before itscontainer_open, then every tick while the block burns. The smelted output arrives asinventory_slotof the window. Bedrock has neither/datanor/blockdata, so a cook cannot be shortened by command. - bedboo: Bedrock properties are mapped onto the Java ones (0→2, 1→0, 2→1) and normalised exactly as upstream does
it.
totalProgressis set from the block type when the window opens. Values that arrived before the window opened are applied when it opens.updatefires for everycontainer_set_dataof the window, like upstream fires it for every window property.
A click into a just-placed furnace is now and then rejected
- Affects:
Furnace.putInput/putFuel/takeOutputright after the furnace was placed and opened - Java: no such rejection.
- Bedrock: in about 1 of 10 smelts the first transfer into a furnace placed a moment before failed with
Server rejected transaction for clicking on slot 1, on window with id N.(BDS answered the item stack request with an error); the same transfer a few hundred ms later succeeded. Not reproduced in isolation (furnaces that have stood for a while are not affected). - bedboo: not fixed in the library. Retry a rejected furnace transfer (the agent retries twice after 600 ms,
recomputing what is still missing, in
packages/agent/src/skills/smelt.js).
Enchanting: options, seed and the enchant request
- Affects:
EnchantmentTable.enchantments,.xpseed,.enchant(choice), theEnchantmentTablereadyevent - Java: window properties 0–9 give the three level requirements, the enchantment seed and one hinted
enchantment (id + level) per option.
enchant_item { windowId, enchantment }enchants, and the server sends the new item. - Bedrock:
player_enchant_options { options: [{ cost, slot_flags, equip_enchants, held_enchants, self_enchants, name, option_id }] }is sent before the table'scontainer_open(empty) and again after every change of the item slot.costis the level requirement. The three lists are all the enchantments the option applies, not a single hint. The seed is never sent to the client. Enchanting is a single item stack request:craft_recipe { recipe_network_id: zigzag32(option_id) }, thenresults_deprecated, thenconsumeof choice + 1 lapis (nothing in creative), thenconsumeof the item, thenplaceof the produced item fromcreative_output50 back into the item slot. BDS applies its own result (a wrongresults_deprecatedis ignored) and charges choice + 1 levels. The response carries no item. - bedboo:
enchantments[i] = { level: cost, expected: { enchant, level } }, whereexpectedis the first of the option's enchantments (Bedrock ids, as inbot.registry.enchantments).readyfires on the same transitions as upstream, andxpseedstays −1.enchant(choice)predicts the enchanted item from the option: equip, held and self enchantments in that order, the sameenchNBT as BDS (a book becomes anenchanted_book). This prediction was verified byte-equal against the server's copy for swords, pickaxes, chestplates and books with options 0–2. It resolves with the item in slot 0 once the server accepts the request. Missing lapis or levels reject with an explanatory error instead of waiting forever.
Enchanting options without enchantments
- Affects:
EnchantmentTable.enchant,enchantments[i].expected - Java: every offered option applies at least one enchantment.
- Bedrock: BDS can offer options whose enchantment lists are all empty (a fishing rod or a carrot on a stick at a table without bookshelves); it charges levels and lapis for them and leaves the item unchanged.
- bedboo: such an option has
expected: { enchant: -1, level: -1 };enchant()predicts the item unchanged (no emptyenchlist), so the bot's copy equals the server's.
Anvil: the name travels in the request, the client predicts the result
- Affects:
Anvil.combine(itemOne, itemTwo[, name]),Anvil.rename(item[, name]),bot.openAnvil - Java: the name is typed with
name_item(orMC|ItemName), the server shows the result in slot 2, andputAway(2)takes it. The costs that upstream checks come from prismarine-item'sItem.anvil. - Bedrock: there is no name packet. The operation is a single item stack request:
optional { recipe_network_id: 0, filtered_string_index: 0 (rename) | -1 }, thenconsumeof the target and the sacrifice, thenplaceof the produced item fromcreative_output50. The new name goes in the request'scustom_names(causeanvil_text). BDS shows nothing in slot 2 and never sends the result back.- Bedrock's rules differ from Java's: an enchantment costs multiplier × the levels it adds (Sharpness IV + IV → V
costs 1, not 5), an incompatible enchantment is dropped at no cost, Impaling has multiplier 2/1, a rename alone
keeps the prior work penalty (a fresh renamed item has
RepairCost0), and a rename can also be "Too Expensive" (Java caps it at 39). - Names are at most 30 characters, and a custom name is a plain string, not a JSON text component.
- prismarine-item has no Bedrock anvil (
Item.anvilis undefined).
- Bedrock's rules differ from Java's: an enchantment costs multiplier × the levels it adds (Sharpness IV + IV → V
costs 1, not 5), an incompatible enchantment is dropped at no cost, Impaling has multiplier 2/1, a rename alone
keeps the prior work penalty (a fresh renamed item has
- bedboo:
lib/bedrock/anvil.js(adapted from prismarine-item'sanvil.js) applies the Bedrock rules for the guards ("Not anvil-able…", "not enough xp", input order) and for the predicted result. That result has BDS's NBT layout:RepairCost,display.Name,ench,Damage. Both are verified against the server's copy for combine, combine + rename, rename, material repair, stack rename and enchantment upgrade.- The result goes into the inventory the way upstream's
putAway(2)puts it: first a matching stack with room, else the first empty slot. - The experience check uses the cost of the direction actually used (upstream's
smallestis 0 whenever one direction is impossible, so it never refuses; BDS would reject the request and leave the items in the anvil). - A sacrifice that changes nothing gives no result even with a name, as in vanilla, and the slots are cleared of leftovers (e.g. unused repair materials) before an operation.
renamein creative mode does not wait for an experience change (upstream waits for one in creative too, though none comes).- A name longer than 30 characters is refused with upstream's "Name is too long." (upstream: 35).
- The result goes into the inventory the way upstream's
Villager trading: update_trade opens the window, the offers are NBT
- Affects:
bot.openVillager,bot.trade,Villager.trades,.selectedTrade,villager.trade(tradeIndex, [times]),Villager.close(), theVillagerreadyevent - Java:
open_window(merchant) plustrade_list(only the unlocked offers, withspecialPrice,demandandpriceMultiplier).select_trademakes the server move the inputs, andputAway(2)takes the output. The opener accepts thevillagerentity only. - Bedrock: interacting with a villager sends
update_trade { window_id, window_type: 'trading', trade_tier, villager_unique_id, offers: NBT { Recipes: [{ buyA, buyCountA, buyB, buyCountB, sell, uses, maxUses, tier, traderExp, demand, priceMultiplierA/B, netId }] } }and nocontainer_open: that packet opens the screen.- The offers list every tier (locked ones too).
buyA.Countis the price the server charges, with discounts such as Hero of the Village folded in (verified), andbuyCountAis the base price. ADamageof 32767 means any data value. - There is no
select_trade. One use is a single item stack request:craft_recipe { recipe_network_id: netId }, thenconsumefromtrade2_ingredient1/trade2_ingredient2(ui slots 4 / 5, verified), thenplaceof the output fromcreative_output50. The server counts the use (usesin the nextupdate_trade). - The villager is
villager_v2;wandering_traderopens the same window.
- The offers list every tier (locked ones too).
- bedboo: the villager plugin hands
update_tradeto the inventory plugin as atradingcontainer (minecraft:merchant, see window types; a refresh while another window is open, or for a window being closed, opens nothing).tradeslists the unlocked offers (tier ≤trade_tier), like Java.- Prices:
inputItem1.countis the base price andrealPricethe server's.specialPriceis the difference minus Java's demand term, so upstream's formula yieldsrealPriceagain.xpistraderExp. An input whoseDamageis 32767 matches any data value when counting and depositing. The recipe network id is the non-enumerabletrade.bedrock.netId. trade()keeps upstream's guards and messages and runs one request per use (putRequirementsdeposits the inputs as upstream does). It first clears ingredient slots that hold something else, since Bedrock has noselect_trade. The output is predicted from the offer'ssellitem (NBT included, e.g. enchanted books) and lands likeputAway(2). A used-up trade rejects with "trade blocked".openVillageracceptsvillager_v2,villagerandwandering_trader. Items left in the trade input slots by a dead trader come back with the next trade window (container gone).
- Prices: