Sitelet https://github.com/Drawethree/X-Prison/wiki/Developer-API
Skip to content

Developer API

Jan Kluka edited this page Sep 16, 2026 · 50 revisions

Developer API

The X-Prison public API lives in a dedicated project: X-PrisonAPI on GitHub


Adding X-PrisonAPI as a Dependency

Add the API artifact to your plugin's build tool. The API is published to the Drawethree Maven repository.

Maven:

<repository>
    <id>drawethree-repo</id>
    <url>https://repo.drawethree.dev/releases</url>
</repository>

<dependency>
    <groupId>dev.drawethree</groupId>
    <artifactId>X-PrisonAPI</artifactId>
    <version>LATEST</version>
    <scope>provided</scope>
</dependency>

Gradle (Kotlin DSL):

repositories {
    maven("https://repo.drawethree.dev/releases")
}

dependencies {
    compileOnly("dev.drawethree:X-PrisonAPI:LATEST")
}

Declare X-Prison as a dependency in your plugin.yml:

depend: [X-Prison]
# or if optional:
softdepend: [X-Prison]

Accessing the API

import dev.drawethree.xprison.api.XPrisonAPI;

XPrisonAPI api = XPrisonAPI.getInstance();

Each module exposes its own sub-API through the main API instance:

api.getEnchantsApi()      // Enchant system
api.getRanksApi()         // Ranks
api.getPrestigesApi()     // Prestiges
api.getRebirthApi()       // Rebirths
api.getBlocksApi()        // Block tracking
api.getCurrencyApi()      // Currency balances
api.getGangsApi()         // Gangs
api.getMultipliersApi()   // Global, player, and rank multipliers
api.getAutoSellApi()      // Auto-sell prices and regions
api.getMinesApi()         // Mine management
api.getBombsApi()         // Bomb items
api.getAutoMinerApi()     // Auto-miner time
api.getBattlePassApi()    // Battle Pass tiers, XP and premium
api.getQuestsApi()        // Quests progress and rerolls
api.getDailyRewardsApi()  // Daily login streaks and claims
api.getMilestonesApi()    // Milestone ladders and per-track progress
api.getPickaxeLevelsApi() // Pickaxe level and XP
api.getPickaxeQualityApi()// Pickaxe quality tiers
api.getPickaxeSkinsApi()  // Skins on a pickaxe
api.getMiningStatsApi()   // Per-player mining rates (blocks/sec, income/min)
api.getHistoryApi()       // The player history log
api.getVirtualBlocksApi() // Packet (virtual) mine blocks
api.getTextApi()          // MiniMessage rendering that also works on Spigot
api.getTimeApi()          // 1.10 - the server's time zone, date format and duration labels

api.getPluginVersion()    // e.g. "2026.3.8.5"
api.getModules()          // every module and whether it is enabled
api.getLoadedAddons()     // what AddonManager loaded from plugins/X-Prison/addons
api.getDashboardUrl()     // null unless the Dashboard addon is running

// 2026.3.7.0 - optional, see "Diagnostics API" and "Player Preferences API" below
api.getDiagnosticsApi()         // Config lint findings, startup warnings, permission catalog
api.getPlayerPreferencesApi()   // Per-player on/off switches shown in /toggles

Battle Pass / Quests / Daily Rewards each expose read and write access plus their own Bukkit events (e.g. BattlePassXpGainEvent, BattlePassTierUpEvent, QuestCompleteEvent, QuestClaimEvent, PlayerDailyRewardClaimEvent):

// Battle Pass
api.getBattlePassApi().addXp(uuid, 500, XpSource.OTHER);
int tier = api.getBattlePassApi().getTier(uuid);
boolean premium = api.getBattlePassApi().isPremium(uuid);

// Quests
List<ActiveQuest> daily = api.getQuestsApi().getActiveQuests(uuid, QuestCategory.DAILY);
api.getQuestsApi().addProgress(uuid, "daily_mine_blocks", 100);

// Daily Rewards
int streak = api.getDailyRewardsApi().getStreak(uuid);
boolean claimed = api.getDailyRewardsApi().claim(uuid);

Milestones (since 2026.3.8.0) splits into two halves: getRegistry() is what exists, getProgress() is where a player stands. Values are BigDecimal, so a track may measure an OP-scale currency without overflowing.

XPrisonMilestonesAPI milestones = api.getMilestonesApi();

// Ladders
MilestoneRegistry registry = milestones.getRegistry();
List<MilestoneTrack> tracks = registry.getConfiguredTracks();
MilestoneTrack prestige = registry.getTrack("prestige");
List<Milestone> ladder = registry.getMilestones(prestige);
Milestone rung = registry.getMilestone("prestige-100");

// A player's standing
MilestoneProgress progress = milestones.getProgress();
BigDecimal blocks = progress.getCurrentValue(player, registry.getTrack("blocks"));
int reached = progress.getLevel(player, registry.getTrack("blocks"));
Milestone next = progress.getNextMilestone(player, registry.getTrack("blocks"));

// The high-water mark, offline-capable - this is what a rebirth does not reset
BigDecimal highest = progress.getHighestProgress(uuid, prestige);
progress.setHighestProgress(uuid, prestige, 0);   // let the ladder be earned again
progress.resetPlayer(uuid);                       // clear every track

// Hand one out on demand - ignores whether it was reached or already paid
milestones.awardMilestone(player, "playtime-1440");
milestones.reloadConfig();

awardMilestone does not touch recorded progress, so a milestone the player already earned is paid a second time. setHighestProgress is the switch that decides what they may still earn.

A Milestone is the rung itself - getId(), getTrack(), getThreshold(), isExact() and the getCommands() that pay it. What the player sees is grouped beside it, so a consumer only walks into the half it needs:

