Skip to content

Suspending Plugin

This guide explains how Kotlin Coroutines can be used in minecraft plugins in various ways using MCCoroutine. For this, a new plugin is developed from scratch to handle asynchronous and synchronous code.

Important

Make sure you have already installed MCCoroutine. See Installation for details.

Plugin Main class

MCCoroutine does not need to be called explicitly in your plugin main class. It is started implicitly when you use it for the first time and disposed automatically when you reload your plugin.

The first decision for Bukkit API based plugins is to decide between JavaPlugin or SuspendingJavaPlugin, which is a new base class extending JavaPlugin.

If you want to perform async operations or call other suspending functions from your plugin class, go with the newly available type SuspendingJavaPlugin otherwise use JavaPlugin.

import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin

class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
    override suspend fun onEnableAsync() {
        // Minecraft Main Thread
    }

    override suspend fun onDisableAsync() {
        // Minecraft Main Thread
    }
}

How onEnableAsync works

The implementation which calls the onEnableAsync function manipulates the Bukkit Server implementation in the following way: If a context switch is made, it blocks the entire minecraft main thread until the context is given back. This means, in this method, you can switch contexts as you like but the plugin is not considered enabled until the context is given back. It allows for a clean startup as the plugin is not considered "enabled" until the context is given back. Other plugins which are already enabled, may or may not already perform work in the background. Plugins, which may get enabled in the future, wait until this plugin is enabled.

The first decision for BungeeCord API based plugins is to decide between Plugin or SuspendingPlugin, which is a new base class extending Plugin.

If you want to perform async operations or call other suspending functions from your plugin class, go with the newly available type SuspendingPlugin otherwise use Plugin.

import com.github.shynixn.mccoroutine.bungeecord.SuspendingPlugin

class MCCoroutineSamplePlugin : SuspendingPlugin() {
    override suspend fun onEnableAsync() {
        // BungeeCord Startup Thread
    }

    override suspend fun onDisableAsync() {
        // BungeeCord Shutdown Thread (Not the same as the startup thread)
    }
}

How onEnableAsync works

The implementation which calls the onEnableAsync function manipulates the BungeeCord Server implementation in the following way: If a context switch is made, it blocks the entire bungeecord startup thread until the context is given back. This means, in this method, you can switch contexts as you like but the plugin is not considered enabled until the context is given back. It allows for a clean startup as the plugin is not considered "enabled" until the context is given back. Other plugins which are already enabled, may or may not already perform work in the background. Plugins, which may get enabled in the future, wait until this plugin is enabled.

MCCoroutine for Fabric does not have an dependency on Minecraft itself, therefore it is version independent from Minecraft. It only depends on the Fabric Api. This however means, we need to manually setup and dispose MCCoroutine. Register the SERVER_STARTING event and connect the native Minecraft Scheduler with MCCoroutine using an Executor. Dispose MCCoroutine in SERVER_STOPPING.

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
        // Your startup code with suspend support

        this.launch {
            // Launch new corroutines
        }
    }
}

The first decision for Bukkit API based plugins is to decide between JavaPlugin or SuspendingJavaPlugin, which is a new base class extending JavaPlugin.

If you want to perform async operations or call other suspending functions from your plugin class, go with the newly available type SuspendingJavaPlugin otherwise use JavaPlugin.

import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin

class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
    override suspend fun onEnableAsync() {
        // Global Region Thread
    }

    override suspend fun onDisableAsync() {
        // Global Region Thread
    }
}

How onEnableAsync works

The implementation which calls the onEnableAsync function manipulates the Bukkit Server implementation in the following way: If a context switch is made, it blocks the entire global region thread until the context is given back. This means, in this method, you can switch contexts as you like but the plugin is not considered enabled until the context is given back. It allows for a clean startup as the plugin is not considered "enabled" until the context is given back. Other plugins which are already enabled, may or may not already perform work in the background. Plugins, which may get enabled in the future, wait until this plugin is enabled.

MCCoroutine can be used on server or on extension level. The example below shows using MCCoroutine on server level. If you are developing an extension, you can use the instance of your Extension instead of the MinecraftServer

import com.github.shynixn.mccoroutine.minestom.launch
import net.minestom.server.MinecraftServer

fun main(args: Array<String>) {
    val minecraftServer = MinecraftServer.init() 
    minecraftServer.launch {
        // Suspendable operations   
    }
    minecraftServer.start("0.0.0.0", 25565)
}

The first decision for Sponge API based plugins is to decide, if you want to call other suspending functions from your plugin class. If so, add a field which injects the type SuspendingPluginContainer. This turns your main class into a suspendable listener.

import com.github.shynixn.mccoroutine.sponge.SuspendingPluginContainer
@Plugin(
    id = "mccoroutinesample",
    name = "MCCoroutineSample",
    description = "MCCoroutineSample is sample plugin to use MCCoroutine in Sponge."
)
class MCCoroutineSamplePlugin {
    @Inject
    private lateinit var suspendingPluginContainer: SuspendingPluginContainer

