MagmaLib —
the scheduler that flows
One file, zero dependencies, three platforms. Type-safe pipelines, global task tracking, async teleport, and TaskGroups. Everything FoliaLib has — and more.
cancelAllTasks() for clean shutdown · TaskGroup for per-player/region cancellation · teleportAsync() with platform fallbacks · isPaper() / isSpigot() 3-platform detection · runTimerWhile() · @MainThread / @AnyThread annotations
Installation
Paper 1.20+ · Folia 1.20+ · Spigot 1.20+ · Java 17+
Maven
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.MagmaEnginers</groupId>
<artifactId>MagmaLib</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
Gradle (Kotlin DSL)
repositories { maven("https://jitpack.io") }
dependencies {
compileOnly("com.github.MagmaEnginers:MagmaLib:3.0.0")
}
Alternative: single-file copy
No build tool? Copy MagmaLib.java into your project — zero external dependencies, zero relocation needed.
- Download
MagmaLib.java - Place it at
src/main/java/your/package/MagmaLib.java - Update the
packageline at the top. Done.
Quick Start
public class MyPlugin extends JavaPlugin {
private final TaskGroup tasks = MagmaLib.newTaskGroup("my-plugin");
@Override public void onEnable() {
// 1. Init — auto-detects Folia / Paper / Spigot
MagmaLib.init(this);
// 2. Simple task — correct thread on every platform
tasks.add(MagmaLib.task(() -> player.sendMessage("Hello!"))
.with(player).afterTicks(20).run());
// 3. Type-safe pipeline
MagmaLib.<Integer>task(() -> computeScore(player))
.thenApply(s -> s > 100 ? "S rank!" : "Keep going")
.thenAccept(player::sendMessage)
.exceptionally(e -> { getLogger().warning(e.getMessage()); return "Error"; })
.run();
// 4. Async teleport with platform fallbacks
MagmaLib.teleportAsync(player, destination)
.thenAccept(ok -> player.sendMessage(ok ? "Teleported!" : "Failed"));
}
@Override public void onDisable() {
MagmaLib.cancelAllTasks(); // clean shutdown — cancels everything
}
}
Migration from Bukkit Scheduler
Replacing Bukkit.getScheduler() is mechanical — one-to-one replacements below.
Bukkit.getScheduler()
.runTask(plugin, r);MagmaLib.task(r).run();runTaskLater(plugin, r, 20);.afterTicks(20).run();runTaskTimer(plugin, r, 0, 20);.everyTicks(20).run();runTaskAsynchronously(plugin, r);.async().run();.async() with .with(entity)They are mutually exclusive. .async() overrides entity routing and sends everything to AsyncScheduler — any Bukkit API call inside will throw AsyncCatcher. Use .with(entity) alone; it already picks the right thread on every platform.
MagmaLib v3 vs FoliaLib
Objective feature comparison
Both libraries solve the same problem. Here's what differs on paper (no pun intended).
| Feature | MagmaLib v3 ★ | FoliaLib |
|---|---|---|
| Type-safe composition pipeline | ✅ thenApply / thenAccept / exceptionally | ❌ |
| Global task tracking + cancelAllTasks() | ✅ ConcurrentHashMap registry | ❌ |
| TaskGroup (per-player/region cancel) | ✅ Named groups | ❌ |
| teleportAsync() with platform fallbacks | ✅ Folia + Paper + Spigot | ✅ Folia + Paper |
| 3-platform detection (isFolia/isPaper/isSpigot) | ✅ | ⚠️ 2 only |
| runTimerWhile() readable inverse timer | ✅ | ❌ |
| @MainThread / @AnyThread annotations | ✅ IDE thread hints | ❌ |
| Zero-allocation Direct API | ✅ runDirect* | ⚠️ Partial |
| Zero external dependencies | ✅ Single file | ❌ Requires shade |
| Backward compatibility (1.8.8+) | ❌ 1.20+ only | ✅ 1.8.8+ |
| Production track record | ⚠️ v3 is new | ✅ Years in prod |
| Unit test suite | ✅ 19 tests | ⚠️ Limited |
- MagmaLib — Paper/Folia 1.20+, you want composition pipelines, per-player cancellation, or you're tired of relocation.
- FoliaLib — you need Spigot 1.8.8 support or want a battle-tested library with years of production history.
Initialization
@Override
public void onEnable() {
MagmaLib.init(this); // one call — detects platform, caches result
}
@Override
public void onDisable() {
MagmaLib.cancelAllTasks(); // cancels everything scheduled through MagmaLib
}
init(Plugin)
Call once in onEnable(). Detects Folia/Paper/Spigot via reflection and caches all three results. Throws IllegalArgumentException if plugin is null.
cancelAllTasks()
Cancels every task ever scheduled through MagmaLib and clears the global registry. Always call in onDisable().
getTrackedTasks()
Returns an unmodifiable snapshot of all live tasks. Useful for diagnostics.
pruneRegistry()
Removes cancelled/completed tasks from the global registry to prevent memory leaks on long-running servers.
Platform Detection v3.0
v3 detects three platforms, not two. All results are cached in volatile fields after the first call — safe in hot paths.
isFolia()
true when RegionizedServer is present. Uses RegionScheduler, EntityScheduler, GlobalRegionScheduler.
isPaper()
true on Paper (non-Folia). Enables native Entity.teleportAsync() in teleportAsync().
isSpigot()
true when neither Folia nor Paper extensions are detected. Falls back to synchronous Bukkit scheduler.
After init(), exactly one of isFolia(), isPaper(), isSpigot() returns true. They are mutually exclusive and exhaustive.
Task Builder
Fluent API that automatically routes to the correct Folia scheduler. Every run() call registers the task in the global registry and optionally adds it to a TaskGroup.
task(Runnable) v1.x
Entry point for fire-and-forget tasks.
task(Supplier<T>) v2.1
Entry point for typed composition pipelines.
.at(Location)
.with(Entity)
.afterTicks(n)
Initial delay in ticks. 1 tick = 50 ms.
.everyTicks(n)
Repeating period. Makes the task a timer.
.cancelIf()
Checked before each execution. If true, skips the run.
.handleException()
Custom error handler. Falls back to plugin logger with full context.
.named(String) New
Tags the task. Appears in error logs: [task='X' location=…].
.inGroup(TaskGroup) v3
Auto-registers with a TaskGroup on run(). Group can cancel it later.
.unsafe() ⚡ hot path
Skips chunk-loaded and entity-valid checks. Only when you guarantee them manually.
.run()
Dispatches the task. Registers it globally. Returns a cancellable handle.
Composition Pipeline v2.1
Functional API modelled after CompletableFuture. Every step is fully type-checked via generics — no raw casts, no ClassCastException.
MagmaLib.<ItemStack[]>task(() -> player.getInventory().getContents())
.named("InventoryProcessor")
.at(player.getLocation())
.inGroup(playerGroup) // v3: linked to group
.thenApply(items -> Arrays.stream(items)
.filter(Objects::nonNull).count()) // ItemStack[] → long
.thenApply(count -> "You have " + count + " items") // long → String
.thenAccept(player::sendMessage) // consume
.exceptionally(e -> {
getLogger().warning("Error: " + e.getMessage());
return new ItemStack[0];
})
.run();
.thenApply()
Transforms and changes the builder's generic type. Chainable multiple times — safe, no raw casts.
.thenAccept()
Consumes for a side effect. Multiple calls chain in order.
.exceptionally()
Catches any exception and returns a fallback to keep the pipeline alive.
thenApply, thenAccept, exceptionally are only available when using task(Supplier<T>). Calling them on task(Runnable) throws IllegalStateException immediately at config time, not at runtime.
Direct API ⚡ Hot Paths
Zero-builder methods for high-frequency loops. No allocations beyond the lambda. Not registered in the global task registry.
These skip all safety checks. You must ensure: location.getWorld() != null, chunk loaded, entity.isValid(). No exceptions will be thrown for bad inputs.
runDirect(Runnable)
Next tick on global/main thread.
runDirectAt(Location, Runnable)
Immediate, region-routed. Best for bulk block operations.
runDirectWith(Entity, Runnable)
Immediate on entity scheduler. For tight per-entity loops.
runDirectLater(Runnable, ticks)
Delayed global task. Folia: GlobalRegionScheduler receives ticks directly.
runDirectTimer(Runnable, ticks)
Repeating global task. Initial delay is always ≥ 1 tick on Folia.
runTimerUntilFast()
Self-cancelling timer. Cancels via its own handle — not just skips execution.
Global Task Tracking v3.0
Every task dispatched via the TaskBuilder is automatically registered in a ConcurrentHashMap. This enables clean plugin shutdowns with a single call.
@Override
public void onDisable() {
// Cancels ALL tasks scheduled through MagmaLib — timers, delayed, repeating.
// Logs: "[MagmaLib] Cancelled N task(s) on shutdown."
MagmaLib.cancelAllTasks();
}
FoliaLib requires you to track and cancel each task manually. With MagmaLib, one call on onDisable() is enough regardless of how many tasks are running.
cancelAllTasks()
Cancels all running tasks, clears the registry. Logs how many were cancelled.
getTrackedTasks()
Returns an unmodifiable snapshot. Iterate to inspect or cancel selectively.
pruneRegistry()
Removes dead tasks from the registry. Call periodically on high-throughput plugins to keep memory clean.
TaskGroup v3.0
Named groups of tasks that cancel together. Ideal for per-player effects, per-region machines, or any feature you need to shut down atomically.
// On player join — create a group
TaskGroup group = MagmaLib.newTaskGroup("player-" + player.getUniqueId());
group.add(MagmaLib.task(() -> updateAura(player))
.with(player).everyTicks(5).run());
group.add(MagmaLib.task(() -> checkCooldown(player))
.everyTicks(20).run());
// Or use .inGroup() directly on the builder
MagmaLib.task(() -> sendBossBar(player))
.with(player).everyTicks(2)
.inGroup(group) // auto-adds on run()
.run();
// On player quit — one call cancels everything
group.cancelAll();
add(Task)
Registers a task with the group. Returns the task for further chaining.
cancelAll()
Cancels every running task in the group and clears it.
prune()
Removes dead tasks without cancelling live ones. Call on a tick to keep the group lean.
activeCount()
Returns the number of currently running tasks in the group.
teleportAsync() v3.0
Platform-aware async teleport that does the right thing on every server type.
| Platform | How it teleports |
|---|---|
| Folia | Via EntityScheduler.execute() on the entity's region thread. Fully concurrent-safe. |
| Paper | Via native Entity.teleportAsync() — Paper's own async teleport API. |
| Spigot | Fallback: schedules sync teleport on next tick via BukkitScheduler. |
// Basic — uses TeleportCause.PLUGIN
MagmaLib.teleportAsync(player, destination)
.thenAccept(success -> {
if (success) player.sendMessage("Teleported!");
else player.sendMessage("Teleport failed.");
});
// Custom cause
MagmaLib.teleportAsync(player, destination, TeleportCause.COMMAND)
.thenAccept(ok -> logTeleport(player, ok))
.exceptionally(e -> { getLogger().warning("TP failed: " + e.getMessage()); return null; });
teleportAsync(null, location) and teleportAsync(entity, null) both return a future that completes immediately with false — no NPE, no exception.
Async & CompletableFuture
runAsync(Runnable)
Async scheduler, exceptions propagate via completeExceptionally.
MagmaLib.runAsync(() -> heavyIO())
.thenAccept(v ->
MagmaLib.runSync(() -> player.sendMessage("Done")));callAsync(Supplier<T>)
Like runAsync but returns a value.
MagmaLib.callAsync(() -> db.query(uuid))
.thenAccept(r ->
MagmaLib.runSync(() -> updateHUD(r)));callSync(Supplier<T>)
Runs on global/main thread, returns value via future. Use when you need Bukkit API data from async code.
runSync(Runnable)
Schedules on global/main thread. Platform-agnostic shorthand.
Helpers
forAllPlayers()
Main thread. Per-player exception isolation — one player crashing doesn't affect others.
forAllLoadedChunks()
Main thread (fixed in v2.1.2 — was incorrectly async). getLoadedChunks() is not thread-safe.
runTimerUntil()
Self-cancelling when condition is true. Actually cancels the handle — not just skips.
runTimerWhile() v3
Inverse of runTimerUntil: runs while condition is true, cancels when it becomes false.
runWithRetry()
First attempt immediate, retries delayed. Throws if maxAttempts < 1.
executeIfLoaded()
Silent no-op if chunk is unloaded. Otherwise routes to the chunk's region.
ticksToMs() / msToTicks()
Inline conversion. ×50 and ÷50 respectively.
safeMessage(Throwable)
Null-safe error description. Falls back to class name when getMessage() is null.
runTimerWhile() in practice
// Runs every tick while player is alive and online — cancels automatically
MagmaLib.runTimerWhile(
() -> updateHUD(player),
1,
() -> player.isOnline() && !player.isDead()
);Entity Scheduling
Always use .with(entity) for entity operations. Folia routes to the EntityScheduler for that entity's region. Paper runs on the main thread. In both cases Bukkit API is safe to call.
MagmaLib.task(() -> {
entity.setCustomName("§aProcessed");
entity.setGlowing(true);
})
.with(entity)
.cancelIf(() -> !entity.isValid())
.handleException(e -> getLogger().warning("Entity error: " + e.getMessage()))
.run();Never combine them. .async() discards entity routing and forces AsyncScheduler. Any Bukkit API call — playSound, setVelocity, sendMessage — will throw AsyncCatcher at runtime. The fix: use .with(entity) alone.
Platform Compatibility
| Feature | Paper / Spigot | Folia |
|---|---|---|
task().run() | ✅ BukkitScheduler | ✅ GlobalRegionScheduler |
.at(location) | ✅ Main thread | ✅ RegionScheduler |
.with(entity) | ✅ Main thread | ✅ EntityScheduler |
.async() | ✅ BukkitScheduler async | ✅ AsyncScheduler |
thenApply / thenAccept | ✅ | ✅ |
teleportAsync() | ✅ Paper native async | ✅ EntityScheduler |
cancelAllTasks() | ✅ | ✅ |
TaskGroup | ✅ | ✅ |
runTimerWhile() | ✅ | ✅ |
isPaper() / isSpigot() | ✅ | ✅ |
@MainThread / @AnyThread | ✅ IDE hints | ✅ IDE hints |
Changelog
v3.0.0
cancelAllTasks() New
Global task registry + single-call shutdown for onDisable(). FoliaLib has no equivalent.
TaskGroup New
Named groups for per-player or per-feature cancellation. .inGroup() on the builder.
teleportAsync() New
Platform-aware: Folia EntityScheduler → Paper native → Spigot next-tick.
isPaper() / isSpigot() New
3-platform detection. All three are mutually exclusive, cached as volatile.
runTimerWhile() New
Readable inverse of runTimerUntil. Runs while condition is true, cancels when false.
@MainThread / @AnyThread New
Source annotations to give IDE thread-safety hints. Zero runtime overhead.
v2.1.2 — 9 Bug Fixes
#1 thenApply unsafe cast
Multi-level chains used raw cast → silent ClassCastException. Replaced with typed List<Function>.
#2 runAsync Folia units
AsyncScheduler.runAtFixedRate takes ms — delay was inconsistent. Fixed.
#3 runDirectLater ×50
GlobalRegionScheduler.runDelayed takes ticks. Old code passed ms → 50× delay.
#4 runDirectTimer ×50
Same unit confusion. Folia timers ran at 1/50th the intended frequency.
#5 runTimerUntilFast no cancel
Stop condition fired but timer kept running. Now cancels via its own handle.
#6 forAllLoadedChunks async
getLoadedChunks() is not thread-safe. Was running async → CME risk. Now sync.
#7 runTimerUntil no cancel
Same as #5 for the builder-based version. cancelIf only skipped body.
#8 runWithRetry extra dispatch
First attempt went through runLater(0ms). Now runs immediately.
#9 maxAttempts < 1
runWithRetry(…, 0, …) silently ran and logged "failed after 0 attempts". Now throws IllegalArgumentException.
FAQ
TaskGroup, teleportAsync, etc. are opt-in..async() overrides entity routing and sends everything to AsyncScheduler. Any Bukkit API call inside (sounds, teleports, messages) will trigger AsyncCatcher. Use .with(entity) alone — it already picks the correct thread on every platform.TaskGroup: create one per player on join with MagmaLib.newTaskGroup("player-" + uuid), add tasks with .inGroup(group) or group.add(task.run()), and call group.cancelAll() on quit. Clean, zero manual tracking.runDirectTimer and runDirectLater passed milliseconds to Folia schedulers that expect ticks. Fixed in v2.1.2 (bugs #3 and #4). Upgrade to v3.0.0..inGroup(group).run() calls group.add(task) internally after dispatch. .inGroup() is syntactic sugar that keeps the builder chain readable. Use whichever feels cleaner.MagmaLib.cancelAllTasks() in onDisable() — it cancels everything regardless of groups. If you also have groups, their tasks get cancelled twice (harmless, since cancel() is idempotent). You can optionally call group.cancelAll() earlier for fine-grained cleanup.MagmaLib.init() attempts Class.forName("io.papermc.paper.threadedregions.RegionizedServer") for Folia, then Class.forName("io.papermc.paper.configuration.GlobalConfiguration") for Paper, and defaults to Spigot if both fail. Results are stored in three volatile Boolean fields — each subsequent call is a single field read with zero overhead.