Milestone rung = registry.getMilestone("prestige-100");

// the menu half
MilestoneDisplay display = rung.getDisplay();
display.getDescription(); display.getRewardLines(); display.getMaterial(); display.getCustomModelData();

// the "you reached it" half
MilestoneAnnouncement announcement = rung.getAnnouncement();
announcement.getTitle(); announcement.getSubtitle(); announcement.getMessage();
announcement.getBroadcast(); announcement.getSound(); announcement.isFirework();

Adding a milestone track

A track is the extension point: implement MilestoneTrack, register it, and server owners can write type: <your key> in milestones.yml and build a ladder on it. X-Prison registers its own six (prestige, rebirth, rank, blocks, playtime, pickaxe-level) the same way.

public final class GangLevelTrack implements MilestoneTrack {

    @Override public @NotNull String getKey() { return "gang-level"; }
    @Override public @NotNull String getDisplayName() { return "Gang Level"; }
    @Override public @NotNull String getDescription() { return "Level your gang to climb this track!"; }

    @Override
    public @NotNull BigDecimal getValue(@NotNull Player player) {
        return BigDecimal.valueOf(myGangApi.getLevel(player));   // cheap, main-thread, never blocks
    }

    // optional
    @Override public boolean isAvailable() { return myPluginIsRunning(); }
    @Override public @NotNull String format(@NotNull BigDecimal value) { return "Lv. " + value; }
}

api.getMilestonesApi().getRegistry().registerTrack(new GangLevelTrack());

Registration order does not matter: milestones naming a track that is not registered yet are held aside and adopted the moment it registers, so an addon that enables after the core still works. A track that is only registered is drawn in the menu but never pays out - X-Prison drives its own tracks and cannot know when yours moves. Report movement through MilestoneProgress:

MilestoneProgress progress = api.getMilestonesApi().getProgress();

// a counter that only climbs (blocks, playtime, a lifetime total)
progress.reportCounter(player, track, previousValue, currentValue);

// a value that can go back down (prestige, rebirth, rank)
progress.reportValue(player, track, newValue);

// a jump on such a value - every threshold between the two fires, plus non-exact repeats
progress.reportChange(player, track, previousValue, currentValue);

Keys are unique and matched ignoring case; registerTrack returns false if the key is taken, and X-Prison's own tracks cannot be unregistered.

Dashboard integration (requires Dashboard addon):

api.setDashboardUrl(String url)   // called by the Dashboard addon on startup
api.getDashboardUrl()             // returns the active panel URL, or null if not running

Module and addon management:

api.getModules()                        // List<XPrisonModule> — all registered modules
api.isModuleEnabled(String configKey)   // true if module is active
api.enableModule(String configKey)
api.disableModule(String configKey)

api.getLoadedAddons()                          // List<XPrisonAddon>
api.enableAddon(String name)
api.disableAddon(String name)
api.reloadAddon(String name)                   // runs the addon's onReload() hook; false if unknown, disabled or it threw (API 1.10)
api.loadAddonFromFile(File jar)                // runtime load without restart

Building an Addon (XPrisonAddonContext)

Addons are JARs placed in plugins/X-Prison/addons/. Declare the main class and metadata in the JAR manifest:

X-Prison-Addon-Class: com.example.MyAddon
X-Prison-Addon-Name: MyAddon
X-Prison-Addon-Description: Does something useful
X-Prison-Addon-Version: 1.0.0
X-Prison-Addon-Author: YourName
X-Prison-Min-Version: 2026.2.2.4-BETA
X-Prison-Priority: 50
X-Prison-Depends: OtherAddon, AnotherAddon
Attribute Required Description
X-Prison-Addon-Class Yes Fully-qualified class name implementing XPrisonAddon
X-Prison-Addon-Name No Display name shown in the addon manager GUI and logs
X-Prison-Addon-Version No Version string
X-Prison-Addon-Author No Author name
X-Prison-Addon-Description No Short description
X-Prison-Min-Version No Minimum X-Prison version (e.g. 2026.2.0). A warning is printed if the running version is older.
X-Prison-Priority No Integer load-order priority. Lower values load first. Default: 50.
X-Prison-Depends No Comma-separated list of addon names that must be loaded before this one. The addon is skipped if any dependency is missing.

Implement XPrisonAddon:

public class MyAddon implements XPrisonAddon {

    private XPrisonAddonContext context;

    @Override
    public void onEnable(XPrisonAddonContext context) {
        this.context = context;

        XPrisonAPI api = context.getAPI();           // full X-Prison API
        File dataFolder = context.getDataFolder();   // plugins/X-Prison/addons/MyAddon/
        Logger log = context.getLogger();            // prefixed logger
        String name = context.getAddonName();
        String version = context.getAddonVersion();

        // Register Bukkit event listeners
        context.registerEvents(new MyListener());

        log.info("MyAddon enabled.");
    }

    @Override
    public void onDisable() {
        context.getLogger().info("MyAddon disabled.");
    }

    @Override
    public void onReload() {
        // Called by /xprison reload, /xprison reload MyAddon, the Addons Manager GUI
        // and XPrisonAPI#reloadAddon. Re-read your config here; the addon stays enabled,
        // so listeners, commands and runtime state survive. Core modules are reloaded first.
        myConfig.load();
    }
}

Tip: Use context.getDataFolder() for your own config files. Do not read or write to X-Prison's own data folder.

onReload() is a default no-op (API 1.10+), so addons that never override it keep loading on every X-Prison version. Throw from it to have the reload summary report your addon as failed.


Addon Load Order

X-Prison resolves load order across all addon JARs before instantiating any of them. The rules, in priority order:

  1. Dependencies first — if your manifest declares X-Prison-Depends: CoreAddon, CoreAddon is always loaded before your addon, regardless of priority numbers.
  2. Lower priority number loads first — among addons with no dependency relationship, the one with the smaller X-Prison-Priority value loads first. Default is 50.
  3. Alphabetical tiebreak — addons sharing the same priority and no dependency relationship are loaded alphabetically by name for determinism.

Skipped addons — an addon is skipped (with a console warning) if:

  • A declared dependency is not present in the addons folder.
  • A dependency was itself skipped (cascade).
  • The addon is part of a circular dependency chain (A depends on B, B depends on A).

Unload order is the exact reverse of load order — dependents are always shut down before the addons they rely on.

Example manifest for an addon that must load after a hypothetical CoreAddon foundation:

X-Prison-Addon-Class: com.example.MyFeatureAddon
X-Prison-Addon-Name: MyFeatureAddon
X-Prison-Addon-Version: 1.0.0
X-Prison-Depends: CoreAddon
X-Prison-Priority: 60

Registering a Custom Enchant

Custom enchants extend XPrisonEnchantmentBase (or one of its subclasses) and are driven by a JSON file, exactly like the built-in enchants. The base class reads every shared field - id, rawName, name, maxLevel, initialCost, increaseCostBy, currency, prestige, refund, gui - and hands you the parsed JsonObject for anything of your own.

@Override
public void onEnable(XPrisonAddonContext context) {
    File file = new File(context.getDataFolder(), "myenchant.json");
    // copy your bundled default if it is missing, then merge new keys into an existing one
    EnchantJsonUpdater.update(file, getClass().getResourceAsStream("/myenchant.json"));

    enchant = new MyCustomEnchant(file);
    enchant.load();
    context.getAPI().getEnchantsApi().registerEnchant(enchant);
}

@Override
public void onDisable() {
    enchant.unload();
    context.getAPI().getEnchantsApi().unregisterEnchant(enchant);
}

What your class implements depends on what the enchant does:

Base / interface Gives you
XPrisonEnchantmentBase The JSON plumbing. Override loadCustomProperties(JsonObject) for your own fields, getAuthor(), and unload().
BlockBreakEnchant onBlockBreak(BlockBreakEvent, int level) - called for every block a pickaxe with this enchant breaks in a mine.
ChanceBasedEnchant getChanceToTrigger(int level) - X-Prison rolls the chance and fires XPrisonEnchantPreTriggerEvent for you.
EquipabbleEnchantment onEquip / onUnequip(Player, ItemStack, int level) - for passive effects such as potion effects or flight.
RequiresPickaxeLevel getRequiredPickaxeLevel() - the enchant is locked in the menu below that pickaxe level.
CommandRewardEnchantBase A ready-made chance enchant that runs weighted console commands (what the Key Finder family uses).
AreaBreakEnchant A ready-made multi-block enchant - see below.

A full walkthrough, including the JSON file, is on Creating Custom Enchants.

Area (multi-block) enchants

If your enchant breaks many blocks at once, extend AreaBreakEnchant (dev.drawethree.xprison.api.enchants.area) instead of implementing onBlockBreak yourself. You override selectTargets(...) to say which blocks are affected; drops, auto-sell, Fortune, prestige scaling, currency payout, the proc message, mine-reset accounting, pickaxe progression and packet-mine support are all handled by the shared pipeline.

Full guide, including the available hooks and the eventStrategy performance trade-off: Area Enchants: AreaBreakEnchant.

Building your own break pipeline (1.9)

If you cannot extend AreaBreakEnchant — for example your enchant already extends something else — these API methods expose the individual pieces. All of them are default, so an addon compiled against 1.9 still loads on an older core (the feature simply becomes a no-op).

XPrisonEnchantsAPI enchants = api.getEnchantsApi();

// The cuboid an area enchant may affect (highest-priority region that permits enchants).
Optional<AreaBounds> region = enchants.getEnchantRegionBounds(location);

// Fortune level on a pickaxe, and blocks Fortune must not multiply.
int fortune = enchants.getItemFortuneLevel(pickaxe);
boolean skip = enchants.isFortuneBlacklisted(block);

// Suppress X-Prison's own gated listeners for a synthetic BlockBreakEvent you fire.
enchants.ignoreBlockBreakEvent(event);

XPrisonBlocksAPI blocks = api.getBlocksApi();

// Run the shared post-break pipeline once for a set of blocks: blocks-broken statistic,
// lucky blocks, and the aggregate XPrisonBlockBreakEvent that quests / battle pass /
// block boosters consume. Call this ONLY when you are not firing a Bukkit event per
// block — otherwise X-Prison's own listener already ran it and you would double-count.
blocks.handleBlockBreak(player, brokenBlocks, countBlocksBroken);

// Bulk form for breaks too large to enumerate (a whole packet mine): O(block types).
blocks.handleBulkBlockBreak(player, typeCounts);

// Credit the pickaxe with blocks and XP from a named source.
api.getPickaxeLevelsApi()
   .addBlocksAndExp(player, pickaxe, blockCount, exp, PickaxeExpSource.AREA_ENCHANTS);

// Read a pickaxe's permanent quality tier and what it multiplies. Absent tag = tier 0 = 1.0x.
XPrisonPickaxeQualityAPI quality = api.getPickaxeQualityApi();
double tokenBonus = quality.getCurrencyMultiplier(pickaxe, "tokens");

// PlayerPickaxeQualityUpgradeEvent is cancellable and its cost is mutable, so an addon can
// block an upgrade or discount it before the player is charged.

