Anvil
AstralItems ships a packet-level reimplementation of the vanilla anvil window. The plugin owns the session state — Bukkit's InventoryView is bypassed entirely — and fires AnvilResultEvent every time the output slot needs to be recomputed. This is what lets the custom-item system layer "books only ever enchant up to available-enchantments " on top of the vanilla rename / combine logic without monkey-patching anything.
Opening the anvil
/items anvil— opens the window for the sender (requiresitems.command).Right-clicking a vanilla anvil block (
ANVIL,CHIPPED_ANVIL,DAMAGED_ANVIL) withEquipmentSlot.HANDis intercepted byAnvilListener. The block-place fallback is preserved: if the player is sneaking AND holding a non-air item, the click falls through to vanilla so they can still place the held block.Any other vanilla anvil GUI — opened via
Player.openAnvil, NMS, or another plugin — is also intercepted throughInventoryOpenEvent(atHIGHEST) and rerouted to the custom anvil. The one exception is a location-less plugin menu UI built withBukkit.createInventory(holder, InventoryType.ANVIL, …), which is left alone (it has no world location).From code:
plugin.anvil().open(player).
Lifecycle
Click handling matches the protocol exactly — vanilla click types (PICKUP, QUICK_MOVE, SWAP, CLONE, THROW, QUICK_CRAFT, PICKUP_ALL) are all routed to dedicated handlers. The session keeps an authoritative copy of the three anvil slots and the cursor; output-slot interactions consume both inputs and charge XP from the player.
VanillaAnvilLogic
VanillaAnvilLogic.apply(AnvilResultEvent) replays Minecraft's AnvilMenu.createResult algorithm:
Rename — costs 1 level when the new name differs from the current display name. Setting a blank name removes any existing custom name (also costs 1).
Same-type combine with damageables — merges durability (
leftRemaining + rightRemaining + 12% of left's max) and costs 2 levels.Enchantment merge — equal levels →
+1, otherwisemax. Per-enchant cost isanvilCost * newLevel, halved (min 1) when the right input is an enchanted book. Incompatible enchants add a+1conflict penalty; if every enchant from the right is rejected, the combine fails. Non-book results are clamped to each enchantment's naturalgetMaxLevel()(soSharpness V + VstaysVon a sword), while enchanted-book combines are allowed to exceed it (V + V → VIon a book); a book enchant above its natural cap is rejected onto a vanilla item.Prior-work surcharge — cost adds
RepairCost(left) + RepairCost(right). On a successful combine the result's RepairCost is bumped to2x + 1(rename-only ops keep it untouched).40-level hard cap — refuses survival takes. Rename-only ops are clamped to 39 instead.
Stacked items push cost to the cap so they can't be anvil-enchanted.
Intentionally NOT modeled — material-based repair (e.g. iron sword + iron ingot). Bukkit doesn't expose vanilla's
Item.isValidRepairItem. Plugins can layer that behavior via a higher-priorityAnvilResultEventlistener.
The logic runs before AnvilResultEvent is dispatched, so listeners always see a populated result + cost and can either tweak the existing values or fully overwrite them.
AnvilListener — the built-in custom-item rules
AnvilListener layers the custom-item rules onto AnvilResultEvent at HIGH priority:
HIGH — enchanted-book combines for custom items
When inputLeft is a custom item and inputRight is a vanilla enchanted book:
Reject anything in
inputRightthat isn't an enchanted book — cancel the combine.Reject custom items without a registered
metadata.available-enchantmentswhitelist.Reject books containing only enchantments not in the whitelist.
Reject books that would push any enchantment past its cap from
available-enchantments.Build the result from
inputLeft.clone()so existing enchantments are preserved, then apply whitelisted book enchantments capped to their max.Re-write the
ItemInstancePDC + refresh lore viaItemService.updateItemStack.Compute the XP cost from
priorWorkPenalty + perLevel * resultLevelper applied enchant (perLevel = max(1, anvilCost / 2)because the right input is a book).
This is the only place where custom-item enchantment limits are enforced — vanilla code paths see a normal anvil and don't know the item has caps. Plugin authors who need to tighten or loosen these rules can listen at a higher priority and overwrite event.result()/event.cost() before this listener runs.
Session cleanup
AnvilListener also cleans up dangling sessions so anvil contents are never lost or duplicated:
On quit (
PlayerQuitEvent) — the session's items are returned to the player / dropped.On death (
PlayerDeathEvent) — the two input items follow vanilla death semantics: routed intoevent.getDrops(), or kept whenkeepInventoryis set. The output slot is excluded.
AnvilResultEvent — extending the anvil
See the event reference for the full field list. Common patterns:
Add a custom recipe:
Override the XP cost:
Cancel a combine entirely by calling event.setCancelled(true).
AnvilSession
AnvilSession exposes the full window state to listeners through AnvilResultEvent.session():
Method | Returns | Use |
|---|---|---|
|
| Read any of the 39 slots ( |
|
| Write a slot. Trigger a follow-up |
|
| Read / write the carried cursor stack. |
|
| The text the player typed in the rename box ( |
|
| Last-computed XP cost. |
Most plugins should mutate state only via event.result(...)/event.cost(...) — those go through the session and the reconciliation step automatically. Direct slot writes are reserved for advanced cases (e.g. consuming an extra ingredient from the player's inventory mirror).