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

Enchanting: the five paths, and what each one is allowed to do

Verified against Minecraft 26.3 · Part VII · A player reads three offers off an enchanting table and buys one, and then the same sword meets the four other paths that change what it is enchanted with — an anvil, a grindstone running backwards, a spawning pillager, and a command.

A sword goes in the left slot of an enchanting table and three lapis in the right, and the table answers with three lines of Standard Galactic Alphabet, three level numbers, and — if you hover — one enchantment named outright. None of that is guessed. The server has already run the entire selection, and the client holds the answer as ten numbers the menu syncs. One of those ten is Player.enchantmentSeed, and it is the reason the page is worth a lecture: one number per player, saved in the player file, carried across death and dimension change, sent to the client, and re-rolled by nothing in the game but the enchanting table itself and a load that finds it still zero. Spend thirty levels at an anvil and come back and the table is offering exactly what it offered before — and the gibberish is in the same handwriting, because the client is drawing it from the low sixteen bits of that same number.

The table is one of five paths that change what a stack is enchanted with — four of them adding and the grindstone taking away — and they differ far more than the shared vocabulary suggests. This page is about those differences. A sixth writer hides outside all of them, in the crafting grid: RepairItemRecipe carries every curse from both inputs onto the item it makes (recipes). What an enchantment is — the record, the effect components, the hooks that fire in combat — is the next page along and is not re-taught here.

The cast

classwhat it decidesthread
EnchantmentMenuthe three offers, the clue, and what the click costsServer, with a Render-thread copy that can only say no
EnchantmentHelperthe cost curve, the weighted selection, and the write every path ends inwhichever side asks
Playerthe seed and the levelsServer
AnvilMenuthe merge arithmetic and the priceServer
GrindstoneMenuthe removal made on purpose, and the refundServer
EnchantmentProviderwhat a mob’s spawn equipment getsServer
EnchantRandomlyFunctionchest loot and villager tradesServer
EnchantCommandthe operator’s path, and the fewest checksServer

EnchantWithLevelsFunction, SetEnchantmentsFunction and EnchantedCountIncreaseFunction are the other three loot functions, named below where they differ; EnchantmentScreen is the client half of the table and gets its own section.

The five paths at a glance

Read this table as the page’s table of contents rather than as something to use now: every cell is a compressed forward reference, and each row below is the section that unpacks it.

enchanting tableanvilgrindstoneproviders and loot/enchant
what it costs1, 2 or 3 levels and the same count of lapisthe full price in levels, and a chance the anvil chipspays you, in orbs at the blocknothingnothing
the gate on the itemItemStack.isEnchantable — enchantable and not already enchantedEnchantmentHelper.canStoreEnchantmentsdamageable or already enchantedDataComponents.ENCHANTABLE, with three exceptions (below)any non-empty main-hand item
which item filterEnchantment.isPrimaryItem — the narrow setEnchantment.canEnchant — the supported setn/aone of three, depending on the path (below)Enchantment.canEnchant
the level ceilingwhatever the cost brackets allowclamped to Enchantment.getMaxLeveln/aclamped, except SetEnchantmentsFunctionrejected above Enchantment.getMaxLevel
exclusivityfiltered out mid-selectiondropped, and it raises the pricecurses survive, everything else goesfiltered among the cost-based picks, unasked by the restrejected with an error
randomnessthe player’s saved seednone in the arithmetic; a 12% roll for the chipnone in the strip; a roll on the refundthe random source its caller hands innone
decided onserver, with the click predictedserver, with the price syncedserverserverserver

Every row of that table differs. The one thing that would not — and so has no row — is the last step, where all five converge. Not on the same method: the table, /enchant, EnchantRandomlyFunction and EnchantWithLevelsFunction go through ItemStack.enchant, the grindstone, SetEnchantmentsFunction and the providers (through EnchantmentHelper.enchantItemFromProvider) call EnchantmentHelper.updateEnchantments, and the anvil writes with EnchantmentHelper.setEnchantments. But on the same decision, and that is where the page starts.

