GroupeZ Docs
Development

Player Data API

Working with player data storage in zMenu

Learn how to work with zMenu's player data storage system programmatically.

Overview

zMenu provides a built-in data storage system that persists player-specific values. This data is:

  • Stored in a database (SQLite, MySQL, or MariaDB)
  • Accessible via placeholders (%zmenu_player_value_<key>%)
  • Persistent across server restarts
  • Available through the API

DataManager

Get the data manager to work with player data:

DataManager dataManager = menuPlugin.getDataManager();

Reading Data

Get a Single Value

// Returns Optional<String>
Optional<String> value = dataManager.getData(player, "coins");

// Check and use the value
if (value.isPresent()) {
    int coins = Integer.parseInt(value.get());
    player.sendMessage("You have " + coins + " coins");
} else {
    player.sendMessage("No coins data found");
}

Get with Default Value

String coins = dataManager.getData(player, "coins").orElse("0");
int coinAmount = Integer.parseInt(coins);

Get by UUID

UUID uuid = player.getUniqueId();
Optional<String> value = dataManager.getData(uuid, "coins");

Check If Data Exists

boolean hasCoins = dataManager.getData(player, "coins").isPresent();

Writing Data

Set a Value

// Set a string value
dataManager.setData(player, "coins", "100");

// Set a numeric value (as string)
dataManager.setData(player, "level", String.valueOf(5));

// Set a boolean (as string)
dataManager.setData(player, "premium", "true");

Add to a Numeric Value

// Add 50 to the current value
dataManager.addData(player, "coins", 50);

Subtract from a Value

// Subtract by adding a negative value
dataManager.addData(player, "coins", -25);

Remove Data

// Remove a specific key for a player
dataManager.removeData(player, "coins");

Bulk Operations

Get All Keys for a Player

// This functionality depends on implementation
// Generally, you would track your own keys
List<String> knownKeys = Arrays.asList("coins", "level", "premium", "last-login");

for (String key : knownKeys) {
    dataManager.getData(player, key).ifPresent(value -> {
        player.sendMessage(key + ": " + value);
    });
}

Complete Example: Currency System

package com.example.currency;

import fr.maxlego08.menu.api.MenuPlugin;
import fr.maxlego08.menu.api.DataManager;
import org.bukkit.entity.Player;

public class CurrencyManager {

    private final DataManager dataManager;
    private static final String COINS_KEY = "coins";

    public CurrencyManager(MenuPlugin menuPlugin) {
        this.dataManager = menuPlugin.getDataManager();
    }

    public int getCoins(Player player) {
        return dataManager.getData(player, COINS_KEY)
            .map(Integer::parseInt)
            .orElse(0);
    }

    public void setCoins(Player player, int amount) {
        dataManager.setData(player, COINS_KEY, String.valueOf(Math.max(0, amount)));
    }

    public void addCoins(Player player, int amount) {
        int current = getCoins(player);
        setCoins(player, current + amount);
    }

    public boolean removeCoins(Player player, int amount) {
        int current = getCoins(player);
        if (current >= amount) {
            setCoins(player, current - amount);
            return true;
        }
        return false;
    }

    public boolean hasCoins(Player player, int amount) {
        return getCoins(player) >= amount;
    }
}

Usage

CurrencyManager currency = new CurrencyManager(menuPlugin);

// Check balance
int balance = currency.getCoins(player);
player.sendMessage("Balance: " + balance);

// Add coins
currency.addCoins(player, 100);
player.sendMessage("Added 100 coins!");

// Remove coins (with check)
if (currency.removeCoins(player, 50)) {
    player.sendMessage("Spent 50 coins");
} else {
    player.sendMessage("Not enough coins!");
}

Complete Example: Statistics Tracker

package com.example.stats;

import fr.maxlego08.menu.api.MenuPlugin;
import fr.maxlego08.menu.api.DataManager;
import org.bukkit.entity.Player;

public class StatsTracker {

    private final DataManager dataManager;

    public StatsTracker(MenuPlugin menuPlugin) {
        this.dataManager = menuPlugin.getDataManager();
    }

    // Kills
    public int getKills(Player player) {
        return getIntValue(player, "kills");
    }

    public void incrementKills(Player player) {
        dataManager.addData(player, "kills", 1);
    }

    // Deaths
    public int getDeaths(Player player) {
        return getIntValue(player, "deaths");
    }

    public void incrementDeaths(Player player) {
        dataManager.addData(player, "deaths", 1);
    }

    // K/D Ratio
    public double getKDRatio(Player player) {
        int kills = getKills(player);
        int deaths = getDeaths(player);
        if (deaths == 0) return kills;
        return (double) kills / deaths;
    }

    // Playtime (in minutes)
    public long getPlaytime(Player player) {
        return getLongValue(player, "playtime");
    }

    public void addPlaytime(Player player, long minutes) {
        long current = getPlaytime(player);
        dataManager.setData(player, "playtime", String.valueOf(current + minutes));
    }

