Skip to content

Modules ​

BetterPlugin ships a few small API modules that you can use with any Paper plugin, whether or not you extend PluginBase.

All modules are plain Java classes; you can create them anywhere in your plugin. If you extend PluginBase, some helpers are already integrated (see Plugin entry).


Scheduler ​

TaskScheduler schedules tasks for one plugin and lets you cancel them all at once.

java
import org.coffeepop.betterPlugin.api.scheduler.TaskScheduler;

public final class MyPlugin extends JavaPlugin {

    private TaskScheduler scheduler;

    @Override
    public void onEnable() {
        scheduler = new TaskScheduler(this);

        // Run once on the main thread.
        scheduler.runSync(() -> getLogger().info("started"));

        // Run on a background thread.
        scheduler.runAsync(() -> fetchRemoteData());

        // Run later on the main thread.
        scheduler.runSyncLater(() -> getLogger().info("one second later"), 20L);

        // Repeat every 5 minutes on the main thread.
        scheduler.runSyncTimer(() -> saveData(), 0L, 20L * 60 * 5);

        // Repeat on a background thread.
        scheduler.runAsyncTimer(() -> fetchRemoteData(), 0L, 20L * 60);
    }

    @Override
    public void onDisable() {
        // Stops every task owned by this scheduler.
        scheduler.cancelAll();
    }
}

Practical notes ​

  • If you extend PluginBase, use tasks() instead of creating a TaskScheduler yourself. It is cancelled automatically on plugin disable.
  • Always call cancelAll() on plugin disable when you create a standalone scheduler. If you don't, repeating tasks keep running after your plugin is disabled.
  • cancelAll() closes the scheduler. After calling it, any task created through the same scheduler is cancelled immediately. Create a new TaskScheduler if you need to schedule again after a reload.
  • Background tasks must not touch Bukkit API that requires the main thread. For example, opening inventories, modifying worlds, or sending packets usually must happen on the main thread. Use runSync/runSyncTimer for those.
  • Task callbacks run on whatever thread the scheduler picks. Keep data shared between threads in thread-safe containers or synchronize explicitly.

Config ​

PluginConfig is a typed wrapper around config.yml.

java
import org.coffeepop.betterPlugin.api.config.PluginConfig;
import java.util.List;

public final class MyPlugin extends JavaPlugin {

    private PluginConfig config;

    @Override
    public void onEnable() {
        config = new PluginConfig(this, this::reloadSettings);

        // Read values with defaults.
        int interval = config.getInt("auto-save.interval-seconds", 300);
        boolean announce = config.getBoolean("join-announcement.enabled", true);
        String prefix = config.getString("message-prefix", "[MyPlugin] ");
        List<String> greetings = config.getStringList("greetings");

        // Reload manually when a server operator asks for it.
        config.reload();
    }

    private void reloadSettings() {
        // Called after every reload; rebuild timers or cached values here.
    }
}

Practical notes ​

  • reload() applies missing defaults first, then reloads from disk. It never overwrites values the server operator has changed.
  • After reload(), read values again through PluginConfig. Do not keep long-lived copies of primitive settings if you support reload.
  • The reload callback runs on the thread that called reload(). If you rebuild scheduler tasks inside it, make sure you are on the main thread (or delegate with TaskScheduler.runSync).
  • If your plugin has no config.yml, reload() is safe. It simply reloads the (empty) config and runs the callback.

Placeholders ​

PlaceholderFormatter replaces {key} tokens in a template.

java
import org.coffeepop.betterPlugin.api.utils.PlaceholderFormatter;
import java.util.Map;

String message = PlaceholderFormatter.format(
        "Hello {player}, you have {amount} items.",
        Map.of(
                "player", player.getName(),
                "amount", String.valueOf(amount)
        )
);

Practical notes ​

  • Replacement is single-pass. If a replacement value contains {another}, that text is kept literally and not replaced again.
  • Unknown placeholders are left as-is. That is usually better for debugging than silently removing them.
  • Build a Map for each message. It is cheap and keeps the code readable; do not mutate the map while formatting.

Events ​

ListenerRegistry registers event handlers without writing annotated Listener classes.

java
import org.coffeepop.betterPlugin.api.event.ListenerRegistry;
import org.bukkit.event.player.PlayerJoinEvent;

public final class MyPlugin extends JavaPlugin {

    private ListenerRegistry listeners;

    @Override
    public void onEnable() {
        listeners = new ListenerRegistry(this);

        listeners.register(PlayerJoinEvent.class, event ->
                event.getPlayer().sendPlainMessage("Welcome!"));

        // You can register as many handlers as you want.
        listeners.register(PlayerQuitEvent.class, event ->
                getLogger().info(event.getPlayer().getName() + " left"));
    }

    @Override
    public void onDisable() {
        listeners.unregisterAll();
    }
}

Practical notes ​

  • If you extend PluginBase, use listeners() instead of creating a ListenerRegistry yourself. It is unregistered automatically on plugin disable.
  • Call unregisterAll() on plugin disable when you use a standalone registry. Otherwise handlers keep firing for a disabled plugin.
  • Handlers run on the thread that fired the event. Most Bukkit events fire on the main thread, but async events (for example, async chat/player login events) do not. Check the event's documentation before touching shared state.
  • Do not run long work inside an event handler. If you need to save data or call a web service, schedule it with TaskScheduler.runAsync and return quickly.
  • ListenerRegistry is reusable. After unregisterAll() you can register new handlers again; it creates a fresh internal listener.

