docs/paper/inventory/SKILL.md
# Inventory Skill — Paper ## Purpose Reference this skill when building custom clickable inventory GUIs (chest menus, shop UIs, settings screens) in Paper 1.21. Covers inventory creation, `InventoryHolder` pattern, and event-driven click handling. ## When to Use This Skill - Creating a chest-style menu that players open with a command - Building a shop or upgrade UI where clicks perform actions - Handling `InventoryClickEvent` to route button clicks to plugin logic - Preventing players from ta
npx skillsauth add MrPippi/MPS docs/paper/inventoryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference this skill when building custom clickable inventory GUIs (chest menus, shop UIs, settings screens) in Paper 1.21. Covers inventory creation, InventoryHolder pattern, and event-driven click handling.
InventoryClickEvent to route button clicks to plugin logic| Class / Method | Purpose | Notes |
|---------------|---------|-------|
| Bukkit.createInventory(holder, size, title) | Create a custom inventory | size must be multiple of 9 (9–54) |
| Inventory#setItem(slot, ItemStack) | Place an item in a slot | Slots 0–53 for double chest |
| Player#openInventory(Inventory) | Open for a player | Main thread only |
| InventoryHolder | Tag interface to identify plugin inventories | Implement on your GUI class |
| InventoryClickEvent | Fires when any slot is clicked | Check event.getInventory() |
| InventoryOpenEvent | Fires when inventory is opened | |
| InventoryCloseEvent | Fires when inventory is closed | |
| event.setCancelled(true) | Prevent item movement | Use in GUI click handler |
| event.getSlot() | Clicked slot index | Top inventory slots only |
| event.getClick() | ClickType (LEFT, RIGHT, SHIFT_LEFT…) | |
| event.getWhoClicked() | HumanEntity (cast to Player) | |
package com.yourorg.myplugin.gui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.List;
// --- 1. Inventory Holder: identifies this as a plugin GUI ---
public class MainMenuGui implements InventoryHolder {
private final Inventory inventory;
public MainMenuGui() {
// 27-slot (3-row) chest with Adventure title
this.inventory = Bukkit.createInventory(this, 27,
Component.text("Main Menu").color(NamedTextColor.DARK_AQUA));
buildContents();
}
private void buildContents() {
// Fill background with grey glass panes
ItemStack filler = makeButton(Material.GRAY_STAINED_GLASS_PANE, " ", List.of());
for (int i = 0; i < inventory.getSize(); i++) {
inventory.setItem(i, filler);
}
// Shop button — slot 11
inventory.setItem(11, makeButton(
Material.GOLD_INGOT,
"Shop",
List.of(Component.text("Click to open the shop.").color(NamedTextColor.GRAY))
));
// Stats button — slot 13
inventory.setItem(13, makeButton(
Material.BOOK,
"Stats",
List.of(Component.text("View your stats.").color(NamedTextColor.GRAY))
));
// Close button — slot 15
inventory.setItem(15, makeButton(
Material.BARRIER,
"Close",
List.of(Component.text("Close this menu.").color(NamedTextColor.RED))
));
}
private ItemStack makeButton(Material mat, String name, List<Component> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
meta.displayName(Component.text(name)
.color(NamedTextColor.WHITE)
.decoration(TextDecoration.ITALIC, false));
meta.lore(lore.stream()
.map(c -> c.decoration(TextDecoration.ITALIC, false))
.toList());
item.setItemMeta(meta);
return item;
}
public void open(Player player) {
player.openInventory(inventory);
}
@Override
public @NotNull Inventory getInventory() {
return inventory;
}
}
// --- 2. Event Listener: handles all GUI clicks ---
class GuiListener implements Listener {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
// Only handle clicks inside MainMenuGui inventories
if (!(event.getInventory().getHolder() instanceof MainMenuGui gui)) return;
// Always cancel to prevent item theft
event.setCancelled(true);
// Ignore clicks on the player's own bottom inventory
if (event.getClickedInventory() != event.getInventory()) return;
if (!(event.getWhoClicked() instanceof Player player)) return;
switch (event.getSlot()) {
case 11 -> player.performCommand("shop");
case 13 -> player.performCommand("stats");
case 15 -> player.closeInventory();
}
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (event.getInventory().getHolder() instanceof MainMenuGui) {
// Clean up resources if needed when GUI is closed
}
}
}
Not cancelling InventoryClickEvent: Without event.setCancelled(true), players can pick up the glass-pane filler items or swap them with their own inventory.
Not checking getClickedInventory(): InventoryClickEvent fires for BOTH the top (GUI) inventory and the player's bottom inventory. Filter by event.getClickedInventory() == event.getInventory() to only handle GUI slots.
Creating a new Inventory object per click: Calling Bukkit.createInventory() on every click creates thousands of objects. Create the inventory once (in the constructor or open()) and reuse it.
Identifying GUIs by title string: Titles are user-visible and can collide with other plugins. Always use InventoryHolder (instanceof check) to identify your GUIs reliably.
Updating items while the GUI is open: Use inventory.setItem(slot, newItem) to update live — the client sees the change without reopening.
Bukkit.createInventory(holder, size, Component title) is the standard Adventure-title overload. The String overload is deprecated.development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag