-
Notifications
You must be signed in to change notification settings - Fork 57
Developer API
The X-Prison public API lives in a dedicated project: X-PrisonAPI on GitHub
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]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 /togglesBattle 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();
awardMilestonedoes not touch recorded progress, so a milestone the player already earned is paid a second time.setHighestProgressis 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();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 runningModule 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 restartAddons 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 adefaultno-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.
X-Prison resolves load order across all addon JARs before instantiating any of them. The rules, in priority order:
-
Dependencies first — if your manifest declares
X-Prison-Depends: CoreAddon,CoreAddonis always loaded before your addon, regardless of priority numbers. -
Lower priority number loads first — among addons with no dependency relationship, the one with the smaller
X-Prison-Priorityvalue loads first. Default is50. - 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
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.
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.
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();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.stripColoron config text. It does nothing to MiniMessage tags, so a name written as<gradient:#FFD700:#FFAA00>Elite</gradient>measures 5 characters throughstripTags()and 46 throughstripColor. UsestripTags()for every length check and name comparison.
All methods are thread-safe and never throw on malformed input.
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.
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 offXPrisonPrestigesAPI 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();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();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.
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);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();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);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);⚠
setPickaxeLevelonly 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 — usewithPickaxeLevelfor those.Before 2026.3.7.0
setPickaxeLeveldiscarded the rebuilt item entirely, so it silently did nothing for every API caller. If you support older cores, guard thewithPickaxeLevelcall: it is adefaultmethod that throwsUnsupportedOperationExceptionon them.
Both methods fire PlayerPickaxeLevelUpEvent, and both are no-ops if the level does not exist or is
outside the configured range.
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.
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.
Listen to X-Prison events like any Bukkit event. All events are in the dev.drawethree.xprison.api package hierarchy.
| 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. |
| 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. |
| 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. |
| 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(). |
| 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. |
| 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(). |
| 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(). |
| 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(). |
| 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). |
| 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. |
| 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(). |
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(). |
| 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. |
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.
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-basedBoth 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.
Both 2026.3.7.0 APIs are optional.
getDiagnosticsApi()andgetPlayerPreferencesApi()aredefaultmethods that throwUnsupportedOperationExceptionon 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
providedscope — 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.
- Currencies
- Ranks
- Prestiges
- Rebirths
- Mines
- AutoSell
- AutoMiner
- Enchants
- Pickaxe Levels
- Pickaxe Skins
- Pickaxe Quality
- Pickaxe Settings
- Gangs
- Multipliers
- Blocks
- Bombs
- History
- Mining Stats
- Nicknames
- Battle Pass
- Quests
- Daily Rewards
- Milestones
- config.yml
- autominer.yml
- autosell.yml
- block-rewards.yml
- enchants.yml
- currencies.yml
- multipliers.yml
- ranks.yml
- prestiges.yml
- pickaxe-levels.yml
- pickaxe-skins.yml
- pickaxe-quality.yml
- gangs.yml
- mines.yml
- bombs.yml
- blocks.yml
- history.yml
- logging.yml
- mining-stats.yml
- rebirths.yml
- battlepass.yml
- quests.yml
- dailyrewards.yml
- milestones.yml
- efficiency.json
- fortune.json
- unbreaking.json
- haste.json
- speed.json
- fly.json
- nightvision.json
- jumpboost.json
- autosell.json
- tokenfinder.json
- gemfinder.json
- salary.json
- charity.json
- blessing.json
- gangvaluefinder.json
- prestigefinder.json
- explosive.json
- nuke.json
- layer.json
- laserbeam.json
- second-hand.json
- sixth-hand.json
- doomfall.json
- prism-break.json
- gem-merchant.json
- token-merchant.json
- super-token-miner.json
- double-strike.json
- prodigy.json
- blockbooster.json
