Paper Plugin Guide
File mode0

Projects

Project: Shop menu

A config-driven chest shop with categories, prices in XP levels and a confirm screen.

IntermediateProject, about 70 min read

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:

4: experience_bottle, 10: bricks, 11: golden_apple, 12: diamond_pickaxe, 22: barrier

Clicking a category opens its items, each with the amount you get and the price:

0: oak_log, 1: stone_bricks, 2: glass, 3: lantern, 31: arrow, 35: experience_bottle

Clicking an item opens a confirm screen, so nobody spends levels by accident:

11: lime_concrete, 13: stone_bricks, 15: red_concrete

After a click on Buy the player gets a chat message, and the shop goes back to the item list:

You bought 32x Stone Bricks for 3 levels.

If the player is short of levels, or the backpack is full, the shop says so and takes nothing:

You need 8 levels, but you only have 3.
Your inventory has no room for that. Make some space and try again.

These are the rules the plugin follows:

RuleHow the plugin keeps it
The shop's contents live in config.ymlA loader turns the file into small data objects when the plugin starts and on /shop reload
Nobody can take items out of the menuOne listener cancels every click and drag in a shop window
Nothing is bought by accidentA confirm screen sits between the click and the payment
Nobody pays for items they cannot carryThe plugin checks for room before it takes any levels
A typo in the config does not break the shopThe 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:

The three shop screens and the checks before a purchase The player types /shop and sees the category screen. Clicking a category opens the items screen. Clicking an item opens the confirm screen. Clicking Buy runs three steps: check the levels, check the inventory has room, then take the levels and give the items. A failed check sends a message and nothing is taken. CategoryMenu pick a category ItemsMenu pick an item ConfirmMenu Buy or Cancel Cancel or any result goes back Buy Enough levels? no Message: not enough levels yes Room in the inventory? no Message: inventory is full yes Take the levels, give the items then tell the player Nothing is taken unless both checks pass.
Three screens lead to one Buy button. The plugin takes levels and hands over items only when both checks pass.

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:

RequirementThe piece that does itChapter
Items and prices in a fileShopCatalog reads config.yml into ShopCategory and ShopItem recordsConfiguration files, Enums, records and modern Java
A window with buttonsMenu, an InventoryHolder that remembers what each slot doesChest menus
Items cannot be stolenMenuListener cancels the click eventsPriorities and canceling
Three screensCategoryMenu, ItemsMenu, ConfirmMenuChest menus
Paying and receivingShop.buy: levels, room check, addItemPlayers, Inventories
Open it with a commandShopCommand, a BasicCommandCommands, part 1

The build steps are:

  1. Write plugin.yml and config.yml

    Decide what the server owner can change before writing any Java.

  2. Turn the config into records

    Two tiny data classes and a loader that checks every entry.

  3. Build the menu base and protect the items

    A holder class, plus the listener that stops every kind of item theft.

  4. Make the item icons

    One helper that builds buttons, and a clear line between the icon and the real goods.

  5. Build the category and items screens

    Buttons that open the next screen.

  6. Build the confirm screen and the purchase

    The check for levels, the check for room, the payment and the messages.

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

plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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
  1. Lets a player open the shop. It defaults to true, which means everyone has it, because a shop is for everybody.
  2. Lets a player reload the prices. op means 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:

config.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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
  1. 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.
  2. A message with placeholders. <amount>, <item> and <price> are filled in by the plugin, as in the Heal and Feed project.
  3. 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.
  4. The block that represents this category in the first screen. Any item name from the Material list works.
  5. What the category is called in the menu, again in MiniMessage.
  6. 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.
  7. What the player receives. The loader checks that this name really exists.
  8. How many the player gets for one purchase.
  9. 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.

ShopItem.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A record with five values. After new ShopItem(...) you read them with item.price(), item.amount() and so on. A record never changes after it is created, which makes it safe to share between menus.
  2. What the player receives. Material is the list of every block and item in the game.
  3. The optional display name. It is null when the config does not give one.
ShopCategory.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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:

ShopCatalog.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A new empty list that we fill while reading.
  2. A configuration section is one indented block of the YAML file. This asks for the block called categories. It returns null when the block is missing, so the next lines check for that.
  3. Gives the names directly under categories: building, food, tools. The false means "only the direct children, do not go deeper".
  4. 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.
  5. Writes a yellow warning line into the server console. The server owner sees exactly what to fix.
  6. The helper returns null for a broken category. We only keep the good ones.
  7. 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:

ShopCatalog.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A short label such as food.steak, used in every warning so the owner knows where to look.
  2. Turns the text OAK_LOG into a Material, or null if there is no such material.
  3. 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.
  4. Most blocks stack to 64, ender pearls to 16, swords to 1. Asking the material avoids guessing.
  5. 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.
  6. Rejects zero and negative prices. A negative price would hand out levels for free, and a price of 0 gives items away.
ShopCatalog.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Looks a name up in the list of materials. It accepts OAK_LOG, oak_log and even minecraft:oak_log, and returns null for anything unknown.
  2. Some materials exist only as blocks, such as WATER or FIRE. A player can never hold them, so the shop must not sell them.
  3. 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.

Menu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. This marks the class as the owner of a window. The listener will use it to tell shop windows from normal chests.
  2. 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.
  3. Creates the empty chest window. The first argument, this, makes this menu the window's holder. Nine slots per row.
  4. Every screen fills itself in its own way. abstract means "each subclass must write this method".
  5. Shows the window to the player.
  6. Puts an icon in a slot and remembers what the slot does. Compare with icon below, which only displays something.
  7. Fills every empty slot with a gray glass pane, which makes the screen look tidy.
  8. Looks up the action for the clicked slot. Slots with no action, such as the glass panes, do nothing.
  9. The click event says who clicked as a HumanEntity. This check converts it to a Player, 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:

MenuListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Runs late, after most other plugins have had their say, so our cancel is not overruled. See Priorities and canceling.
  2. Asks the window who owns it. The false means "do not make a snapshot of the block", which is faster and exactly what we want.
  3. True only for our shop windows. Chests, furnaces and other plugins' menus return here, untouched.
  4. Cancels every click inside a shop window, no matter what kind of click. This single line is the whole protection.
  5. 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.
  6. 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 doesWhat would happen without the cancel
Picks up an icon with the mouseThe icon, with its price in the lore, ends up in their backpack
Shift-clicks an item in their backpackIt jumps into the shop window and is lost, or fills the menu
Presses a number key over an iconThe icon swaps with the item in that hotbar slot
Drags a stack across the windowItems are spread out into the menu (this is the drag event)
Double-clicks to gather matching itemsIcons are collected onto the cursor
Clones an icon in creative modeFree 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:

Icons.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Creates a brand new stack of the right size. Nothing is shared with any other stack.
  2. Only rename the stack when the config gave a name.
  3. Sets the name the player sees. text turns MiniMessage into a component and switches off Minecraft's default italics for renamed items.
Icons.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Starts from a fresh stack of the goods, so the icon looks exactly like what the player will get.
  2. 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:

CategoryMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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.
  2. The player's current experience level, the green number above the hotbar.
  3. Asks the shop for the list of categories that the loader built.
  4. If the config has no usable categories, show a clear sign instead of a blank window.
  5. 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.
  6. 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 a lambda.
  7. A method reference: "call closeInventory on the player who clicked". It does the same as player -> 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:

ItemsMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. 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.
  2. The icon looks like the real item, with the amount, the price and a hint written in its lore.
  3. 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.
  4. 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:

ConfirmMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The item in the middle is only a picture, so it uses icon, not button: clicking it does nothing.
  2. Green means yes. The color is a good signal, because players read buttons by color before text.
  3. A method reference: when the Buy button is clicked, run this class's purchase method.
  4. Cancel opens the items screen again, without paying anything.
ConfirmMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Does all the work and returns one of three results. The menu never touches levels or inventories itself.
  2. Sends the right message for that result.
  3. A short if inside a line: pick the happy sound after a purchase, the "no" sound of a villager after a failure.
  4. 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:

PurchaseResult.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The purchase worked.
  2. Stopped at the first check: the player is too poor.
  3. 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.

Shop.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Check 1: can the player afford it? If not, return at once. Nothing has been changed.
  2. Builds the real stack that the player would receive.
  3. Check 2: would the whole stack fit? The method is below. Again, nothing changed yet.
  4. Only now do we take the payment. The level count drops by the price.
  5. 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:

Shop.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The 36 slots of the main backpack and hotbar. Armor and the off hand are not included, because items from addItem never go there.
  2. An empty slot can hold a whole stack of the goods.
  3. A slot with the same kind of item, with the same name and data, can take more until it reaches its limit.
  4. How many more fit on top of the stack. Math.max(0, ...) protects against a stack that is somehow over the limit.
  5. 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:

Shop.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Picks one branch for each enum value. The arrow form case X -> runs only that branch, with no fall-through. Note that a switch statement 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.
  2. Fills <amount> in the message. "Unparsed" means the text is inserted as plain text, so it cannot be used to sneak in formatting tags.
  3. The item name is a full component, because a name can have colors. nameOf reads 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:

ShopCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The player typed /shop reload. The first condition makes sure there is an argument before we read it, because reading args[0] on an empty array crashes.
  2. Only a player can have a chest window open. The console gets a short message instead.
  3. Builds the first screen for this player and shows it.
ShopCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The normal command permission only lets players in. Reloading needs this stronger one, checked by hand.
  2. Reads config.yml from disk again and rebuilds the records. Screens that are already open keep the old prices until they are reopened.
  3. 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:

ShopPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Copies config.yml from the jar into plugins/ShopGui/ the first time the plugin runs. Later starts keep the owner's edits.
  2. Creates the shop, which loads the config as it is created. All screens share this one object.
  3. Without this line nothing is protected: a listener that was never registered is never called.
  4. Registers /shop. The permission comes from the command's own permission() method.
  5. 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
        • resources/
          • plugin.ymlName, main class and the two permissions
          • config.ymlTitle, messages and the whole shop

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.

Server console
[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 /shop again 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 creative and try to pick up an icon. It stays in place.
  • Edit a price in config.yml, run /shop reload and 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 says Item '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. /shop still works, but /shop reload says you may not.

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:

plugin.yml (new line)
1softdepend: [Vault]
  1. If Vault is missing, the shop still loads. You would then fall back to experience levels.
  • The payment. Shop.buy would 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 MenuListener does.
  • 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 Menu does.
  • 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 no config.yml on 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

Try it

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.

config.yml (new category)
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: 3

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

Try it

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.

ItemsMenu.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. How many levels the player lacks. Zero or less means they can pay.
  2. An icon has no action in the notebook, so the click handler ignores it.
  3. Skips the rest of this loop round, so the clickable button below is not created for this item.
Try it

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-until time 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, an InventoryHolder that 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

  1. Why does MenuListener cancel the click event for every click in a shop window, instead of only left clicks?

  2. In Shop.buy, why is setLevel called after the check hasRoomFor?

  3. What does Material.matchMaterial("DIAMONDD") return?

  4. Why does the plugin find out that a window is a shop screen with getHolder(false) instanceof Menu instead of comparing the window's title?

  5. An item in config.yml has price: -5. What does the loader do with it?

Next steps