Learn how to listen to and handle events in Paper plugins
Events are the primary way plugins interact with server and player actions. Paper’s event system allows you to listen for and respond to hundreds of different events.
public abstract class Event { private final boolean isAsync; public Event() { this(false); // Synchronous by default } public Event(boolean isAsync) { this.isAsync = isAsync; } public abstract HandlerList getHandlers(); public final boolean isAsynchronous() { return this.isAsync; }}
Control the order in which your event handler is called:
@EventHandler(priority = EventPriority.HIGH)public void onPlayerJoin(PlayerJoinEvent event) { // This runs with HIGH priority}
Priority order (from org.bukkit.event.EventPriority):
LOWEST - Called first, for early modifications
LOW - Called early
NORMAL - Default priority
HIGH - Called late
HIGHEST - Called very late, final modifications
MONITOR - Read-only, for observation (don’t modify events!)
@EventHandler(priority = EventPriority.LOWEST)public void onDamage(EntityDamageEvent event) { // Runs first - good for canceling events early if (event.getEntity() instanceof Player) { event.setCancelled(true); }}
The MONITOR priority should only be used for observing events, not modifying them. Changes made at this priority may not be respected by other plugins.
import org.bukkit.event.player.PlayerJoinEvent;import org.bukkit.event.player.PlayerQuitEvent;import org.bukkit.event.player.PlayerMoveEvent;import org.bukkit.event.player.PlayerInteractEvent;public class PlayerListener implements Listener { @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); // Modify join message event.joinMessage(Component.text("Welcome " + player.getName())); } @EventHandler public void onQuit(PlayerQuitEvent event) { // Player is leaving savePlayerData(event.getPlayer()); } @EventHandler public void onMove(PlayerMoveEvent event) { // Player moved // Be careful - this fires very frequently! if (event.getFrom().getBlock().equals(event.getTo().getBlock())) { return; // Only head movement, not position change } }}
import org.bukkit.event.block.BlockBreakEvent;import org.bukkit.event.block.BlockPlaceEvent;public class BlockListener implements Listener { @EventHandler public void onBlockBreak(BlockBreakEvent event) { Player player = event.getPlayer(); Block block = event.getBlock(); if (!player.hasPermission("myplugin.break")) { event.setCancelled(true); player.sendMessage("You don't have permission!"); } } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockPlace(BlockPlaceEvent event) { // Only runs if no other plugin cancelled it awardPoints(event.getPlayer()); }}
import org.bukkit.event.entity.EntityDamageEvent;import org.bukkit.event.entity.EntityDeathEvent;public class EntityListener implements Listener { @EventHandler public void onEntityDamage(EntityDamageEvent event) { if (event.getEntity() instanceof Player player) { // Player took damage if (isInSafeZone(player)) { event.setCancelled(true); } } } @EventHandler public void onEntityDeath(EntityDeathEvent event) { // Entity died event.getDrops().clear(); // Remove all drops }}
// Create custom eventpublic class MyCustomEvent extends Event { private static final HandlerList HANDLER_LIST = new HandlerList(); private final Player player; public MyCustomEvent(Player player) { this.player = player; } public Player getPlayer() { return player; } @Override public HandlerList getHandlers() { return HANDLER_LIST; } public static HandlerList getHandlerList() { return HANDLER_LIST; }}// Call the eventMyCustomEvent event = new MyCustomEvent(player);getServer().getPluginManager().callEvent(event);
Or use the convenience method from the Event class:
MyCustomEvent event = new MyCustomEvent(player);boolean wasNotCancelled = event.callEvent();
Some events are fired asynchronously (off the main thread):
@EventHandlerpublic void onAsyncChat(AsyncChatEvent event) { // This runs on a separate thread! // Be careful with thread safety if (event.isAsynchronous()) { // Returns true // Don't call Bukkit API methods that aren't thread-safe }}
Asynchronous event handlers must be thread-safe. Most Bukkit API methods should not be called from async events. Use Bukkit.getScheduler().runTask() to schedule tasks on the main thread if needed.
@EventHandlerpublic void onPlayerMove(PlayerMoveEvent event) { // This fires VERY frequently - keep it fast! if (event.getFrom().getBlock().equals(event.getTo().getBlock())) { return; // Only head movement, ignore }}
Unregister when done:
HandlerList.unregisterAll(listener);
Use ignoreCancelled wisely:
Set to true when you don’t want to process cancelled events
Set to false when you need to see all events regardless of cancellation