Skip to content

Suspending Commandexecutors

This page explains how you can use Kotlin Coroutines using the suspend key word for command executors in minecraft plugins.

Create the CommandExecutor

Create a traditional command executor but implement SuspendingCommandExecutor instead of CommandExecutor. Please consider, that the return value true is automatically assumed, if the function is suspended in one branch.

import com.github.shynixn.mccoroutine.bukkit.SuspendingCommandExecutor
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player

class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommandExecutor {
    override suspend fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
        if (sender !is Player) {
            return false
        }

        if (args.size == 2 && args[0].equals("rename", true)) {
            val name = args[1]
            val playerData = database.getDataFromPlayer(sender)
            playerData.name = name
            database.saveData(sender, playerData)
            return true
        }

        return false
    }
}

Create a traditional command executor but extend from SuspendingCommand instead of Command.

import com.github.shynixn.mccoroutine.bungeecord.SuspendingCommand
import net.md_5.bungee.api.CommandSender
import net.md_5.bungee.api.connection.ProxiedPlayer

class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommand("playerdata") {
    override suspend fun execute(sender: CommandSender, args: Array<out String>) {
        if (sender !is ProxiedPlayer) {
            return
        }

        if (args.size == 2 && args[0].equals("rename", true)) {
            val name = args[1]
            val playerData = database.getDataFromPlayer(sender)
            playerData.name = name
            database.saveData(sender, playerData)
            return
        }
    }
}

Create a traditional command executor but extend from SuspendingCommand instead of SuspendingCommand.

import com.github.shynixn.mccoroutine.fabric.SuspendingCommand
import com.mojang.brigadier.context.CommandContext
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.server.command.ServerCommandSource

class PlayerDataCommandExecutor : SuspendingCommand<ServerCommandSource> {
    override suspend fun run(context: CommandContext<ServerCommandSource>): Int {
        if (context.source.entity is PlayerEntity) {
            val sender = context.source.entity as PlayerEntity
            println("[PlayerDataCommandExecutor] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}")
        }

        return 1
    }
}

Folia schedules threads on the region of the entity who executed this command. For the console (globalregion) and command blocks (region) this rule applies as well. Other than that, usage is almost identical to Bukkit.

import com.github.shynixn.mccoroutine.folia.SuspendingCommandExecutor
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player

class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommandExecutor {
    override suspend fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
        // In Folia, this will be the global region thread, or entity execution thread.
        // In Bukkit, this will be the main thread.

        if (sender !is Player) {
            return false
        }

        if (args.size == 2 && args[0].equals("rename", true)) {
            val name = args[1]
            withContext(plugin.mainDispatcher) {
                // Make sure you switch to your plugin main thread before you do anything in your plugin.
                val playerData = database.getDataFromPlayer(sender)
                playerData.name = name
                database.saveData(sender, playerData)
            }

            return true
        }

        return false
    }
}

Create a traditional command and user server.launch or extension.launch in the addSyntax handler.

import com.github.shynixn.mccoroutine.minestom.launch
import net.minestom.server.MinecraftServer
import net.minestom.server.command.builder.Command
import net.minestom.server.command.builder.arguments.ArgumentType
import net.minestom.server.entity.Player

class PlayerDataCommandExecutor(private val server: MinecraftServer, private val database: Database) : Command("mycommand") {
    init {
        val nameArgument = ArgumentType.String("name")
        addSyntax({ sender, context ->
            server.launch {
                if (sender is Player) {
                    val name : String = context.get(nameArgument)
                    val playerData = database.getDataFromPlayer(sender)
                    playerData.name = name
                    database.saveData(sender, playerData)
                }
            }
        })
    }
}

Create a traditional command executor but extend from SuspendingCommandExecutor instead of CommandExecutor. Please consider, that the return value CommandResult.success() is automatically assumed, if the function is suspended in one branch.

import com.github.shynixn.mccoroutine.sponge.SuspendingCommandExecutor
import org.spongepowered.api.command.CommandResult
import org.spongepowered.api.command.CommandSource
import org.spongepowered.api.command.args.CommandContext
import org.spongepowered.api.entity.living.player.Player

class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommandExecutor {
    override suspend fun execute(src: CommandSource, args: CommandContext): CommandResult {
        if (src !is Player) {
            return CommandResult.empty()
        }

        if (args.hasAny("name")) {
            val name = args.getOne<String>("name").get()
            val playerData = database.getDataFromPlayer(src)
            playerData.name = name
            database.saveData(src, playerData)
            return CommandResult.success()
        }

        return CommandResult.empty()
    }
}

There are multiple ways to create command executors in Velocity. MCCoroutine provides extensions for both the SimpleCommand and the BrigadierCommand to allow flexibility.

A SimpleCommand can be created by implementing SuspendingSimpleCommand instead of SimpleCommand

import com.github.shynixn.mccoroutine.velocity.SuspendingSimpleCommand
import com.velocitypowered.api.command.SimpleCommand
import com.velocitypowered.api.proxy.Player

class PlayerDataCommandExecutor(private val database: Database) : SuspendingSimpleCommand {
    override suspend fun execute(invocation: SimpleCommand.Invocation) {
        val source = invocation.source()

        if (source !is Player) {
            return
        }

        val args = invocation.arguments()

        if (args.size == 2 && args[0].equals("rename", true)) {
            val name = args[1]
            val playerData = database.getDataFromPlayer(source)
            playerData.name = name
            database.saveData(source, playerData)
            return
        }
    }
}

A BrigadierCommand can be executed asynchronously using the executesSuspend extension function. More details below.

Register the CommandExecutor

Instead of using setExecutor, use the provided extension method setSuspendingExecutor to register a command executor.

Important

Do not forget to declare the playerdata command in your plugin.yml.

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

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

    override suspend fun onEnableAsync() {
        // Minecraft Main Thread
        database.createDbIfNotExist()
        server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this)
        getCommand("playerdata")!!.setSuspendingExecutor(PlayerDataCommandExecutor(database))
    }

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

Instead of using registerCommand, use the provided extension method registerSuspendingCommand to register a command executor.

Important

Do not forget to declare the playerdata command in your plugin.yml.

import com.github.shynixn.mccoroutine.bungeecord.SuspendingPlugin
import com.github.shynixn.mccoroutine.bungeecord.registerSuspendingCommand
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))
        proxy.pluginManager.registerSuspendingCommand(this, PlayerDataCommandExecutor(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) {
        // Register command
        val command = PlayerDataCommandExecutor()
        server.commandManager.dispatcher.register(CommandManager.literal("mccor").executesSuspend(this, command))
    }
}

Instead of using setExecutor, use the provided extension method setSuspendingExecutor to register a command executor.

Important

Do not forget to declare the playerdata command in your plugin.yml.

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

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

    override suspend fun onEnableAsync() {
        // Global Region Thread.
        database.createDbIfNotExist()
        server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this)
        getCommand("playerdata")!!.setSuspendingExecutor(PlayerDataCommandExecutor(database))
    }

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

Register the command in the same way as a traditional command.

Instead of using executor, use the provided extension method suspendingExecutor to register a command executor.

import com.github.shynixn.mccoroutine.sponge.SuspendingPluginContainer
import com.github.shynixn.mccoroutine.sponge.registerSuspendingListeners
import com.github.shynixn.mccoroutine.sponge.suspendingExecutor
import com.google.inject.Inject
import org.spongepowered.api.Sponge
import org.spongepowered.api.command.args.GenericArguments
import org.spongepowered.api.command.spec.CommandSpec
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
import org.spongepowered.api.text.Text

@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))
        val commandSpec = CommandSpec.builder()
            .description(Text.of("Command for operations."))
            .permission("mccoroutine.sample")
            .arguments(
                GenericArguments.onlyOne(GenericArguments.string(Text.of("name")))
            )
            .suspendingExecutor(pluginContainer, PlayerDataCommandExecutor(database))
        Sponge.getCommandManager().register(pluginContainer, commandSpec.build(), listOf("playerdata"))
    }

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

Instead of using register, use the provided extension method registerSuspend to register a simple command executor.

@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))
        val meta = proxyServer.commandManager.metaBuilder("playerdata").build()

        // Register SimpleCommand
        proxyServer.commandManager.registerSuspend(meta, PlayerDataCommandExecutor(database), this)

        // Register BrigadierCommand
        val helloCommand =
            LiteralArgumentBuilder.literal<CommandSource>("test")
                .executesSuspend(this, { context: CommandContext<CommandSource> ->
                    val message = Component.text("Hello World", NamedTextColor.AQUA)
                    context.getSource().sendMessage(message)
                    1 // indicates success
                })
                .build()
        proxyServer.commandManager.register(BrigadierCommand(helloCommand))
    }
}

Test the CommandExecutor

Join your server and use the playerData command to observe getDataFromPlayer and saveData messages getting printed to your server log.