Why
A Bukkit listener is a class with @EventHandler, registration in PluginManager and a separate file per group. A delayed task is a BukkitRunnable with manual tick counting, and a three-phase chain becomes three nested Runnables.
The framework gives two things: event<T> { } — a one-line listener, and MCCoroutine — plugin.launch { delay(20.ticks) } instead of schedulers. The framework itself is written entirely on them.
Minimal example
Listeners
override fun onEnable() {
event<PlayerJoinEvent> { it.joinMessage(null) }
event<BlockBreakEvent>(priority = EventPriority.HIGH, ignoreCancelled = true) {
if (it.block.type == Material.BEDROCK) it.isCancelled = true
}
val listener = event<PlayerMoveEvent> { … }
listener.unregister()
}Inside the plugin class — event<T>, elsewhere — MyPlugin.instance.event<T>. The listener is bound to the plugin and removed on unload.
Coroutines
MyPlugin.instance.launch {
player.sendMessage("3")
delay(20.ticks)
player.sendMessage("2")
delay(20.ticks)
player.sendMessage("1")
delay(20.ticks)
Arena.start()
}
val job = MyPlugin.instance.launch {
while (true) {
Arena.tick()
delay(1.ticks)
}
}
job.cancel()In the child build.gradle.kts: compileOnly("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.22.0") — the gradle plugin already handles relocation.
Pitfalls
- Inside the event<T> lambda the event is it. Name parameters in nested lambdas, otherwise it gets shadowed.
- return from a handler — return@event.
- launch runs on the main server thread — Bukkit API is safe; for heavy work use withContext(Dispatchers.IO) and back.
- An endless while in launch without delay will hang the server.