Items ​

ItemBuilder creates ItemStacks with display names and lore.

java
import org.coffeepop.betterPlugin.api.item.ItemBuilder;
import org.bukkit.Material;

ItemStack sword = ItemBuilder.of(Material.DIAMOND_SWORD)
        .amount(1)
        .name("Hero Sword")
        .lore("Legendary", "Right-click to activate")
        .build();

// You can also use Adventure components directly.
ItemStack fancy = ItemBuilder.of(Material.PAPER)
        .name(Component.text("Notice").color(NamedTextColor.GOLD))
        .lore(List.of(Component.text("Line 1"), Component.text("Line 2")))
        .build();

Practical notes ​

  • Build items once and reuse them when possible. Creating item meta repeatedly is wasteful; cache static items in fields.
  • amount(int) must be positive. The builder rejects zero and negative amounts.
  • Lore lines are appended. Calling lore(...) twice adds both sets of lines; call it once with all lines if you want a single list.
  • The builder does not apply enchantments, flags, or damage yet. Use item.getItemMeta() directly after build() for advanced meta.

GUI ​

InventoryGui is a minimal inventory menu helper.

java
import org.coffeepop.betterPlugin.api.gui.InventoryGui;
import org.coffeepop.betterPlugin.api.item.ItemBuilder;
import org.bukkit.Material;

InventoryGui gui = InventoryGui.builder(this, 9, "Menu")
        .item(0, ItemBuilder.of(Material.DIAMOND).name("Click me").build(), event ->
                event.getWhoClicked().sendPlainMessage("You clicked diamond!"))
        .item(4, ItemBuilder.of(Material.BARRIER).name("Close").build(), event ->
                event.getWhoClicked().closeInventory())
        .build();

// Open for a player.
gui.open(player);

// Later, update an item while the GUI is open.
gui.setItem(0, ItemBuilder.of(Material.EMERALD).name("Changed").build());

// Close callback example:
InventoryGui withClose = InventoryGui.builder(this, 9, "Menu")
        .onClose(p -> p.sendPlainMessage("Menu closed"))
        .build();

Practical notes ​

  • All clicks in the GUI view are cancelled, including clicks in the player's bottom inventory while the GUI is open. This keeps the menu read-only.
  • The internal click listener is registered lazily on first open(). It is automatically unregistered when the last viewer closes the GUI. You do not normally need to call close().
  • onClose(Consumer<Player>) adds a callback that runs each time a player closes the GUI.
  • setItem(int, ItemStack) updates a slot while the GUI is open.
  • Calling close() manually is safe. It unregisters listeners and clears viewers; the GUI can be reopened later and the listener is re-registered.
  • Click handlers run on the main thread (inventory events are main-thread events). Do not block there.
  • The handler receives the event after it is already cancelled. You can still read slot, click type, and player from it.
  • Inventory size must be a positive multiple of 9. Slots outside the size are rejected at build time.

Registry ​

Registry<T> is a named, thread-safe map. SimpleRegistry<T> is the default implementation.

java
import org.coffeepop.betterPlugin.api.registry.Registry;
import org.coffeepop.betterPlugin.api.registry.SimpleRegistry;

// Standalone usage:
Registry<Storage> storages = new SimpleRegistry<>();
storages.register("main", new FileStorage());
storages.get("main").ifPresent(Storage::load);

If you extend PluginBase, a shared registry is already available:

java
registry().register("storage", new FileStorage());
registry().get("storage").ifPresent(Storage::load);

Modules ​

ModuleRegistry manages named modules with enable/disable lifecycle.

java
import org.coffeepop.betterPlugin.api.registry.ModuleRegistry;
import org.coffeepop.betterPlugin.api.registry.PluginModule;

public final class MyPlugin extends JavaPlugin {

    private ModuleRegistry modules;

    @Override
    public void onEnable() {
        modules = new ModuleRegistry(this);

        modules.register("backup", new PluginModule() {
            @Override
            public void onEnable(JavaPlugin plugin) {
                // start backup task
            }

            @Override
            public void onDisable() {
                // stop backup task
            }
        });

        modules.enableAll();
    }

    @Override
    public void onDisable() {
        modules.disableAll();
    }
}

Practical notes ​

  • If you extend PluginBase, use modules() instead of creating a ModuleRegistry yourself. It is disabled automatically on plugin disable.
  • Registry operations are thread-safe. You can register and read from async tasks.
  • register(...) replaces an existing value with the same key. Use unique keys to avoid accidental overwrites.
  • enable(...) is idempotent. Enabling an already-enabled module does not call onEnable again.
  • disable(...) is also idempotent. Disabling a module that is not enabled does nothing.
  • unregister(...) disables the module first if it is enabled, then removes it.
  • Call disableAll() on plugin disable so every module can clean up its own tasks/listeners.

Common pitfalls checklist ​

  • [ ] Cancel schedulers on disable (cancelAll()).
  • [ ] Unregister event listeners on disable (unregisterAll()).
  • [ ] Disable all modules on disable (disableAll()).
  • [ ] Use main-thread tasks for Bukkit API that must run on the main thread.
  • [ ] Re-read config values after reload().
  • [ ] Don't keep GUI references after the last viewer closes unless you reopen it.
  • [ ] Give registry keys unique, stable names.
  • [ ] Build static items once and reuse them.

Powered by VitePress