GroupeZ Docs
Development

MenuItemStack

Creating and manipulating dynamic items with MenuItemStack

Learn how to use MenuItemStack to create dynamic, placeholder-aware items in zMenu.

Overview

MenuItemStack is an abstraction over Bukkit's ItemStack that provides:

  • Placeholder support in names, lores, and other properties
  • Dynamic item building based on player context
  • Easy manipulation of item properties
  • Support for custom model data, enchantments, and more

Getting MenuItemStack

From Button Configuration

In custom buttons, retrieve the configured item:

public class MyButton extends Button {

    @Override
    public ItemStack getCustomItemStack(Player player) {
        // Get the MenuItemStack from YAML configuration
        MenuItemStack menuItemStack = this.getItemStack();

        // Build and return the ItemStack
        return menuItemStack.build(player);
    }
}

From InventoryManager

Load a MenuItemStack from configuration:

InventoryManager manager = menuPlugin.getInventoryManager();
MenuItemStack itemStack = manager.loadItemStack(configuration, "path.to.item");

Building ItemStacks

Basic Build

MenuItemStack menuItemStack = getItemStack();

// Simple build with placeholder parsing
ItemStack item = menuItemStack.build(player);

Build with Cache Control

// Build without caching (always fresh)
ItemStack item = menuItemStack.build(player, false);

// Build with caching (reuses cached result when possible)
ItemStack itemCached = menuItemStack.build(player, true);

Build with Custom Placeholders

import fr.maxlego08.menu.api.utils.Placeholders;

MenuItemStack menuItemStack = getItemStack();
Placeholders placeholders = new Placeholders();

// Register custom placeholders
placeholders.register("price", "1000");
placeholders.register("currency", "coins");
placeholders.register("stock", "42");

// Build with custom placeholders
ItemStack item = menuItemStack.build(player, false, placeholders);

Custom Placeholders

Creating Placeholders

Placeholders placeholders = new Placeholders();

// String values
placeholders.register("player_name", player.getName());
placeholders.register("server_name", "MySever");

// Numeric values (converted to string)
placeholders.register("balance", String.valueOf(economy.getBalance(player)));
placeholders.register("level", String.valueOf(player.getLevel()));

// Boolean states
placeholders.register("is_premium", player.hasPermission("premium") ? "Yes" : "No");

// Formatted values
placeholders.register("health", String.format("%.1f", player.getHealth()));

Using in YAML

item:
  material: DIAMOND
  name: "&6Purchase for %price% %currency%"
  lore:
    - "&7Stock remaining: &e%stock%"
    - "&7Seller: &b%seller_name%"
    - ""
    - "&aClick to buy!"

Complete Example: Dynamic Shop Item

SellBuyButton.java

package com.example.shop.buttons;

import fr.maxlego08.menu.api.MenuItemStack;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.engine.InventoryEngine;
import fr.maxlego08.menu.api.utils.Placeholders;
import com.example.shop.ShopPlugin;
import com.example.shop.ShopItem;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

import java.math.BigDecimal;

public class SellBuyButton extends Button {

    private final ShopPlugin plugin;

    public SellBuyButton(Plugin plugin) {
        this.plugin = (ShopPlugin) plugin;
    }

    @Override
    public ItemStack getCustomItemStack(Player player) {
        // Get the MenuItemStack from configuration
        MenuItemStack menuItemStack = this.getItemStack();

        // Get data from player cache
        var cache = plugin.getCacheManager().getCache(player);
        ShopItem shopItem = cache.get("current_item");
        BigDecimal price = cache.get("price", BigDecimal.ZERO);

        // Create placeholders
        Placeholders placeholders = new Placeholders();
        placeholders.register("price", plugin.getEconomyManager().format(price));
        placeholders.register("item_name", shopItem.getName());
        placeholders.register("quantity", String.valueOf(shopItem.getQuantity()));

        // Check affordability and add status
        boolean canAfford = plugin.getEconomyManager().has(player, price);
        placeholders.register("status", canAfford ? "&aYou can afford this!" : "&cNot enough money!");

        // Build and return
        return menuItemStack.build(player, false, placeholders);
    }

    @Override
    public void onClick(Player player, InventoryClickEvent event,
                       InventoryEngine inventory, int slot, Placeholders placeholders) {
        super.onClick(player, event, inventory, slot, placeholders);

        var cache = plugin.getCacheManager().getCache(player);
        ShopItem shopItem = cache.get("current_item");
        BigDecimal price = cache.get("price", BigDecimal.ZERO);

        if (plugin.getEconomyManager().withdraw(player, price)) {
            shopItem.give(player);
            player.sendMessage("§aPurchase successful!");
            player.closeInventory();
        } else {
            player.sendMessage("§cYou don't have enough money!");
        }
    }
}

YAML Configuration