The one question all five ask

The three write methods above funnel into one private line, EnchantmentHelper.getComponentType, so all five paths ask it in the end — and it does the thing worth knowing before any of them make sense.

It routes by item identity: the component the write lands in is DataComponents.STORED_ENCHANTMENTS if the stack is Items.ENCHANTED_BOOK and DataComponents.ENCHANTMENTS otherwise — a hard identity test against one item, not a tag. That is why every path that can be handed a plain Items.BOOK transmutes it first, a creative player’s anvil apart, through ItemStack.transmuteCopy or by building a fresh stack. The transmute is not cosmetic: enchant a plain book and the levels would land in the active component and the book would start working.

EnchantmentHelper.updateEnchantments adds one more rule of its own: it silently does nothing if the component is absent, returning ItemEnchantments.EMPTY on a null read. In practice every item gets DataComponents.ENCHANTMENTS from DataComponents.COMMON_ITEM_COMPONENTS, so the case arises only when a patch removes the component from a stack, and then the table and the anvil refuse the item and every other path becomes a no-op, with no error anywhere (data components).

The merge itself is ItemEnchantments.Mutable.upgrade: keep the higher level, cap at 255, ignore a level of zero. Only ItemEnchantments.Mutable.set can lower a level, and only the anvil — where it clamps an over-maximum level down — SetEnchantmentsFunction and the crafting grid’s RepairItemRecipe reach for it. The grindstone does not lower anything; it removes.

What it costs, and who pays

The enchanting table’s headline number is not its price. The level requirement for a slot is the cost the table computed for it — up to thirty at the bottom slot — but the amount Player.onEnchantmentPerformed actually subtracts is the slot’s index plus one, and the lapis consumed is the same one, two or three.

what the forum sayswhat the decompile does
the bottom offer costs thirty levelsEnchantmentMenu.clickMenuButton requires thirty levels and takes three
more bookshelves make better enchantmentsmore bookshelves raise the cost, which is what buys higher levels and extra enchantments, and EnchantmentHelper.getEnchantmentCost floors the bottom slot at twice the shelf count
the anvil’s “Too Expensive” is a level capit is a result cap — at a price of forty or more AnvilMenu.createResult empties the output slot unless the player has infinite materials

The anvil charges what it shows, and taxes what came before

The anvil is the opposite: it charges the whole displayed price, through Player.giveExperienceLevels with a negative amount, in AnvilMenu.onTake, and then rolls a small chance to damage or destroy the block. The price is a prior-work tax read from DataComponents.REPAIR_COST on both inputs, plus one per repair material consumed, plus two when merging with a second item of its kind repairs it, plus Enchantment.getAnvilCost times the resulting level for every enchantment transferred (halved with a floor of one when the addition is a book), plus one for a rename or for clearing a name. The larger of the two inputs’ DataComponents.REPAIR_COST is then doubled and incremented by AnvilMenu.calculateIncreasedRepairCost and written onto the result — the whole of the prior-work spiral, and the one step a pure rename skips. Two cases escape that arithmetic: an input stack of more than one item sets the price to 40, before the prior-work tax is added, the moment any enchantment transfers, which — forty being exactly the threshold at which the result is withheld — makes enchanting a stack not expensive but forbidden outside creative; and a rename with no other change is capped at 39, which is why renaming never hits “Too Expensive”.

The grindstone pays you

The grindstone runs the transaction backwards. It strips everything not in EnchantmentTags.CURSE, turns an emptied Items.ENCHANTED_BOOK back into a plain Items.BOOK with ItemStack.transmuteCopy, and rebuilds DataComponents.REPAIR_COST from zero, so a clean item leaves with its prior work erased. The refund is the sum of Enchantment.getMinCost at each stripped level, halved upward with a random bonus of up to one less than that half again, and it arrives as orbs from ExperienceOrb.award at the block — on the ground, not in the player (hunger and experience).

