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

Enchantments

Verified against Minecraft 26.2 · Part VII · A player hits a zombie with a Fire Aspect sword, and everything that makes the zombie burn is data.

You swing a Fire Aspect sword at a zombie and the zombie catches fire. Go looking for the class that does that — the one with the on hit, set them alight method — and there is nothing to find. There are no enchantment subclasses. The forty-three vanilla enchantments are JSON files in the built-in data pack, Enchantments is a bag of forty-three registry keys that only four files outside the data generator ever name, and the whole Fire Aspect is melee only rule is one loot condition, sitting on one entry of a DataComponentMap that no item ever holds.

That is the pattern the rest of the page is about. An enchantment is a named modifier that holds no code; every behaviour in the game an enchantment can change is a component key that some other system asks about at a well-defined moment.

The cast

classwhat it decidesthread
Enchantmentthe record — a description, a definition, an exclusive set and a map of effects. No behaviour of its ownbuilt at data-pack load, read on both sides
Enchantment.EnchantmentDefinitionwhat it can go on, what it costs, which slots it counts indata-pack load
EnchantmentEffectComponentsthe thirty-one keys that name every moment an enchantment can changestatic, both sides
EnchantmentHelperthe static hook surface — walks stacks and slots, calls into the record, holds no stateserver main for every effect, with some read-only entry points on the client
ConditionalEffect / TargetedConditionalEffectwhether this effect fires here, and on whomserver main
EnchantmentEntityEffectthe thing that finally happensserver main only — the signature demands a ServerLevel
ItemEnchantmentsthe id-to-level map on the stack, under one of two componentsboth sides
EnchantedItemInUsethe stack, its slot, its owner and a break callback, built by the slot-aware walk and handed to the effects that act on the worldserver main

A record with a definition and a bag of components

Enchantment is a record of four things: a description Component, an Enchantment.EnchantmentDefinition, a HolderSet of enchantments it is exclusive with, and a DataComponentMap of effects. Enchantment.getEffects looks a component type up in that map and returns an empty list if it is absent, which is the whole dispatch mechanism.

The definition is where the enchanting rules live. It carries the supported items and a narrower, optional set of primary items — both normally item tags — so Enchantment.isPrimaryItem falls back to the supported set when the second is absent, while Enchantment.isSupportedItem only ever asks the first. It carries the weight, the maximum level, two Enchantment.Cost curves (a base plus a per-level-above-first increment, read by Enchantment.Cost.calculate), the anvil cost, and a list of EquipmentSlotGroup — the slots in which this enchantment counts at all, tested by Enchantment.matchingSlot. The codec bounds the weight to 1–1024 and the maximum level to 1–255, which is Enchantment.MAX_LEVEL; Enchantment.getMinLevel is the one number that is not data, hardcoded to 1.

What a stack carries is not that record but ItemEnchantments, an immutable id-to-level map under DataComponents.ENCHANTMENTS — or, for an enchanted book’s inert set, under DataComponents.STORED_ENCHANTMENTS. It is changed only through ItemEnchantments.Mutable, whose ItemEnchantments.Mutable.upgrade merges by maximum and clamps at 255. EnchantmentInstance, a holder-and-level pair with a EnchantmentInstance.weight delegate, is what the weighted selection path is built on — and that path, along with the table, the anvil, the grindstone, the providers and /enchant, is a separate machine with its own arithmetic, on enchanting.

Thirty-one keys, three registries and one curve

EnchantmentEffectComponents registers thirty-one component types into BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE. Twenty-four hold a list of ConditionalEffect (or TargetedConditionalEffect); the other seven are plain — EnchantmentEffectComponents.ATTRIBUTES, two sound lists, two unconditional values, and the two Unit-valued flags EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP and EnchantmentEffectComponents.PREVENT_ARMOR_CHANGE, true by being present.

Three registries supply the effect objects, and they are not disjoint. EnchantmentValueEffect has six shapes and modifies a running number (AddValue, MultiplyValue, SetValue, RemoveBinomial, ScaleExponentially and AllOf.ValueEffects). EnchantmentEntityEffect has fifteen and does something to an entity — Ignite, DamageEntity, ApplyMobEffect, SummonEntityEffect, AllOf.EntityEffects and ten more. And it extends EnchantmentLocationBasedEffect, whose registry has sixteen entries: those same fifteen ids plus one. The odd one out is attribute, EnchantmentAttributeEffect, which installs an AttributeModifier (attributes) and is the only effect that is location-based without also being an entity effect.

LevelBasedValue turns a level into a number, and six shapes are in its dispatch registry (LevelBasedValue.Linear, LevelBasedValue.Clamped, LevelBasedValue.Fraction, LevelBasedValue.LevelsSquared, LevelBasedValue.Exponent, LevelBasedValue.Lookup). LevelBasedValue.Constant is deliberately not among them: it is the other arm of an either-codec, which is what makes a bare float legal anywhere a curve is expected.

