Content generated with AI — it may contain mistakes.

Foundations

Tasks and threads

One scheduler that runs on Spigot, Paper and Folia without a branch in your code.

Folia does not have a main thread. It has region threads, and a task that touches an entity has to run on the thread that owns that entity. Writing for both means writing every scheduled call twice — or calling this module, which decides for you.

TaskScheduler tasks = Tasks.of(this);
 
tasks.run(() -> broadcast());
tasks.runLater(20L, () -> start());
tasks.runTimer(0L, 20L, () -> tick());

Where a task runs

CallRuns on
run, runLater, runTimerThe global thread — Bukkit's main thread, or Folia's global region.
runAsync, runAsyncLater, runAsyncTimerA pool thread. Never touch the Bukkit API from one.
runAtEntity, runAtEntityLater, runAtEntityTimerThe thread owning that entity. On Folia, its region.
runAtLocation, runAtLocationLater, runAtLocationTimerThe thread owning that location.
executeNow if already on the right thread, scheduled otherwise.
Anything about a player is `runAtEntity`

Teleporting, changing inventory, sending a title, reading their location: all of it belongs to the player's own thread. run is for work about the server as a whole — a broadcast, a global counter. Getting this wrong is invisible on Paper and a crash on Folia.

Cancelling

Every call returns a TaskHandle:

TaskHandle timer = tasks.runTimer(0L, 20L, () -> tick());
timer.cancel();

A repeating task that needs to cancel itself takes the handle as an argument:

tasks.runTimer(0L, 20L, handle -> {
    if (--countdown <= 0) {
        handle.cancel();
        return;
    }
    show(countdown);
});

runAtEntity also takes a retired callback, run when the entity is gone before the task could:

tasks.runAtEntity(player, () -> give(player, kit), () -> queueForLater(player));

Asking where you are

if (!tasks.isOwnedBy(player)) {
    tasks.runAtEntity(player, () -> act(player));
    return;
}
act(player);
CallAnswers
isGlobalThread()Whether this is the global thread.
isOwnedBy(entity)Whether this thread may touch that entity.

Coming back from a database query is the usual reason to ask: the future completes on a pool thread, and everything you want to do with the answer belongs to the player.

kits.find(id).thenAccept(found ->
        tasks.runAtEntity(player, () -> found.ifPresent(kit -> give(player, kit))));

Ticks

Delays and periods are in ticks: 20 ticks is one second. The library keeps ticks rather than seconds everywhere a Bukkit API would, so a number copied from an old config still means what it meant.

Cleanup

A plugin's tasks are cancelled when it disables. Menus are closed first, so a button cannot answer one last click into a dying classloader.

Something missing on this page? Tell us on Discord