docs/paper/scheduling/SKILL.md
# Scheduling Skill — Paper ## Purpose Reference this skill when running code on a delayed, repeating, or asynchronous basis in a Paper plugin. Covers `BukkitScheduler` task types, `BukkitTask` cancellation, and composing async work with `CompletableFuture`. ## When to Use This Skill - Running code after a delay (e.g., countdown timers, delayed kicks) - Running code on every tick or at fixed intervals (e.g., game loops, health regeneration) - Offloading IO or database work to an async thread wi
npx skillsauth add MrPippi/MPS docs/paper/schedulingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference this skill when running code on a delayed, repeating, or asynchronous basis in a Paper plugin. Covers BukkitScheduler task types, BukkitTask cancellation, and composing async work with CompletableFuture.
| Class / Method | Purpose | Notes |
|---------------|---------|-------|
| Bukkit.getScheduler() | Get the BukkitScheduler instance | |
| runTask(plugin, runnable) | Run on next server tick (main thread) | |
| runTaskLater(plugin, runnable, delay) | Run after delay ticks (20 ticks = 1 s) | Returns BukkitTask |
| runTaskTimer(plugin, runnable, delay, period) | Repeating task every period ticks | Returns BukkitTask |
| runTaskAsynchronously(plugin, runnable) | Run immediately on async thread | |
| runTaskLaterAsynchronously(plugin, runnable, delay) | Async with delay | |
| runTaskTimerAsynchronously(plugin, runnable, delay, period) | Async repeating | |
| BukkitTask#cancel() | Cancel a scheduled task | Safe to call from any thread |
| BukkitTask#isCancelled() | Check if already cancelled | |
| BukkitTask#getTaskId() | Integer task ID | Use with scheduler.cancelTask(id) |
| Bukkit.getScheduler().cancelAllTasks() | Cancel every task | Use in onDisable for cleanup |
package com.yourorg.myplugin.tasks;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.concurrent.CompletableFuture;
public class TaskExamples {
private final JavaPlugin plugin;
private BukkitTask repeatingTask;
public TaskExamples(JavaPlugin plugin) {
this.plugin = plugin;
}
// --- One-shot delayed task (main thread) ---
public void delayedWelcome(Player player) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return; // guard: player may have left
player.sendMessage(
net.kyori.adventure.text.Component.text("Welcome! Enjoy your stay.")
);
}, 60L); // 60 ticks = 3 seconds
}
// --- Repeating task (main thread) ---
public void startHealLoop() {
// Cancel any existing task first
stopHealLoop();
repeatingTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
Bukkit.getOnlinePlayers().forEach(p -> {
if (p.getHealth() < p.getMaxHealth()) {
p.setHealth(Math.min(p.getHealth() + 1.0, p.getMaxHealth()));
}
});
}, 0L, 40L); // start immediately, repeat every 2 seconds
}
public void stopHealLoop() {
if (repeatingTask != null && !repeatingTask.isCancelled()) {
repeatingTask.cancel();
repeatingTask = null;
}
}
// --- Async task → back to main thread ---
public void loadAndApplyData(Player player) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Safe: no Bukkit object mutations here
String data = fetchFromDatabase(player.getUniqueId().toString());
// Schedule Bukkit mutations back on the main thread
Bukkit.getScheduler().runTask(plugin, () -> {
if (!player.isOnline()) return;
applyData(player, data);
});
});
}
// --- CompletableFuture async chain ---
public CompletableFuture<Void> loadAsync(Player player) {
return CompletableFuture
.supplyAsync(() -> fetchFromDatabase(player.getUniqueId().toString()))
.thenAccept(data ->
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline()) applyData(player, data);
})
)
.exceptionally(ex -> {
plugin.getSLF4JLogger().error("Failed to load data for {}", player.getName(), ex);
return null;
});
}
private String fetchFromDatabase(String uuid) { return "data"; }
private void applyData(Player player, String data) { }
}
Cancel all tasks on shutdown:
@Override
public void onDisable() {
Bukkit.getScheduler().cancelAllTasks();
}
Calling Bukkit methods from async tasks: runTaskAsynchronously runs on a thread pool. Any call to player.teleport(), world.getBlock(), etc. from inside it will throw IllegalStateException or corrupt data. Always schedule mutations back with runTask(plugin, ...).
Not guarding player.isOnline() in delayed tasks: Between scheduling and execution, the player may disconnect. Always check player.isOnline() before acting on a Player reference in delayed/async callbacks.
Starting repeating tasks without stopping previous ones: Calling startHealLoop() twice creates two independent tasks that both run. Store the BukkitTask reference and cancel it before creating a new one.
Forgetting to cancel tasks in onDisable: Running tasks hold a reference to the plugin. If they continue after onDisable, they'll operate on a disabled plugin's state and cause errors on the next tick. Call cancelAllTasks() or cancel individual tasks in onDisable.
Using Thread.sleep() in scheduler callbacks: The BukkitScheduler runs repeating tasks on the main thread tick. Sleeping inside a main-thread task freezes the entire server. Use runTaskLater for delays instead.
BukkitScheduler API is unchanged. Paper also exposes Folia's RegionScheduler for region-threaded scheduling, but that requires a separate Folia build — standard Paper uses BukkitScheduler.runTaskTimerAsynchronously starts the first execution after delay ticks (0 = immediately on next async cycle).development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag