Pages: Events & event bus

Events & event bus

If a whole listener class feels like too much for a single handler, you can register one inline. on just registers it, subscribe gives you back an EventSubscription so you can unregister it later.

on<PlayerJoinEvent> { it.player.sendMessage("Welcome!".green()) }

val subscription = subscribe<BlockBreakEvent>(EventPriority.HIGH, ignoreCancelled = true) {
    it.player.sendMessage("You broke ${it.block.type}")
}

subscription.unregister()

Both take a priority and ignoreCancelled, same as @EventHandler would.

Waiting for an event

awaitEvent suspends until a matching event fires, which is much nicer than keeping a map of "players I'm waiting on" around. Pass timeoutTicks and you get null back if nothing matched in time.

SporeCoroutines.launch {
    val move = awaitEvent<PlayerMoveEvent>(timeoutTicks = 100) { it.player == player }

    if (move == null) {
        player.sendMessage("You didn't move in time".red())
        return@launch
    }
}

Suspending handlers

Handlers can suspend. Inline, that's onAsync / subscribeAsync:

onAsync<PlayerJoinEvent> { event ->
    val profile = profileRepository.find(event.player.uniqueId.toString())

    if (!event.player.isOnline) return@onAsync
    applyProfile(event.player, profile)
}

In a @RegisterListener class, just mark the method suspend - SporeListeners spots the signature and registers it for you:

@RegisterListener
class JoinListener : Listener {

    @EventHandler(priority = EventPriority.MONITOR)
    suspend fun onJoin(event: PlayerJoinEvent) {
        val profile = profileRepository.find(event.player.uniqueId.toString())

        if (!event.player.isOnline) return
        applyProfile(event.player, profile)
    }
}

Plain Bukkit can't do this - a suspend fun compiles to a method with an extra Continuation parameter, and registerEvents rejects anything that isn't a single Event parameter, so the handler would silently never fire. SporeAPI registers those methods itself instead.

Three things to keep in mind:

  • You can't cancel or change the event past a suspension point. The event finishes and the server moves on while your coroutine is still parked. SporeAPI logs a warning if you attach a suspending handler to a Cancellable event for exactly this reason.
  • Priority only decides when the handler starts. Ordering against other plugins holds up to your first suspension point and means nothing after it.
  • Re-check state after resuming. The player may have disconnected while you were away, so guard with player.isOnline.

Everything before the first real suspension point still runs inline on the main thread, so a suspend handler that never actually suspends behaves exactly like a normal one.

Event bus (your own events)

The helpers above are for Bukkit events. For your own events, on any platform (Paper, Velocity, a bot), there's SporeBus:

class PunishmentIssuedEvent(val target: UUID, val reason: String) : Cancellable {
    override var isCancelled = false
}

SporeBus.on<PunishmentIssuedEvent> { event -> Webhook(url).setMessage(event.reason).send() }
SporeBus.on<PunishmentIssuedEvent>(BusPriority.LOWEST) { if (isExempt(it.target)) it.isCancelled = true }

val event = SporeBus.post(PunishmentIssuedEvent(uuid, "Spam"))
if (event.isCancelled) return
  • Handlers run in priority order (LOWEST first, MONITOR last), on the thread that calls post.
  • A handler for a parent class or interface also gets every subtype.
  • on returns a BusSubscription you can unregister(). ignoreCancelled = true skips cancelled events.
  • A handler that throws is reported as an incident, the other handlers still run.
  • onAsync { } takes a suspending handler. Like with Bukkit events, it can't cancel the event, because post returns before it finishes.