    // Helper methods
    private int getIntValue(Player player, String key) {
        return dataManager.getData(player, key)
            .map(Integer::parseInt)
            .orElse(0);
    }

    private long getLongValue(Player player, String key) {
        return dataManager.getData(player, key)
            .map(Long::parseLong)
            .orElse(0L);
    }
}

Complete Example: Daily Rewards System

package com.example.rewards;

import fr.maxlego08.menu.api.MenuPlugin;
import fr.maxlego08.menu.api.DataManager;
import org.bukkit.entity.Player;

public class DailyRewardsManager {

    private final DataManager dataManager;
    private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;

    public DailyRewardsManager(MenuPlugin menuPlugin) {
        this.dataManager = menuPlugin.getDataManager();
    }

    public boolean canClaimDaily(Player player) {
        long lastClaim = getLastClaimTime(player);
        long now = System.currentTimeMillis();
        return (now - lastClaim) >= DAY_IN_MILLIS;
    }

    public long getTimeUntilNextClaim(Player player) {
        long lastClaim = getLastClaimTime(player);
        long now = System.currentTimeMillis();
        long nextClaim = lastClaim + DAY_IN_MILLIS;
        return Math.max(0, nextClaim - now);
    }

    public void claimDaily(Player player) {
        dataManager.setData(player, "daily-last-claim",
            String.valueOf(System.currentTimeMillis()));
        incrementStreak(player);
    }

    public int getStreak(Player player) {
        return dataManager.getData(player, "daily-streak")
            .map(Integer::parseInt)
            .orElse(0);
    }

    private void incrementStreak(Player player) {
        dataManager.addData(player, "daily-streak", 1);
    }

    public void resetStreak(Player player) {
        dataManager.setData(player, "daily-streak", "0");
    }

    private long getLastClaimTime(Player player) {
        return dataManager.getData(player, "daily-last-claim")
            .map(Long::parseLong)
            .orElse(0L);
    }

    // Format time remaining
    public String formatTimeRemaining(Player player) {
        long millis = getTimeUntilNextClaim(player);
        long hours = millis / (60 * 60 * 1000);
        long minutes = (millis % (60 * 60 * 1000)) / (60 * 1000);
        return String.format("%dh %dm", hours, minutes);
    }
}

Working with Complex Data

For complex data structures, serialize to JSON:

import com.google.gson.Gson;

public class ComplexDataManager {

    private final DataManager dataManager;
    private final Gson gson = new Gson();

    public ComplexDataManager(MenuPlugin menuPlugin) {
        this.dataManager = menuPlugin.getDataManager();
    }

    // Save a list
    public void saveStringList(Player player, String key, List<String> list) {
        String json = gson.toJson(list);
        dataManager.setData(player, key, json);
    }

    // Load a list
    public List<String> loadStringList(Player player, String key) {
        return dataManager.getData(player, key)
            .map(json -> gson.fromJson(json,
                new TypeToken<List<String>>(){}.getType()))
            .orElse(new ArrayList<>());
    }

    // Save custom object
    public void savePlayerStats(Player player, PlayerStats stats) {
        String json = gson.toJson(stats);
        dataManager.setData(player, "player-stats", json);
    }

    // Load custom object
    public PlayerStats loadPlayerStats(Player player) {
        return dataManager.getData(player, "player-stats")
            .map(json -> gson.fromJson(json, PlayerStats.class))
            .orElse(new PlayerStats());
    }
}

// Example data class
public class PlayerStats {
    public int kills = 0;
    public int deaths = 0;
    public long playtime = 0;
    public List<String> achievements = new ArrayList<>();
}

Using Data in Inventories

The data you set is automatically available as placeholders:

items:
  stats-display:
    slot: 4
    item:
      material: PLAYER_HEAD
      playerHead: "%player%"
      name: "&6&l%player%'s Stats"
      lore:
        - "&7Coins: &e%zmenu_player_value_coins%"
        - "&7Kills: &a%zmenu_player_value_kills%"
        - "&7Deaths: &c%zmenu_player_value_deaths%"
        - "&7Streak: &b%zmenu_player_value_daily-streak%"

Best Practices

  1. Use consistent key names - Establish a naming convention
  2. Handle missing data - Always use orElse() for defaults
  3. Validate numeric parsing - Wrap in try-catch for safety
  4. Don't overwrite important data - Check before setting
  5. Use appropriate data types - Store as strings, parse when needed
  6. Clean up unused data - Remove keys no longer needed

Key Naming Conventions

// Good naming conventions
"coins"              // Simple, clear
"daily-streak"       // Kebab-case for multi-word
"quest-tutorial-1"   // Include identifiers
"stats-kills"        // Prefix with category

// Avoid
"c"                  // Too short
"playerCoinsData"    // CamelCase (use kebab)
"x1y2z3"            // Meaningless

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.