ConditionalEffect is an effect plus an optional LootItemCondition. TargetedConditionalEffect adds two EnchantmentTarget fields — which side of the fight the enchantment lives on (TargetedConditionalEffect.enchanted) and which side it lands on (TargetedConditionalEffect.affected) — except for the equipment-drops variant, whose codec reads only the first and pins the second to EnchantmentTarget.VICTIM.

Both implement Validatable, and this is where the effect codecs do something unusual: those twenty-four list components wrap their element codec in Validatable.listValidatorForContext, so a condition is checked at decode time against the parameter set the hook will actually supply. A post_attack effect asking about a block state fails to load rather than failing quietly at runtime.

Seven families of moment

Everything above is inert until something calls EnchantmentHelper. The enchantment package barely calls anything and everything calls it, so the artefact worth keeping is a table of who calls what: every entry point with its callers is the enchantment hook table, and these are the seven kinds of moment it falls into.

familya representative hook or twowho makes it real
damage and protectionEnchantmentHelper.modifyDamage, EnchantmentHelper.getDamageProtectionServerPlayer.getEnchantedDamage, LivingEntity.getDamageAfterMagicAbsorb
post-attack effectsEnchantmentHelper.doPostAttackEffectsWithItemSource, EnchantmentHelper.doPostPiercingAttackEffectsPlayer.itemAttackInteraction, LivingEntity.postPiercingAttack
durability and dropsEnchantmentHelper.processDurabilityChange, EnchantmentHelper.processEquipmentDropChanceItemStack.hurtAndBreak, Mob.dropCustomDeathLoot
projectiles and the weapon in handEnchantmentHelper.processProjectileCount, EnchantmentHelper.getPiercingCountProjectileWeaponItem.draw, the AbstractArrow constructor
location and tick effectsEnchantmentHelper.runLocationChangedEffects, EnchantmentHelper.tickEffectsLivingEntity.onChangedBlock, LivingEntity.baseTick
experience and repairEnchantmentHelper.processBlockExperience, EnchantmentHelper.modifyDurabilityToRepairFromXpBlock.tryDropExperience, ExperienceOrb.repairPlayerItems
the flag questionsEnchantmentHelper.has, EnchantmentHelper.hasTagArmorSlot.mayPickup for Curse of Binding, four blocks for the four prevents tags

The location row is the one that keeps state: LivingEntity.activeLocationDependentEnchantments remembers which location-based effects are running in which slot, so the system can tell became active from still active, and so an attribute effect can be taken off cleanly by EnchantmentHelper.stopLocationBasedEffects when the armour comes off.

How one hook fires

Six of those seven rows are the same shape underneath. A system reaches a moment and asks EnchantmentHelper; the helper picks which stacks to walk, filtering by slot if it was given one; the record picks which effect entries apply; a loot condition decides whether this one fires. Only then does anything happen. The flag row is the exception and is the reason the shape is worth drawing: EnchantmentHelper.has and EnchantmentHelper.hasTag build no context and run no condition — they ask the record whether the key is present at all.

flowchart TD
    Caller["a system reaches a moment: Player.itemAttackInteraction, Block.tryDropExperience, LivingEntity.baseTick"]
    Caller --> EH["an EnchantmentHelper entry point"]
    EH --> Walk["walk one stack, or every EquipmentSlot of one entity"]
    Walk --> Slot["on the slot-aware walk, keep entries whose Enchantment.matchingSlot accepts this slot"]
    Slot --> Comp["read the list under one EnchantmentEffectComponents key"]
    Comp --> Target["for the targeted keys, keep entries whose enchanted target matches this pass"]
    Target --> Ctx["build a LootContext on that hook's parameter set"]
    Ctx --> Cond["ConditionalEffect.matches runs the LootItemCondition"]
    Cond -- "no" --> Drop["nothing happens"]
    Cond -- "yes" --> Apply["apply: fold a value, or run the effect on the affected entity"]

The LootContext in the middle is the same machinery loot tables and /execute if predicate use, and Enchantment builds five of them — Enchantment.damageContext plus four private siblings for items, entities, locations and block hits, one per parameter set the components name. Contexts and predicates owns that half.

Fire Aspect, from the click to the flame

Fire Aspect as data is forty-three lines of JSON: supported and primary items are tags, weight 2, maximum level 2, anvil cost 4, dynamic cost curves, one slot (mainhand), and one effect — a EnchantmentEffectComponents.POST_ATTACK entry whose enchanted target is the attacker, whose affected target is the victim, whose effect is Ignite with a linear duration of four seconds per level, behind one damage-source predicate requiring a direct hit.

