Project: Shop menu
A config-driven chest shop with categories, prices in XP levels and a confirm screen.
In this project you build a chest shop. Players type /shop, pick a category, click an item and confirm the purchase. Prices are paid in experience levels, and every item and price comes from config.yml, so the server owner can change the shop without touching any Java.
What you will build
Shops are one of the first things a server owner asks for, and a chest menu is the friendliest way to show one. This project puts several earlier chapters to work in one plugin: chest menus, config files, items, inventories and events. You pay with the player's experience levels (the green number above the hotbar). That way the plugin needs no other money plugin, and a player earns "money" just by mining, smelting and killing mobs.
The shop has three screens. First the player sees the categories:
Clicking a category opens its items, each with the amount you get and the price:
Clicking an item opens a confirm screen, so nobody spends levels by accident:
After a click on Buy the player gets a chat message, and the shop goes back to the item list:
If the player is short of levels, or the backpack is full, the shop says so and takes nothing:
Your inventory has no room for that. Make some space and try again.
These are the rules the plugin follows:
| Rule | How the plugin keeps it |
|---|---|
The shop's contents live in config.yml | A loader turns the file into small data objects when the plugin starts and on /shop reload |
| Nobody can take items out of the menu | One listener cancels every click and drag in a shop window |
| Nothing is bought by accident | A confirm screen sits between the click and the payment |
| Nobody pays for items they cannot carry | The plugin checks for room before it takes any levels |
| A typo in the config does not break the shop | The loader skips bad entries and writes a warning in the console |
The plan
The shop is a small chain of screens, and only the last click does anything that matters. This picture shows the path, and the two checks that happen before a purchase:
Every piece of the plan is one small class. This table shows which class does what, and where to read more about the idea behind it:
| Requirement | The piece that does it | Chapter |
|---|---|---|
| Items and prices in a file | ShopCatalog reads config.yml into ShopCategory and ShopItem records | Configuration files, Enums, records and modern Java |
| A window with buttons | Menu, an InventoryHolder that remembers what each slot does | Chest menus |
| Items cannot be stolen | MenuListener cancels the click events | Priorities and canceling |
| Three screens | CategoryMenu, ItemsMenu, ConfirmMenu | Chest menus |
| Paying and receiving | Shop.buy: levels, room check, addItem | Players, Inventories |
| Open it with a command | ShopCommand, a BasicCommand | Commands, part 1 |
The build steps are:
Write plugin.yml and config.yml
Decide what the server owner can change before writing any Java.
Turn the config into records
Two tiny data classes and a loader that checks every entry.
Build the menu base and protect the items
A holder class, plus the listener that stops every kind of item theft.
Make the item icons
One helper that builds buttons, and a clear line between the icon and the real goods.
Build the category and items screens
Buttons that open the next screen.
Build the confirm screen and the purchase
The check for levels, the check for room, the payment and the messages.
Add /shop and register everything
The command, the main class, and a testing checklist.
This page reuses the menu design from Chest menus and explains it more briefly. If the words holder or slot feel new, read that chapter first.
Step 1: write plugin.yml and config.yml
Create a project the same way as in the earlier projects (copy the starter, or run new-plugin.cmd -Name ShopGui from the paper-templates folder) with the package com.example.projectshopgui. The plugin.yml declares two permissions:
Where this file livesproject-shop-guisrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: ShopGui2version: '1.0.0'3main: com.example.projectshopgui.ShopPlugin4api-version: '26.3'5description: A chest shop with categories, a confirm screen and prices paid in experience levels.6permissions:7 shopgui.use:8 description: Lets a player open the shop with /shop.9 default: true10 shopgui.reload:11 description: Lets a player reload the shop prices with /shop reload.12 default: op- Lets a player open the shop. It defaults to
true, which means everyone has it, because a shop is for everybody. - Lets a player reload the prices.
opmeans only server operators have it, because changing the shop is an admin job.
Now the most important file: config.yml. Everything a server owner may want to change goes here: the menu title, the messages, and the whole shop. Read the file from the top. The shop is a tree of categories, and every category holds items:
Where this file livesproject-shop-guisrcmainresourcesconfig.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlyou are hereDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1menu-title: "<dark_green><bold>Shop"2 3messages:4 bought: "<green>You bought <white><amount>x <item></white> for <yellow><price> levels</yellow>."5 not-enough-levels: "<red>You need <yellow><price> levels</yellow>, but you only have <yellow><have></yellow>."6 inventory-full: "<red>Your inventory has no room for that. Make some space and try again."7 reloaded: "<green>The shop was reloaded: <white><categories></white> categories, <white><items></white> items."8 9categories:10 building:11 icon: BRICKS12 name: "<gold>Building blocks"13 items:14 oak-logs:15 material: OAK_LOG16 amount: 1617 price: 2- The title of the first screen. It is written in MiniMessage, so owners can pick their own colors. The text is in quotes because it contains
<characters. - A message with placeholders.
<amount>,<item>and<price>are filled in by the plugin, as in the Heal and Feed project. - The top of the shop. Each name below it, such as
building, is one category. The name is only an id for the plugin; players never see it. - The block that represents this category in the first screen. Any item name from the
Materiallist works. - What the category is called in the menu, again in MiniMessage.
- One item in the shop. Using a named section for each item, instead of a list, makes the file easy to read and lets us report mistakes by name.
- What the player receives. The loader checks that this name really exists.
- How many the player gets for one purchase.
- The cost in experience levels. It must be 1 or more.
The rest of the file has two more categories, food and tools, built the same way. An item can also have an optional name, like "<aqua>Hero Sword", which the player sees on the item itself.
Step 2: turn the config into records
You could read getInt("categories.food.items.steak.price") every time a player opens the shop, but the code would be full of long text paths, and a typo would only show up when a player clicks that exact button. A better design reads the file once, checks it, and turns it into small Java objects. The objects are records: classes whose only job is to hold a few values. Java writes the constructor and the getter methods for you.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopItem.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javayou are hereRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3import org.bukkit.Material;4 5public record ShopItem(String id, Material material, int amount, int price, String customName) {6}- A record with five values. After
new ShopItem(...)you read them withitem.price(),item.amount()and so on. A record never changes after it is created, which makes it safe to share between menus. - What the player receives.
Materialis the list of every block and item in the game. - The optional display name. It is
nullwhen the config does not give one.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCategory.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javayou are hereRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3import java.util.List;4import org.bukkit.Material;5 6public record ShopCategory(String id, Material icon, String name, List<ShopItem> items) {7}- A category owns its list of items, so a menu only needs the category to draw itself.
The loader is the longest class of the plugin, and the most important one for beginners to understand: it is the place where untrusted text meets your code. A server owner can type anything into config.yml. The loader reads it with suspicion and skips bad entries instead of crashing. This is its main method:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCatalog.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javayou are hereHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21public static ShopCatalog load(FileConfiguration config, Logger logger) {22 List<ShopCategory> categories = new ArrayList<>();23 ConfigurationSection section = config.getConfigurationSection("categories");24 if (section == null) {25 logger.warning("config.yml has no 'categories' section, so the shop is empty.");26 return new ShopCatalog(categories);27 }28 for (String categoryId : section.getKeys(false)) {29 if (categories.size() == MAX_CATEGORIES) {30 logger.warning("The shop shows at most " + MAX_CATEGORIES + " categories. Skipping '" + categoryId + "'.");31 continue;32 }33 ConfigurationSection categorySection = section.getConfigurationSection(categoryId);34 ShopCategory category = readCategory(categoryId, categorySection, logger);35 if (category != null) {36 categories.add(category);37 }38 }39 return new ShopCatalog(List.copyOf(categories));40}- A new empty list that we fill while reading.
- A
configuration section is one indented block of the YAML file. This asks for the block calledcategories. It returnsnullwhen the block is missing, so the next lines check for that. - Gives the names directly under
categories:building,food,tools. Thefalsemeans "only the direct children, do not go deeper". - A chest screen has only seven free slots for categories. Extra ones are skipped with a warning, instead of crashing later with an index error.
- Writes a yellow warning line into the server console. The server owner sees exactly what to fix.
- The helper returns
nullfor a broken category. We only keep the good ones. - Makes a list that can never be changed again, so no other class can modify the shop by mistake.
Reading one item is where the checks happen. Every line below protects the server from one specific mistake an owner might make:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCatalog.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javayou are hereHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
70private static ShopItem readItem(String categoryId, String itemId, ConfigurationSection section, Logger logger) {71 String where = categoryId + "." + itemId;72 if (section == null) {73 logger.warning("Item '" + where + "' is not a section. Skipping it.");74 return null;75 }76 Material material = readMaterial(section.getString("material"));77 if (material == null) {78 logger.warning("Item '" + where + "' has a missing or unknown material. Skipping it.");79 return null;80 }81 int amount = Math.clamp(section.getInt("amount", 1), 1, material.getMaxStackSize());82 int price = section.getInt("price", -1);83 if (price < 1) {84 logger.warning("Item '" + where + "' needs a price of at least 1. Skipping it.");85 return null;86 }87 return new ShopItem(itemId, material, amount, price, section.getString("name"));88}- A short label such as
food.steak, used in every warning so the owner knows where to look. - Turns the text
OAK_LOGinto aMaterial, ornullif there is no such material. - Forces the amount to stay between 1 and the largest stack of that material. A stack of 99 swords does not exist, and a stack of 0 would give nothing.
- Most blocks stack to 64, ender pearls to 16, swords to 1. Asking the material avoids guessing.
- Reads the price. The second value,
-1, is what you get when the setting is missing. A missing price therefore looks like an invalid one. - Rejects zero and negative prices. A negative price would hand out levels for free, and a price of 0 gives items away.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCatalog.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javayou are hereHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
90private static Material readMaterial(String text) {91 if (text == null) {92 return null;93 }94 Material material = Material.matchMaterial(text);95 if (material == null || !material.isItem() || material.isAir()) {96 return null;97 }98 return material;99}- Looks a name up in the list of materials. It accepts
OAK_LOG,oak_logand evenminecraft:oak_log, and returnsnullfor anything unknown. - Some materials exist only as blocks, such as
WATERorFIRE. A player can never hold them, so the shop must not sell them. - Air is a material too, but a stack of air is nothing at all, so it is rejected on purpose.
Step 3: build the menu base and protect the items
Now the window. The base class is the same idea as in Chest menus: a holder is an object that owns a chest window. Because every shop screen is a Menu, our code can later ask any open window "who owns you?" and know at once that it is a shop screen.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiMenu.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javayou are hereMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3import java.util.HashMap;4import java.util.Map;5import java.util.function.Consumer;6import net.kyori.adventure.text.Component;7import org.bukkit.Bukkit;8import org.bukkit.Sound;9import org.bukkit.entity.Player;10import org.bukkit.event.inventory.InventoryClickEvent;11import org.bukkit.inventory.Inventory;12import org.bukkit.inventory.InventoryHolder;13import org.bukkit.inventory.ItemStack;14 15public abstract class Menu implements InventoryHolder {16 17 private final Inventory inventory;18 private final Map<Integer, Consumer<Player>> actions = new HashMap<>();19 20 protected Menu(int rows, Component title) {21 this.inventory = Bukkit.createInventory(this, rows * 9, title);22 }23 24 protected abstract void render(Player viewer);25 26 public final void open(Player viewer) {27 redraw(viewer);28 viewer.openInventory(inventory);29 }30 31 protected final void redraw(Player viewer) {32 inventory.clear();33 actions.clear();34 render(viewer);35 }36 37 protected final void button(int slot, ItemStack icon, Consumer<Player> action) {38 inventory.setItem(slot, icon);39 actions.put(slot, action);40 }41 42 protected final void icon(int slot, ItemStack icon) {43 inventory.setItem(slot, icon);44 }45 46 protected final void fillEmpty() {47 for (int slot = 0; slot < inventory.getSize(); slot++) {48 if (inventory.getItem(slot) == null) {49 inventory.setItem(slot, Icons.filler());50 }51 }52 }53 54 final void handleClick(InventoryClickEvent event) {55 Consumer<Player> action = actions.get(event.getSlot());56 if (action != null && event.getWhoClicked() instanceof Player player) {57 player.playSound(player, Sound.UI_BUTTON_CLICK, 0.6f, 1.0f);58 action.accept(player);59 }60 }61 62 @Override63 public final Inventory getInventory() {64 return inventory;65 }66}- This marks the class as the owner of a window. The listener will use it to tell shop windows from normal chests.
- A notebook: slot number on the left, the code to run when that slot is clicked on the right. A
Consumer<Player>is a piece of code that takes a player and returns nothing. - Creates the empty chest window. The first argument,
this, makes this menu the window's holder. Nine slots per row. - Every screen fills itself in its own way.
abstractmeans "each subclass must write this method". - Shows the window to the player.
- Puts an icon in a slot and remembers what the slot does. Compare with
iconbelow, which only displays something. - Fills every empty slot with a gray glass pane, which makes the screen look tidy.
- Looks up the action for the clicked slot. Slots with no action, such as the glass panes, do nothing.
- The click event says who clicked as a
HumanEntity. This check converts it to aPlayer, which has the methods we need.
The menu does nothing yet, because nobody calls handleClick. That is the listener's job. It is also the plugin's theft protection, so read it carefully:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiMenuListener.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javayou are hereListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.EventPriority;5import org.bukkit.event.Listener;6import org.bukkit.event.inventory.InventoryClickEvent;7import org.bukkit.event.inventory.InventoryDragEvent;8 9public final class MenuListener implements Listener {10 11 @EventHandler(priority = EventPriority.HIGH)12 public void onClick(InventoryClickEvent event) {13 if (!(event.getInventory().getHolder(false) instanceof Menu menu)) {14 return;15 }16 event.setCancelled(true);17 if (event.getClickedInventory() == event.getView().getTopInventory()) {18 menu.handleClick(event);19 }20 }21 22 @EventHandler(priority = EventPriority.HIGH)23 public void onDrag(InventoryDragEvent event) {24 if (event.getInventory().getHolder(false) instanceof Menu) {25 event.setCancelled(true);26 }27 }28}- Runs late, after most other plugins have had their say, so our cancel is not overruled. See Priorities and canceling.
- Asks the window who owns it. The
falsemeans "do not make a snapshot of the block", which is faster and exactly what we want. - True only for our shop windows. Chests, furnaces and other plugins' menus return here, untouched.
- Cancels every click inside a shop window, no matter what kind of click. This single line is the whole protection.
- A click can land in the shop window (top) or in the player's own backpack (bottom). Only a click on the top window is a button press.
- Dragging a stack across several slots is a different event. It has to be canceled too.
Why cancel every click, even those in the player's own backpack? Because players have many ways to move items, and most of them look harmless:
| What the player does | What would happen without the cancel |
|---|---|
| Picks up an icon with the mouse | The icon, with its price in the lore, ends up in their backpack |
| Shift-clicks an item in their backpack | It jumps into the shop window and is lost, or fills the menu |
| Presses a number key over an icon | The icon swaps with the item in that hotbar slot |
| Drags a stack across the window | Items are spread out into the menu (this is the drag event) |
| Double-clicks to gather matching items | Icons are collected onto the cursor |
| Clones an icon in creative mode | Free copies. Creative clicks arrive as InventoryCreativeEvent, a kind of InventoryClickEvent, so the same handler catches them |
Step 4: make the item icons
All screens build their icons with one helper class, Icons. Most of it is the button builder from the Chest menus chapter: an item with a name and lore. The two methods that are new are the ones about shop goods:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiIcons.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javayou are hereHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
34static ItemStack goods(ShopItem item) {35 ItemStack stack = ItemStack.of(item.material(), item.amount());36 if (item.customName() != null) {37 stack.editMeta(meta -> meta.displayName(text(item.customName())));38 }39 return stack;40}- Creates a brand new stack of the right size. Nothing is shared with any other stack.
- Only rename the stack when the config gave a name.
- Sets the name the player sees.
textturns MiniMessage into a component and switches off Minecraft's default italics for renamed items.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiIcons.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javayou are hereHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
42static ItemStack display(ShopItem item, String... lore) {43 ItemStack icon = goods(item);44 icon.editMeta(meta -> meta.lore(Arrays.stream(lore).map(Icons::text).toList()));45 return icon;46}- Starts from a fresh stack of the goods, so the icon looks exactly like what the player will get.
- Adds the price and click hint under the name. This lore belongs to the icon only.
The shop deliberately has two methods that build almost the same stack. goods is the real thing a player receives. display is a copy with extra lore, used only in menus. Handing out the display stack would give players an Oak Log that says "Click to buy" forever, and it would not stack with a normal Oak Log. Each call to goods or display builds a new stack, so the icon in the menu and the item in the backpack never become the same object.
Step 5: build the category and items screens
With the base class ready, a screen is just a render method. This is the first screen, the list of categories:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiCategoryMenu.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javayou are hereHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22protected void render(Player viewer) {23 icon(LEVELS_SLOT, Icons.button(Material.EXPERIENCE_BOTTLE,24 "<green>Your levels: <white>" + viewer.getLevel(),25 "<gray>Everything here is paid in",26 "<gray>experience levels."));27 28 List<ShopCategory> categories = shop.catalog().categories();29 if (categories.isEmpty()) {30 icon(EMPTY_SLOT, Icons.button(Material.STRUCTURE_VOID, "<gray>The shop is empty"));31 }32 for (int index = 0; index < categories.size(); index++) {33 ShopCategory category = categories.get(index);34 button(CATEGORY_SLOTS[index],35 Icons.button(category.icon(), category.name(),36 "<gray>" + category.items().size() + " items",37 "",38 "<yellow>Click to browse"),39 player -> new ItemsMenu(shop, category).open(player));40 }41 42 button(CLOSE_SLOT, Icons.button(Material.BARRIER, "<red>Close"), Player::closeInventory);43 fillEmpty();44}- A button-less icon at the top that shows how many levels the player has. It is drawn each time the screen is opened, so it is always current.
- The player's current experience level, the green number above the hotbar.
- Asks the shop for the list of categories that the loader built.
- If the config has no usable categories, show a clear sign instead of a blank window.
- Category number 0 goes to slot 10, number 1 to slot 11, and so on. The array lists the seven slots in the middle row.
- This tiny function is the button's action. Clicking the category creates the items screen for that category and opens it. The arrow
->makes it alambda . - A method reference: "call
closeInventoryon the player who clicked". It does the same asplayer -> player.closeInventory().
The items screen has the same shape. Each item becomes one button, in slots 0 to 26, and the bottom row holds the Back button:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiItemsMenu.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22protected void render(Player viewer) {23 List<ShopItem> items = category.items();24 for (int slot = 0; slot < items.size(); slot++) {25 ShopItem item = items.get(slot);26 button(slot,27 Icons.display(item,28 "<gray>Amount: <white>" + item.amount(),29 "<gray>Price: <yellow>" + item.price() + " levels",30 "",31 "<yellow>Click to buy"),32 player -> new ConfirmMenu(shop, category, item).open(player));33 }34 35 button(BACK_SLOT, Icons.button(Material.ARROW, "<yellow>Back", "<gray>Return to the categories"),36 player -> new CategoryMenu(shop).open(player));37 icon(LEVELS_SLOT, Icons.button(Material.EXPERIENCE_BOTTLE, "<green>Your levels: <white>" + viewer.getLevel()));38 fillEmpty();39}- The loader limits a category to 27 items, so the item buttons fit in the first three rows and never reach the Back button in the bottom row.
- The icon looks like the real item, with the amount, the price and a hint written in its lore.
- Clicking an item does not buy it. It opens the confirm screen and passes along everything that screen needs: the shop, the category (so Cancel can go back) and the item.
- The Back arrow opens a new category screen.
Step 6: build the confirm screen and the purchase
The confirm screen shows the item once more, the price, and two big buttons. Its purchase method runs when the player presses Buy:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiConfirmMenu.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javayou are hereHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
24@Override25protected void render(Player viewer) {26 icon(GOODS_SLOT, Icons.display(item, "<gray>Price: <yellow>" + item.price() + " levels"));27 28 button(BUY_SLOT, Icons.button(Material.LIME_CONCRETE, "<green><bold>Buy",29 "<gray>Pay <yellow>" + item.price() + " levels",30 "<gray>You have <white>" + viewer.getLevel() + " levels"),31 this::purchase);32 33 button(CANCEL_SLOT, Icons.button(Material.RED_CONCRETE, "<red><bold>Cancel", "<gray>Go back without buying"),34 player -> new ItemsMenu(shop, category).open(player));35 36 fillEmpty();37}- The item in the middle is only a picture, so it uses
icon, notbutton: clicking it does nothing. - Green means yes. The color is a good signal, because players read buttons by color before text.
- A method reference: when the Buy button is clicked, run this class's
purchasemethod. - Cancel opens the items screen again, without paying anything.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiConfirmMenu.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javayou are hereHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
39private void purchase(Player player) {40 PurchaseResult result = shop.buy(player, item);41 shop.tell(player, item, result);42 Sound sound = result == PurchaseResult.BOUGHT ? Sound.ENTITY_EXPERIENCE_ORB_PICKUP : Sound.ENTITY_VILLAGER_NO;43 player.playSound(player, sound, 1.0f, 1.0f);44 new ItemsMenu(shop, category).open(player);45}- Does all the work and returns one of three results. The menu never touches levels or inventories itself.
- Sends the right message for that result.
- A short
ifinside a line: pick the happy sound after a purchase, the "no" sound of a villager after a failure. - Whatever happened, the player returns to the item list, with the level display updated.
The result type is a three-value enum. Using an enum instead of true and false lets one method report why something failed:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiPurchaseResult.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javayou are hereEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3public enum PurchaseResult {4 BOUGHT,5 NOT_ENOUGH_LEVELS,6 INVENTORY_FULL7}- The purchase worked.
- Stopped at the first check: the player is too poor.
- Stopped at the second check: nothing would fit in the backpack.
Now the heart of the shop, the buy method. The order of the lines is the whole trick: check everything first, change things last. If we took the levels before checking for room, a player with a full backpack would lose levels and get nothing.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShop.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javayou are hereHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
42public PurchaseResult buy(Player player, ShopItem item) {43 if (player.getLevel() < item.price()) {44 return PurchaseResult.NOT_ENOUGH_LEVELS;45 }46 ItemStack goods = Icons.goods(item);47 if (!hasRoomFor(player.getInventory(), goods)) {48 return PurchaseResult.INVENTORY_FULL;49 }50 player.setLevel(player.getLevel() - item.price());51 player.getInventory().addItem(goods);52 return PurchaseResult.BOUGHT;53}- Check 1: can the player afford it? If not, return at once. Nothing has been changed.
- Builds the real stack that the player would receive.
- Check 2: would the whole stack fit? The method is below. Again, nothing changed yet.
- Only now do we take the payment. The level count drops by the price.
- Puts the items in the backpack. We already know they fit, so nothing is left over.
You might ask: why not just call addItem and look at what comes back? addItem returns the items that did not fit, but by then some of the stack is already in the backpack. Undoing a half-delivered purchase is harder than checking first. So we count the room ourselves:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShop.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javayou are hereHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
68private static boolean hasRoomFor(PlayerInventory inventory, ItemStack goods) {69 int room = 0;70 for (ItemStack slot : inventory.getStorageContents()) {71 if (slot == null || slot.isEmpty()) {72 room += goods.getMaxStackSize();73 } else if (slot.isSimilar(goods)) {74 room += Math.max(0, slot.getMaxStackSize() - slot.getAmount());75 }76 }77 return room >= goods.getAmount();78}- The 36 slots of the main backpack and hotbar. Armor and the off hand are not included, because items from
addItemnever go there. - An empty slot can hold a whole stack of the goods.
- A slot with the same kind of item, with the same name and data, can take more until it reaches its limit.
- How many more fit on top of the stack.
Math.max(0, ...)protects against a stack that is somehow over the limit. - The purchase is possible only if the total room is at least the amount we want to hand over.
The messages come from the config. The tell method chooses the right one with a switch over the enum:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShop.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javayou are hereHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
55public void tell(Player player, ShopItem item, PurchaseResult result) {56 switch (result) {57 case BOUGHT -> send(player, "bought",58 Placeholder.unparsed("amount", String.valueOf(item.amount())),59 Placeholder.component("item", Icons.nameOf(item)),60 Placeholder.unparsed("price", String.valueOf(item.price())));61 case NOT_ENOUGH_LEVELS -> send(player, "not-enough-levels",62 Placeholder.unparsed("price", String.valueOf(item.price())),63 Placeholder.unparsed("have", String.valueOf(player.getLevel())));64 case INVENTORY_FULL -> send(player, "inventory-full");65 }66}- Picks one branch for each enum value. The arrow form
case X ->runs only that branch, with no fall-through. Note that aswitchstatement like this one does not force you to cover every value, so when you add a fourth result later, remember to add its branch here too. - Fills
<amount>in the message. "Unparsed" means the text is inserted as plain text, so it cannot be used to sneak in formatting tags. - The item name is a full component, because a name can have colors.
nameOfreads the custom name, or the item's normal name in the player's own language.
Step 7: add /shop and register everything
The command is a BasicCommand, the simple kind from Commands, part 1. It opens the first screen, and an admin can add the word reload to re-read the config:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCommand.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javayou are hereCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
19@Override20public void execute(CommandSourceStack source, String[] args) {21 CommandSender sender = source.getSender();22 if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {23 reload(sender);24 return;25 }26 if (!(source.getExecutor() instanceof Player player)) {27 sender.sendRichMessage("<red>Only players can open the shop.");28 return;29 }30 new CategoryMenu(shop).open(player);31}- The player typed
/shop reload. The first condition makes sure there is an argument before we read it, because readingargs[0]on an empty array crashes. - Only a player can have a chest window open. The console gets a short message instead.
- Builds the first screen for this player and shows it.
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopCommand.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javayou are hereCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
33private void reload(CommandSender sender) {34 if (!sender.hasPermission("shopgui.reload")) {35 sender.sendRichMessage("<red>You may not reload the shop.");36 return;37 }38 shop.reload();39 shop.send(sender, "reloaded",40 Placeholder.unparsed("categories", String.valueOf(shop.catalog().categories().size())),41 Placeholder.unparsed("items", String.valueOf(shop.catalog().itemCount())));42}- The normal command permission only lets players in. Reloading needs this stronger one, checked by hand.
- Reads
config.ymlfrom disk again and rebuilds the records. Screens that are already open keep the old prices until they are reopened. - Tells the admin how many categories and items were loaded, so a typo that skipped half the shop is obvious right away.
And the main class ties the pieces together. It is short, because every class has just one job:
Where this file livesproject-shop-guisrcmainjavacomexampleprojectshopguiShopPlugin.java
The package com.example.projectshopgui is the folder path com/example/projectshopgui inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui/
- src/main/
- java/com/example/projectshopgui/Package com.example.projectshopgui
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javaHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopgui/Package com.example.projectshopgui
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectshopgui;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class ShopPlugin extends JavaPlugin {6 7 @Override8 public void onEnable() {9 saveDefaultConfig();10 Shop shop = new Shop(this);11 getServer().getPluginManager().registerEvents(new MenuListener(), this);12 registerCommand("shop", "Open the shop", new ShopCommand(shop));13 getLogger().info("Shop ready: " + shop.catalog().categories().size() + " categories, " + shop.catalog().itemCount() + " items.");14 }15}- Copies
config.ymlfrom the jar intoplugins/ShopGui/the first time the plugin runs. Later starts keep the owner's edits. - Creates the shop, which loads the config as it is created. All screens share this one object.
- Without this line nothing is protected: a listener that was never registered is never called.
- Registers
/shop. The permission comes from the command's ownpermission()method. - Prints how many categories and items were loaded. This line is the first thing to check in the console when a shop looks empty.
- project-shop-gui/
- src/
- main/
- java/
- com/example/projectshopgui/
- ShopPlugin.javaMain class: loads the shop, registers the listener and /shop
- Shop.javaThe one place that charges levels, checks room and sends messages
- ShopCatalog.javaReads and checks config.yml
- ShopCategory.javaRecord: a category with its items
- ShopItem.javaRecord: one thing for sale
- PurchaseResult.javaEnum: BOUGHT, NOT_ENOUGH_LEVELS or INVENTORY_FULL
- Menu.javaBase class: a holder with buttons and actions
- MenuListener.javaCancels every click and drag in a shop window
- Icons.javaBuilds buttons, goods and display stacks
- CategoryMenu.javaScreen 1: the categories
- ItemsMenu.javaScreen 2: the items of one category
- ConfirmMenu.javaScreen 3: Buy or Cancel
- ShopCommand.java/shop and /shop reload
- com/example/projectshopgui/
- resources/
- plugin.ymlName, main class and the two permissions
- config.ymlTitle, messages and the whole shop
- java/
- main/
- src/
You can download the finished project and compare it with yours.
Testing checklist
Build the plugin and start the test server (see Build and run). Look at the console first: you should see a line like this.
[14:02:11 INFO]: [ShopGui] Enabling ShopGui v1.0.0[14:02:11 INFO]: [ShopGui] Shop ready: 3 categories, 10 items.Then join the server and go through these checks in order:
- Run
/shop. You see three categories and the level display. Click Close. - Give yourself levels with
/xp add @s 10 levels, open/shopagain and check that the bottle shows 10. - Open a category, click an item, then click Cancel. You come back to the item list and nothing was paid.
- Buy an item. The levels go down by the price, the items arrive and the chat message names them.
- Spend all your levels, then try to buy something you cannot afford. You see the red message and keep your levels.
- Fill your backpack with
/give @s dirt 2304(36 full stacks), then buy anything. You get the "no room" message and keep your levels. Empty some slots and buy again. - Try to steal: left-click, right-click, shift-click, press the number keys, drag across the slots and double-click on icons. Nothing may leave the menu, and nothing from your backpack may enter it.
- Switch to creative with
/gamemode creativeand try to pick up an icon. It stays in place. - Edit a price in
config.yml, run/shop reloadand open the shop again. The new price is shown and the console line did not complain. - Write a mistake on purpose, such as
material: DIAMONDD, and reload. That one item disappears and the console saysItem 'food.steak' has a missing or unknown material(with your own names). The rest of the shop still works. - Take away operator status with
deop YourName./shopstill works, but/shop reloadsays you may not.
Sidebar: using real money with Vault
Many servers already have an economy plugin that gives every player a money balance. Vault is a small plugin that sits between economy plugins and the plugins that want to charge money, so your plugin can say "take 50 coins from this player" without knowing which economy plugin is installed.
Switching this shop to Vault touches exactly three places, and the first is plugin.yml. A soft dependency tells Paper to load Vault before your plugin when it is installed:
1softdepend: [Vault]- If Vault is missing, the shop still loads. You would then fall back to experience levels.
- The payment.
Shop.buywould ask Vault for the player's balance and take money instead of levels. The "check first, change last" order stays the same. - The texts. The words "levels" in the lore and in the config messages become your currency name.
- The balance icon. The experience bottle would show the player's money.
This page does not contain compiled Vault code, because Vault's classes are not part of the Paper API, and adding another plugin's API to a project needs extra build setup. Working with other plugins shows how to do that step by step, including the crash to avoid when the other plugin is missing.
Mistakes to avoid
- Canceling only left clicks. Right clicks, shift-clicks, number keys, drags and double-clicks are all different ways to move items. Cancel the whole event, as
MenuListenerdoes. - Checking for the menu by its title. Titles can be changed or copied by other plugins, and a player can rename a chest in an anvil. Use a holder, as
Menudoes. - Taking the payment before the checks. The order is: check the price, check the room, then charge. Anything else loses players' levels in bad moments.
- Handing out the icon. The icon has extra lore and may share state with the menu. Always give a freshly built stack of the real goods.
- Trusting the config. A bad material name should skip one item, not crash the plugin. A negative price should never reach
buy. - Forgetting
saveDefaultConfig(). Without it there is noconfig.ymlon a fresh server, and the shop is empty. - More than 27 items in a category. They would overwrite the Back button's row. The loader limits the count and warns.
- Forgetting to register the listener. The menu looks fine, but players can walk away with the icons.
Exercises
Add a redstone category without writing any Java
Add a fourth category to config.yml called Redstone, with an icon of your choice and three items: 16 redstone dust, 2 pistons and 4 repeaters. Then run /shop reload and look at the main screen. What happens to the menu when you add an eighth category?
Hint 1
Copy the whole tools: block and change the names. Keep the indentation exactly the same: two spaces per level.
Hint 2
The material names are REDSTONE, PISTON and REPEATER.
Show the solution
The new category goes under categories:, at the same indentation as tools:. No Java changes and no restart are needed: /shop reload rebuilds the shop.
1redstone:2 icon: REDSTONE3 name: "<red>Redstone"4 items:5 redstone-dust:6 material: REDSTONE7 amount: 168 price: 29 pistons:10 material: PISTON11 amount: 212 price: 313 repeaters:14 material: REPEATER15 amount: 416 price: 3The main screen now shows four categories, in slots 10 to 13. A shop can hold seven categories, because that is how many slots the middle row has. The loader skips an eighth one and prints a warning in the console, instead of crashing with an array index error.
Show which items the player can afford
Players often click items they cannot pay for. Change ItemsMenu so that an item that costs more levels than the player has shows a red line "You need N more levels" instead of "Click to buy", and clicking it does nothing.
Hint 1
Inside the loop, calculate item.price() - viewer.getLevel(). A positive result is the number of levels the player is missing.
Hint 2
An icon without a click action is made with icon(...). After drawing it, use continue to jump to the next item.
Show the solution
The check happens each time the screen is drawn, so the lines are always right for the player who is looking. After a purchase the screen is opened again, which refreshes them.
Where this file livesproject-shop-gui-exercisesrcmainjavacomexampleprojectshopguiexerciseItemsMenu.java
The package com.example.projectshopguiexercise is the folder path com/example/projectshopguiexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- project-shop-gui-exercise/
- src/main/
- java/com/example/projectshopguiexercise/Package com.example.projectshopguiexercise
- CategoryMenu.javaHelper class
- ConfirmMenu.javaHelper class
- Icons.javaHelper class
- ItemsMenu.javayou are hereHelper class
- Menu.javaMenu (inventory holder)
- MenuListener.javaListener: reacts to events
- PurchaseResult.javaEnum: a fixed list of choices
- Shop.javaHelper class
- ShopCatalog.javaHelper class
- ShopCategory.javaRecord: a small data class
- ShopCommand.javaCommand (BasicCommand)
- ShopItem.javaRecord: a small data class
- ShopPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projectshopguiexercise/Package com.example.projectshopguiexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@Override22protected void render(Player viewer) {23 List<ShopItem> items = category.items();24 for (int slot = 0; slot < items.size(); slot++) {25 ShopItem item = items.get(slot);26 int missing = item.price() - viewer.getLevel();27 if (missing > 0) {28 icon(slot, Icons.display(item,29 "<gray>Amount: <white>" + item.amount(),30 "<gray>Price: <yellow>" + item.price() + " levels",31 "",32 "<red>You need " + missing + " more levels"));33 continue;34 }35 button(slot,36 Icons.display(item,37 "<gray>Amount: <white>" + item.amount(),38 "<gray>Price: <yellow>" + item.price() + " levels",39 "",40 "<yellow>Click to buy"),41 player -> new ConfirmMenu(shop, category, item).open(player));42 }43 44 button(BACK_SLOT, Icons.button(Material.ARROW, "<yellow>Back", "<gray>Return to the categories"),45 player -> new CategoryMenu(shop).open(player));46 icon(LEVELS_SLOT, Icons.button(Material.EXPERIENCE_BOTTLE, "<green>Your levels: <white>" + viewer.getLevel()));47 fillEmpty();48}- How many levels the player lacks. Zero or less means they can pay.
- An
iconhas no action in the notebook, so the click handler ignores it. - Skips the rest of this loop round, so the clickable button below is not created for this item.
Break the shop on purpose
Delete the line event.setCancelled(true); from onClick in MenuListener, rebuild, and try to take things out of the shop. Which of the ways in the table above work? Put the line back afterwards.
Hint
Try shift-clicking an icon, and pressing a number key while hovering over one.
Show the solution
Without the cancel, the shop windows behave like a normal chest: you can lift an icon onto your cursor, shift-click it into your backpack, and swap icons into your hotbar. You now own a "Click to buy" item that cost no levels. Seeing it once makes you remember why a menu listener has to cancel first and ask questions afterwards.
Extension ideas
- Selling. Add a second screen where players sell items for levels. Remember the same rule: check first, change last.
- Buy more than one. Let a shift-click on Buy purchase five times. The price check needs to multiply.
- Pages. Replace the 27-item limit with Previous and Next buttons, as in the paged menu of Chest menus.
- Per-item permissions. Add an optional
permission:to each item and hide the items a player may not buy. See Permissions. - A purchase log. Write every sale to a file in a background task, so the owner can see what sells.
- Sale prices. Add a
sale-untiltime in the config and show a discount in the lore. - Sounds and particles. Play a louder sound for expensive items. See Effects, particles and sounds.
The next project, Live sidebar scoreboard, shows live information on the right side of the screen and updates it every second.
Recap
- A shop is a chain of chest screens. Each screen is a
Menu, anInventoryHolderthat remembers what each slot does. - Read the config once into records, and check every value. Skip bad entries and warn, instead of crashing.
- The listener cancels every click and every drag in a shop window. That one rule stops all item theft.
- Show an icon that looks like the goods, but give the player a freshly built stack of the real goods.
- In a purchase, check everything first (levels, then room) and change things last.
- Use an enum for the result, so one method can say why a purchase failed.
- Keep the payment in one method, so you can change the currency later without touching the screens.
Quick quiz
Why does
MenuListenercancel the click event for every click in a shop window, instead of only left clicks?There is no list of "dangerous" clicks to remember. Canceling the whole event is simple and complete; the menu's own button action runs afterwards by calling the stored action for that slot.In
Shop.buy, why issetLevelcalled after the checkhasRoomFor?Check everything first, change things last. If a check fails, nothing has changed, so there is nothing to undo.What does
Material.matchMaterial("DIAMONDD")return?It does not guess and does not throw. Because it can returnnull, always check the result before using it.Why does the plugin find out that a window is a shop screen with
getHolder(false) instanceof Menuinstead of comparing the window's title?Any chest can be named "Shop" in an anvil. The holder is created by your code, so only your menus have it.An item in
config.ymlhasprice: -5. What does the loader do with it?A negative price would pay players to buy things. The loader rejects it before any menu can show it.
Next steps
- Project: Live sidebar scoreboard: show live stats that update every second.
- Chest menus: paged menus and more ways to build screens.
- Working with other plugins: add Vault or another plugin's API the safe way.
- Practice Arena: challenges for menus, inventories and configs.