// A skin's boosts (API 1.10). Each is 1.0 when the skin does not define it. The core already
// applies them - read them for lore/placeholders, do not multiply again.
api.getPickaxeSkinsApi().getPickaxeSkin(pickaxe).ifPresent(skin -> {
    double tokens = skin.getMultiplier("tokens");        // 0.0 when not defined
    double procs  = skin.getEnchantProcMultiplier();     // scales every chance-based enchant's chance
    double exp    = skin.getPickaxeExpMultiplier();      // mining pickaxe XP (never API grants)
    double bpXp   = skin.getBattlePassXpMultiplier();    // Battle Pass XP from mining
});

// Currencies can be capped, so credit and report what was ACTUALLY added — otherwise a
// "you earned %amount%" message overstates the payout whenever the cap clamps it.
BigDecimal credited = api.getCurrencyApi()
        .addBalance(player, currency, amount, ReceiveCause.MINING);

// Drops belong in the backpack, not the inventory, when this is on. Note UltraBackpacks
// reads real world state, so bypass it whenever the affected blocks are virtual.
boolean backpacks = api.isUltraBackpacksEnabled();

Text API

Use this instead of depending on Adventure directly. X-Prison does not shade Adventure — it uses the copy Paper bundles, and on servers that provide none (Spigot, CraftBukkit) it renders MiniMessage down to classic colour codes instead. An addon that imports net.kyori itself therefore works on Paper but fails to link on Spigot, and nothing warns you at compile time, because Paper's API pulls Adventure in transitively.

XPrisonTextAPI text = api.getTextApi();

// Sending — PlaceholderAPI placeholders are resolved for you
text.sendMessage(player, "<gradient:#FFD700:#FFAA00>Well mined!</gradient>");
text.sendMessage(player, List.of("<gray>Line one", "<gray>Line two"));
text.sendActionBar(player, "<green>+100 tokens");
text.sendTitle(player, "<gold><bold>LEVEL UP", "<gray>You reached level %level%", 10, 40, 10);

// Rendering to a String, for sinks that take coloured text rather than sending it
String hologramLine = text.toLegacySection(configLine);  // legacy section codes
String plain        = text.stripTags(gangName);          // no tags at all

// Feature check — hover and click cannot be shown on Spigot
if (text.isRich()) {
    text.sendMessage(player, "<click:open_url:'https://example.com'>Click me</click>");
} else {
    text.sendMessage(player, "<gray>Visit https://example.com");
}
Method Description
sendMessage(CommandSender, String) Send one message
sendMessage(CommandSender, List<String>) Send several lines
sendActionBar(Player, String) Send an action bar message
sendTitle(Player, String, String, int, int, int) Title, subtitle and timings in ticks
toLegacySection(String) Render to legacy § codes for holograms, scoreboards, PAPI returns
stripTags(String) Plain text, for length limits and name comparisons
isRich() true when hover, click and fonts are available (Paper-family servers)

Pass raw config text. MiniMessage tags and legacy & codes are both accepted, and X-Prison renders once, for whichever server it is on. Do not pre-render before passing it in — rendering twice is how player-supplied values (names, nicknames) end up recolouring the message around them.

Never use ChatColor.stripColor on config text. It does nothing to MiniMessage tags, so a name written as <gradient:#FFD700:#FFAA00>Elite</gradient> measures 5 characters through stripTags() and 46 through stripColor. Use stripTags() for every length check and name comparison.

All methods are thread-safe and never throw on malformed input.


Time API (1.10)

Everything time-related in X-Prison runs on one clock: the time: section of config.yml sets the zone every reset and season date is evaluated in, the pattern dates are shown with, and the labels countdowns and unit names use. Addons that show cooldowns, countdowns or timestamps should render them through this API, so a server that reads 5 min in one menu never reads 5m in another, and a translated server never leaks an English Minutes.

XPrisonTimeAPI time = api.getTimeApi();

ZoneId zone = time.getZone();                       // configured zone, or the system zone
LocalDate today = time.now().toLocalDate();         // "today" as the server owner defines it

String left = time.formatDuration(remainingSeconds);            // "1d 4h 20m 5s" - zero units omitted
String when = time.formatDateTime(Instant.ofEpochMilli(at));    // "2026/09/17 21:05:00" by default
String unit = time.getUnitName(TimeUnit.MINUTES);               // "Minutes", or whatever the owner set
String give = time.formatAmount(5, TimeUnit.MINUTES);           // "5 Minutes"
Method Description
getZone() The configured ZoneId; the server's system zone when time.timezone is empty
now() Current ZonedDateTime in that zone
formatDuration(long seconds) Countdown with time.duration-units; zero and negative render as 0s
formatDateTime(Instant) Timestamp with time.date-format in the configured zone
getUnitName(TimeUnit) Display name from time.unit-names, or the capitalised unit name
formatAmount(long, TimeUnit) amount + " " + unit name

All methods are thread-safe and reflect the current configuration after /xprison reload.


Ranks API

XPrisonRanksAPI ranks = api.getRanksApi();

// Online and offline reads
Rank rank = ranks.getPlayerRank(player);
Rank rank = ranks.getPlayerRankOffline(uuid);           // DB lookup, works for offline players
Rank next = ranks.getNextPlayerRank(player);
double progress = ranks.getRankupProgress(player);      // 0.0 – 1.0
boolean isMax = ranks.isMaxRank(player);

// Writes
ranks.setPlayerRank(player, rank);
ranks.setPlayerRankOffline(uuid, rank);                 // persists to DB immediately
ranks.resetPlayerRank(player);

// Leaderboard
List<RankLeaderboardEntry> top = ranks.getTopByRank(10);

// All player UUIDs that have rank data
Set<UUID> uuids = ranks.getAllPlayerUUIDs();

// Rank list
List<Rank> allRanks = ranks.getAllRanks();
Rank max = ranks.getMaxRank();
Rank defaultRank = ranks.getDefaultRank();
Rank byId = ranks.getRankById(id);

