Skip to content

BukkitOfUtils — Developer Guide ​

BOU provides a framework for building Bukkit plugins with built-in Folia support, command building, configuration management, GUI menus, Modrinth update checks, and async task scheduling.

Example Project: GitHub — ExampleProject

Adding the Dependency ​

Gradle ​

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

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

Maven ​

xml
<repositories>
    <repository>
        <id>streamline-essentials</id>
        <url>https://repo.codemc.io/repository/streamline-essentials/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>gg.drak</groupId>
        <artifactId>BukkitOfUtils</artifactId>
        <version>1.19.4</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Don't forget to add depend: [BukkitOfUtils] to your plugin.yml.

BOU targets Java 11. The published artifact is the shaded plugin jar and its POM declares no dependencies:

  • TheBase is included. Its classes ship inside BOU under gg.drak.thebase (with its libraries relocated under gg.drak.thebase.lib.*). Do not add a separate gg.drak:TheBase dependency.
  • The inventory API is included. BOU provides the mc.obliviate.inventory API (Gui, Icon, GuiIcon, InventoryAPI) itself, so existing imports keep compiling. Do not depend on OblivateInvs. GUI titles are plain Strings, and 1.8 servers reject titles longer than 32 characters.
  • Runtime libraries are not transitive. HikariCP, Gson, Caffeine, SQLite/MySQL drivers, and the HTTP clients are provided at runtime (see Runtime Libraries), but if your code references their classes directly, declare them as compileOnly / provided yourself.

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 {
    public static final String MODRINTH_ID = "your-modrinth-project-id";

    @Override
    public String getModrinthId() {
        return MODRINTH_ID; // used by /boup info, menus, and the version checker
    }

    @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
        setPluginMenuBuilder((player, plugin) -> {
            // Optional: custom GUI opened from /boup menu <plugin>
            return new MyPluginMenu(player, plugin);
        });
    }

    @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
getModrinthId()Modrinth project id/slug for update checks (override to enable)
getFormattedUptime()Human-readable uptime since last enable (Disabled when off)
setPluginMenuBuilder(BetterPluginMenuBuilder)Register a custom screen for /boup menu <plugin>
getAsInfoComponent()Formatted info string used by /boup info

Plugin Menu Builder ​

BetterPluginMenuBuilder builds a ScreenInstance opened from the BOU plugin info menu:

java
@FunctionalInterface
public interface BetterPluginMenuBuilder {
    ScreenInstance build(Player player, BetterPlugin plugin);
}

If you do not set a builder, the info menu shows that no custom menu was provided.

HelpfulPlugin ​

Implement HelpfulPlugin to appear in /bouhelp (GUI for players, text for console). Provide getHelpfulInfo() and getHelpful(); BOU handles sending the document or opening the help GUI.

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.isArgUsable(0)
            ? ctx.getStringArg(0)
            : ctx.getPlayerOrNull().getName();

        ctx.sendMessage("&aHello, " + name + "!");
        return CommandResult.SUCCESS;
    })
    .setTabCompleter(ctx -> {
        // Tab completions are a ConcurrentSkipListSet<String>
        return PlayerUtils.getOnlinePlayerNames();
    })
    .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
getStringArg(int)Get an argument as a string
getIntArg(int) / getDoubleArg(int) / getBooleanArg(int)Parse an argument (Optional)
isArgUsable(int)Whether an argument exists at that index
getArgCount()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);

// The runTask* methods return null if BOU's task system is not running.

// 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);

task.whenComplete(() -> { /* runs after the task has run */ });
task.cancel();

A whenComplete callback added after the task has already run is called right away; a callback on a cancelled task never runs. Entity, chunk, and teleport overloads mirror the runTask* methods above.

Task Ownership ​

Tasks scheduled through TaskManager, CompletableTask, and BaseRunnable belong to the plugin whose class provides the runnable (lambdas and method references belong to the plugin that wrote them). When that plugin is disabled, BOU cancels its BaseRunnables and pending CompletableTasks, so you do not need to track them yourself for cleanup.

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);
    }
}

Constructing a BaseRunnable registers and starts it, so do not call TaskManager.start on it again. A run that takes longer than the period delays the next run instead of overlapping it. For a one-shot delayed task, extend BaseDelayedRunnable (fires exactly once).

