BukkitOfUtils — Developer Guide
BOU provides a framework for building Bukkit plugins with built-in Folia support, command building, configuration management, and async task scheduling.
Example Project: GitHub — ExampleProject
Adding the Dependency
Gradle
repositories {
mavenCentral()
mavenLocal()
maven {
url = uri("https://codemc.io/repository/streamline-essentials/")
}
}
dependencies {
compileOnly("gg.drak:BukkitOfUtils:1.18.0")
}Maven
<repositories>
<repository>
<id>streamline-essentials</id>
<url>https://codemc.io/repository/streamline-essentials/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>gg.drak</groupId>
<artifactId>BukkitOfUtils</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
</dependencies>Don't forget to add depend: [BukkitOfUtils] to your plugin.yml.
BetterPlugin
All plugins using BOU should extend BetterPlugin instead of JavaPlugin. It provides lifecycle hooks and thread-safe utilities.
public class MyPlugin extends BetterPlugin {
@Override
public void onBaseConstruct() {
// Called during construction
}
@Override
public void onBaseLoad() {
// Called when the plugin loads (like onLoad)
}
@Override
public void onBaseEnabling() {
// Called at the start of enabling, before base initialization
}
@Override
public void onBaseEnabled() {
// Called after the plugin is fully enabled
}
@Override
public void onBaseDisable() {
// Called when the plugin is disabled — cleanup here
}
}Utility Methods
| Method | Description |
|---|---|
sync(Runnable) | Run a task on the main thread immediately |
sync(Runnable, delay) | Run a task on the main thread after a delay (ticks) |
sync(Runnable, delay, period) | Run a repeating task on the main thread |
isSync() | Check if the current thread is the main server thread |
registerListener(Listener) | Register an event listener |
unregisterListener(Listener) | Unregister an event listener |
getFactory("key") | Get a registered RetrievableItem by key |
getItem("key") | Get an ItemStack from a registered factory |
Command Builder
BOU provides a fluent CommandBuilder API for registering commands without plugin.yml entries.
new CommandBuilder("greet", this)
.setDescription("Greets the player")
.setUsage("/greet <name>")
.setBasePermission("myplugin.greet")
.addAliases("hello", "hi")
.setExecutionHandler(ctx -> {
if (!ctx.isPlayer()) {
ctx.sendMessage("&cOnly players can use this.");
return CommandResult.FAILURE;
}
String name = ctx.getArgSize() > 0
? ctx.getArg(0).toString()
: ctx.getPlayerOrNull().getName();
ctx.sendMessage("&aHello, " + name + "!");
return CommandResult.SUCCESS;
})
.setTabCompleter((ctx) -> {
// Return tab completions
return PlayerUtils.getOnlinePlayerNames()
.stream().collect(Collectors.toList());
})
.build();CommandContext
The CommandContext object passed to execution handlers provides:
| Method | Description |
|---|---|
isPlayer() / isConsole() | Check sender type |
getPlayer() | Optional<Player> — sender as player |
getPlayerOrNull() | Player or null |
getUuid() | UUID of the sender as a string |
sendMessage(String) | Send a color-coded message |
sendMessage(BaseComponent...) | Send component messages |
sendTitle(title, subtitle, fadeIn, stay, fadeOut) | Send a title screen |
getArg(int) | Get a CommandArgument by index |
getArgSize() | Number of arguments |
getArgsAsString() | All arguments joined as one string |
getFullCommand() | The full command string |
CommandResult
| Value | Returns |
|---|---|
CommandResult.SUCCESS | true |
CommandResult.FAILURE | false |
CommandResult.ERROR | false |
Folia-Compatible Scheduling
Folia uses regionalized threading — each chunk region has its own thread. Any code that interacts with location-bound objects (entities, blocks, worlds) must run on that object's region thread. BOU's TaskManager handles this automatically.
Region-specific objects include: Entities (Players, Mobs), Worlds, Locations, Blocks, and anything with a location.
TaskManager
// Immediate execution on main thread
TaskManager.runTask(() -> { /* code */ });
// Delayed execution (in ticks)
TaskManager.runTaskLater(() -> { /* code */ }, 20L); // 1 second
// Repeating task
TaskManager.runTaskTimer(() -> { /* code */ }, 0L, 20L); // Every second
// Entity-bound (routes to entity's region thread for Folia)
TaskManager.runTask(entity, () -> { /* code */ });
TaskManager.runTaskLater(entity, () -> { /* code */ }, 20L);
TaskManager.runTaskTimer(entity, () -> { /* code */ }, 0L, 20L);
// Region/Chunk-bound
TaskManager.runTask(world, chunkX, chunkZ, () -> { /* code */ });
TaskManager.runTask(chunk, () -> { /* code */ });
// Entity teleportation (Folia-safe)
TaskManager.teleport(entity, location);
// Thread checking
TaskManager.isThreadSync(); // On primary thread?
TaskManager.isThreadSync(entity); // On entity's region thread?
TaskManager.isThreadSync(location); // On location's region thread?CompletableTask
TaskManager.schedule() returns a CompletableTask for more control:
CompletableTask task = TaskManager.schedule(() -> { /* code */ });
CompletableTask task = TaskManager.schedule(() -> { /* code */ }, delay);
CompletableTask task = TaskManager.schedule(() -> { /* code */ }, delay, period);Example: Damaging Entities Safely
// Pass true if already on the main thread, false if async.
public static void damageAllEntities(boolean isCurrentlySync, double amount) {
if (isCurrentlySync) {
damageAllEntitiesInSync(amount);
} else {
TaskManager.runTask(() -> {
damageAllEntitiesInSync(amount);
});
}
}
// Must be called from the main thread.
public static void damageAllEntitiesInSync(double amount) {
EntityUtils.collectEntitiesThenDo(entity -> {
// Routes to the entity's region thread for Folia support.
TaskManager.runTask(entity, () -> {
entity.damage(amount);
});
});
}Async Task Timers
BOU provides BaseRunnable for repeating and delayed async timers.
WARNING
Task timers run asynchronously. When accessing entities, locations, or worlds inside a timer, use TaskManager to switch to the correct thread.
public class BasicRepeatingTimer extends BaseRunnable {
public BasicRepeatingTimer() {
super(0, 20); // delay: 0 ticks, period: 20 ticks (1 second)
}
@Override
public void run() {
// Runs every second, asynchronously.
DamageHandler.damageAllEntities(false, 1d);
}
}Register and manage runnables:
TaskManager.load(runnable); // Register without starting
TaskManager.start(runnable); // Register and start
TaskManager.cancel(runnable); // Stop and unregisterSimple Configurations
BOU's SimpleConfiguration class provides easy config file management with automatic I/O caching (reads/writes to disk at most every 5 seconds).
public class ExampleConfig extends SimpleConfiguration {
public ExampleConfig(BetterPlugin bouPlugin) {
super(
"config.yml", // File name (created automatically)
bouPlugin, // Your plugin instance
false // true if the file is embedded in your jar's resources folder
);
}
@Override
public void init() {
}
public String getMyLovelyString() {
reloadResource(); // Safe to call frequently — cached in memory
return getOrSetDefault("my.lovely.string", "BOU is really great!");
}
}TIP
reloadResource() is safe to call as often as you want (even every tick) because BOU caches the file in memory and only performs disk I/O at most every 5 seconds.
Utility Classes
MessageUtils
// Send colored messages
MessageUtils.sendMessage(sender, "&aColored message");
MessageUtils.doReplaceAndSend(sender, "Line 1%newline%Line 2", "&7Prefix: ");
// Logging (respects plugin log-level config)
MessageUtils.logInfo("message", plugin);
MessageUtils.logWarning("message", plugin);
MessageUtils.logSevere("message", plugin);
MessageUtils.logDebug("message", plugin);
// String utilities
String coded = MessageUtils.codedString(input); // Apply color codes
String newlined = MessageUtils.newLined(text); // Replace %newline% with \n
String truncated = MessageUtils.truncateString(s, 50); // Truncate to lengthColorUtils
// Colorize with & codes
String colored = ColorUtils.colorize("&aGreen &cRed text");
// Colorize with hex support (&#RRGGBB format)
String hex = ColorUtils.colorizeHard("&#FF5733Orange text");
// Create chat components
BaseComponent[] components = ColorUtils.color(message);
BaseComponent[] clickable = ColorUtils.colorWithClickable(message, clickEvent);
BaseComponent[] hoverable = ColorUtils.colorWithHoverable(message, hoverEvent);
// Strip all formatting
String clean = ColorUtils.stripFormatting("&aColored §btext");ItemUtils
// Create items
ItemStack item = ItemUtils.make(Material.DIAMOND, "&bShiny Diamond", "&7A precious gem");
ItemStack item = ItemUtils.make("DIAMOND", "&bShiny Diamond", "Line 1", "Line 2");
// With player context (processes placeholders)
ItemStack item = ItemUtils.make(player, Material.DIAMOND, "&bDiamond", true, "Lore");
// NBT serialization
String nbt = ItemUtils.getItemNBT(itemStack);
Optional<ItemStack> fromNbt = ItemUtils.getItem(nbtJsonString);
// Persistent data tags
ItemUtils.setTag(itemStack, plugin, "key", "value");
Optional<String> value = ItemUtils.getTag(itemStack, plugin, "key");
// Comparison
boolean equal = ItemUtils.isItemEqual(item1, item2);
boolean empty = ItemUtils.isNothingItem(itemStack);EntityUtils
// Iterate entities (thread-safe)
EntityUtils.collectEntitiesThenDo(entity -> { /* per entity */ });
EntityUtils.collectLivingEntitiesThenDo(living -> { /* per living entity */ });
EntityUtils.collectEntitiesInWorldThenDoSet("world", entities -> { /* collection */ });
// Entity counts
int total = EntityUtils.totalEntities();
int inWorld = EntityUtils.totalEntities(world);
// Damage tracking
Optional<Player> damager = EntityUtils.getLastDamager(entity);LocationUtils
// Parse and serialize
Optional<Location> loc = LocationUtils.fromString("world,100.5,64,200.5");
Optional<Location> loc = LocationUtils.fromString("world,100.5,64,200.5,90,0", true); // with yaw/pitch
String str = LocationUtils.toString(location);
// Location helpers
Location centered = LocationUtils.getCenteredLocation(location); // Center of block
Location top = LocationUtils.getTopLocation(location); // Highest block at XZ
boolean safe = LocationUtils.checkForTopableBlock(location); // Is it safe to stand?
// Teleportation (Folia-safe)
LocationUtils.teleport(entity, location);
// Item dropping
LocationUtils.dropItems(location, itemMap);
LocationUtils.dropItemsWithDirection(location, itemMap, direction);PlayerUtils
// Online player data
ConcurrentSkipListSet<String> names = PlayerUtils.getOnlinePlayerNames();
ConcurrentSkipListSet<String> uuids = PlayerUtils.getOnlinePlayerUUIDs();
// Offline player data
ConcurrentSkipListSet<String> allNames = PlayerUtils.getOfflinePlayerNames();
Stream<OfflinePlayer> all = PlayerUtils.getOfflinePlayersStream();StringUtils
// Capitalization
String result = StringUtils.capitalize(input, CapitalizationType.UPPER_FIRST);
// Types: LOWER_ALL, LOWER_FIRST, UPPER_ALL, UPPER_FIRST,
// WORD_LOWER_ALL, WORD_LOWER_FIRST, WORD_UPPER_ALL, WORD_UPPER_FIRST
// Truncation
String truncated = StringUtils.truncate(input, maxLength);
String decimals = StringUtils.truncateDecimals("3.14159", 2); // "3.14"Database Operations
BOU includes DBOperator for database access with HikariCP connection pooling.
public class MyDatabase extends DBOperator {
public MyDatabase(ConnectorSet connectorSet, BetterPlugin plugin) {
super(connectorSet, plugin);
}
@Override
public void ensureDatabase() { /* CREATE DATABASE IF NOT EXISTS */ }
@Override
public void ensureTables() { /* CREATE TABLE IF NOT EXISTS */ }
}Usage
// Connect
ConnectorSet connector = new ConnectorSet(
DatabaseType.MYSQL,
"jdbc:mysql://localhost:3306/mydb",
"username",
"password"
);
MyDatabase db = new MyDatabase(connector, this);
db.ensureUsable();
// Execute statements (supports multiple separated by ;)
List<ExecutionResult> results = db.execute(
"INSERT INTO players (name) VALUES (?)",
stmt -> stmt.setString(1, "Steve")
);
// Execute queries
db.executeQuery(
"SELECT * FROM players WHERE name = ?",
stmt -> stmt.setString(1, "Steve"),
resultSet -> {
while (resultSet.next()) {
String name = resultSet.getString("name");
}
}
);
// Shutdown when done
db.shutdown();Events
PluginDisableEvent
Subscribe to plugin disable events:
// Any plugin disabling
BetterPlugin.subscribeDisable(event -> {
// Handle any BetterPlugin disable
});
// Only your plugin
myPlugin.subscribeDisableIfSame(event -> {
// Handle only myPlugin disable
});