Event listeners

registerListener hooks your script into the Bukkit event system. It is the main way scripts react to anything happening on the server.

registerListener(eventClass, callback, priority?)

Parameter Description
eventClass The Bukkit event class, for example $.PlayerJoinEvent
callback Called with the event object when it fires
priority Optional. Defaults to $.EventPriority.NORMAL
registerListener($.PlayerJoinEvent, (event) => {
  const player = event.getPlayer();
  player.sendRichMessage('<green>Welcome to the server!');
});

The callback receives the real event object, so everything in the Paper javadocs is available on it.

Getting the event class

Common Bukkit events resolve through $. Anything else, including Paper’s own newer events and events from other plugins, needs Script.loadClass:

const AsyncChatEvent = Script.loadClass('io.papermc.paper.event.player.AsyncChatEvent');

registerListener(AsyncChatEvent, (event) => {
  event.renderer(myChatRenderer);
});
const CustomPluginEvent = Script.loadClass('com.example.plugin.events.CustomPluginEvent');

registerListener(CustomPluginEvent, (event) => {
  console.log('Custom event fired!');
});

Event priority

Priorities decide the order listeners run in when several of them handle the same event. Lowest runs first, so it sees the event before anyone has modified it; monitor runs last and sees the final outcome.

  • $.EventPriority.LOWEST
  • $.EventPriority.LOW
  • $.EventPriority.NORMAL (default)
  • $.EventPriority.HIGH
  • $.EventPriority.HIGHEST
  • $.EventPriority.MONITOR

MONITOR is for observing only. Never modify the event or cancel it from a monitor listener.

registerListener($.PlayerSpawnLocationEvent, (event) => {
  if (event.getPlayer().hasPermission('admin.spawn')) {
    event.setSpawnLocation(adminSpawn);
  }
}, $.EventPriority.HIGH);

Cancelling events

Many events are cancellable. Cancelling stops the default behaviour from happening.

registerListener($.BlockBreakEvent, (event) => {
  const loc = event.getBlock().getLocation();

  if (isInProtectedZone(loc)) {
    event.setCancelled(true);
    event.getPlayer().sendRichMessage('<red>You cannot break blocks here.</red>');
  }
});

Only cancellable events have setCancelled. Calling it on one that is not will throw.

Threading

Listeners run on whatever thread fires the event. Most Bukkit events are on the main thread, so you can touch game state freely. A few are not, notably AsyncChatEvent and AsyncPlayerPreLoginEvent.

Do not do slow work in a listener on the main thread. Hand it to Scheduler.runAsync and come back with runSync when you need to touch the world again:

registerListener($.PlayerJoinEvent, (event) => {
  const player = event.getPlayer();

  Scheduler.runAsync(() => {
    const rank = lookupRankInDatabase(player.getUniqueId().toString());

    Scheduler.runSync(() => {
      player.sendRichMessage(`<gray>Welcome back, <white>${rank}</white>.`);
    });
  });
});

See the concurrency model for the full picture.

Errors

If a listener throws, the error is caught, formatted with a script stack trace and logged. The event continues to other listeners. A broken script does not take the server with it, but it also means a silent bug can hide in the console, so read your logs.

Automatic cleanup

Listeners registered this way are unregistered when the script unloads or reloads. You never need to unregister them by hand, and you cannot leak a listener across a reload.

Anything you set up around the listener is still yours to clean up. Boss bars, spawned entities and scoreboards need Script.addUnloadCallback:

const bossBar = Bukkit.createBossBar('Welcome!', $.BarColor.BLUE, $.BarStyle.SEGMENTED_6);

registerListener($.PlayerJoinEvent, (event) => bossBar.addPlayer(event.getPlayer()));
registerListener($.PlayerQuitEvent, (event) => bossBar.removePlayer(event.getPlayer()));

Script.addUnloadCallback(() => {
  bossBar.removeAll();
  bossBar.setVisible(false);
});

// Reloading mid-session should not leave current players out
Bukkit.getOnlinePlayers().forEach(player => bossBar.addPlayer(player));

That last line is a habit worth forming. A script that only reacts to join events will look broken every time you reload with players already online.

Worked example: spawn by permission

const spawn = newLocation('world', -57.5, 65.5, -1542.5);
const vipSpawn = newLocation('world', -57.5, 71, -1008.5);

registerListener($.PlayerSpawnLocationEvent, (event) => {
  const player = event.getPlayer();

  if (player.hasPermission('spawn.bypass')) return;

  event.setSpawnLocation(player.hasPermission('spawn.vip') ? vipSpawn : spawn);
});