items:
  buy-button:
    type: MY_SELL_BUY
    slot: 22
    item:
      material: EMERALD
      name: "&a&lConfirm Purchase"
      lore:
        - "&7Item: &f%item_name%"
        - "&7Quantity: &e%quantity%"
        - "&7Price: &6%price%"
        - ""
        - "%status%"
        - ""
        - "&eClick to purchase!"

Complete Example: Sort Button with States

SortButton.java

package com.example.sort.buttons;

import fr.maxlego08.menu.api.MenuItemStack;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.engine.InventoryEngine;
import fr.maxlego08.menu.api.utils.Placeholders;
import com.example.sort.SortPlugin;
import com.example.sort.SortType;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

import java.util.List;

public class SortButton extends Button {

    private final SortPlugin plugin;
    private final String enableText;
    private final String disableText;
    private final MenuItemStack loadingItemStack;
    private final List<SortType> sortTypes;

    public SortButton(Plugin plugin, String enableText, String disableText,
                     MenuItemStack loadingItemStack, List<SortType> sortTypes) {
        this.plugin = (SortPlugin) plugin;
        this.enableText = enableText;
        this.disableText = disableText;
        this.loadingItemStack = loadingItemStack;
        this.sortTypes = sortTypes;
    }

    @Override
    public boolean isPermanent() {
        return true;
    }

    @Override
    public ItemStack getCustomItemStack(Player player) {
        var cache = plugin.getCacheManager().getCache(player);
        SortType currentSort = cache.get("current_sort", SortType.DATE_DESC);

        MenuItemStack itemStack = this.getItemStack();
        Placeholders placeholders = new Placeholders();

        // Register state for each sort type
        for (SortType sortType : sortTypes) {
            String state = (sortType == currentSort) ? enableText : disableText;
            String displayName = state.replace("%sorting%", sortType.getDisplayName());
            placeholders.register(sortType.name(), displayName);
        }

        return itemStack.build(player, false, placeholders);
    }

    @Override
    public void onClick(Player player, InventoryClickEvent event,
                       InventoryEngine inventory, int slot, Placeholders placeholders) {
        super.onClick(player, event, inventory, slot, placeholders);

        var cache = plugin.getCacheManager().getCache(player);
        SortType currentSort = cache.get("current_sort", SortType.DATE_DESC);

        // Show loading state
        for (Integer buttonSlot : getSlots()) {
            inventory.getSpigotInventory().setItem(buttonSlot, loadingItemStack.build(player));
        }

        // Calculate next sort type
        int currentIndex = sortTypes.indexOf(currentSort);
        int direction = event.isRightClick() ? -1 : 1;
        int nextIndex = (currentIndex + direction + sortTypes.size()) % sortTypes.size();
        SortType nextSort = sortTypes.get(nextIndex);

        // Update cache and refresh
        cache.set("current_sort", nextSort);

        plugin.getScheduler().runAsync(task -> {
            // Perform async sorting
            plugin.getSortManager().sortItems(player, nextSort);

            // Update inventory on main thread
            plugin.getScheduler().runNextTick(w -> {
                inventory.updateInventory();
            });
        });
    }
}

YAML Configuration

items:
  sort-button:
    type: MY_SORT_BUTTON
    slot: 49
    is-permanent: true
    item:
      material: HOPPER
      name: "&e&lSort Items"
      lore:
        - "&7Current sorting options:"
        - ""
        - " %DATE_DESC%"
        - " %DATE_ASC%"
        - " %PRICE_DESC%"
        - " %PRICE_ASC%"
        - " %NAME_ASC%"
        - ""
        - "&eLeft-click &7for next"
        - "&eRight-click &7for previous"

Complete Example: Toggle Button

ToggleButton.java

package com.example.settings.buttons;

import fr.maxlego08.menu.api.MenuItemStack;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.engine.InventoryEngine;
import fr.maxlego08.menu.api.utils.Placeholders;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

public class ToggleButton extends Button {

    private final Plugin plugin;
    private final String settingKey;
    private final MenuItemStack enabledItem;
    private final MenuItemStack disabledItem;

    public ToggleButton(Plugin plugin, String settingKey,
                       MenuItemStack enabledItem, MenuItemStack disabledItem) {
        this.plugin = plugin;
        this.settingKey = settingKey;
        this.enabledItem = enabledItem;
        this.disabledItem = disabledItem;
    }

    @Override
    public ItemStack getCustomItemStack(Player player) {
        boolean isEnabled = getSettingValue(player);

        // Use different MenuItemStack based on state
        MenuItemStack itemStack = isEnabled ? enabledItem : disabledItem;

        Placeholders placeholders = new Placeholders();
        placeholders.register("status", isEnabled ? "&aEnabled" : "&cDisabled");
        placeholders.register("action", isEnabled ? "disable" : "enable");

        return itemStack.build(player, false, placeholders);
    }

    @Override
    public void onClick(Player player, InventoryClickEvent event,
                       InventoryEngine inventory, int slot, Placeholders placeholders) {
        super.onClick(player, event, inventory, slot, placeholders);

        // Toggle the setting
        boolean currentValue = getSettingValue(player);
        setSettingValue(player, !currentValue);

        // Refresh inventory
        inventory.updateInventory();

        // Notify player
        String newStatus = !currentValue ? "enabled" : "disabled";
        player.sendMessage("§aSetting " + settingKey + " " + newStatus + "!");
    }