sequenceDiagram
    participant SGPL as ServerGamePacketListenerImpl
    participant Player as Player
    participant EH as EnchantmentHelper
    participant Ench as Enchantment
    participant Ignite as Ignite
    participant Entity as Entity
    participant SED as SynchedEntityData

    SGPL->>Player: handleAttack passes the range checks, then Player.attack
    Player->>Player: createAttackSource, whose direct and causing entity are one
    Player->>EH: on a hit that landed, itemAttackInteraction calls doPostAttackEffectsWithItemSource
    EH->>EH: the victim's whole equipment first, then the attacker's main hand
    EH->>Ench: doPostAttack for the main-hand stack, in the ATTACKER pass
    Ench->>Ench: damageContext, then TargetedConditionalEffect.matches asks is the hit direct
    Ench->>Ignite: apply, with the victim as the affected target
    Ignite->>Entity: igniteForSeconds, raised only if the new value is larger
    Entity->>SED: baseTick sets shared flag zero
    Note over SED: the flame travels as ClientboundSetEntityDataPacket, the enchantment never does
    Note over Entity: one point of fire damage every twentieth tick from here on

The swing, and the source that is the whole melee rule. ServerGamePacketListenerImpl.handleAttack runs the range checks and calls Player.attack — unless the main-hand item has DataComponents.PIERCING_WEAPON, in which case the packet is dropped and none of this happens (see the sword swing for the spear’s path). Player.createAttackSource then asks ItemStack.getDamageSource, and every branch of it reaches the single-entity DamageSource constructor — the one that sets the direct entity and the causing entity to the same object, making DamageSource.isDirect true. An arrow’s source has an arrow as the direct entity and a player as the causing entity, so it is false. That one comparison, read by one loot condition in one JSON file, is why Fire Aspect never fires through a bow.

The damage, then the hook. ServerPlayer.getEnchantedDamage — the override, not Player’s base version, which returns its argument unchanged — folds Sharpness and friends in through EnchantmentHelper.modifyDamage, and the hit goes through Entity.hurtOrSimulate to LivingEntity.hurtServer (damage and death). Only if that returned true does Player.itemAttackInteraction call EnchantmentHelper.doPostAttackEffectsWithItemSource.

Three branches, not two. The helper walks the victim’s whole equipment first, with EnchantmentTarget.VICTIM as the pass — that is Thorns’ lane, and why Thorns works from a chestplate while Fire Aspect does not work from boots. Then, for a living causing entity, it walks the attacker’s main hand only, in the EnchantmentTarget.ATTACKER pass, keeping enchantments whose declared slots include that slot. A third branch handles a causing entity that is not living: a slotless pass with no filter at all, reached through EnchantmentHelper.doPostAttackEffectsWithItemSourceOnBreak with a break callback, whose only vanilla caller is ThrownTrident. An attacker’s armour can never contribute a post-attack effect.

The condition and the target. Enchantment.doPostAttack keeps the entries whose enchanted target matches the pass, builds the enchanted-damage context — victim, level, origin, damage source, and both attacking entities as optional parameters — and runs the predicate. The affected field then picks who receives the effect: attacker, direct entity, or victim. Fire Aspect says victim, and if that works out to null the effect is dropped in silence.

The burn. Ignite.apply calls Entity.igniteForSeconds, which floors to ticks and hands Entity.igniteForTicks a number it applies only if it is larger than the counter already there — while clearing any freeze regardless, so re-hitting a burning target with a weaker Fire Aspect does nothing except thaw it. No damage happens here. Entity.baseTick deals it, one point every twentieth tick, on a ServerLevel, skipped in lava and replaced by Entity.clearFire for a fire-immune entity. The same method sets shared flag zero through Entity.setSharedFlagOnFire (synched entity data), which becomes Entity.displayFireAnimation on the client — whose own Entity.baseTick, finding no ServerLevel, clears the fire counter instead of burning.

Questions the pattern raises

Where does Fortune live, if not in a hook? In the loot table. ApplyBonusCount and BonusLevelTableCondition read the tool parameter out of a LootContext and call EnchantmentHelper.getItemEnchantmentLevel on it — a level, not an effect. Looting is the same trick from the other end: EnchantedCountIncreaseFunction reads the attacking entity parameter and calls EnchantmentHelper.getEnchantmentLevel, the overload that walks a LivingEntity’s equipment and keeps the best. Fortune has no effect component whatsoever, and Looting’s only one is an equipment_drops entry that has nothing to do with mob loot. Mending inverts it once more: ExperienceOrb.repairPlayerItems asks EnchantmentHelper.getRandomItemWith for a stack carrying EnchantmentEffectComponents.REPAIR_WITH_XP, so the orb, not the item, drives the repair.