    @Listener
    suspend fun onEnable(event: GameStartedServerEvent) {
        // Minecraft Main Thread
    }

    @Listener
    suspend fun onDisable(event: GameStoppingServerEvent) {
        // Minecraft Main Thread
    }
}

MCCoroutine requires to initialize the plugin coroutine scope manually in your plugin main class. This also allows to call suspending functions in your plugin main class.

import com.github.shynixn.mccoroutine.velocity.SuspendingPluginContainer
@Plugin(
    id = "mccoroutinesample",
    name = "MCCoroutineSample",
    description = "MCCoroutineSample is sample plugin to use MCCoroutine in Velocity."
)
class MCCoroutineSamplePlugin {
     @Inject
    constructor(suspendingPluginContainer: SuspendingPluginContainer) {
        suspendingPluginContainer.initialize(this)
    }

    @Subscribe
    suspend fun onProxyInitialization(event: ProxyInitializeEvent) {
        // Velocity Thread Pool
    }
}

Calling a Database from Plugin Main class

Create a class containing properties of data, which we want to store into a database.

class PlayerData(var uuid: UUID, var name: String, var lastJoinDate: Date, var lastQuitDate: Date) {
}

Create a class Database, which is responsible to store/retrieve this data into/from a database. Here, it is important that we perform all IO calls on async threads and returns on the minecraft main thread.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bukkit.entity.Player
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on minecraft thread " + Thread.currentThread().id)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id)
            // ... create tables
        }
        println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id)
    }

    suspend fun getDataFromPlayer(player : Player) : PlayerData {
        println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uniqueId, player.name, Date(), Date())
        }

        println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id)
        return playerData;
    }

    suspend fun saveData(player : Player, playerData : PlayerData) {
        println("[saveData] Start on minecraft thread " + Thread.currentThread().id)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().id)
            // insert or update playerData
        }

        println("[saveData] End on minecraft thread " + Thread.currentThread().id)
    }
}

Important

BungeeCord does not have a main thread or minecraft thread. Instead it operates on different types of thread pools. This means, the thread id is not always the same if we suspend an operation. Therefore, it is recommend to print the name of the thread instead of the id to see which threadpool you are currently on.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.md_5.bungee.api.connection.ProxiedPlayer
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on any thread " + Thread.currentThread().name)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().name)
            // ... create tables
        }
        println("[createDbIfNotExist] End on bungeecord plugin threadpool " + Thread.currentThread().name)
    }   

    suspend fun getDataFromPlayer(player : ProxiedPlayer) : PlayerData {
        println("[getDataFromPlayer] Start on any thread " + Thread.currentThread().name)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().name)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uniqueId, player.name, Date(), Date())
        }

        println("[getDataFromPlayer] End on bungeecord plugin threadpool " + Thread.currentThread().name)
        return playerData;
    }

    suspend fun saveData(player : ProxiedPlayer, playerData : PlayerData) {
        println("[saveData] Start on any thread " + Thread.currentThread().name)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().name)
            // insert or update playerData
        }

        println("[saveData] End on bungeecord plugin threadpool " + Thread.currentThread().name)
    }
}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.minecraft.entity.player.PlayerEntity
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on minecraft thread " + Thread.currentThread().id)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id)
            // ... create tables
        }
        println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id)
    }

    suspend fun getDataFromPlayer(player: PlayerEntity) : PlayerData {
        println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uuid, player.name.toString(), Date(), Date())
        }

        println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id)
        return playerData;
    }

    suspend fun saveData(player: PlayerEntity, playerData : PlayerData) {
        println("[saveData] Start on minecraft thread " + Thread.currentThread().id)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().id)
            // insert or update playerData
        }

        println("[saveData] End on minecraft thread " + Thread.currentThread().id)
    }
}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bukkit.entity.Player
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on the caller thread " + Thread.currentThread().id)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id)
            // ... create tables
        }
        println("[createDbIfNotExist] End on the caller thread " + Thread.currentThread().id)
    }

    suspend fun getDataFromPlayer(player : Player) : PlayerData {
        println("[getDataFromPlayer] Start on the caller thread " + Thread.currentThread().id)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uniqueId, player.name, Date(), Date())
        }

        println("[getDataFromPlayer] End on the caller thread  " + Thread.currentThread().id)
        return playerData;
    }

    suspend fun saveData(player : Player, playerData : PlayerData) {
        println("[saveData] Start on the caller thread  " + Thread.currentThread().id)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().id)
            // insert or update playerData
        }

        println("[saveData] End on the caller thread  " + Thread.currentThread().id)
    }
}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.minestom.server.entity.Player
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on minecraft thread " + Thread.currentThread().id)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id)
            // ... create tables
        }
        println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id)
    }

    suspend fun getDataFromPlayer(player : Player) : PlayerData {
        println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uuid, player.username, Date(), Date())
        }

        println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id)
        return playerData;
    }

    suspend fun saveData(player : Player, playerData : PlayerData) {
        println("[saveData] Start on minecraft thread " + Thread.currentThread().id)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().id)
            // insert or update playerData
        }

        println("[saveData] End on minecraft thread " + Thread.currentThread().id)
    }
}
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.spongepowered.api.entity.living.player.Player
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on minecraft thread " + Thread.currentThread().id)
        withContext(Dispatchers.IO){
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id)
            // ... create tables
        }
        println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id)
    }

    suspend fun getDataFromPlayer(player : Player) : PlayerData {
        println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uniqueId, player.name, Date(), Date())
        }

        println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id)
        return playerData;
    }

    suspend fun saveData(player : Player, playerData : PlayerData) {
        println("[saveData] Start on minecraft thread " + Thread.currentThread().id)

        withContext(Dispatchers.IO){
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().id)
            // insert or update playerData
        }

        println("[saveData] End on minecraft thread " + Thread.currentThread().id)
    }
}

