Skip to content

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

kotlin
repositories {
    mavenCentral()
    mavenLocal()
    maven {
        url = uri("https://codemc.io/repository/streamline-essentials/")
    }
}

dependencies {
    compileOnly("gg.drak:BukkitOfUtils:1.18.0")
}

Maven

xml
<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.

java
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

MethodDescription
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.

java
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:

MethodDescription
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

ValueReturns
CommandResult.SUCCESStrue
CommandResult.FAILUREfalse
CommandResult.ERRORfalse

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

java
// 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:

java
CompletableTask task = TaskManager.schedule(() -> { /* code */ });
CompletableTask task = TaskManager.schedule(() -> { /* code */ }, delay);
CompletableTask task = TaskManager.schedule(() -> { /* code */ }, delay, period);

Example: Damaging Entities Safely

java
// 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.

java
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:

java
TaskManager.load(runnable);    // Register without starting
TaskManager.start(runnable);   // Register and start
TaskManager.cancel(runnable);  // Stop and unregister

Simple Configurations

BOU's SimpleConfiguration class provides easy config file management with automatic I/O caching (reads/writes to disk at most every 5 seconds).

java
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

java
// 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 length

ColorUtils

java
// 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

java
// 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

java
// 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

java
// 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

java
// 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

java
// 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.

java
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

java
// 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:

java
// Any plugin disabling
BetterPlugin.subscribeDisable(event -> {
    // Handle any BetterPlugin disable
});

// Only your plugin
myPlugin.subscribeDisableIfSame(event -> {
    // Handle only myPlugin disable
});