java
BasicRepeatingTimer timer = new BasicRepeatingTimer(); // Starts immediately
timer.pause();                                         // Pause / resume
timer.resume();
TaskManager.cancel(timer);                             // Stop and unregister

Simple Configurations ​

Config helpers come from TheBase (gg.drak.thebase.storage.resources.flat.simple.SimpleConfiguration), which is bundled inside BOU. They give easy config file management, and reads pick up edits made to the file on disk (checked at most every 5 seconds).

java
import gg.drak.thebase.storage.resources.flat.simple.SimpleConfiguration;

public class ExampleConfig extends SimpleConfiguration {
    public ExampleConfig(BetterPlugin bouPlugin) {
        super(
            "config.yml",  // File name (created automatically)
            bouPlugin,     // Your plugin instance
            false          // true: copy the default file from your jar's resources; false: start empty
        );
    }

    @Override
    public void init() {
    }

    public String getMyLovelyString() {
        return getOrSetDefault("my.lovely.string", "BOU is really great!");
    }
}

TIP

get(...), getOrSetDefault(...), and getResource() re-read the file only if it changed on disk, checking at most every 5 seconds, so they are safe to call as often as you want (even every tick). No explicit reloadResource() call is needed before reading.

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 X/Z, resets yaw/pitch
Location centerH = LocationUtils.getCenteredHorizontally(location); // Center X/Z, keeps facing
Location center3d = LocationUtils.getCenteredFully(location);       // Center X/Y/Z, keeps facing
Location top = LocationUtils.getTopLocation(location);              // Nearest safe spot (searches down, then up)
Location topMost = LocationUtils.getTopMostTopLocation(location);   // Top-most safe spot 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 (MySQL or SQLite) 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, // or DatabaseType.SQLITE
    "localhost",        // host
    3306,               // port
    "mydb",             // database
    "username",
    "password",
    "myplugin_",        // table prefix
    "database.db"       // SQLite file name (used for SQLITE)
);
MyDatabase db = new MyDatabase(connector, this);
db.ensureUsable(); // builds the pool, then runs ensureDatabase / ensureTables / alters

// Execute statements (supports multiple separated by ;)
// Statement builders and result handlers are plain Consumers, so catch SQLException inside.
List<ExecutionResult> results = db.execute(
    "INSERT INTO players (name) VALUES (?)",
    stmt -> {
        try {
            stmt.setString(1, "Steve");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
);

// Execute queries
db.executeQuery(
    "SELECT * FROM players WHERE name = ?",
    stmt -> {
        try {
            stmt.setString(1, "Steve");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    },
    resultSet -> {
        try {
            while (resultSet.next()) {
                String name = resultSet.getString("name");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
);

// Shutdown when done
db.shutdown();
  • SQLite files are stored in your plugin's storage/ folder.
  • connector.prefixTable("players") applies the configured table prefix.
  • After shutdown(), the pool is not rebuilt implicitly; call ensureUsable() or buildDataSource() to reopen it. shutdown() waits (up to 5 seconds) only while connections are still in use.
  • For MySQL, autoReconnect=true is added to the JDBC URL, and allowPublicKeyRetrieval=true only for loopback hosts; the driver's default SSL mode applies. Extra driver options can be appended to the database name (for example mydb?sslMode=DISABLED).

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
});

Modrinth Version Checking ​

BOU can check Modrinth for updates via the Drak API. For BOU itself this is controlled in base-config.yml (version-checker.*). Dependent plugins opt in by overriding getModrinthId():

java
@Override
public String getModrinthId() {
    return "your-modrinth-project-id-or-slug";
}

Once set, /boup info <plugin> and /boup menu <plugin> show Modrinth status (up to date / behind / ahead) and can provide a download link when outdated.

Runtime Libraries ​

Large Maven Central dependencies (HikariCP, SQLite/MySQL drivers, Gson, Caffeine, HTTP clients, etc.) are declared in BOU's plugin.yml libraries: block so modern Spigot/Paper can download them. On older servers that ignore libraries:, BOU's RuntimeLibraryLoader downloads the same artifacts at runtime into plugins/BukkitOfUtils/libraries/, loading each jar only if its SHA-256 matches a pinned hash. Dependent plugins do not need to shade those libraries when relying on BOU, but they are not exposed as Maven dependencies, so declare any you compile against as compileOnly / provided.