Important

Velocity does not have a main thread or minecraft thread. Instead it operates on different types of thread pools. This means, the thread id is not always the same if we suspend an operation. Therefore, it is recommend to print the name of the thread instead of the id to see which threadpool you are currently on.

import com.velocitypowered.api.proxy.Player
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*

class Database() {
    suspend fun createDbIfNotExist() {
        println("[createDbIfNotExist] Start on any thread " + Thread.currentThread().name)
        withContext(Dispatchers.IO) {
            println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().name)
            // ... create tables
        }
        println("[createDbIfNotExist] End on velocity plugin threadpool " + Thread.currentThread().name)
    }

    suspend fun getDataFromPlayer(player: Player): PlayerData {
        println("[getDataFromPlayer] Start on any thread " + Thread.currentThread().name)
        val playerData = withContext(Dispatchers.IO) {
            println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().name)
            // ... get from database by player uuid or create new playerData instance.
            PlayerData(player.uniqueId, player.username, Date(), Date())
        }

        println("[getDataFromPlayer] End on velocity plugin threadpool " + Thread.currentThread().name)
        return playerData;
    }

    suspend fun saveData(player: Player, playerData: PlayerData) {
        println("[saveData] Start on any thread " + Thread.currentThread().name)

        withContext(Dispatchers.IO) {
            println("[saveData] Saving player data on database io thread " + Thread.currentThread().name)
            // insert or update playerData
        }

        println("[saveData] End on velocity plugin threadpool " + Thread.currentThread().name)
    }
}

Create a new instance of the database and call it in your main class.

import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin

class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
    private val database = Database()

    override suspend fun onEnableAsync() {
        // Minecraft Main Thread
        database.createDbIfNotExist()
    }

    override suspend fun onDisableAsync() {
    }
}
import com.github.shynixn.mccoroutine.bungeecord.SuspendingPlugin

class MCCoroutineSamplePlugin : SuspendingPlugin() {
    private val database = Database()

    override suspend fun onEnableAsync() {
        // BungeeCord Startup Thread
        database.createDbIfNotExist()
    }

    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()
    }
}
import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin

class MCCoroutineSamplePlugin : SuspendingJavaPlugin() {
    private val database = Database()

    override suspend fun onEnableAsync() {
        // Global Region Thread
        database.createDbIfNotExist()
    }

    override suspend fun onDisableAsync() {
    }
}
import com.github.shynixn.mccoroutine.minestom.launch
import net.minestom.server.MinecraftServer

fun main(args: Array<String>) {
    val minecraftServer = MinecraftServer.init() 
    minecraftServer.launch {
        // Minecraft Main Thread
        val database = Database()
        database.createDbIfNotExist()
    }
    minecraftServer.start("0.0.0.0", 25565)
}
import com.github.shynixn.mccoroutine.sponge.SuspendingPluginContainer
@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

    @Listener
    suspend fun onEnable(event: GameStartedServerEvent) {
        // Minecraft Main Thread
        database.createDbIfNotExist()
    }

    @Listener
    suspend fun onDisable(event: GameStoppingServerEvent) {
        // Minecraft Main Thread
    }
}

MCCoroutine requires to initialize the plugin coroutine scope manually in your plugin main class. This also allows to call suspending functions in your plugin main class.

import com.github.shynixn.mccoroutine.velocity.SuspendingPluginContainer
@Plugin(
    id = "mccoroutinesample",
    name = "MCCoroutineSample",
    description = "MCCoroutineSample is sample plugin to use MCCoroutine in Velocity."
)
class MCCoroutineSamplePlugin {
    private val database = Database()

     @Inject
    constructor(suspendingPluginContainer: SuspendingPluginContainer) {
        suspendingPluginContainer.initialize(this)
    }

    @Subscribe
    suspend fun onProxyInitialization(event: ProxyInitializeEvent) {
        // Velocity Thread Pool
        database.createDbIfNotExist()
    }
}

Test the Plugin

Start your server to observe the createDbIfNotExist messages getting printed to your server log. Extend it with real database operations to get familiar with how it works.