Astral Realms Documentation Help

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 (requires items.command).

  • Right-clicking a vanilla anvil block (ANVIL, CHIPPED_ANVIL, DAMAGED_ANVIL) with EquipmentSlot.HAND is intercepted by AnvilListener. 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 through InventoryOpenEvent (at HIGHEST) and rerouted to the custom anvil. The one exception is a location-less plugin menu UI built with Bukkit.createInventory(holder, InventoryType.ANVIL, …), which is left alone (it has no world location).

  • From code: plugin.anvil().open(player).

Lifecycle

open(player) ──► WrapperPlayServerOpenWindow (id=8, "container.repair") │ ▼ AnvilSession created │ ▼ ┌──────────────────────────────────────────────────────┐ │ Click / Rename / Close packets │ │ → AnvilPacketListener → AnvilService.handleClick/ │ │ handleRename/handleClose │ │ → Bukkit.getScheduler().runTask(...) │ └──────────────────────────────────────────────────────┘ │ ▼ recomputeResult(session) │ ▼ VanillaAnvilLogic.apply(event) │ ▼ AnvilResultEvent fires │ ▼ session.setItemAt(2, result) + session.cost(cost) │ ▼ session.sync() / syncDiff()

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, otherwise max. Per-enchant cost is anvilCost * newLevel, halved (min 1) when the right input is an enchanted book. Incompatible enchants add a +1 conflict penalty; if every enchant from the right is rejected, the combine fails. Non-book results are clamped to each enchantment's natural getMaxLevel() (so Sharpness V + V stays V on a sword), while enchanted-book combines are allowed to exceed it (V + V → VI on 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 to 2x + 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-priority AnvilResultEvent listener.

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:

  1. Reject anything in inputRight that isn't an enchanted book — cancel the combine.

  2. Reject custom items without a registered metadata.available-enchantments whitelist.

  3. Reject books containing only enchantments not in the whitelist.

  4. Reject books that would push any enchantment past its cap from available-enchantments.

  5. Build the result from inputLeft.clone() so existing enchantments are preserved, then apply whitelisted book enchantments capped to their max.

  6. Re-write the ItemInstance PDC + refresh lore via ItemService.updateItemStack.

  7. Compute the XP cost from priorWorkPenalty + perLevel * resultLevel per 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 into event.getDrops(), or kept when keepInventory is 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:

@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onAnvilResult(AnvilResultEvent event) { if (event.inputRight().getType() != Material.NETHER_STAR) return; ItemStack result = event.inputLeft().clone(); result.editMeta(meta -> meta.setCustomModelData(9999)); event.result(result); event.cost(10); }

Override the XP cost:

@EventHandler(priority = EventPriority.MONITOR) public void freeAnvilForVips(AnvilResultEvent event) { if (!event.getPlayer().hasPermission("anvil.free")) return; event.cost(0); }

Cancel a combine entirely by calling event.setCancelled(true).

AnvilSession

AnvilSession exposes the full window state to listeners through AnvilResultEvent.session():

Method

Returns

Use

itemAt(int slot)

ItemStack

Read any of the 39 slots (0 = left input, 1 = right input, 2 = output, 3..38 = player inventory mirror).

setItemAt(int slot, ItemStack)

void

Write a slot. Trigger a follow-up sync() to push to the client.

cursor()/cursor(ItemStack)

ItemStack

Read / write the carried cursor stack.

renameText()

String

The text the player typed in the rename box (null when empty).

cost()/cost(int)

int

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).

Last modified: 25 July 2026