// Rank cost scaling (2026.3.8.3) - what this player actually pays
BigDecimal price = ranks.getRankCostFor(player, next);   // rank.getCostExact() x multiplier
BigDecimal factor = ranks.getRankCostMultiplier(player); // BigDecimal.ONE when scaling is off

Prestiges API

XPrisonPrestigesAPI prestiges = api.getPrestigesApi();

Prestige prestige = prestiges.getPlayerPrestige(player);
Prestige prestige = prestiges.getPlayerPrestigeOffline(uuid);   // offline lookup
double progress = prestiges.getPrestigeProgress(player);
boolean isMax = prestiges.isMaxPrestige(player);
Prestige next = prestiges.getNextPlayerPrestige(player);

prestiges.setPlayerPrestige(player, prestige);
prestiges.setPlayerPrestigeOffline(uuid, prestige);             // persists to DB immediately
prestiges.resetPlayerPrestige(player);

List<PrestigeLeaderboardEntry> top = prestiges.getTopByPrestige(10);
Set<UUID> uuids = prestiges.getAllPlayerUUIDs();

List<Prestige> all = prestiges.getAllPrestiges();
Prestige max = prestiges.getMaxPrestige();

Rebirth API

XPrisonRebirthAPI rebirth = api.getRebirthApi();

Rebirth r = rebirth.getPlayerRebirth(player);
Rebirth r = rebirth.getPlayerRebirthOffline(uuid);   // offline lookup
boolean isMax = rebirth.isMaxRebirth(player);
Rebirth next = rebirth.getNextPlayerRebirth(player);

rebirth.setPlayerRebirth(player, r);
rebirth.setPlayerRebirthOffline(uuid, r);            // persists to DB immediately
rebirth.resetPlayerRebirth(player);
rebirth.tryRebirth(player);                          // attempts rebirth, checks requirements

List<RebirthLeaderboardEntry> top = rebirth.getTopByRebirth(10);
Set<UUID> uuids = rebirth.getAllPlayerUUIDs();

List<Rebirth> all = rebirth.getAllRebirths();
Rebirth max = rebirth.getMaxRebirth();

Currency API

XPrisonCurrencyAPI currency = api.getCurrencyApi();

// Balance operations — all work for online AND offline players
BigDecimal balance = currency.getBalance(uuid, currencyName);
currency.addBalance(uuid, currencyName, amount);
currency.removeBalance(uuid, currencyName, amount);    // clamped to 0
currency.setBalance(uuid, currencyName, amount);
boolean has = currency.has(uuid, currencyName, amount);
currency.transferBalance(fromUuid, toUuid, currencyName, amount);

// Leaderboard
List<CurrencyLeaderboardEntry> top = currency.getTopByBalance(currencyName, 10);
List<CurrencyLeaderboardEntry> top = currency.getTopByBalance(currencyName, 10, offset);

// Currency CRUD (live — changes apply immediately and persist to currencies.yml)
currency.createCurrency(name, displayName, prefix, suffix, format, startingBalance);
currency.updateCurrency(name, displayName, prefix, suffix, format);
currency.deleteCurrency(name);
XPrisonCurrency c = currency.getCurrency(name);
List<XPrisonCurrency> all = currency.getAllCurrencies();

Bound currencies (2026.3.8.0). XPrisonCurrency#isTransferable() is false for a currency configured with transferable: false. The pay and withdraw commands refuse such a currency; the API itself does not enforce the flag - tryTransferBalance still moves the funds - so an addon that trades currency between players should check it first.


Multipliers API

XPrisonMultipliersAPI multipliers = api.getMultipliersApi();

// Global multipliers (server-wide, per currency)
GlobalMultiplier g = multipliers.getGlobalMultiplier(currencyName);
multipliers.setGlobalMultiplier(currencyName, value, duration, unit);
multipliers.addGlobalMultiplier(currencyName, value, duration, unit);  // extends if active
multipliers.resetGlobalMultiplier(currencyName);

// Player multipliers (per player, per currency)
PlayerMultiplier p = multipliers.getPlayerMultiplier(player, currencyName);
multipliers.setPlayerMultiplier(player, currencyName, value, duration, unit);

// Rank multipliers (assigned via permission node xprison.multiplier.<rank>)
RankMultiplier r = multipliers.getRankMultiplier(player, currencyName);

Mines API

XPrisonMinesAPI mines = api.getMinesApi();

List<Mine> allMines = mines.getMines();
Mine mine = mines.getMine(name);

// Mine interface — key methods
String name = mine.getName();
World world = mine.getWorld();
int filled = mine.getFilledBlocks();
int total = mine.getTotalBlocks();
double pct = mine.getPercentageFull();            // 0.0 – 1.0
int players = mine.getPlayerCount();

// Added for Dashboard / addon support:
Map<String, Integer> effects = mine.getEffects();         // uppercase effect name → amplifier
String resetType = mine.getResetTypeName();               // "INSTANT" or "GRADUAL"

// Block palette
BlockPalette palette = mine.getBlockPalette();
List<MineBlock> blocks = palette.getBlocks();
palette.setPaletteByIds(Map<String, Double> blockIdToPercentage);  // save and apply new palette

mine.reset();

Auto-Sell API

XPrisonAutoSellAPI autoSell = api.getAutoSellApi();

// Global prices
Map<String, Double> global = autoSell.getGlobalPrices();
autoSell.addSellPrice(MineBlock block, double price);
autoSell.removeSellPrice(MineBlock block);

// Sell regions (per WorldGuard region)
List<SellRegion> regions = autoSell.getSellRegions();
SellRegion region = regions.get(i);
String id = region.getId();
World world = region.getWorld();
String permission = region.getRequiredPermission();
Map<String, Double> prices = region.getPrices();

