Making your own events
Create events other parts of your plugin (or other plugins) can listen to.
Paper's events tell your plugin what happens in the game. Your own events let your plugin tell other code what happens in your plugin: "this player just joined for the first time", "this player reached a diamond milestone". On this page you will write two custom events, fire them, listen to them, and make one that listeners can cancel and change.
Why make your own events?
Imagine you want three things to happen when a brand new player joins: they get a starter kit, everyone sees a welcome announcement, and later maybe a tutorial starts. You could put all three in one big PlayerJoinEvent handler. It works, but the handler grows with every feature, and every feature has to repeat the "is this the first join?" check.
A custom event splits the job in two:
- One place decides that something happened, and announces it by firing the event.
- Any number of listeners react, each in its own small class, without knowing about each other.
Programmers call this decoupling: the parts of your plugin depend on the event, not on each other. Adding a fourth feature means adding one more listener, and nothing else changes. It also opens your plugin to other plugins: they can listen to your events exactly like they listen to Paper's, and even cancel or change them. That is how big plugins such as economy, protection and minigame plugins let others build on top of them.
Your first custom event
The FirstJoin plugin fires a PlayerFirstJoinEvent whenever a player joins the server for the very first time. An event is a normal Java class with a few required parts. Here is all of it:
Where this file livescustom-events-firstsrcmainjavacomexamplecustomeventsfirstPlayerFirstJoinEvent.java
The package com.example.customeventsfirst is the folder path com/example/customeventsfirst 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.
- custom-events-first/
- src/main/
- java/com/example/customeventsfirst/Package com.example.customeventsfirst
- FirstJoinAnnouncer.javaListener: reacts to events
- FirstJoinDetector.javaListener: reacts to events
- FirstJoinPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- PlayerFirstJoinEvent.javayou are hereCustom event
- StarterKitListener.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/customeventsfirst/Package com.example.customeventsfirst
- 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.customeventsfirst;2 3import org.bukkit.entity.Player;4import org.bukkit.event.Event;5import org.bukkit.event.HandlerList;6 7public final class PlayerFirstJoinEvent extends Event {8 9 private static final HandlerList HANDLERS = new HandlerList();10 11 private final Player player;12 13 public PlayerFirstJoinEvent(Player player) {14 this.player = player;15 }16 17 public Player getPlayer() {18 return player;19 }20 21 @Override22 public HandlerList getHandlers() {23 return HANDLERS;24 }25 26 public static HandlerList getHandlerList() {27 return HANDLERS;28 }29}- Every event extends
Event, directly or through a parent likePlayerEvent. That is what lets Paper deliver it to listeners. By convention, the class name ends withEvent. - The
handler list : the list of every@EventHandlermethod registered for this event. It isstatic , so there is exactly one list for the whole event class, shared by every event object you create. - The information the event carries. Listeners need to know which player joined, so the event stores it.
finalbecause it never changes. - The
constructor . The code that fires the event passes the player in here. - A getter, so listeners can read the player with
event.getPlayer(), just like with Paper's events. - Required by
Event. Paper calls it on an event object to find the listeners. It must return the static list. - Also required, and easy to forget because the compiler does not check it. Paper looks for this exact static method when a listener for this event is registered.
Every custom event has this same skeleton. The only parts that change from event to event are the class name and the information it carries. The handler-list part is boilerplate: copy it as it is.
Firing the event
An event does nothing until something fires it. This listener watches Paper's PlayerJoinEvent and fires the custom event for first joins only:
Where this file livescustom-events-firstsrcmainjavacomexamplecustomeventsfirstFirstJoinDetector.java
The package com.example.customeventsfirst is the folder path com/example/customeventsfirst 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.
- custom-events-first/
- src/main/
- java/com/example/customeventsfirst/Package com.example.customeventsfirst
- FirstJoinAnnouncer.javaListener: reacts to events
- FirstJoinDetector.javayou are hereListener: reacts to events
- FirstJoinPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- PlayerFirstJoinEvent.javaCustom event
- StarterKitListener.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/customeventsfirst/Package com.example.customeventsfirst
- 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.customeventsfirst;2 3import org.bukkit.entity.Player;4import org.bukkit.event.EventHandler;5import org.bukkit.event.Listener;6import org.bukkit.event.player.PlayerJoinEvent;7 8public final class FirstJoinDetector implements Listener {9 10 @EventHandler11 public void onJoin(PlayerJoinEvent event) {12 Player player = event.getPlayer();13 if (player.hasPlayedBefore()) {14 return;15 }16 PlayerFirstJoinEvent firstJoin = new PlayerFirstJoinEvent(player);17 firstJoin.callEvent();18 }19}- A normal listener for a Paper event. This is the one place that decides what "first join" means.
- Returning players are ignored. The check lives here once, instead of in every feature.
- Creates the event object with the information listeners need.
- Fires the event. Paper runs every registered handler for
PlayerFirstJoinEvent, in priority order, and only then does this line finish.
callEvent() is a Paper shortcut. In older code you will see the same thing written as Bukkit.getPluginManager().callEvent(firstJoin); both do the same job.
Listening to it
Listening to a custom event is exactly the same as listening to a Paper event. The parameter type is your event class:
Where this file livescustom-events-firstsrcmainjavacomexamplecustomeventsfirstStarterKitListener.java
The package com.example.customeventsfirst is the folder path com/example/customeventsfirst 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.
- custom-events-first/
- src/main/
- java/com/example/customeventsfirst/Package com.example.customeventsfirst
- FirstJoinAnnouncer.javaListener: reacts to events
- FirstJoinDetector.javaListener: reacts to events
- FirstJoinPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- PlayerFirstJoinEvent.javaCustom event
- StarterKitListener.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/customeventsfirst/Package com.example.customeventsfirst
- 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.customeventsfirst;2 3import org.bukkit.Material;4import org.bukkit.entity.Player;5import org.bukkit.event.EventHandler;6import org.bukkit.event.Listener;7import org.bukkit.inventory.ItemStack;8 9public final class StarterKitListener implements Listener {10 11 @EventHandler12 public void onFirstJoin(PlayerFirstJoinEvent event) {13 Player player = event.getPlayer();14 player.getInventory().addItem(ItemStack.of(Material.STONE_SWORD), ItemStack.of(Material.BREAD, 8));15 player.sendRichMessage("<green>Here is a starter kit. Good luck out there!");16 }17}- The parameter type tells Paper which event this handler wants, the same as with
BlockBreakEventor any other event. - Uses the getter from the event class.
- Puts a stone sword and 8 bread into the player's inventory.
Where this file livescustom-events-firstsrcmainjavacomexamplecustomeventsfirstFirstJoinAnnouncer.java
The package com.example.customeventsfirst is the folder path com/example/customeventsfirst 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.
- custom-events-first/
- src/main/
- java/com/example/customeventsfirst/Package com.example.customeventsfirst
- FirstJoinAnnouncer.javayou are hereListener: reacts to events
- FirstJoinDetector.javaListener: reacts to events
- FirstJoinPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- PlayerFirstJoinEvent.javaCustom event
- StarterKitListener.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/customeventsfirst/Package com.example.customeventsfirst
- 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.customeventsfirst;2 3import net.kyori.adventure.text.minimessage.MiniMessage;4import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;5import org.bukkit.Bukkit;6import org.bukkit.event.EventHandler;7import org.bukkit.event.Listener;8 9public final class FirstJoinAnnouncer implements Listener {10 11 @EventHandler12 public void onFirstJoin(PlayerFirstJoinEvent event) {13 Bukkit.broadcast(MiniMessage.miniMessage().deserialize(14 "<gold>Everyone welcome <yellow><name></yellow>, who just joined for the first time!",15 Placeholder.unparsed("name", event.getPlayer().getName())));16 }17}- Sends the message to every online player and the console.
- Inserts the player's name as plain text into the
<name>tag.
Neither listener knows about the other, or about how "first join" is detected. The main class registers all three listeners like any others:
Where this file livescustom-events-firstsrcmainjavacomexamplecustomeventsfirstFirstJoinPlugin.java
The package com.example.customeventsfirst is the folder path com/example/customeventsfirst 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.
- custom-events-first/
- src/main/
- java/com/example/customeventsfirst/Package com.example.customeventsfirst
- FirstJoinAnnouncer.javaListener: reacts to events
- FirstJoinDetector.javaListener: reacts to events
- FirstJoinPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- PlayerFirstJoinEvent.javaCustom event
- StarterKitListener.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/customeventsfirst/Package com.example.customeventsfirst
- 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.customeventsfirst;2 3import org.bukkit.plugin.PluginManager;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class FirstJoinPlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 PluginManager pluginManager = getServer().getPluginManager();11 pluginManager.registerEvents(new FirstJoinDetector(), this);12 pluginManager.registerEvents(new StarterKitListener(), this);13 pluginManager.registerEvents(new FirstJoinAnnouncer(), this);14 }15}- The listener that fires the custom event is registered like any other listener.
- The two listeners that react to it.
When Alex joins for the first time, everyone sees the announcement, and Alex also gets the kit message:
Everyone welcome Alex, who just joined for the first time!
Here is a starter kit. Good luck out there!
- custom-events-first/
- src/
- main/
- java/
- com/example/customeventsfirst/
- FirstJoinPlugin.javaMain class: registers the three listeners
- PlayerFirstJoinEvent.javaThe custom event: carries the player
- FirstJoinDetector.javaListens to PlayerJoinEvent and fires PlayerFirstJoinEvent
- StarterKitListener.javaReacts by giving a starter kit
- FirstJoinAnnouncer.javaReacts by announcing the new player
- com/example/customeventsfirst/
- resources/
- plugin.ymlName, version, main class, api-version
- java/
- main/
- src/
Events that can be canceled and changed
The most useful custom events fire before your plugin does something, so listeners get a say. Paper's own BlockBreakEvent works this way: it fires before the block breaks, and listeners can cancel it. Your events can work the same way.
The DiamondMilestones plugin counts diamond ores each player mines. Every 10 ores, it wants to give the player experience and announce it. Before it does, it fires a DiamondMilestoneEvent, so that:
- a listener can cancel the milestone (no rewards in creative mode),
- a listener can change the reward (double experience for VIPs),
- a listener can add something (a bonus diamond every 50 ores),
- and other plugins can do the same without touching your code.
The template project example-events in paper-templates contains a smaller version of this event, if you want to compare.
Where this file livescustom-events-milestonesrcmainjavacomexamplecustomeventsmilestoneDiamondMilestoneEvent.java
The package com.example.customeventsmilestone is the folder path com/example/customeventsmilestone 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.
- custom-events-milestone/
- src/main/
- java/com/example/customeventsmilestone/Package com.example.customeventsmilestone
- DiamondMilestoneEvent.javayou are hereCustom event
- JackpotListener.javaListener: reacts to events
- MilestonePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MiningListener.javaListener: reacts to events
- NoCreativeListener.javaListener: reacts to events
- VipBonusListener.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/customeventsmilestone/Package com.example.customeventsmilestone
- 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.customeventsmilestone;2 3import org.bukkit.entity.Player;4import org.bukkit.event.Cancellable;5import org.bukkit.event.HandlerList;6import org.bukkit.event.player.PlayerEvent;7 8public final class DiamondMilestoneEvent extends PlayerEvent implements Cancellable {9 10 private static final HandlerList HANDLERS = new HandlerList();11 12 private final int total;13 private int rewardXp;14 private boolean canceled;15 16 public DiamondMilestoneEvent(Player player, int total, int rewardXp) {17 super(player);18 this.total = total;19 this.rewardXp = rewardXp;20 }21 22 public int getTotal() {23 return total;24 }25 26 public int getRewardXp() {27 return rewardXp;28 }29 30 public void setRewardXp(int rewardXp) {31 this.rewardXp = Math.max(0, rewardXp);32 }33 34 @Override35 public boolean isCancelled() {36 return canceled;37 }38 39 @Override40 public void setCancelled(boolean cancel) {41 this.canceled = cancel;42 }43 44 @Override45 public HandlerList getHandlers() {46 return HANDLERS;47 }48 49 public static HandlerList getHandlerList() {50 return HANDLERS;51 }52}- Two upgrades.
PlayerEventis Paper's parent class for events about a player: it stores the player and providesgetPlayer(), so this class does not have to.Cancellableis the interface that makes an event cancelable. - How many ores the player has mined. Listeners may read it but not change it, so it is
finaland has no setter. - The reward. Not
final, because listeners are allowed to change it. - Remembers whether a listener canceled the event. A new
booleanfield starts asfalse. - Hands the player to
PlayerEvent, which stores it forgetPlayer(). - The setter listeners use to change the reward.
Math.max(0, ...)stops a listener from setting a negative reward by mistake. Events are an API other people use, so guard against bad values. - Required by
Cancellable. The API spells it with two l's; keep that spelling for the method names. - Also required by
Cancellable. Listeners call this to cancel, and it only flips the field. - The same handler-list boilerplate as before. Every event class needs its own.
Firing it and acting on the answer
For a cancelable event, callEvent() returns an answer: true if the event was not canceled, false if a listener canceled it. The firing code then does its job only when allowed, and reads back any values listeners changed:
Where this file livescustom-events-milestonesrcmainjavacomexamplecustomeventsmilestoneMiningListener.java
The package com.example.customeventsmilestone is the folder path com/example/customeventsmilestone 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.
- custom-events-milestone/
- src/main/
- java/com/example/customeventsmilestone/Package com.example.customeventsmilestone
- DiamondMilestoneEvent.javaCustom event
- JackpotListener.javaListener: reacts to events
- MilestonePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MiningListener.javayou are hereListener: reacts to events
- NoCreativeListener.javaListener: reacts to events
- VipBonusListener.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/customeventsmilestone/Package com.example.customeventsmilestone
- 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.
25@EventHandler(ignoreCancelled = true)26public void onBreak(BlockBreakEvent event) {27 if (!DIAMOND_ORES.contains(event.getBlock().getType())) {28 return;29 }30 Player player = event.getPlayer();31 int total = diamondsMined.merge(player.getUniqueId(), 1, Integer::sum);32 if (total % MILESTONE_EVERY != 0) {33 return;34 }35 36 DiamondMilestoneEvent milestone = new DiamondMilestoneEvent(player, total, total * XP_PER_DIAMOND);37 if (!milestone.callEvent()) {38 return;39 }40 41 player.giveExp(milestone.getRewardXp());42 Bukkit.broadcast(MiniMessage.miniMessage().deserialize(43 "<aqua><name></aqua> <gray>mined <white><total></white> diamond ores and earned <green><xp> XP</green>!",44 Placeholder.unparsed("name", player.getName()),45 Placeholder.unparsed("total", String.valueOf(total)),46 Placeholder.unparsed("xp", String.valueOf(milestone.getRewardXp()))));47}- If a protection plugin blocked the break, the ore was not mined and does not count. You learned this in Event priorities and canceling.
- Adds one to this player's count and returns the new total.
- Not a multiple of 10, so this is not a milestone. Stop here.
- The suggested reward: 5 experience per ore mined so far, so 50 at the first milestone.
- Fires the event and reads the answer.
falsemeans a listener canceled it, so the reward and announcement are skipped. - Reads the reward after the listeners ran, so a VIP bonus is included. Never use the original number here.
- Announces the milestone with the final reward.
The listeners
Each listener is tiny and does one thing. The first one cancels:
Where this file livescustom-events-milestonesrcmainjavacomexamplecustomeventsmilestoneNoCreativeListener.java
The package com.example.customeventsmilestone is the folder path com/example/customeventsmilestone 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.
- custom-events-milestone/
- src/main/
- java/com/example/customeventsmilestone/Package com.example.customeventsmilestone
- DiamondMilestoneEvent.javaCustom event
- JackpotListener.javaListener: reacts to events
- MilestonePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MiningListener.javaListener: reacts to events
- NoCreativeListener.javayou are hereListener: reacts to events
- VipBonusListener.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/customeventsmilestone/Package com.example.customeventsmilestone
- 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.customeventsmilestone;2 3import org.bukkit.GameMode;4import org.bukkit.event.EventHandler;5import org.bukkit.event.EventPriority;6import org.bukkit.event.Listener;7 8public final class NoCreativeListener implements Listener {9 10 @EventHandler(priority = EventPriority.LOW)11 public void onMilestone(DiamondMilestoneEvent event) {12 if (event.getPlayer().getGameMode() == GameMode.CREATIVE) {13 event.setCancelled(true);14 }15 }16}- Canceling happens early, so the listeners after it can skip canceled milestones.
- Players in creative mode can place diamond ore and mine it again, so they get no milestone rewards.
- Makes
callEvent()returnfalseinMiningListener.
The second one changes the reward:
Where this file livescustom-events-milestonesrcmainjavacomexamplecustomeventsmilestoneVipBonusListener.java
The package com.example.customeventsmilestone is the folder path com/example/customeventsmilestone 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.
- custom-events-milestone/
- src/main/
- java/com/example/customeventsmilestone/Package com.example.customeventsmilestone
- DiamondMilestoneEvent.javaCustom event
- JackpotListener.javaListener: reacts to events
- MilestonePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MiningListener.javaListener: reacts to events
- NoCreativeListener.javaListener: reacts to events
- VipBonusListener.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/customeventsmilestone/Package com.example.customeventsmilestone
- 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.customeventsmilestone;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.Listener;5 6public final class VipBonusListener implements Listener {7 8 @EventHandler(ignoreCancelled = true)9 public void onMilestone(DiamondMilestoneEvent event) {10 if (event.getPlayer().hasPermission("milestones.vip")) {11 event.setRewardXp(event.getRewardXp() * 2);12 event.getPlayer().sendRichMessage("<gold>VIP bonus: double experience!");13 }14 }15}- No bonus message for milestones that were canceled.
- A permission from
plugin.ymlwithdefault: false, so nobody has it until a permissions plugin grants it. - Reads the current reward and doubles it. Because it reads first instead of setting a fixed number, it still works if another listener changed the reward earlier.
A VIP reaching 20 diamond ores sees this. The broadcast shows the doubled reward because MiningListener reads it after the listeners ran:
Steve mined 20 diamond ores and earned 200 XP!
Designing good events
- Name it after what happened, ending in
Event:PlayerFirstJoinEvent,DiamondMilestoneEvent,ShopPurchaseEvent. - Fire it before the action if listeners should be able to stop or change it, and make it
Cancellable. Fire it after the action if it is only news, like "a player finished a parkour run". - Carry everything a listener needs: the player, the amounts, the item. Listeners should not have to look things up in your plugin's internals.
- Make fields
finalunless listeners are meant to change them, and check values in setters. - Always read changeable values back after
callEvent(), and always respect the answer.
Async custom events
Most events are fired on the main thread. If your plugin does slow work on another thread, such as loading a player's data from a database, and wants to fire an event from there, the event must say that it is asynchronous. You do that by passing true to the parent constructor:
1public PlayerDataLoadedEvent(UUID playerId) {2 super(true);3 this.playerId = playerId;4}The rest of the class looks exactly like PlayerFirstJoinEvent. The rules are strict, and Paper checks them: an async event must be fired from a thread that is not the main thread, and a normal event must be fired from the main thread. Getting it wrong throws an IllegalStateException. Firing an async event looks like this:
1getServer().getScheduler().runTaskAsynchronously(this, () -> {2 new PlayerDataLoadedEvent(playerId).callEvent();3});Listeners of async events follow the same rules as listeners of AsyncChatEvent: no touching the world without hopping back to the main thread first. Most plugins never need an async event; reach for one only when you are already working off the main thread. Threads, performance and lag explains threads.
Letting other plugins listen
Another plugin can listen to DiamondMilestoneEvent just like you do. It needs two things:
- Your plugin's classes when it compiles, added as a
compileOnlydependency, the same way you depend on the Paper API. For a jar you share directly, that can be as simple ascompileOnly(files("libs/custom-events-milestone-1.0.0.jar"))in itsbuild.gradle.kts. depend: [DiamondMilestones](orsoftdepend) in itsplugin.yml, so Paper loads your plugin first. See plugin.yml explained.
Then it writes a normal @EventHandler for your event. Because the other plugin builds against your event class, renaming the class or its methods later breaks every plugin that uses it. Treat your event classes as a promise. Working with other plugins covers sharing an API in more detail.
Mistakes to avoid
- Forgetting the static
getHandlerList(). It compiles, then fails when a listener registers. - Creating a new
HandlerListingetHandlers(), such asreturn new HandlerList();. Paper would find an empty list every time and nobody would hear the event. Always return the one static field. - Ignoring the result of
callEvent()on a cancelable event. If you reward the player anyway, canceling does nothing. - Using the original value instead of the event's after firing, which throws away every listener's change.
- Firing an async event from the main thread, or a normal event from an async task. Paper throws an exception.
- Firing events in a tight loop. Every call runs every listener. Fire on meaningful moments, like a milestone, not on every tick.
A jackpot every 50 ores
Without changing MiningListener or the event class, add a listener that gives the player one extra diamond at every 50th diamond ore (50, 100, 150...). It should not pay out for canceled milestones.
Hint 1
The event already knows how many ores were mined: event.getTotal(). Which numbers divide by 50 with nothing left over?
Hint 2
Use ignoreCancelled = true, and a priority later than LOW so the creative check has already run. Remember to register the new listener in MilestonePlugin.
Show the solution
The new listener only reads the total and gives an item. Notice that you added a feature without touching the code that fires the event: that is the point of custom events.
Where this file livescustom-events-milestonesrcmainjavacomexamplecustomeventsmilestoneJackpotListener.java
The package com.example.customeventsmilestone is the folder path com/example/customeventsmilestone 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.
- custom-events-milestone/
- src/main/
- java/com/example/customeventsmilestone/Package com.example.customeventsmilestone
- DiamondMilestoneEvent.javaCustom event
- JackpotListener.javayou are hereListener: reacts to events
- MilestonePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- MiningListener.javaListener: reacts to events
- NoCreativeListener.javaListener: reacts to events
- VipBonusListener.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/customeventsmilestone/Package com.example.customeventsmilestone
- 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.customeventsmilestone;2 3import org.bukkit.Material;4import org.bukkit.entity.Player;5import org.bukkit.event.EventHandler;6import org.bukkit.event.EventPriority;7import org.bukkit.event.Listener;8import org.bukkit.inventory.ItemStack;9 10public final class JackpotListener implements Listener {11 12 private static final int JACKPOT_EVERY = 50;13 14 @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)15 public void onMilestone(DiamondMilestoneEvent event) {16 if (event.getTotal() % JACKPOT_EVERY != 0) {17 return;18 }19 Player player = event.getPlayer();20 player.getInventory().addItem(ItemStack.of(Material.DIAMOND));21 player.sendRichMessage("<aqua>Jackpot! Here is a bonus diamond.");22 }23}- Runs after the creative check at
LOWand skips canceled milestones. - A guard clause: only every 50th ore gets the jackpot.
- The bonus diamond.
And one more line in MilestonePlugin.onEnable: pluginManager.registerEvents(new JackpotListener(), this);.
Make the first join cancelable
Staff accounts should not get the starter kit. Change PlayerFirstJoinEvent so it can be canceled, add a LOW listener that cancels it for players with the permission firstjoin.staff, and make the starter kit listener skip canceled events. When the event is canceled, the detector should tell the player "Welcome, staff member. No starter kit for you."
Hint 1
Add implements Cancellable, a boolean cancelled field, and the two methods isCancelled() and setCancelled(boolean), exactly like DiamondMilestoneEvent.
Hint 2
In the detector, use the result of callEvent(): it is false when the event was canceled.
Show the solution
The event gains the Cancellable parts:
Where this file livescustom-events-exercisesrcmainjavacomexamplecustomeventsexercisePlayerFirstJoinEvent.java
The package com.example.customeventsexercise is the folder path com/example/customeventsexercise 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.
- custom-events-exercise/
- src/main/
- java/com/example/customeventsexercise/Package com.example.customeventsexercise
- FirstJoinCancelablePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- FirstJoinDetector.javaListener: reacts to events
- PlayerFirstJoinEvent.javayou are hereCustom event
- StaffSkipListener.javaListener: reacts to events
- StarterKitListener.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/customeventsexercise/Package com.example.customeventsexercise
- 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.customeventsexercise;2 3import org.bukkit.entity.Player;4import org.bukkit.event.Cancellable;5import org.bukkit.event.Event;6import org.bukkit.event.HandlerList;7 8public final class PlayerFirstJoinEvent extends Event implements Cancellable {9 10 private static final HandlerList HANDLERS = new HandlerList();11 12 private final Player player;13 private boolean canceled;14 15 public PlayerFirstJoinEvent(Player player) {16 this.player = player;17 }18 19 public Player getPlayer() {20 return player;21 }22 23 @Override24 public boolean isCancelled() {25 return canceled;26 }27 28 @Override29 public void setCancelled(boolean cancel) {30 this.canceled = cancel;31 }32 33 @Override34 public HandlerList getHandlers() {35 return HANDLERS;36 }37 38 public static HandlerList getHandlerList() {39 return HANDLERS;40 }41}- The event can now be canceled.
- Starts as
false: not canceled. - What the staff listener calls.
The detector reads the answer:
Where this file livescustom-events-exercisesrcmainjavacomexamplecustomeventsexerciseFirstJoinDetector.java
The package com.example.customeventsexercise is the folder path com/example/customeventsexercise 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.
- custom-events-exercise/
- src/main/
- java/com/example/customeventsexercise/Package com.example.customeventsexercise
- FirstJoinCancelablePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- FirstJoinDetector.javayou are hereListener: reacts to events
- PlayerFirstJoinEvent.javaCustom event
- StaffSkipListener.javaListener: reacts to events
- StarterKitListener.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/customeventsexercise/Package com.example.customeventsexercise
- 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.
10@EventHandler11public void onJoin(PlayerJoinEvent event) {12 Player player = event.getPlayer();13 if (player.hasPlayedBefore()) {14 return;15 }16 if (!new PlayerFirstJoinEvent(player).callEvent()) {17 player.sendRichMessage("<gray>Welcome, staff member. No starter kit for you.");18 }19}- Creates and fires the event in one line, and checks whether it was canceled.
The staff listener cancels early, and the kit listener skips canceled events:
Where this file livescustom-events-exercisesrcmainjavacomexamplecustomeventsexerciseStaffSkipListener.java
The package com.example.customeventsexercise is the folder path com/example/customeventsexercise 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.
- custom-events-exercise/
- src/main/
- java/com/example/customeventsexercise/Package com.example.customeventsexercise
- FirstJoinCancelablePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- FirstJoinDetector.javaListener: reacts to events
- PlayerFirstJoinEvent.javaCustom event
- StaffSkipListener.javayou are hereListener: reacts to events
- StarterKitListener.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/customeventsexercise/Package com.example.customeventsexercise
- 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.
9@EventHandler(priority = EventPriority.LOW)10public void onFirstJoin(PlayerFirstJoinEvent event) {11 if (event.getPlayer().hasPermission("firstjoin.staff")) {12 event.setCancelled(true);13 }14}- Cancels before the kit listener at
NORMALruns.
Where this file livescustom-events-exercisesrcmainjavacomexamplecustomeventsexerciseStarterKitListener.java
The package com.example.customeventsexercise is the folder path com/example/customeventsexercise 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.
- custom-events-exercise/
- src/main/
- java/com/example/customeventsexercise/Package com.example.customeventsexercise
- FirstJoinCancelablePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- FirstJoinDetector.javaListener: reacts to events
- PlayerFirstJoinEvent.javaCustom event
- StaffSkipListener.javaListener: reacts to events
- StarterKitListener.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/customeventsexercise/Package com.example.customeventsexercise
- 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.
11@EventHandler(ignoreCancelled = true)- No kit for canceled first joins.
Download DiamondMilestones or download FirstJoin to try them on your test server.
Recap
- Custom events let one part of your code announce something and any number of listeners react, in your plugin or in others.
- An event class extends
Event(or a parent likePlayerEvent), has a staticHandlerList, and returns it from bothgetHandlers()and the staticgetHandlerList(). - Fire an event with
event.callEvent(); listen to it with a normal@EventHandler. - Implement
Cancellableto let listeners cancel;callEvent()then returnsfalsewhen canceled, and you must respect it. - Setters let listeners change values; read them back after firing.
- Async events pass
truetosuper(...)and must be fired off the main thread.
Quick quiz
Which method must every custom event have, even though the compiler does not force you to write it?
Paper looks for the staticgetHandlerList()when a listener is registered.getHandlers()is enforced by the compiler,callEvent()is inherited fromEvent, andisCancelled()is only needed for cancelable events.For a cancelable event, what does
callEvent()return?callEvent()answers "may I go ahead?".falsemeans a listener canceled the event, so your code should skip the action.A listener doubles the reward with
event.setRewardXp(...). What should the firing code give the player?The listener only changed a value on the event. The firing code must read that value back after all listeners ran, or the change is lost.Why make
DiamondMilestoneEventextendPlayerEventinstead ofEvent?PlayerEventis a convenient parent for events about one player. Cancelability comes from theCancellableinterface, not from the parent class.You fire an event created with
super(true)from a normal@EventHandleron the main thread. What happens?super(true)promises the event is fired asynchronously. Paper checks that promise and throws when it is broken.
Next steps
- Commands, part 1: let players run your code with
/commands. - Organizing a bigger plugin: events are one of the best tools for keeping features apart.
- Working with other plugins: listen to other plugins' events and share your own API.