Does anything enchantment-shaped run on the client? No effect can: EnchantmentEntityEffect and EnchantmentLocationBasedEffect both demand a ServerLevel. But two values do. Enchantment.modifyUnfilteredValue takes only a RandomSource, and its two users are Enchantment.modifyCrossbowChargeTime and Enchantment.modifyTridentSpinAttackStrength. CrossbowItem.getChargeDuration is called by three entity renderers, by ItemInHandRenderer and by the CrossbowPull item property, so Quick Charge is evaluated on the render thread every frame a crossbow is being drawn; and MultiPlayerGameMode.releaseUsingItem runs TridentItem.releaseUsing on the client’s own copy, which asks EnchantmentHelper.getTridentSpinAttackStrength, so Riptide’s strength is computed client-side too — which is what lets the riptide push be predicted at all. Everything else the client does is drawing: ItemEnchantments.addToTooltip for the tooltip, ItemStack.hasFoil for the glint, and EnchantmentHelper.forEachModifier for the attribute lines — which means the client evaluates the LevelBasedValue curve itself.

What actually crosses the wire? Usually an id and nothing else. Registries.ENCHANTMENT is in RegistryDataLoader.SYNCHRONIZED_REGISTRIES with the full Enchantment.DIRECT_CODEC, but RegistrySynchronization.packRegistry sends a bare Identifier for every element whose pack the client already has — for a vanilla client against a vanilla server, all forty-three. Full definitions cross only for a data pack’s custom or overridden enchantments. Beyond that, ClientboundUpdateTagsPacket for EnchantmentTags and, per stack, the DataComponents.ENCHANTMENTS component: registry ids and levels.

Why is an enchanted book inert? Because EnchantmentHelper.runIterationOnItem — the private walk under every hook — reads DataComponents.ENCHANTMENTS and nothing else. A book’s set lives under DataComponents.STORED_ENCHANTMENTS, and the routing between the two, in EnchantmentHelper.getComponentType, is keyed on the exact item Items.ENCHANTED_BOOK. The component is not so exclusive: ItemStack puts DataComponents.STORED_ENCHANTMENTS in any stack’s tooltip, and AnvilMenu decides it is being handed a book by testing for that component rather than for the item — so a data pack can make a stick priced like one, but never make it behave like one.

Is the main hand really the main hand? No — it is a label. EnchantmentHelper.doPostAttackEffectsWithItemSourceOnBreak and EnchantmentHelper.doPostPiercingAttackEffects both hand EquipmentSlot.MAINHAND to the slot filter regardless of where the weapon came from, and KineticWeapon.damageEntities reaches LivingEntity.stabAttack with whichever slot the use was in. An off-hand spear’s enchantments are therefore tested against the main-hand slot group.

Three more small ones. ItemStack.isEnchantable reads two components: DataComponents.ENCHANTABLE must be present, and DataComponents.ENCHANTMENTS must be present and empty, so an item with no enchantments component at all is not enchantable either. The two EnchantmentHelper.forEachModifier overloads test different things — the EquipmentSlot one asks Enchantment.matchingSlot, the EquipmentSlotGroup one asks whether the definition declares that exact group — and ItemStack.forEachModifier uses one of each. And the instance method is spelled Enchantment.modifyArmorEffectivness, Mojang’s typo, while the helper beside it is EnchantmentHelper.modifyArmorEffectiveness.

And does Fire Aspect cook the loot? Not through the enchantment. That is a loot-table condition on EnchantmentTags.SMELTS_LOOT, a tag whose only member is Fire Aspect (loot tables). EnchantmentTags holds twenty-nine tags. Twenty-five fall into five families — the seven exclusivity sets, the tooltip order, pool membership for the table and for mob, trade and loot equipment, the behaviour flags (curse, smelts-loot and the four prevents tags), and the seven biome trade tables — and the last four are the trading and treasure axes the villager and the loot tables sort on.

What does a whole enchantment look like, then? Enchantments.LUNGE is the only user of EnchantmentEffectComponents.POST_PIERCING_ATTACK in the game, and its single effect is an AllOf.EntityEffects of four — a ChangeItemDamage, an ApplyExhaustion scaled per level, an ApplyEntityImpulse forward with its vertical component scaled away, and a PlaySoundEffect holding three sounds, indexed by level rather than shuffled — behind a four-clause condition checking that the user is not riding, not elytra-flying, not in water, and either not a player, in creative, or fed. One JSON file, four effect objects, four predicates, no Java. That is what an enchantment is.

Where to look

Enchantment · Enchantment.EnchantmentDefinition · Enchantment.Cost · EnchantmentEffectComponents · ConditionalEffect · TargetedConditionalEffect · EnchantmentTarget · LevelBasedValue · EnchantmentValueEffect · EnchantmentEntityEffect · EnchantmentLocationBasedEffect · EnchantmentAttributeEffect · AllOf · Ignite · EnchantmentHelper · EnchantedItemInUse · ItemEnchantments · EnchantmentInstance · Enchantments · EnchantmentTags


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