autoSell.addRegionSellPrice(String regionId, MineBlock block, double price);
autoSell.removeRegionSellPrice(String regionId, MineBlock block);

// 1.9 — exact-precision pricing. Prefer this when multiplying one type's price by a
// large count (an area enchant prices per block *type* and multiplies by how many were
// broken), where a double would lose precision on OP-scale servers.
BigDecimal exact = autoSell.getSellPriceForBlockExact(mineBlock);

Pickaxe Levels API

Levelling rebuilds the pickaxe — it writes the level into NBT, regenerates the display name and re-applies the enchant lore — so it produces a new ItemStack rather than editing the one you passed in. Which method you want depends on where the pickaxe currently lives.

XPrisonPickaxeLevelsAPI levels = api.getPickaxeLevelsApi();

// The pickaxe is already in the player's inventory - the rebuilt item is stored back for you.
levels.setPickaxeLevel(player, heldPickaxe, 25);

// You are still holding the pickaxe yourself - use the returned item. (2026.3.7.0+)
ItemStack pickaxe = new ItemStack(Material.DIAMOND_PICKAXE);
pickaxe = levels.withPickaxeLevel(player, pickaxe, 25);
player.getInventory().addItem(pickaxe);

⚠ setPickaxeLevel only works on an item already in the player's inventory, because that is where it writes the rebuilt stack back to. An item you are still assembling has no slot to write to, and the call is silently lost — use withPickaxeLevel for those.

Before 2026.3.7.0 setPickaxeLevel discarded the rebuilt item entirely, so it silently did nothing for every API caller. If you support older cores, guard the withPickaxeLevel call: it is a default method that throws UnsupportedOperationException on them.

Both methods fire PlayerPickaxeLevelUpEvent, and both are no-ops if the level does not exist or is outside the configured range.


Diagnostics API

Added in 2026.3.7.0. Read-only access to X-Prison's own self-checks — the data behind /xprison lint, /xprison health and /xprison perms. Nothing here mutates plugin state.

XPrisonDiagnosticsAPI diagnostics = api.getDiagnosticsApi();

// The configuration linter. MUST be called from the server main thread - it reads live
// mine and module state. Empty list = a consistent configuration.
List<ConfigFinding> findings = diagnostics.lintConfiguration();
for (ConfigFinding finding : findings) {
    finding.severity();   // FindingSeverity.ERROR or .WARNING
    finding.source();     // the file or section it belongs to, e.g. "enchants.yml"
    finding.message();    // human-readable, ready to show to a server owner
    finding.isError();    // convenience for severity() == ERROR
}

// Warnings collected while the plugin enabled. Fixed once startup completes.
List<String> warnings = diagnostics.getStartupWarnings();

// Every permission node X-Prison checks, grouped by area and sorted by node.
Map<String, List<PermissionEntry>> catalog = diagnostics.getPermissionCatalog();

PermissionEntry is node() / description() / prefix() / defaultValue(). defaultValue() (X-PrisonAPI 1.10, core 2026.3.8.5) is the Bukkit PermissionDefault X-Prison registers the node with at startup - TRUE for every player, OP for operators only, FALSE for a sold perk - so the catalog answers "who holds this out of the box" as well as "what does it gate". A prefix() node is only the stem of the real node — a per-mine or per-currency suffix is appended before the check — so it is never checked verbatim.

The linter reports enchants fighting over the same GUI slot across all four enchant menus, enchants priced in a currency that does not exist, an empty supported-pickaxes, and mines whose block percentages do not total 100 % or that have no teleport point.


Player Preferences API

Added in 2026.3.7.0. The per-player on/off switches surfaced in /toggles. X-Prison ships three, and any module or addon may store its own under its own key — it then appears in /toggles automatically. Prefix your own key with your addon's name to avoid collisions.

XPrisonPlayerPreferencesAPI prefs = api.getPlayerPreferencesApi();

// Works for offline players - the value is read from, and written to, the database.
boolean auto = prefs.isEnabled(uuid, XPrisonPlayerPreferencesAPI.AUTO_REBIRTH, true);
prefs.set(uuid, XPrisonPlayerPreferencesAPI.AUTO_REBIRTH, false);

// Only the keys this player has explicitly set.
Map<String, Boolean> all = prefs.getPreferences(uuid);
prefs.getPreferencesAsync(uuid).thenAccept(map -> { /* ... */ });

Constants: AUTO_RANKUP, AUTO_PRESTIGE, AUTO_REBIRTH.

A preference has three states, not two: on, off, and never set. A player who has never opened /toggles has no stored value, which is why every read takes an explicit fallback rather than defaulting to false — pass the server-wide default for that feature.

A server switch still wins. These preferences decide whether a feature applies to a player, not whether it exists. Auto-rebirth does nothing while auto-rebirth is false in rebirths.yml, whatever this returns.

Threading. Reads for an online player are served from an in-memory cache and are safe anywhere. Reads for an offline player hit the database and block — use getPreferencesAsync(UUID), or run them off the main thread yourself. Writes never block.


Events

Listen to X-Prison events like any Bukkit event. All events are in the dev.drawethree.xprison.api package hierarchy.

Enchant Events

