Event priorities and canceling
Who runs first, how canceling works, and how plugins share the same event.
On a real server, many plugins listen to the same events. On this page you will learn the order in which their handlers run, how canceling really works, how to skip events another plugin already canceled, and how to handle damage, projectiles and async events safely. You will build SpawnGuard, a plugin that protects the area around spawn.
One event, many listeners
In Events and listeners you wrote handlers that run when something happens. Your plugin is rarely alone, though. When Steve breaks a block, a protection plugin wants to check whether he is allowed to, a jobs plugin wants to pay him, and a logging plugin wants to record it. Every one of them has a handler for the same BlockBreakEvent.
Paper calls all of those handlers one after another, passing the same event object along. Each handler can read it and change it. That raises two questions:
- Who goes first? If the protection plugin cancels the break, the jobs plugin should know before it pays out.
- Who has the final say? If two plugins change the same thing, the later change wins.
The answer to both is the event priority.
The six priorities
Every handler has one of six priorities. Paper runs all LOWEST handlers first, then all LOW handlers, and so on, with MONITOR handlers last:
| Priority | Runs | Typical use |
|---|---|---|
LOWEST | First | Setting things up early so other plugins can build on them. |
LOW | Second | Protection: canceling actions that are not allowed, before features react. |
NORMAL | Third | Almost everything. This is the default when you do not choose. |
HIGH | Fourth | Features that should respect what most plugins decided, such as chat formatting. |
HIGHEST | Fifth | Having the final say over every other plugin. Use rarely. |
MONITOR | Last | Only reading the final result: logs and statistics. Never change the event here. |
The names trip everyone up at first: HIGHEST sounds like "goes first", but it means "most important, so it decides last". Think of it as "who gets the last word".
You choose the priority inside the @EventHandler annotation:
1@EventHandler(priority = EventPriority.LOW)2public void onBreak(BlockBreakEvent event) {3 event.setCancelled(true);4}EventPriority lives in org.bukkit.event; IntelliJ adds the import when you press Alt+Enter. Inside one priority level the order between plugins is not something you can rely on, so never write code that needs to run "just before" another plugin's NORMAL handler. Pick a different level instead.
How canceling really works
Events that can be stopped implement the Cancellable interface. You already met its main method, setCancelled(true). Here is the surprising part:
That has three consequences:
- Later handlers can see that an event was canceled by asking
event.isCancelled(). - Later handlers can un-cancel it with
setCancelled(false). AHIGHESThandler could allow a break that aLOWprotection handler blocked. This is legal, but it overrides other plugins' decisions, so do it only on purpose, for example in a plugin whose whole job is to make exceptions. - Handlers that do not care about canceled events must say so, or they react to actions that never happen. That is what
ignoreCancelledis for.
ignoreCancelled = true
Imagine a plugin that pays players a coin for every block they break, written without thinking about canceling. Spawn is protected, so the blocks there never break, but the payment handler still runs for every attempt. Players stand at spawn and click the same block all day, getting paid for nothing.
The fix is one setting:
1@EventHandler(ignoreCancelled = true)2public void onBreak(BlockBreakEvent event) {3 event.getPlayer().sendRichMessage("<gold>+1 coin");4}With ignoreCancelled = true, Paper skips this handler whenever the event is already canceled when its turn comes. You can combine both settings: @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true).
Not every event can be canceled. PlayerJoinEvent and PlayerQuitEvent cannot, because there is nothing left to stop. If event.setCancelled is not offered by autocomplete, the event is not cancelable. The Event Finder marks the ones that are.
Building SpawnGuard
SpawnGuard uses three listeners at three different priorities, which is exactly how separate plugins share an event on a real server:
- GuardListener at
LOWprotects a square of 61 by 61 blocks around the world spawn: no breaking, no placing, no damage to players. - BlockCounterListener at
NORMALcounts blocks players break and congratulates them every 100. It usesignoreCancelled, so protected blocks do not count. - AuditListener at
MONITORwrites every break attempt at spawn to the console, with the final result.
First, a small helper class answers one question: is this location inside the zone?
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneSpawnZone.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javaListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javayou are hereHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelzone;2 3import org.bukkit.Location;4import org.bukkit.World;5 6public final class SpawnZone {7 8 private final int radius;9 10 public SpawnZone(int radius) {11 this.radius = radius;12 }13 14 public boolean contains(Location location) {15 World world = location.getWorld();16 if (world == null || world.getEnvironment() != World.Environment.NORMAL) {17 return false;18 }19 Location spawn = world.getSpawnLocation();20 return Math.abs(location.getBlockX() - spawn.getBlockX()) <= radius21 && Math.abs(location.getBlockZ() - spawn.getBlockZ()) <= radius;22 }23}- How many blocks the zone reaches in each direction from spawn. A radius of 30 makes a square 61 blocks wide (30 on each side, plus the spawn block itself).
- Only the overworld has a protected spawn. The Nether and the End are left alone.
- Asked every time instead of saved once, so the zone follows if an admin moves the world spawn with
/setworldspawn. - The distance along each axis, ignoring direction. Height does not matter, so the zone reaches from bedrock to the build limit.
The guard: cancel early
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneGuardListener.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javayou are hereListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
27@EventHandler(priority = EventPriority.LOW)28public void onBreak(BlockBreakEvent event) {29 Player player = event.getPlayer();30 if (!zone.contains(event.getBlock().getLocation()) || player.hasPermission(BYPASS)) {31 return;32 }33 event.setCancelled(true);34 player.sendActionBar(PROTECTED);35}- Protection runs early, so that every feature running later at
NORMALorHIGHalready knows the break is canceled. - Outside the zone, or for players with the bypass permission, there is nothing to do. This is a
guard clause : return early and keep the rest flat. - Flips the flag. The block stays, nothing drops. The handlers after this one still run.
- Tells the player why. The action bar is the line above the hotbar, which does not fill up chat when someone keeps clicking.
The onPlace handler is the same idea for BlockPlaceEvent. When a player without the bypass permission tries to build at spawn, this appears above their hotbar:
The counter: skip what was canceled
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneBlockCounterListener.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javayou are hereListener: reacts to events
- GuardListener.javaListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelzone;2 3import java.util.HashMap;4import java.util.Map;5import java.util.UUID;6import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;7import org.bukkit.entity.Player;8import org.bukkit.event.EventHandler;9import org.bukkit.event.Listener;10import org.bukkit.event.block.BlockBreakEvent;11 12public final class BlockCounterListener implements Listener {13 14 private static final int ANNOUNCE_EVERY = 100;15 16 private final Map<UUID, Integer> blocksBroken = new HashMap<>();17 18 @EventHandler(ignoreCancelled = true)19 public void onBreak(BlockBreakEvent event) {20 Player player = event.getPlayer();21 int total = blocksBroken.merge(player.getUniqueId(), 1, Integer::sum);22 if (total % ANNOUNCE_EVERY == 0) {23 player.sendRichMessage("<green>You have broken <total> blocks this session!",24 Placeholder.unparsed("total", String.valueOf(total)));25 }26 }27}- No priority given, so this is
NORMAL. Because the guard already canceled breaks at spawn, Paper skips this handler for them and they are never counted. - Adds one to the player's count and returns the new total.
- The remainder of dividing by 100 is zero at 100, 200, 300 and so on.
The auditor: read the final result
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneAuditListener.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javayou are hereListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javaListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelzone;2 3import java.util.logging.Logger;4import org.bukkit.block.Block;5import org.bukkit.event.EventHandler;6import org.bukkit.event.EventPriority;7import org.bukkit.event.Listener;8import org.bukkit.event.block.BlockBreakEvent;9 10public final class AuditListener implements Listener {11 12 private final SpawnZone zone;13 private final Logger logger;14 15 public AuditListener(SpawnZone zone, Logger logger) {16 this.zone = zone;17 this.logger = logger;18 }19 20 @EventHandler(priority = EventPriority.MONITOR)21 public void onBreakResult(BlockBreakEvent event) {22 Block block = event.getBlock();23 if (!zone.contains(block.getLocation())) {24 return;25 }26 String result = event.isCancelled() ? "blocked" : "allowed";27 logger.info(event.getPlayer().getName() + " tried to break " + block.getType()28 + " at " + block.getX() + " " + block.getY() + " " + block.getZ() + ": " + result);29 }30}- The plugin's logger, handed in through the constructor, so this class can write to the console.
- Runs after every other handler of every plugin, so
isCancelled()here is the final answer. NoignoreCancelled: this handler wants to see blocked attempts too. - Reads the flag. The
? :picks "blocked" when it is true and "allowed" when it is false. - Only reads and logs. A MONITOR handler must never change the event, because no plugin after it could react.
The main class wires it together. All three listeners get the same SpawnZone object:
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneSpawnGuardPlugin.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javaListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelzone;2 3import org.bukkit.plugin.PluginManager;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class SpawnGuardPlugin extends JavaPlugin {7 8 private static final int ZONE_RADIUS = 30;9 10 @Override11 public void onEnable() {12 SpawnZone zone = new SpawnZone(ZONE_RADIUS);13 AuditListener auditListener = new AuditListener(zone, getLogger());14 15 PluginManager pluginManager = getServer().getPluginManager();16 pluginManager.registerEvents(new GuardListener(zone), this);17 pluginManager.registerEvents(new BlockCounterListener(), this);18 pluginManager.registerEvents(auditListener, this);19 20 registerCommand("guardlog", "Turns the spawn break log on or off", new GuardLogCommand(this, auditListener));21 }22}- Saved in a variable, because the
/guardlogcommand needs this exact object later to switch it off and on. - The order of these lines does not decide the order of the handlers. The priorities do.
- Registers
/guardlog, explained further down.
Here is the console when Steve (no permission) and Jace (an operator) each break a block at spawn:
[15:20:41 INFO]: [SpawnGuard] Steve tried to break GRASS_BLOCK at 3 64 -7: blocked[15:20:55 INFO]: [SpawnGuard] Jace tried to break GRASS_BLOCK at 5 64 -2: allowedJace's break was allowed, so his block also counts toward his total in BlockCounterListener. Steve's did not.
Which priority should I use?
Ask what your handler does:
| Your handler... | Use |
|---|---|
| forbids something (protection, anti-cheat, "no TNT") | LOW |
| adds a normal feature (rewards, effects, messages) | NORMAL with ignoreCancelled = true |
| changes something other plugins may also change, and should win (chat format, death message) | HIGH |
| must override everything, like an admin bypass | HIGHEST, rarely |
| only records what happened (logs, statistics) | MONITOR, change nothing |
When in doubt, use the default NORMAL. Most handlers never need anything else.
Event inheritance: damage, attackers and projectiles
Events are Java classes, and some of them extend others. Damage is the most important example:
EntityDamageEventfires for all damage: falls, fire, drowning, lava, mobs and players.EntityDamageByEntityEventextends EntityDamageEvent. It fires when an entity caused the damage, and addsgetDamager(): the zombie, the player or the arrow that hit.
A handler for the parent event runs for the child events too. GuardListener's damage handler listens to EntityDamageEvent, so it protects players at spawn from every kind of damage at once:
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneGuardListener.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javayou are hereListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
47@EventHandler(priority = EventPriority.LOW)48public void onDamage(EntityDamageEvent event) {49 if (!(event.getEntity() instanceof Player victim) || !zone.contains(victim.getLocation())) {50 return;51 }52 event.setCancelled(true);53 if (event instanceof EntityDamageByEntityEvent byEntity) {54 Player attacker = attackingPlayer(byEntity);55 if (attacker != null) {56 attacker.sendRichMessage("<red>No fighting at spawn.");57 }58 }59}- The parent event: this handler sees every kind of damage to every entity.
- Only players are protected; mobs at spawn can still be hurt. The pattern
instanceof Player victimchecks the type and creates the variablevictimin one go. - No damage at all inside the zone.
- Is this damage one of the "by an entity" kind? If so,
byEntitygives access togetDamager(). - Only the attacking player gets the warning. A creeper does not read chat.
Who attacked? getDamager() is the entity that touched the victim. For a sword hit that is the player, but for a bow shot it is the arrow. To find the player behind a projectile, ask the projectile for its shooter:
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneGuardListener.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javayou are hereListener: reacts to events
- GuardLogCommand.javaCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
61private static Player attackingPlayer(EntityDamageByEntityEvent event) {62 Entity damager = event.getDamager();63 if (damager instanceof Player player) {64 return player;65 }66 if (damager instanceof Projectile projectile && projectile.getShooter() instanceof Player shooter) {67 return shooter;68 }69 return null;70}- The thing that hit: a player, a mob, an arrow, a trident, a snowball, a firework...
- A direct hit by a player.
- Arrows, tridents, snowballs and other
Projectiles remember who shot them. The shooter can also be a skeleton, a dispenser or nobody, which is why it is checked withinstanceoftoo. - No player was behind this damage, for example a zombie or a skeleton's arrow. The caller checks for
null.
Unregistering listeners
Usually a listener stays registered until your plugin is disabled, and then Paper removes it for you. Sometimes you want to switch one off while the server runs. HandlerList.unregisterAll(listener) removes every handler of that one listener object from every event:
Where this file livesevent-priorities-cancel-zonesrcmainjavacomexampleeventprioritiescancelzoneGuardLogCommand.java
The package com.example.eventprioritiescancelzone is the folder path com/example/eventprioritiescancelzone inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-zone/
- src/main/
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- AuditListener.javaListener: reacts to events
- BlockCounterListener.javaListener: reacts to events
- GuardListener.javaListener: reacts to events
- GuardLogCommand.javayou are hereCommand (BasicCommand)
- SpawnGuardPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- SpawnZone.javaHelper class
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelzone/Package com.example.eventprioritiescancelzone
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelzone;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.command.CommandSender;6import org.bukkit.event.HandlerList;7 8public final class GuardLogCommand implements BasicCommand {9 10 private final SpawnGuardPlugin plugin;11 private final AuditListener auditListener;12 private boolean logging = true;13 14 public GuardLogCommand(SpawnGuardPlugin plugin, AuditListener auditListener) {15 this.plugin = plugin;16 this.auditListener = auditListener;17 }18 19 @Override20 public void execute(CommandSourceStack source, String[] args) {21 CommandSender sender = source.getSender();22 if (logging) {23 HandlerList.unregisterAll(auditListener);24 logging = false;25 sender.sendRichMessage("<yellow>Spawn break log is now <red>off</red>.");26 } else {27 plugin.getServer().getPluginManager().registerEvents(auditListener, plugin);28 logging = true;29 sender.sendRichMessage("<yellow>Spawn break log is now <green>on</green>.");30 }31 }32 33 @Override34 public String permission() {35 return "spawnguard.admin";36 }37}- The exact listener object that was registered in
onEnable. Unregistering a newAuditListenerwould do nothing, because that object was never registered. - Remembers whether the log is on, so the command can switch between the two states.
- Removes the listener from every event it handles. From now on its MONITOR handler is not called.
- Switching it back on is a normal registration, with the same object.
- Only players with this permission can use
/guardlog, and others do not even see the command.
Spawn break log is now on.
Other ways you will see: HandlerList.unregisterAll(plugin) removes all of a plugin's listeners, and BlockBreakEvent.getHandlerList().unregister(listener) removes a listener from just one event. Often an even simpler design is a boolean field that the handler checks with a guard clause, and the listener stays registered.
Async events
Most events fire on the server's main thread, the one that runs the game 20 times a second. A few fire on a different thread so slow work in them cannot freeze the game. Their names start with Async, and event.isAsynchronous() returns true for them. Two of them you will use often:
AsyncChatEvent: a player sent a chat message.AsyncPlayerPreLoginEvent: a player is connecting, before they enter the world.
The rule for async handlers: you may read the event, check your own thread-safe data and send messages, but you must not touch the world. No blocks, no entities, no inventories, no teleporting, no spawning. The game is changing those on the main thread at the same moment. Paper often stops you with an IllegalStateException that mentions "async", and when it does not, you get rare, random bugs that are very hard to find.
When an async handler needs to change the world, it hands that job back to the main thread with the scheduler. Say "thunder" in chat with this plugin and lightning strikes next to you:
Where this file livesevent-priorities-cancel-asyncsrcmainjavacomexampleeventprioritiescancelasyncThunderListener.java
The package com.example.eventprioritiescancelasync is the folder path com/example/eventprioritiescancelasync inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-async/
- src/main/
- java/com/example/eventprioritiescancelasync/Package com.example.eventprioritiescancelasync
- MaintenanceListener.javaListener: reacts to events
- ThunderChatPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- ThunderListener.javayou are hereListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelasync/Package com.example.eventprioritiescancelasync
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelasync;2 3import io.papermc.paper.event.player.AsyncChatEvent;4import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;5import org.bukkit.entity.Player;6import org.bukkit.event.EventHandler;7import org.bukkit.event.Listener;8 9public final class ThunderListener implements Listener {10 11 private final ThunderChatPlugin plugin;12 13 public ThunderListener(ThunderChatPlugin plugin) {14 this.plugin = plugin;15 }16 17 @EventHandler(ignoreCancelled = true)18 public void onChat(AsyncChatEvent event) {19 String text = PlainTextComponentSerializer.plainText().serialize(event.message());20 if (!text.equalsIgnoreCase("thunder")) {21 return;22 }23 Player player = event.getPlayer();24 plugin.getServer().getScheduler().runTask(plugin, () -> {25 if (player.isOnline()) {26 player.getWorld().strikeLightningEffect(player.getLocation());27 }28 });29 }30}- If a chat filter canceled the message, nothing happens.
- Chat messages are components, with colors and styles. This turns one into plain text so it can be compared with a word.
- The safe hop. The code inside the arrow runs on the main thread during the next tick, where touching the world is allowed.
- The player might have left in the moment between chatting and the task running. Checking first avoids using a player who is gone.
- Lightning you can see and hear, without fire or damage. This is world work, so it lives inside the task.
AsyncPlayerPreLoginEvent is not cancelable at all. To refuse a player, you call disallow with a reason and a message they see on the disconnect screen. Because it is async, this is also the right place for slow checks, such as asking a database whether someone is banned.
Where this file livesevent-priorities-cancel-asyncsrcmainjavacomexampleeventprioritiescancelasyncMaintenanceListener.java
The package com.example.eventprioritiescancelasync is the folder path com/example/eventprioritiescancelasync inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-async/
- src/main/
- java/com/example/eventprioritiescancelasync/Package com.example.eventprioritiescancelasync
- MaintenanceListener.javayou are hereListener: reacts to events
- ThunderChatPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- ThunderListener.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelasync/Package com.example.eventprioritiescancelasync
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelasync;2 3import java.util.Set;4import net.kyori.adventure.text.Component;5import net.kyori.adventure.text.format.NamedTextColor;6import org.bukkit.event.EventHandler;7import org.bukkit.event.Listener;8import org.bukkit.event.player.AsyncPlayerPreLoginEvent;9 10public final class MaintenanceListener implements Listener {11 12 private static final Set<String> TESTERS = Set.of("Jace", "Steve");13 14 @EventHandler15 public void onPreLogin(AsyncPlayerPreLoginEvent event) {16 if (TESTERS.contains(event.getName())) {17 return;18 }19 event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_WHITELIST,20 Component.text("The server is in maintenance. Try again soon!", NamedTextColor.GOLD));21 }22}- A set that can never change after it is made, which makes it safe to read from any thread.
- The name of the player who is connecting. There is no
Playerobject yet, because the player has not joined. - Refuses the connection.
KICK_WHITELISTis the reason, and the component is the message on the player's screen.
Threads, performance and lag explains threads in depth, and Timing and tasks covers runTask and its friends.
Mistakes to avoid
- Rewarding canceled actions. Add
ignoreCancelled = trueto handlers that give something. - Changing an event at MONITOR. Plugins rely on MONITOR being the final, unchanged result. Use
HIGHESTif you really need the last word. - Thinking
setCancelled(true)stops the other handlers. It only sets a flag; everything after you still runs. - Forgetting projectiles. A "no PvP" check that only looks for
instanceof Playerlets players shoot each other with bows. - Touching the world in an async event. Hop to the main thread with
runTaskfirst. - Unregistering a new object.
HandlerList.unregisterAll(new MyListener())does nothing. Keep the registered object.
Stop the XP farm
A server runs SpawnGuard and this second plugin, MinerXp, which gives 1 experience point for every stone block broken. Players discovered they can stand at spawn and click stone forever to farm XP, even though nothing breaks. Fix the handler.
1@EventHandler2public void onBreak(BlockBreakEvent event) {3 if (event.getBlock().getType() == Material.STONE) {4 event.getPlayer().giveExp(1);5 }6}Hint 1
SpawnGuard cancels the break, but MinerXp's handler still runs. What does a handler need to say so Paper skips it for canceled events?
Hint 2
Also think about priority: should MinerXp run before or after protection plugins have decided?
Show the solution
ignoreCancelled = true makes Paper skip the handler for canceled breaks. HIGH lets every protection plugin, even one that cancels at NORMAL, decide first. The handler now pays only for blocks that really break.
Where this file livesevent-priorities-cancel-exercisesrcmainjavacomexampleeventprioritiescancelexerciseMinerXpListener.java
The package com.example.eventprioritiescancelexercise is the folder path com/example/eventprioritiescancelexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- event-priorities-cancel-exercise/
- src/main/
- java/com/example/eventprioritiescancelexercise/Package com.example.eventprioritiescancelexercise
- MinerXpListener.javayou are hereListener: reacts to events
- MinerXpPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/eventprioritiescancelexercise/Package com.example.eventprioritiescancelexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.eventprioritiescancelexercise;2 3import org.bukkit.Material;4import org.bukkit.event.EventHandler;5import org.bukkit.event.EventPriority;6import org.bukkit.event.Listener;7import org.bukkit.event.block.BlockBreakEvent;8 9public final class MinerXpListener implements Listener {10 11 private static final int XP_PER_STONE = 1;12 13 @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)14 public void onBreak(BlockBreakEvent event) {15 if (event.getBlock().getType() != Material.STONE) {16 return;17 }18 event.getPlayer().giveExp(XP_PER_STONE);19 }20}- Both settings together: run late, and only for breaks that are still allowed.
- A guard clause: anything that is not stone is ignored.
No lava at spawn
Players can still empty lava and water buckets at spawn. Add a handler to GuardListener that stops it. The event is PlayerBucketEmptyEvent, and its getBlock() is the block where the liquid would go.
Hint
Copy the shape of onPlace: same priority, same zone and permission check, a different event.
Show the solution
The new handler sits next to onBreak and onPlace, with the same priority and checks:
1@EventHandler(priority = EventPriority.LOW)2public void onBucket(PlayerBucketEmptyEvent event) {3 if (zone.contains(event.getBlock().getLocation()) && !event.getPlayer().hasPermission(BYPASS)) {4 event.setCancelled(true);5 }6}Add import org.bukkit.event.player.PlayerBucketEmptyEvent; at the top (IntelliJ offers it). For a friendlier plugin, also send the "Spawn is protected." action bar, like the other handlers.
Download SpawnGuard to try it on your test server, or open it in the Compile Lab.
Recap
- All handlers for an event run in priority order:
LOWEST,LOW,NORMAL,HIGH,HIGHEST,MONITOR. The default isNORMAL. - Later handlers have more say.
MONITORonly reads and never changes anything. setCancelled(true)sets a flag; later handlers still run and can read it withisCancelled().ignoreCancelled = trueskips your handler for events that are already canceled.- Listening to a parent event like
EntityDamageEventalso catches its children; useinstanceofto tell them apart, and ask projectiles for their shooter. HandlerList.unregisterAll(listener)switches a registered listener off.- In async events, never touch the world; hop back with
runTask.
Quick quiz
Which priority runs last and gets the final say over the outcome?
Handlers run fromLOWESTtoMONITOR.HIGHESTis the last one allowed to change the event, so it has the final say.MONITORruns after it but must not change anything.A
LOWhandler cancels aBlockBreakEvent. What happens to aNORMALhandler for the same event that has noignoreCancelled?Canceling only sets a flag. Every handler still runs unless it asked to be skipped withignoreCancelled = true.A skeleton shoots a player. In
EntityDamageByEntityEvent, what doesgetDamager()return?The damager is the entity that touched the victim, which is the arrow. Projectiles remember who shot them throughgetShooter().What should a
MONITORhandler do?MONITOR runs after everything else so it can see the real outcome. Changing the event there would surprise every other plugin, since none of them can react.In an
AsyncChatEventhandler you want to spawn a firework at the player. What is the safe way?Async events run on another thread, where world changes are not allowed.runTaskmoves the work to the main thread. Priority has nothing to do with threads.
Next steps
- Making your own events: fire events that other code can listen to, cancel and react to.
- Permissions: more about bypass permissions like
spawnguard.bypass. - Event Finder: find the event behind any idea, with a "Cancelable" badge.