What each path is allowed to add

Two predicates are doing the work, and the difference between them is the difference between the enchantments an axe is offered and the enchantments an axe can hold.

Enchantment.canEnchant asks whether the item’s type is in the definition’s supported set; Enchantment.isSupportedItem asks exactly the same question of a stack and is called from nowhere but Enchantment.isPrimaryItem, which asks the supported question and the narrower falling back to the supported set when the definition names no primary items. The narrow one lives in EnchantmentHelper.getAvailableEnchantmentResults, which only EnchantmentHelper.selectEnchantment calls — so the table, the cost-based providers and EnchantWithLevelsFunction all use it, and the anvil and /enchant do not. That is the three-way split the table’s which item filter cell defers: the selection paths ask the narrow question, EnchantRandomlyFunction asks the supported one when its only_compatible flag is on and the target is not a plain book, and SingleEnchantment and SetEnchantmentsFunction ask neither. The three exceptions in the gate row are the three that do not ask the narrow question: each skips DataComponents.ENCHANTABLE. In vanilla exactly five enchantments declare a narrower primary set than their supported one. Three of them are melee enchantments whose supported set reaches axes and whose primary set stops at swords and spears, which is why no enchanting table has ever offered Sharpness on an axe while every anvil will put it there. A fourth does the same to the mace, and the fifth is Thorns, offered only on a chestplate and wearable anywhere.

The anvil uses Enchantment.canEnchant on its left slot, overridden to true when that target is an Items.ENCHANTED_BOOK or the player has infinite materials, so books collect anything. Its right slot is judged differently, and by a different question: whether the addition is a book, for the halved price, is DataComponents.STORED_ENCHANTMENTS being present rather than the item being Items.ENCHANTED_BOOK. One side tests the item, the other tests the component. Its arithmetic per transferred enchantment is short: the same level on both sides merges to one higher, different levels take the maximum, and the winner is clamped to Enchantment.getMaxLevel. An enchantment the target cannot take is dropped and costs nothing; one that conflicts with something already on the result is dropped and adds one to the price per conflicting pair — the anvil’s one punitive rule besides the stack rule above. If nothing survives, the result slot is emptied.

The ceilings, and who ignores them

/enchant is the shortest path and, contrary to its reputation, not the laxest. EnchantCommand rejects a level above Enchantment.getMaxLevel before it looks at any target, then per target requires a LivingEntity whose LivingEntity.getMainHandItem is non-empty, then checks Enchantment.canEnchant and EnchantmentHelper.isEnchantmentCompatible against what the stack already carries — and from there it is the same tail as everything else. What it skips is the primary filter, the enchantability component and the cost, not the supported-items or level rules. It also accepts a level of zero, which passes every check, reports success, and changes nothing.

The genuine ceiling-breaker is elsewhere. SetEnchantmentsFunction writes through ItemEnchantments.Mutable.set, whose only clamp is 255, with no reference to Enchantment.getMaxLevel at all: a loot table can hand out Sharpness 200 and nothing else on this page can.

Exclusivity, one test wherever it is asked

Where it is asked at all, exclusivity is one static method — Enchantment.areCompatible, wrapped by EnchantmentHelper.isEnchantmentCompatible and EnchantmentHelper.filterCompatibleEnchantments — and it is symmetric, failing if either side’s exclusive set names the other, and failing an enchantment against itself. Only the anvil and /enchant ask it against what the stack already carries; the selection asks it only among its own picks, and EnchantRandomlyFunction, SingleEnchantment and SetEnchantmentsFunction never ask it.

Where the randomness comes from

