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
Cancellableevent 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 (
LOWESTfirst,MONITORlast), on the thread that callspost. - A handler for a parent class or interface also gets every subtype.
onreturns aBusSubscriptionyou canunregister().ignoreCancelled = trueskips 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, becausepostreturns before it finishes.