Suspending Listeners
This page explains how you can use Kotlin Coroutines using the suspend
key word for listeners in minecraft plugins.
Create the Listener
Create a listener and add suspend to all functions where you perform suspendable operations (e.g. calling other suspendable functions). You can mix suspendable and non suspendable functions in listeners.
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import java.util.*
class PlayerDataListener(private val database: Database) : Listener {
@EventHandler
suspend fun onPlayerJoinEvent(event: PlayerJoinEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
@EventHandler
suspend fun onPlayerQuitEvent(event: PlayerQuitEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
In BungeeCord some events can be handled asynchronously. This allows full
control over consuming, processing and resuming events when performing long running operations. When you create a suspend
function using MCCoroutine, they automatically handle registerIntent
and completeIntent
. You do not have to do anything yourself,
all suspend functions are automatically processed asynchronously.
import net.md_5.bungee.api.event.PostLoginEvent
import net.md_5.bungee.api.event.ServerDisconnectEvent
import net.md_5.bungee.api.plugin.Listener
import net.md_5.bungee.event.EventHandler
import java.util.*
class PlayerDataListener(private val database: Database) : Listener {
@EventHandler
suspend fun onPlayerJoinEvent(event: PostLoginEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
@EventHandler
suspend fun onPlayerQuitEvent(event: ServerDisconnectEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
import net.minecraft.entity.Entity
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.util.Hand
import net.minecraft.util.hit.EntityHitResult
import net.minecraft.world.World
import java.util.*
class PlayerDataListener(private val database: Database) {
suspend fun onPlayerAttackEvent(
player: PlayerEntity,
world: World,
hand: Hand,
entity: Entity,
hitResult: EntityHitResult?
) {
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name.toString()
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
}
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import java.util.*
class PlayerDataListener(private val database: Database) : Listener {
@EventHandler
suspend fun onPlayerJoinEvent(event: PlayerJoinEvent) {
// In Folia, this will be entity thread of the player.
// In Bukkit, this will be the main thread.
withContext(plugin.mainDispatcher) {
// Make sure you switch to your plugin main thread before you do anything in your plugin.
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
}
@EventHandler
fun onPlayerQuitEvent(event: PlayerQuitEvent) {
plugin.launch {
// Make sure you switch to your plugin main thread before you do anything in your plugin.
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
}
import net.minestom.server.event.player.PlayerDisconnectEvent
import net.minestom.server.event.player.PlayerLoginEvent
import java.util.*
class PlayerDataListener(private val database: Database) {
suspend fun onPlayerJoinEvent(event: PlayerLoginEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.username
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
suspend fun onPlayerQuitEvent(event: PlayerDisconnectEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.username
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
import org.spongepowered.api.event.Listener
import org.spongepowered.api.event.network.ClientConnectionEvent
import java.util.*
class PlayerDataListener(private val database: Database) {
@Listener
suspend fun onPlayerJoinEvent(event: ClientConnectionEvent.Join) {
val player = event.targetEntity
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
@Listener
suspend fun onPlayerQuitEvent(event: ClientConnectionEvent.Disconnect) {
val player = event.targetEntity
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
In Velocity events can be handled asynchronously. This allows full
control over consuming, processing and resuming events when performing long running operations. When you create a suspend
function using MCCoroutine, they automatically handle Continuation
and EventTask
. You do not have to do anything yourself,
all suspend functions are automatically processed asynchronously.
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.connection.DisconnectEvent
import com.velocitypowered.api.event.connection.PostLoginEvent
import java.util.*
class PlayerDataListener(private val database: Database) {
@Subscribe
suspend fun onPlayerJoinEvent(event: PostLoginEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.username
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
@Subscribe
suspend fun onPlayerQuitEvent(event: DisconnectEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.username
playerData.lastQuitDate = Date()
database.saveData(player, playerData)
}
}
Register the Listener
Instead of using registerEvents
, use the provided extension method registerSuspendingEvents
to allow
suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions.
import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin
import com.github.shynixn.mccoroutine.bukkit.registerSuspendingEvents
class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
private val database = Database()
override suspend fun onEnableAsync() {
// Minecraft Main Thread
database.createDbIfNotExist()
server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this)
}
override suspend fun onDisableAsync() {
// Minecraft Main Thread
}
}
Instead of using registerListener
, use the provided extension method registerSuspendingListener
to allow
suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions.
import com.github.shynixn.mccoroutine.bungeecord.SuspendingPlugin
import com.github.shynixn.mccoroutine.bungeecord.registerSuspendingListener
class MCCoroutineSamplePlugin : SuspendingPlugin() {
private val database = Database()
override suspend fun onEnableAsync() {
// BungeeCord Startup Thread
database.createDbIfNotExist()
proxy.pluginManager.registerSuspendingListener(this, PlayerDataListener(database))
}
override suspend fun onDisableAsync() {
// BungeeCord Shutdown Thread (Not the same as the startup thread)
}
}
class MCCoroutineSampleServerMod : DedicatedServerModInitializer {
override fun onInitializeServer() {
ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server ->
// Connect Native Minecraft Scheduler and MCCoroutine.
mcCoroutineConfiguration.minecraftExecutor = Executor { r ->
server.submitAndJoin(r)
}
launch {
onServerStarting(server)
}
})
ServerLifecycleEvents.SERVER_STOPPING.register { server ->
mcCoroutineConfiguration.disposePluginSession()
}
}
/**
* MCCoroutine is ready after the server has started.
*/
private suspend fun onServerStarting(server : MinecraftServer) {
// Minecraft Main Thread
val database = Database()
database.createDbIfNotExist()
val listener = PlayerDataListener(database)
val mod = this
AttackEntityCallback.EVENT.register(AttackEntityCallback { player, world, hand, entity, hitResult ->
mod.launch {
listener.onPlayerAttackEvent(player, world, hand, entity, hitResult)
}
ActionResult.PASS
})
}
}
Instead of using registerEvents
, use the provided extension method registerSuspendingEvents
to allow
suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions.
import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin
import com.github.shynixn.mccoroutine.folia.registerSuspendingEvents
class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
private val database = Database()
override suspend fun onEnableAsync() {
// Minecraft Main Thread
database.createDbIfNotExist()
val plugin = this
// MCCoroutine for Folia cannot assume the correct dispatcher per event. You need to define how each event
// should find its correct dispatcher in MCCoroutine.
val eventDispatcher = mapOf<Class<out Event>, (event: Event) -> CoroutineContext>(
Pair(PlayerJoinEvent::class.java) {
require(it is PlayerJoinEvent)
plugin.entityDispatcher(it.player) // For a player event, the dispatcher is always player related.
},
Pair(PlayerQuitEvent::class.java) {
require(it is PlayerQuitEvent)
plugin.entityDispatcher(it.player)
}
)
server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this, eventDispatcher)
}
override suspend fun onDisableAsync() {
// Minecraft Main Thread
}
}
Instead of using addListener
, use the provided extension method addSuspendingListener
to allow
suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions.
import com.github.shynixn.mccoroutine.minestom.addSuspendingListener
import com.github.shynixn.mccoroutine.minestom.launch
import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.Database
import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.PlayerDataListener
import net.minestom.server.MinecraftServer
import net.minestom.server.event.player.PlayerLoginEvent
fun main(args: Array<String>) {
val minecraftServer = MinecraftServer.init()
minecraftServer.launch {
val database = Database()
// Minecraft Main Thread
database.createDbIfNotExist()
val listener = PlayerDataListener(database)
MinecraftServer.getGlobalEventHandler()
.addSuspendingListener(minecraftServer, PlayerLoginEvent::class.java) { e ->
listener.onPlayerJoinEvent(e)
}
}
minecraftServer.start("0.0.0.0", 25565)
}
Instead of using registerListeners
, use the provided extension method registerSuspendingListeners
to allow
suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions.
import com.github.shynixn.mccoroutine.sponge.SuspendingPluginContainer
import com.github.shynixn.mccoroutine.sponge.registerSuspendingListeners
import com.google.inject.Inject
import org.spongepowered.api.Sponge
import org.spongepowered.api.event.Listener
import org.spongepowered.api.event.game.state.GameStartedServerEvent
import org.spongepowered.api.event.game.state.GameStoppingServerEvent
import org.spongepowered.api.plugin.Plugin
import org.spongepowered.api.plugin.PluginContainer
@Plugin(
id = "mccoroutinesample",
name = "MCCoroutineSample",
description = "MCCoroutineSample is sample plugin to use MCCoroutine in Sponge."
)
class MCCoroutineSamplePlugin {
private val database = Database()
@Inject
private lateinit var suspendingPluginContainer: SuspendingPluginContainer
@Inject
private lateinit var pluginContainer : PluginContainer
@Listener
suspend fun onEnable(event: GameStartedServerEvent) {
// Minecraft Main Thread
database.createDbIfNotExist()
Sponge.getEventManager().registerSuspendingListeners(pluginContainer, PlayerDataListener(database))
}
@Listener
suspend fun onDisable(event: GameStoppingServerEvent) {
// Minecraft Main Thread
}
}
Instead of using register
, use the provided extension method registerSuspend
to allow
suspendable functions in your listener. There are also method overloads for functional style listeners.
import com.github.shynixn.mccoroutine.velocity.SuspendingPluginContainer
import com.github.shynixn.mccoroutine.velocity.registerSuspend
import com.google.inject.Inject
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
import com.velocitypowered.api.plugin.Plugin
import com.velocitypowered.api.proxy.ProxyServer
@Plugin(
id = "mccoroutinesample",
name = "MCCoroutineSample",
description = "MCCoroutineSample is sample plugin to use MCCoroutine in Velocity."
)
class MCCoroutineSamplePlugin {
private val database = Database()
@Inject
lateinit var proxyServer: ProxyServer
@Inject
constructor(suspendingPluginContainer: SuspendingPluginContainer) {
suspendingPluginContainer.initialize(this)
}
@Subscribe
suspend fun onProxyInitialization(event: ProxyInitializeEvent) {
// Velocity Thread Pool
database.createDbIfNotExist()
proxyServer.eventManager.registerSuspend(this, PlayerDataListener(database))
}
}
Test the Listener
Join and leave your server to observe getDataFromPlayer
and saveData
messages getting printed to your server log.