The anvil and the grindstone roll a die each — for the chip and for the refund — and everything else but /enchant (and, with vanilla’s constant levels, SetEnchantmentsFunction) rolls one to decide what you get. But not in the same place. Six things roll what an item gets in this game, and four of them reach the shared arithmetic in EnchantmentHelper.selectEnchantment: the table, the two cost-based providers, and EnchantWithLevelsFunction through EnchantmentHelper.enchantItem. The remaining two roll their own and much more simply — SingleEnchantment samples a level for an enchantment it already knows, and EnchantRandomlyFunction picks one at random off the loot context’s random source. So the arithmetic below is the cost-based path, and it is a short method with four distinct sources of variance stacked on one another — the last of which is a fresh roll from fifty, tested against the cost, once per extra enchantment.

flowchart TD
    A["a cost arrives, from the table or from a provider"] --> B{"does the stack have DataComponents.ENCHANTABLE"}
    B -- no --> Z["empty list, and the caller adds nothing"]
    B -- yes --> C["raise it by two rolls scaled by Enchantable.value"]
    C --> D["scale by a triangular span of 15 percent, clamp at 1"]
    D --> E["EnchantmentHelper.getAvailableEnchantmentResults: primary here, or a plain book"]
    E --> F["per survivor, the highest level whose bracket holds the value"]
    F --> G{"any candidates at all"}
    G -- no --> Z
    G -- yes --> H["weighted pick by Enchantment.getWeight"]
    H --> I{"a roll from fifty is at most the cost"}
    I -- no --> Y["the list, out"]
    I -- yes --> J["drop every candidate incompatible with the last pick"]
    J --> K{"anything left"}
    K -- no --> Y
    K -- yes --> L["weighted pick again, then halve the cost"]
    L --> I

Four sources of variance in a row, and then a loop. The cost halves on every pass through it, which makes each extra enchantment rarer than the last; the first extra roll, taken before any halving, is certain from a cost of forty-nine.

The enchantability perturbation is the first place the item matters: Enchantable.value — gold’s is famously high — widens two independent rolls that only ever push the cost up. The span that follows is triangular rather than flat, so the extremes are rare. The bracket test in EnchantmentHelper.getAvailableEnchantmentResults walks levels downward and stops at the first fit, so a high cost buys a high level of one enchantment rather than more of them. Buying more is the loop’s job: the cost halves after every extra pick, so by the fourth pass the roll is nearly always lost — while from a cost of forty-nine up the first extra roll is certain.

The table adds one more layer. EnchantmentMenu.slotsChanged seeds its RandomSource with Player.enchantmentSeed for the three costs, then re-seeds it with the seed plus the slot number before each selection, which is why the three offers are independent of each other and yet reproducible. EnchantmentHelper.getEnchantmentCost returns zero outright for an item with no DataComponents.ENCHANTABLE, and a slot whose cost came out below its own index plus one is zeroed too. The weight each surviving candidate carries into the pick is Enchantment.getWeight, straight off the data-pack record.

Bookshelves reach it as a plain integer. EnchantmentMenu walks EnchantingTableBlock.BOOKSHELF_OFFSETS — a fixed list of thirty-two offsets, the outer ring of a five-by-five footprint at two heights — and EnchantingTableBlock.isValidBookShelf requires the block at the offset to be in BlockTags.ENCHANTMENT_POWER_PROVIDER and the block between it and the table to be in BlockTags.ENCHANTMENT_POWER_TRANSMITTER (tags). That between position halves the X and Z offsets but leaves Y alone, so the upper ring’s gap is checked at the bookshelf’s own height, not the table’s. The clamp to fifteen happens inside EnchantmentHelper.getEnchantmentCost, not in the walk.

What is decided on which side

The clue you hover is not a hint about what you might get: it is a genuine member of the exact list you will get. EnchantmentMenu.slotsChanged runs the selection for real, shows one entry of the result at random and throws the rest away, and EnchantmentMenu.clickMenuButton runs the same selection again from the same seed and slot and applies all of it. The one wrinkle is the plain book, which has one random entry deleted from its list before either use — unless the list has only one entry, which survives.