Event Cancellable Description
XPrisonEnchantPreTriggerEvent Yes Fired before a chance-based enchant rolls to trigger. Exposes the enchantment, getLevel() and getChanceToTrigger() (mutable) — adjust the proc chance or cancel the trigger. Also fired for reward-multiplier enchants (Token/Gem Merchant).
XPrisonEnchantTriggerEvent No Fired when an enchant successfully triggers. Exposes the enchantment and getLevel().
XPrisonPlayerEnchantEvent Yes Fired when a player buys enchant levels. Exposes getTokenCostExact() (BigDecimal, mutable via setTokenCostExact) and getLevel(). getTokenCost()/setTokenCost() remain as a saturating long view. Cancelling prevents the purchase.
XPrisonEnchantDisenchantEvent Yes Fired when a player refunds enchant levels. Exposes the enchantment, getCurrentLevel(), getLevelsRemoved(), isAdmin() and getRefundAmountExact() (BigDecimal, mutable via setRefundAmountExact). getRefundAmount()/setRefundAmount() remain as a saturating long view. Cancelling prevents the disenchant.
XPrisonEnchantPrestigeEvent Yes Fired when a player prestiges an enchant. Exposes getOldPrestige(), getNewPrestige() (mutable), and the enchantment. Cancelling prevents the prestige.
XPrisonEnchantRegisterEvent / XPrisonEnchantUnregisterEvent No Fired when an enchant is registered/unregistered in the repository (e.g. by an addon).
PickaxeSoulbindEvent Yes Fired when a pickaxe is soulbound to a player (including automatic bind-on-first-hold). Exposes getPlayer() (new owner) and getItemStack(). Cancelling prevents the bind.
PickaxeUnsoulbindEvent Yes Fired when a pickaxe's soulbind is cleared (e.g. /unsoulbind). Exposes getPreviousOwner() (nullable) and getItemStack(). Cancelling keeps the soulbind.

Progression Events

Event Cancellable Description
PlayerRankUpEvent Yes A player ranks up. Exposes getOldRank() and getNewRank() (mutable). Package api.ranks.events.
PlayerPrestigeEvent Yes A player's prestige changes. Exposes getOldPrestige() and getNewPrestige() (mutable). Package api.prestiges.events.
PlayerRebirthEvent Yes A player rebirths. Exposes getOldRebirth() and getNewRebirth() (mutable). Package api.rebirth.events.
PlayerMilestoneReachedEvent Yes See Milestone Events.

Pickaxe Events

Event Cancellable Description
PlayerPickaxeExpGainEvent Yes A pickaxe is about to gain XP. Exposes getPickaxe(), getSource() (PickaxeExpSource) and getAmount() (mutable; <= 0 suppresses the gain).
PlayerProgressionXpGainEvent Yes A companion plugin's progression is about to award XP (API 1.10). Exposes getProgression() (namespaced key, e.g. xprisonarmors:armor) and getAmount() (mutable; <= 0 suppresses the gain). Lives in api.shared.events.
PlayerPickaxeLevelUpEvent No A pickaxe reached a new level.
PlayerPickaxeQualityUpgradeEvent Yes A player buys the next quality tier.
PlayerPickaxeSkinChangeEvent Yes A skin is about to be applied or removed. Exposes getOldSkin() and getNewSkin() (either may be null). Cancelling keeps the current skin.

Auto-Miner Events

Event Cancellable Description
PlayerAutomineEvent Yes One auto-mine tick for a player standing in the region. Exposes getTimeLeft() in seconds.
PlayerAutoMinerRegionEnterEvent / PlayerAutoMinerRegionLeaveEvent No A player walks into / out of an AutoMiner region. Exposes getRegion().
PlayerAutoMinerTimeModifyEvent No A player's AutoMiner time changes. Exposes getTimeUnit() and getDuration() (negative when time is removed).
PlayerAutoMinerTierUpgradeEvent Yes A player upgrades their AutoMiner tier, after the cost is charged. Exposes getFromTier() and getToTier().

Block Events

Event Cancellable Description
XPrisonBlockBreakEvent No Fired for every block broken by an X-Prison pickaxe
XPrisonBulkBlockBreakEvent Yes (1.9) Fired once for a break too large to enumerate block-by-block — notably a whole packet ("virtual") mine, whose blocks have no real Block handles. Carries a Map<MineBlock, Long> of type → count plus getTotalBlocks(), so consumers scale in O(distinct block types) instead of O(blocks). Listen to this in addition to XPrisonBlockBreakEvent if your plugin tracks mined volume.

Gang Events

Event Cancellable Description
GangCreateEvent Yes A gang is created. Exposes getGangLeader() and getGang().
GangDisbandEvent Yes A gang is disbanded. Exposes getGang().
GangJoinEvent / GangLeaveEvent Yes A player joins/leaves a gang (GangLeaveEvent also covers kicks via getLeaveReason()).
GangInviteEvent Yes A player is invited to a gang. Exposes getInviter(), getPlayer() (the invited) and getGang().
GangRenameEvent Yes A gang is renamed. Exposes getGang(), getOldName(), getNewName() (mutable) and getWhoRenamed().
GangValueChangeEvent Yes A gang's value changes (admin modify/set/add). Exposes getGang(), getOldValue() and getNewValue() (mutable).
GangOwnershipTransferEvent Yes Gang ownership is transferred. Exposes getGang(), getOldOwner() and getNewOwner().

Mine Events

Event Cancellable Description
MineCreateEvent / MineDeleteEvent Yes A mine is created/deleted.
MineRenameEvent Yes A mine is renamed. Exposes getOldName() / getNewName().
MinePreResetEvent / MinePostResetEvent Pre only Fired before/after a mine resets.
MineTeleportEvent Yes A player is about to be teleported into a mine. Exposes getPlayer() and getMine().
MineRedefineEvent Yes A mine's region (bounds) is about to be redefined from a new selection. Exposes getPlayer() and getMine().

Multiplier Events

