Version: 1.5.0
Last Updated: December 10, 2025
The main entry point for all Ion plugins.
public interface IonPlugin {
void onEnable();
void onDisable();
String getName();
Logger getLogger();
IonScheduler getScheduler();
CommandRegistry getCommandRegistry();
ConfigurationProvider getConfigProvider();
EventBus getEventBus();
File getDataFolder();
String getPlatform();
}Methods:
onEnable()- Called when the plugin is enabledonDisable()- Called when the plugin is disabledgetName()- Returns the plugin namegetLogger()- Returns the plugin loggergetScheduler()- Returns the unified schedulergetCommandRegistry()- Returns the command registrygetConfigProvider()- Returns the configuration providergetEventBus()- Returns the event busgetDataFolder()- Returns the plugin's data foldergetPlatform()- Returns the platform name ("paper" or "folia")
Unified thread-safe scheduler for Paper and Folia.
public interface IonScheduler {
IonTask run(Runnable task);
IonTask runAsync(Runnable task);
IonTask runLater(Runnable task, long delay, TimeUnit unit);
IonTask runLaterAsync(Runnable task, long delay, TimeUnit unit);
IonTask runTimer(Runnable task, long delay, long period, TimeUnit unit);
IonTask runTimerAsync(Runnable task, long delay, long period, TimeUnit unit);
void cancelAll();
boolean isMainThread();
}Examples:
// Run immediately on main thread
scheduler.run(() -> {
// Your code here
});
// Run async (off main thread)
scheduler.runAsync(() -> {
// Database query, API call, etc.
});
// Run after 5 seconds
scheduler.runLater(() -> {
player.sendMessage("5 seconds passed!");
}, 5, TimeUnit.SECONDS);
// Run every second, starting after 0 seconds
IonTask task = scheduler.runTimer(() -> {
// Repeating task
}, 0, 1, TimeUnit.SECONDS);
// Cancel the task later
task.cancel();Represents a scheduled task.
public interface IonTask {
int getId();
boolean isCancelled();
void cancel();
boolean isRunning();
Object getOwner();
}Base interface for all commands.
public interface IonCommand {
boolean execute(CommandContext context);
String getName();
String getDescription();
String getUsage();
String getPermission();
}Example Implementation:
public class HelloCommand implements IonCommand {
@Override
public boolean execute(CommandContext ctx) {
if (!ctx.hasPermission(getPermission())) {
ctx.reply("§cNo permission!");
return false;
}
String name = ctx.getArg(0, "World");
ctx.reply("§aHello, " + name + "!");
return true;
}
@Override
public String getName() {
return "hello";
}
@Override
public String getDescription() {
return "Greets a player";
}
@Override
public String getUsage() {
return "/hello [name]";
}
@Override
public String getPermission() {
return "myplugin.hello";
}
}Provides context for command execution.
public interface CommandContext {
Object getSender();
List<String> getArgs();
String getArg(int index);
String getArg(int index, String defaultValue);
int getArgCount();
void reply(String message);
boolean hasPermission(String permission);
}Manages command registration.
public interface CommandRegistry {
void register(IonCommand command);
boolean unregister(String name);
IonCommand getCommand(String name);
List<IonCommand> getCommands();
void unregisterAll();
}Example:
@Override
public void onEnable() {
CommandRegistry registry = getCommandRegistry();
registry.register(new HelloCommand());
registry.register(new GiveItemCommand());
}Type-safe configuration interface.
public interface IonConfig {
Object get(String path);
Object get(String path, Object defaultValue);
String getString(String path);
String getString(String path, String defaultValue);
int getInt(String path);
int getInt(String path, int defaultValue);
double getDouble(String path);
double getDouble(String path, double defaultValue);
boolean getBoolean(String path);
boolean getBoolean(String path, boolean defaultValue);
List<?> getList(String path);
List<String> getStringList(String path);
void set(String path, Object value);
boolean contains(String path);
Set<String> getKeys(boolean deep);
Map<String, Object> getSection(String path);
void save();
void reload();
}Example config.yml:
database:
host: localhost
port: 3306
name: mydb
messages:
welcome: "§aWelcome to the server!"
goodbye: "§cSee you later!"
features:
- anti-cheat
- economy
- shopsExample Usage:
IonConfig config = getConfigProvider().getConfig();
// Get values
String host = config.getString("database.host", "localhost");
int port = config.getInt("database.port", 3306);
boolean enabled = config.getBoolean("enabled", true);
// Get lists
List<String> features = config.getStringList("features");
// Set values
config.set("last-updated", System.currentTimeMillis());
config.save();
// Reload from disk
config.reload();Manages multiple config files.
public interface ConfigurationProvider {
IonConfig loadConfig(String fileName);
IonConfig createConfig(String fileName, IonConfig defaults);
IonConfig getConfig();
void saveAll();
void reloadAll();
}Example:
// Get main config
IonConfig config = getConfigProvider().getConfig();
// Load custom config
IonConfig messages = getConfigProvider().loadConfig("messages.yml");
// Save all configs
getConfigProvider().saveAll();Base interface for custom events.
public interface IonEvent {
String getEventName();
boolean isCancelled();
void setCancelled(boolean cancelled);
boolean isCancellable();
}Example Custom Event:
public class PlayerBalanceChangeEvent implements IonEvent {
private final Player player;
private double oldBalance;
private double newBalance;
private boolean cancelled = false;
public PlayerBalanceChangeEvent(Player player, double oldBalance, double newBalance) {
this.player = player;
this.oldBalance = oldBalance;
this.newBalance = newBalance;
}
public Player getPlayer() { return player; }
public double getOldBalance() { return oldBalance; }
public double getNewBalance() { return newBalance; }
public void setNewBalance(double balance) { this.newBalance = balance; }
@Override
public String getEventName() { return "PlayerBalanceChange"; }
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override
public boolean isCancellable() { return true; }
}Event dispatcher with priority support.
public interface EventBus {
<T extends IonEvent> ListenerHandle subscribe(Class<T> eventClass, Consumer<T> listener);
<T extends IonEvent> ListenerHandle subscribe(Class<T> eventClass, EventPriority priority, Consumer<T> listener);
<T extends IonEvent> T fire(T event);
void unsubscribeAll();
}Example:
// Subscribe to event
EventBus eventBus = getEventBus();
eventBus.subscribe(PlayerBalanceChangeEvent.class, event -> {
Player player = event.getPlayer();
double newBalance = event.getNewBalance();
if (newBalance < 0) {
event.setCancelled(true);
player.sendMessage("§cCannot have negative balance!");
}
});
// Fire event
PlayerBalanceChangeEvent event = new PlayerBalanceChangeEvent(player, 100.0, 150.0);
eventBus.fire(event);
if (!event.isCancelled()) {
// Apply the change
}Priority levels for event handlers.
public enum EventPriority {
LOWEST, // Executed first
LOW,
NORMAL, // Default
HIGH,
HIGHEST, // Executed last
MONITOR // Read-only, observe final state
}Example with Priority:
// High priority - runs late
eventBus.subscribe(PlayerBalanceChangeEvent.class, EventPriority.HIGH, event -> {
getLogger().info("Balance changed for " + event.getPlayer().getName());
});
// Monitor - observe final state
eventBus.subscribe(PlayerBalanceChangeEvent.class, EventPriority.MONITOR, event -> {
if (!event.isCancelled()) {
// Log the change
logToDatabase(event);
}
});Adventure API text formatting utilities.
public final class TextUtil {
static Component parse(String message);
static Component legacyColor(String message);
static Component colored(String message, NamedTextColor color);
static Component bold(String message);
static Component italic(String message);
static Component underlined(String message);
static Component strikethrough(String message);
static String stripColor(String message);
}Examples:
import static com.ionapi.api.util.TextUtil.*;
// MiniMessage format
Component msg = parse("<green>Hello <bold>World</bold>!");
// Legacy color codes
Component legacy = legacyColor("&aGreen &c&lBold Red");
// Simple coloring
Component colored = colored("Error!", NamedTextColor.RED);
// Decorations
Component bold = bold("Important!");
Component italic = italic("Note:");
Component underlined = underlined("Click here");
// Strip colors
String plain = stripColor("§aColored text"); // Returns "Colored text"Check which platform your plugin is running on:
String platform = getPlatform();
if (platform.equals("folia")) {
// Folia-specific logic
getLogger().info("Running on Folia!");
} else {
// Paper or Spigot
getLogger().info("Running on Paper/Spigot!");
}✅ DO:
- Use
runAsync()for database operations, API calls, file I/O - Use
run()orrunLater()for world/entity modifications - Check
isMainThread()when unsure
❌ DON'T:
- Access Bukkit API from async tasks
- Perform blocking operations on main thread
@Override
public void onDisable() {
// Cancel all scheduled tasks
getScheduler().cancelAll();
// Unregister all commands
getCommandRegistry().unregisterAll();
// Unsubscribe all event listeners
getEventBus().unsubscribeAll();
// Save configs
getConfigProvider().saveAll();
}scheduler.runAsync(() -> {
try {
// Potentially failing operation
performDatabaseQuery();
} catch (Exception e) {
getLogger().severe("Database error: " + e.getMessage());
e.printStackTrace();
}
});- Getting Started - Complete tutorial
- Examples - Code examples
- Quick Reference - Cheatsheet
- Migration Guide - Migrate from Bukkit
- Folia Guide - Folia compatibility
Need help? Join our Discord!
Support the project:
Flicker-free scoreboard with per-line updates.
// Create with builder
IonScoreboard board = IonScoreboard.builder()
.title("<gradient:gold:yellow><bold>My Server")
.line(15, "<gray>Welcome, <white>{player}")
.line(14, "")
.line(13, "<gold>Coins: <yellow>{coins}")
.line(12, "<green>Online: <white>{online}")
.placeholder("player", p -> p.getName())
.placeholder("coins", p -> String.valueOf(getCoins(p)))
.placeholder("online", p -> String.valueOf(Bukkit.getOnlinePlayers().size()))
.updateInterval(20) // Auto-update every second
.build();
board.show(player);
board.update(player); // Manual refresh
board.hide(player);
board.destroy();Animated Lines:
IonScoreboard.builder()
.title("<gold>Server")
.animatedLine(15, 10, "Frame 1", "Frame 2", "Frame 3") // Cycles every 10 ticks
.animatedLine(14, "Fast", "Animation") // Default 10 tick interval
.build();Methods:
builder()- Creates new buildertitle(String)- Sets title (MiniMessage)line(int, String)- Adds line at scorelines(int, String...)- Adds multiple linesanimatedLine(int, long, String...)- Animated lineplaceholder(String, Function)- Dynamic placeholderupdateInterval(long)- Auto-update in ticksshow(Player)- Shows to playerupdate(Player)- Updates for playersetLine(Player, int, String)- Update single lineremoveLine(int)- Removes a linehide(Player)- Hides from playerdestroy()- Destroys scoreboard
Simple yes/no dialog for confirmations.
ConfirmationGui.create()
.title("<red>⚠ Confirm")
.message("Delete all data?")
.onConfirm(player -> {
deleteData();
player.sendMessage("<green>Deleted!");
})
.onCancel(player -> player.sendMessage("Cancelled"))
.open(player);
// Danger styling (red theme)
ConfirmationGui.create()
.message("This cannot be undone!")
.danger()
.onConfirm(player -> dangerousAction())
.open(player);
// Success styling (green theme)
ConfirmationGui.simple("Proceed?", player -> doAction())
.success()
.open(player);Methods:
create()- Creates new buildersimple(String, Consumer)- Quick creationtitle(String)- Sets titlemessage(String)- Sets messagerows(int)- Sets rows (3-6)onConfirm(Consumer)- Confirm actiononCancel(Consumer)- Cancel actionconfirmItem(ItemStack)- Custom confirm buttoncancelItem(ItemStack)- Custom cancel buttondanger()- Red stylingsuccess()- Green stylingnoSounds()- Disable soundsopen(Player)- Opens dialog
Skull Textures:
// Custom head texture via base64
ItemStack head = IonItem.builder(Material.PLAYER_HEAD)
.skullTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy4uLiJ9fX0=")
.name("<gold>Custom Head")
.build();Leather Armor Colors:
ItemStack armor = IonItem.builder(Material.LEATHER_CHESTPLATE)
.color(Color.RED)
.color(255, 100, 50) // RGB
.name("<red>Fire Armor")
.build();Potion Effects:
ItemStack potion = IonItem.builder(Material.POTION)
.potionType(PotionType.STRENGTH)
.potionEffect(PotionEffectType.SPEED, 600, 2) // 30 sec Speed III
.potionColor(Color.PURPLE)
.name("<purple>Super Potion")
.build();New Methods:
skullTexture(String)- Base64 texture for PLAYER_HEADcolor(Color)- Color for leather armorcolor(int, int, int)- RGB color for leatherpotionEffect(PotionEffect)- Add potion effectpotionEffect(Type, duration, amplifier)- Add effectpotionType(PotionType)- Set base potion typepotionColor(Color)- Set potion color
Thread-safe cooldown management for player actions.
CooldownManager cooldowns = CooldownManager.create("teleport");
// Check cooldown
if (cooldowns.isOnCooldown(player.getUniqueId())) {
long remaining = cooldowns.getRemainingTime(player.getUniqueId(), TimeUnit.SECONDS);
player.sendMessage("Wait " + remaining + "s!");
return;
}
// Set cooldown
cooldowns.setCooldown(player.getUniqueId(), 30, TimeUnit.SECONDS);
// Remove cooldown
cooldowns.removeCooldown(player.getUniqueId());
// Cleanup expired
int removed = cooldowns.cleanup();Methods:
create(String name)- Creates or gets a named cooldown managersetCooldown(UUID, long, TimeUnit)- Sets a cooldownisOnCooldown(UUID)- Checks if on cooldowngetRemainingTime(UUID, TimeUnit)- Gets remaining timeremoveCooldown(UUID)- Removes cooldownclearAll()- Clears all cooldownscleanup()- Removes expired cooldowns
Sliding window rate limiting for spam prevention.
// Allow 5 requests per 10 seconds
RateLimiter limiter = RateLimiter.create("chat", 5, 10, TimeUnit.SECONDS);
// Try to acquire permit
if (!limiter.tryAcquire(player.getUniqueId())) {
int remaining = limiter.getRemainingPermits(player.getUniqueId());
player.sendMessage("Rate limited! " + remaining + " permits left");
return;
}
// Get reset time
long resetTime = limiter.getResetTime(player.getUniqueId(), TimeUnit.SECONDS);Methods:
create(String, int, long, TimeUnit)- Creates rate limitertryAcquire(UUID)- Attempts to acquire permitgetRemainingPermits(UUID)- Gets remaining permitsgetResetTime(UUID, TimeUnit)- Gets time until resetreset(UUID)- Resets for playerclearAll()- Clears all limits
Fluent MiniMessage builder with templates.
// Simple message
MessageBuilder.of("<green>Hello, <player>!")
.placeholder("player", player.getName())
.send(player);
// Title
MessageBuilder.of("<gold><bold>LEVEL UP!")
.subtitle("<gray>You are now level <level>")
.placeholder("level", "10")
.timing(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
.sendTitle(player);
// Action bar
MessageBuilder.of("<red>❤ <health>/<max>")
.placeholder("health", "15")
.placeholder("max", "20")
.sendActionBar(player);
// Templates
MessageBuilder.registerTemplate("welcome", "<gradient:gold:yellow>Welcome to <server>!");
MessageBuilder.template("welcome")
.placeholder("server", "My Server")
.broadcast();Methods:
of(String)- Creates builder with messagetemplate(String)- Creates from templateplaceholder(String, String)- Adds placeholdersubtitle(String)- Sets subtitletiming(Duration, Duration, Duration)- Sets title timingsend(Player)- Sends messagesendTitle(Player)- Sends as titlesendActionBar(Player)- Sends as action barbroadcast()- Broadcasts to all players
Easy scoreboard creation with MiniMessage.
IonScoreboard board = IonScoreboard.builder()
.title("<gradient:gold:yellow><bold>My Server")
.line(15, "<gray>Welcome, <white>{player}")
.line(14, "")
.line(13, "<gold>Coins: <yellow>{coins}")
.line(12, "<green>Online: <white>{online}")
.placeholder("player", p -> p.getName())
.placeholder("coins", p -> String.valueOf(getCoins(p)))
.placeholder("online", p -> String.valueOf(Bukkit.getOnlinePlayers().size()))
.build();
board.show(player);
board.update(player); // Refresh
board.hide(player);Methods:
builder()- Creates new buildertitle(String)- Sets titleline(int, String)- Adds line at scorelines(int, String...)- Adds multiple linesplaceholder(String, Function<Player, String>)- Adds placeholdershow(Player)- Shows to playerupdate(Player)- Updates for playerhide(Player)- Hides from playerdestroy()- Destroys scoreboard
Boss bar management with MiniMessage.
IonBossBar bar = IonBossBar.builder()
.name("event-progress")
.title("<gradient:red:orange>Event: {progress}%")
.color(BossBar.Color.RED)
.style(BossBar.Overlay.PROGRESS)
.progress(0.5f)
.placeholder("progress", p -> "50")
.build();
bar.show(player);
bar.setProgress(0.75f);
bar.setTitle("<gradient:green:yellow>Almost done!");
bar.hide(player);Methods:
builder()- Creates new buildername(String)- Sets unique nametitle(String)- Sets titlecolor(BossBar.Color)- Sets colorstyle(BossBar.Overlay)- Sets styleprogress(float)- Sets progress (0.0-1.0)placeholder(String, Function)- Adds placeholdershow(Player)- Shows to playersetProgress(float)- Updates progresssetTitle(String)- Updates titlehide(Player)- Hides from player
Lightweight performance monitoring.
// Counters
Metrics.increment("player.join");
Metrics.increment("blocks.broken", 5);
long joins = Metrics.getCount("player.join");
// Gauges
Metrics.gauge("players.online", Bukkit.getOnlinePlayers().size());
long online = Metrics.getGauge("players.online");
// Timing
Metrics.time("database.query", () -> {
// operation
});
String result = Metrics.time("api.call", () -> {
return callApi();
});
// Statistics
double avgTime = Metrics.getAverageTime("database.query");
Metrics.TimingStats stats = Metrics.getTimingStats("database.query");
System.out.println("Min: " + stats.getMinMs());
System.out.println("Max: " + stats.getMaxMs());
System.out.println("Avg: " + stats.getAverageMs());Methods:
increment(String)- Increments counterincrement(String, long)- Increments by amountgetCount(String)- Gets counter valuegauge(String, long)- Sets gauge valuegetGauge(String)- Gets gauge valuetime(String, Runnable)- Times operationtime(String, Supplier<T>)- Times and returns resultgetAverageTime(String)- Gets average time in msgetTimingStats(String)- Gets detailed statsreset()- Resets all metrics
Efficient bulk database operations (10-50x faster).
List<PlayerStats> stats = new ArrayList<>();
// ... populate list
// Batch insert
BatchOperation.BatchResult result = database.batch(PlayerStats.class)
.insertAll(stats)
.batchSize(500)
.execute();
System.out.println("Inserted: " + result.insertedCount());
System.out.println("Time: " + result.executionTimeMs() + "ms");
// Batch update
database.batch(PlayerStats.class)
.updateAll(stats)
.executeAsync()
.thenAccept(r -> {
System.out.println("Updated: " + r.updatedCount());
});
// Mixed operations
database.batch(PlayerStats.class)
.insertAll(newStats)
.updateAll(existingStats)
.deleteAll(oldStats)
.batchSize(1000)
.execute();Methods:
insert(T)- Adds entity for insertioninsertAll(List<T>)- Adds multiple for insertionupdate(T)- Adds entity for updateupdateAll(List<T>)- Adds multiple for updatedelete(T)- Adds entity for deletiondeleteAll(List<T>)- Adds multiple for deletionbatchSize(int)- Sets batch size (default: 1000)execute()- Executes synchronouslyexecuteAsync()- Executes asynchronously
BatchResult:
insertedCount()- Number of insertsupdatedCount()- Number of updatesdeletedCount()- Number of deletestotalAffected()- Total affected rowsexecutionTimeMs()- Execution time in milliseconds