sequenceDiagram
    box transparent your machine
    participant EScr as EnchantmentScreen
    participant CEM as EnchantmentMenu
    end
    participant Wire as the network
    box transparent the server
    participant EM as EnchantmentMenu
    participant EH as EnchantmentHelper
    end

    Note over EM,EH: slot 0 fills, and <br/>EnchantmentMenu.<br/>slotsChanged runs on this side
    EM->>EM: walk BOOKSHELF_OFFSETS, count the valid shelves
    EM->>EH: getEnchantmentCost three times, from the player seed
    EM->>EH: selectEnchantment per slot, re-seeded with the seed plus the slot
    EH-->>EM: a list per slot, one entry of which becomes the clue
    EM->>Wire: one<br/>Clientbound<br/>ContainerSetDataPacket<br/>per changed slot, from broadcastChanges
    Wire->>CEM: the costs and the clues, the seed there since the menu opened
    Note over EScr,CEM: EnchantmentNames.<br/>initSeed fixes <br/>the alphabet for this seed
    EScr->>CEM: clickMenuButton, whose level access here is NULL
    CEM-->>EScr: true only if the lapis and the levels are there, or materials are infinite
    EScr->>Wire: ServerboundContainerButtonClickPacket, via MultiPlayerGameMode
    Wire->>EM: clickMenuButton, the identical call on the server's copy
    EM->>EH: selectEnchantment again, same seed and slot, same list
    Note over EM,EH: Player.onEnchantmentPerformed takes the levels and re-rolls the seed
    EM->>EH: updateEnchantments once per entry, through ItemStack.enchant
    Note over EM,EH: the lapis goes, Stats.ENCHANT_ITEM, CriteriaTriggers.ENCHANTED_ITEM
    EM->>EM: slotsChanged again, and an enchanted item gets no offers
    EM->>Wire: the zeroed costs, the cleared clues and the new seed, from the handler's broadcastChanges

One menu class, two copies, and the boundary drawn. The client’s EnchantmentMenu answers the click before any packet is sent, from the cost it was sent, the item and lapis in its slots, and the player’s levels or infinite materials.

The predicted click is the sharpest thing on that diagram. EnchantmentScreen.mouseClicked calls EnchantmentMenu.clickMenuButton on its own local menu and only sends the packet — a ServerboundContainerButtonClickPacket, through MultiPlayerGameMode — if that call returns true. The server’s run of the same method is where Stats.ENCHANT_ITEM is awarded and CriteriaTriggers.ENCHANTED_ITEM fires. On the client the menu’s level access is ContainerLevelAccess.NULL, whose evaluation returns an empty optional without running the action at all — so the entire enchanting body is skipped, and what the client really evaluates is the guard in front of it: the lapis count, the level requirement, and Player.hasInfiniteMaterials. The affordability check is real on both sides; the enchanting is real on one. Each changed value crosses as one ClientboundContainerSetDataPacket. Containers and menus has the data-slot channel, and where a broadcast happens has the button click’s own timing.

All ten are ordinary DataSlot entries, and nine of them are DataSlot.shared views onto three arrays of three: the costs, the enchantment ids, and the levels. The tenth is the one DataSlot.standalone, holding the seed. So a clue is a pair — which enchantment, and at what level — which is why three offers cost six slots, and why the tooltip can say Sharpness III rather than just Sharpness. A clue with nothing to show carries −1, and a cost 0. They reach the client one ClientboundContainerSetDataPacket each as AbstractContainerMenu.broadcastChanges diffs them against its remote copy. The enchantment half of each pair is a numeric registry id that EnchantmentScreen resolves against its own registry copy — the same registry copy that ItemEnchantments’ stream codec needs to name the enchantments on any stack the client is sent. The seed slot is the only route by which Player.enchantmentSeed ever reaches a client, and what crosses is its low sixteen bits: it is written to the player file as XpSeed, re-rolled on read if it loads back as zero, copied unconditionally by ServerPlayer.restoreFrom when a respawn builds a new player (death, or the way out of the End), and named in no packet of its own. EnchantmentNames.initSeed then seeds one shared RandomSource with it per frame and EnchantmentNames.getRandomName draws three or four words from a fixed list in the alt font — same seed, same three lines, every time.