Event Cancellable Description
PlayerMultiplierReceiveEvent No A player receives a personal multiplier.
PlayerMultiplierExpireEvent No A player's multiplier expires.
PlayerMultiplierResetEvent Yes A player's personal multiplier is reset. Exposes getPlayer(), getCurrency() and getPreviousMultiplier().
GlobalMultiplierSetEvent Yes A server-wide multiplier is set. Exposes getCurrency(), getMultiplier() (mutable), getTimeUnit() and getDuration().
GlobalMultiplierResetEvent Yes A server-wide multiplier is reset. Exposes getCurrency() and getPreviousMultiplier().

Auto-Sell Events

Event Cancellable Description
XPrisonAutoSellEvent / XPrisonSellAllEvent Yes Items are auto-sold on mine / via /sellall. Exposes the mutable itemsToSell map and the sell region. Since 2026.2.8.0 the map is Map<AutoSellItemStack, BigDecimal> (was Double) for exact OP-scale prices — recompile addons that listen to these events.
AutoSellToggleEvent Yes A player's AutoSell preference is about to change. Exposes getPlayer() and isNewState() (true = enabled).

Currency Events

Event Cancellable Description
PlayerCurrencyReceiveEvent Yes A player receives currency. Exposes the cause and getAmount() (mutable).
PlayerCurrencyLoseEvent No A player loses currency. Exposes the cause and getAmount() (mutable).
PlayerCurrencyBalanceSetEvent Yes A player's balance is set. Exposes getOldAmount() and getNewAmount() (mutable).
PlayerCurrencyPayEvent Yes A player pays currency to another player (single transaction). Exposes getSender(), getReceiver(), getCurrency() and getAmount() (mutable). Cancelling blocks the whole pay.

Bomb Events

Event Cancellable Description
BombThrowEvent Yes A player throws a bomb, before it is taken from the inventory and before its fuse starts. getLocation() is the resolved impact point the bomb will land on and explode at; cancelling leaves the bomb in the inventory and starts no cooldown. Exposes getPlayer() and getBomb().
BombExplodeEvent Yes A bomb explodes. Listeners approve affected blocks via addAffectedBlocks(...).
BombGiveEvent Yes A player is given bomb item(s). Exposes getPlayer(), getBomb() and getAmount().

Battle Pass / Quests / Daily Rewards Events

These modules fire their events via Bukkit's callEvent.

Event Cancellable Description
BattlePassXpGainEvent Yes A player gains Battle Pass XP. Exposes the XpSource and getAmount() (mutable).
BattlePassTierUpEvent No A player advances one or more tiers.
BattlePassRewardClaimEvent Yes A player is about to claim a tier reward. Exposes getPlayer(), getTier() and getTrack().
BattlePassPremiumChangeEvent No A player's premium status changes. Exposes getUuid() and isPremium(). May fire off the main thread.
BattlePassSeasonResetEvent No A new season starts. Exposes the old/new season ids.
QuestCompleteEvent / QuestClaimEvent / QuestAssignEvent No A quest is completed / claimed / assigned.
PlayerDailyRewardClaimEvent No A daily reward is claimed. Exposes getStreak() and getCycleDay().

Milestone Events

Event Cancellable Description
PlayerMilestoneReachedEvent Yes A player is about to be paid out for a milestone. Exposes getMilestone() and getValue(). Cancelling suppresses the whole payout - title, message, broadcast, sound, firework and reward commands - but the player's progress is still recorded, so the milestone does not fire again.

PrestigeableEnchant Interface

Enchants that support prestiging implement dev.drawethree.xprison.api.enchants.model.PrestigeableEnchant:

public interface PrestigeableEnchant {
    boolean isPrestigeEnabled();
    int getMaxPrestige();
    double getMultiplierPerPrestige();
    long getRequiredActivations(int currentPrestige);
}

Use this to check whether an enchant supports prestiging and read its configuration at runtime.


GUI Pages

Since X-Prison 2026.3.3.0. The enchant menus are paginated, and two interfaces gained a page accessor to go with the existing slot accessor:

// dev.drawethree.xprison.api.enchants.model.XPrisonEnchantmentGuiProperties
int getGuiSlot();
default int getGuiPage() { return 1; }   // 1-based

// dev.drawethree.xprison.api.enchants.model.RefundableEnchant
int getRefundGuiSlot();
default int getRefundGuiPage() { return 1; }   // 1-based

Both are default methods, so existing addons keep compiling and running unchanged — they simply report page 1. If you extend XPrisonEnchantmentBase, both values are read from your enchant's JSON for free: gui.page and refund.guiPage, each optional and defaulting to 1.

Pages are 1-based. You do not need to guarantee a free slot: if the slot or page you ask for is already taken, or falls outside the menu's configured content region, the core moves your enchant to the next free slot and onto a later page if required. An out-of-range slot can no longer throw and break the menu.

refund.guiSlot is now optional too (defaulting to -1), so an enchant JSON that omits it still loads instead of failing outright.


Notes

Both 2026.3.7.0 APIs are optional. getDiagnosticsApi() and getPlayerPreferencesApi() are default methods that throw UnsupportedOperationException on an older core, so an addon compiled against 2026.3.7.0 still loads on an older one. If you support both, guard the call:

try {
    var diagnostics = api.getDiagnosticsApi();
    // ... use it
} catch (UnsupportedOperationException e) {
    // running on a core older than 2026.3.7.0
}
  • The API jar is provided scope — do not shade it into your plugin or addon.
  • Check the X-PrisonAPI GitHub repository for the most up-to-date interface definitions.
  • For questions about the API, open a ticket in the Discord server.

XPrison Logo

General

Modules

Default Configs

Enchant Configs — Passive

Enchant Configs — Currency Rewards

Enchant Configs — Key & Item Rewards

Enchant Configs — Area of Effect

Enchant Configs — Multipliers

Enchant Configs — Templates

Enchant Configs — Addons

Addons

Support

For Developers

Others

Clone this wiki locally