    private boolean getSettingValue(Player player) {
        // Implementation depends on your storage system
        return plugin.getConfig().getBoolean("players." + player.getUniqueId() + "." + settingKey, false);
    }

    private void setSettingValue(Player player, boolean value) {
        plugin.getConfig().set("players." + player.getUniqueId() + "." + settingKey, value);
        plugin.saveConfig();
    }
}

Loading MenuItemStack from Configuration

Custom ButtonLoader

package com.example.loader;

import fr.maxlego08.menu.api.MenuItemStack;
import fr.maxlego08.menu.api.InventoryManager;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.loader.ButtonLoader;
import com.example.buttons.ToggleButton;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;

public class ToggleButtonLoader implements ButtonLoader {

    private final Plugin plugin;
    private final InventoryManager inventoryManager;

    public ToggleButtonLoader(Plugin plugin, InventoryManager inventoryManager) {
        this.plugin = plugin;
        this.inventoryManager = inventoryManager;
    }

    @Override
    public String getName() {
        return "MY_TOGGLE";
    }

    @Override
    public Button load(YamlConfiguration config, String path, Plugin plugin) {
        // Load basic properties
        String settingKey = config.getString(path + ".setting-key", "default");

        // Load MenuItemStack for enabled state
        MenuItemStack enabledItem = inventoryManager.loadItemStack(config, path + ".enabled-item");

        // Load MenuItemStack for disabled state
        MenuItemStack disabledItem = inventoryManager.loadItemStack(config, path + ".disabled-item");

        return new ToggleButton(this.plugin, settingKey, enabledItem, disabledItem);
    }
}

YAML Configuration

items:
  notifications-toggle:
    type: MY_TOGGLE
    slot: 11
    setting-key: "notifications"
    enabled-item:
      material: LIME_DYE
      name: "&a&lNotifications: %status%"
      lore:
        - "&7You will receive notifications"
        - ""
        - "&eClick to %action%"
    disabled-item:
      material: GRAY_DYE
      name: "&c&lNotifications: %status%"
      lore:
        - "&7Notifications are turned off"
        - ""
        - "&eClick to %action%"

Properties available in YAML configuration:

item:
  # Material
  material: DIAMOND_SWORD

  # Display name
  name: "&6&lLegendary Sword"

  # Lore lines
  lore:
    - "&7A powerful weapon"
    - "&7Damage: &c+50"

  # Amount
  amount: 1

  # Custom model data (1.14+)
  modelId: 12345

  # Durability/damage
  durability: 0

  # Enchantments
  enchants:
    - SHARPNESS,5
    - UNBREAKING,3

  # Item flags
  flags:
    - HIDE_ENCHANTS
    - HIDE_ATTRIBUTES

  # Glow effect (without enchant text)
  glow: true

  # Player head
  playerHead: "%player%"
  # Or custom URL
  url: "eyJ0ZXh0dXJlcyI6..."

  # Potion
  potion: SPEED
  potionExtended: true
  potionUpgraded: false

  # Leather armor color
  color: "#FF5555"

  # Banner patterns
  banner:
    - RED,STRIPE_TOP
    - BLUE,CROSS

Best Practices

  1. Use placeholders - Leverage the placeholder system for dynamic content
  2. Cache when possible - Use build(player, true) for static items
  3. Create reusable placeholders - Build a helper class for common placeholders
  4. Handle null values - Always provide defaults for placeholder values
  5. Keep lore concise - Long lores can cause display issues
  6. Test with PlaceholderAPI - Ensure PAPI placeholders work correctly

Next Steps

On this page

GroupeZ MCP

Documentation in your AI

MCP lets your AI search GroupeZ documentation and read current examples to help you configure your plugins.

Read-only · No documentation API key needed

1. Copy the server URL

Transport: HTTP (Streamable HTTP). Authentication: none. Your tool uses its own model and subscription limits.

  1. Open Plugins in ChatGPT on the web, then + → Add custom MCP server.
  2. Name it ‘GroupeZ Documentation’, paste the URL above and choose ‘No authentication’. Create and install the plugin.
  3. In a conversation, type @ and select the plugin before asking. If needed, enable search, get_page and list_pages in its settings.
Official setup guide

3. Ask a question

Use GroupeZ MCP to help me create a zMenu inventory. Search relevant pages, read the examples and cite your sources.
Other clients and troubleshooting

For clients using mcpServers, adapt this example to their format:

{
  "mcpServers": {
    "groupez-docs": {
      "url": ""
    }
  }
}

Available tools are search, get_page and list_pages. Refresh the connector’s tools after updates. If your AI cannot access them, check that the connector is enabled in the conversation and your account allows custom MCP servers. Opening the URL in a browser is not an MCP test.