The paths that never show a player anything

The provider path, at spawn

Mob.enchantSpawnedEquipment (entity lifecycle) calls EnchantmentHelper.enchantItemFromProvider, which looks a provider up in Registries.ENCHANTMENT_PROVIDER and hands the stack’s mutable enchantment map to EnchantmentProvider.enchant. Registries.ENCHANTMENT_PROVIDER is a world-load dynamic registry like Registries.ENCHANTMENT itself and EnchantmentProviderTypes is the dispatch of kinds inside it (data-driven types). EnchantmentsByCost and EnchantmentsByCostWithDifficulty go through EnchantmentHelper.selectEnchantment, so a mob’s gear is rolled by exactly the arithmetic the table uses, with the local difficulty widening the cost; SingleEnchantment skips selection entirely, upgrading one named enchantment to a sampled level clamped only to that enchantment’s own range, never asking whether the item supports it. Six of the seven providers VanillaEnchantmentProviders registers are that third kind.

The loot path, and the trades on it

The loot path runs wherever a loot table does, and villager trades are on it: a VillagerTrade carries a list of LootItemFunctions applied to what it gives, and the librarian’s enchanted book is EnchantRandomlyFunction with its only_compatible item check turned off, followed by a filter that discards the trade if the book came out with nothing stored in it. EnchantWithLevelsFunction is the cost-based one, in chest loot, fishing and trades alike, and calls EnchantmentHelper.enchantItem — the same selection again; SetEnchantmentsFunction names its enchantments, and vanilla gives it constant levels. Both random ones can set DataComponents.ADDITIONAL_TRADE_COST when the context offers LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED, which is how a strong enchantment makes a trade dearer (loot tables, contexts and predicates). EnchantedCountIncreaseFunction sits in the same package and is the odd one out: it adds nothing, reading a level off the killer with EnchantmentHelper.getEnchantmentLevel to add to a drop count. It consumes this page’s output rather than producing any.

The seventh producer, which is not a path at all

One more producer belongs in nobody’s mental model of enchanting. CreativeModeTabs builds the creative enchanted books with EnchantmentHelper.createBook — maximum level only in the tab, every level in the search, through CreativeModeTabs.generateEnchantmentBookTypesOnlyMaxLevel and CreativeModeTabs.generateEnchantmentBookTypesAllLevels.

Where to look

EnchantmentHelper.selectEnchantment is the one method to read whole. It is short, four callers on two of the five paths run it, and every piece of arithmetic the flowchart above draws is in it, including the halving loop. EnchantmentHelper.getEnchantmentCost beside it is where the bookshelves land and EnchantmentHelper.getAvailableEnchantmentResults is the primary-item filter it calls.

For the table as a machine, read EnchantmentMenu top to bottom: EnchantmentMenu.slotsChanged computes the offers and EnchantmentMenu.clickMenuButton runs the same computation again and applies it, which is the page’s whole point about the clue. EnchantmentScreen is the client’s side, whose copy of the menu can only say no, and EnchantmentNames is the gibberish.

For the other four paths, one class each and they are all small: AnvilMenu.createResult for the price, GrindstoneMenu for the strip and the refund, EnchantmentProvider with VanillaEnchantmentProviders for a mob’s gear, and EnchantCommand for the shortest path of the five.

ItemStack.enchant, EnchantmentHelper.updateEnchantments and the anvil’s EnchantmentHelper.setEnchantments are the ends the paths meet at, and Enchantment.areCompatible is the exclusivity test wherever one is asked.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.