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.
The window is invisible to vanilla's container tracking, so AnvilService also runs a per-tick reconcile pass over every open session — the equivalent of vanilla's AbstractContainerMenu.broadcastChanges. Without it, mutations the server makes outside a click (item pickup, armour durability loss, another plugin writing to the player's inventory) would not reach the client until the next click.
The rule pipeline
AnvilService decides every operation through an ordered list of AnvilRules, consulted in registration order:
Order | Rule | Claims |
|---|---|---|
1 |
| Any operation whose left input is an AstralItems custom item and whose right input is not. |
2 |
| Book-on-book combines, and over-cap books applied to a vanilla item. |
3 |
| Everything else — always claims (terminal). |
AnvilRule.apply(AnvilResultEvent) returns true to claim the operation: the rule fully decided it, either by populating result/cost/materialCost or by cancelling the event. Returning false passes the operation to the next rule untouched. Because the terminal rule always claims, exactly one rule decides each operation before AnvilResultEvent is dispatched to external listeners — so listeners always see a populated result and can tweak or overwrite it.
VanillaAnvilRule — genuine vanilla via headless NMS
The terminal rule delegates to NmsAnvilLogic, which no longer replays vanilla's algorithm by hand. It builds a detached NMS AnvilMenu — never registered as the player's container, never sent to the client — fills its input slots, and reads slot 2, the level cost, and the material count back through the Bukkit AnvilView.
Two consequences matter:
The result is real vanilla behaviour, including material-based repair (iron sword + iron ingot), which the old hand-rolled logic explicitly did not model.
CraftBukkit's
PrepareAnvilEventfires, so external plugins hook the custom anvil exactly as they would a vanilla one — CraftEngine's custom-material repair and its anvil-rename sanitizing among them.
NmsAnvilLogic.MAX_ANVIL_COST is 1500, pushed into the menu via AnvilView#setMaximumRepairCost (vanilla's own field defaults to 40). The old 40-level ceiling is therefore gone on the server side.
The server-side gate on taking a result is the same in both directions: creative bypasses it, otherwise the player needs at least cost levels and cost < 1500.
CustomItemRule — custom-item rules
Claims unconditionally when inputLeft is a custom item and inputRight is not, so neither vanilla logic nor PrepareAnvilEvent listeners ever touch custom items. Combining two custom items is left unclaimed and falls through to the vanilla rule.
Cancel with cost
0when the operation is a real rename attempt, or wheninputRightis empty. Custom items cannot be renamed in either direction (new name or blank-out).Cancel when
inputRightis anything other than an enchanted book.Cancel when the blueprint declares no
metadata.available-enchantmentswhitelist.Cancel when the book contains no enchantment the whitelist lists.
Strip every enchantment the whitelist does not list from the result, then merge the book's whitelisted enchantments — equal levels combine into
level + 1, otherwise the higher of the two wins. A merged level above the whitelist cap cancels the combine outright; it is never clamped.Re-write the
ItemInstancePDC and refresh lore viaItemService.updateItemStack.Bump the result's prior-work cost to
max(resultCost, bookCost) * 2 + 1, like a vanilla combine.Compute the XP cost as the target's prior-work cost plus, per applied enchantment,
max(1, anvilCost / 2) * resultLevel(halved because the sacrifice is a book), floored at1so the result slot stays pickup-able.
Because the whitelist caps are the blueprint's own, they may legitimately exceed vanilla's natural maxima — efficiency: 7 on a hammer is the point of the system.
EnchantedBookRule — the two deliberate deviations
Runs only when inputRight is an enchanted book carrying stored enchantments.
Book + book combines merge unclamped. Equal levels stack past the natural cap (
Efficiency V + V → VIon a book), where vanilla clamps. This is how over-cap books are crafted for blueprints that allow them. Conflicting enchantments are skipped and add a+1penalty each, exactly like vanilla; if every enchantment from the right book is rejected the combine is cancelled, and if none applies the rule passes on. A rename rides along for+1level. The total (priorWork(left) + priorWork(right) + operationCost) is cancelled when it reaches 1500 outside creative.An over-cap book on a vanilla target is rejected outright, instead of vanilla's silent clamp — so a player cannot burn an
Efficiency VIIbook for anEfficiency Vresult.
In-cap books on vanilla targets are left unclaimed and handled by pure vanilla. Custom-item targets never reach here; CustomItemRule runs first and applies the blueprint's own caps.
Skins are not applied in the anvil
CustomItemRule used to apply a skin item placed in the right slot. That branch is gone. It painted the model onto the result stack without ever writing the skin data back to the instance, so the item wore a skin the instance knew nothing about: invisible to removal, and silently lost on the next rebuild. A skin item in the right slot is now simply not an enchanted book, so the combine is cancelled at step 2 like any other invalid sacrifice.
Skins are applied by holding the skin item on the cursor and clicking the item, or with /items set-skin — both persist the data. Items skinned by the old anvil path are repaired automatically the next time they are upgraded; see Skins › Orphan skins.
Session cleanup
AnvilListener routes players into the anvil and forwards lifecycle events; AnvilService does the work, so anvil contents are never lost or duplicated:
On close (
handleClose) — cursor and both input slots are returned to the player, overflow dropped. The output slot is never returned.On quit (
PlayerQuitEvent) — same, synchronously, so the mutations land before the player is saved.On death (
PlayerDeathEvent) — the cursor and the two input items follow vanilla death semantics: routed intoevent.getDrops(), or kept whenkeepInventoryis honoured. 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).
Partial consumption of the right slot
AnvilResultEvent.materialCost() controls how much of the right input is consumed when the result is taken:
0(the default) — the entire right slot is consumed. This is the normal combine / book / rename behaviour.a positive value — only that many items are removed and the remainder stays in the slot. This is what material-based repair uses, where a tool consumes only the ingots it actually needed.
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 in the rename box. |
|
| Last-computed XP cost. |
|
| How much of the right slot the take will consume (see above). |
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).