Skip to content

[RESEARCH] Multiplatform Commands #112

@makeevrserg

Description

@makeevrserg

Status

  • Forge
  • Paper
  • Velocity
  • Fabric
  • Bungee

Goal

The goal is to find a way to create multiplatform commands using mojang's Brigadier.

Command Builder API

Using paper, Forge we are using something like that

import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands

Commands.literal(literal)
Commands.argument(name, argumentType)

Which leads us to create

interface MultiplatformCommands<CommandSourceStack> {
    fun literal(literal: String): LiteralArgumentBuilder<CommandSourceStack>
    fun <T : Any> argument(
        name: String,
        argumentType: ArgumentType<T>
    ): RequiredArgumentBuilder<CommandSourceStack, T>
}

After, we can create multiplatform commands

class MultiplatformCommand<CommandSourceStack>(

    val commands: MultiplatformCommands<CommandSourceStack>
) {
    fun command(
        alias: String,
        block: LiteralArgumentBuilder<CommandSourceStack>.() -> Unit
    ): LiteralArgumentBuilder<CommandSourceStack> {
        val literal = commands.literal(alias)
        literal.block()
        return literal
    }

    fun LiteralArgumentBuilder<CommandSourceStack>.literal(
        alias: String,
        block: LiteralArgumentBuilder<CommandSourceStack>.() -> Unit
    ) {
        val literal = commands.literal(alias)
        literal.block()
        this.then(literal)
    }

    data class BrigadierArgument<T : Any>(
        val alias: String,
        val type: ArgumentType<T>,
        val clazz: Class<T>
    )

    inline fun <reified T : Any> LiteralArgumentBuilder<CommandSourceStack>.argument(
        alias: String,
        type: ArgumentType<T>,
        noinline block: RequiredArgumentBuilder<CommandSourceStack, T>.(BrigadierArgument<T>) -> Unit
    ) = argument(
        alias = alias,
        type = type,
        clazz = T::class.java,
        block = block
    )

    fun <T : Any> LiteralArgumentBuilder<CommandSourceStack>.argument(
        alias: String,
        type: ArgumentType<T>,
        clazz: Class<T>,
        block: RequiredArgumentBuilder<CommandSourceStack, T>.(BrigadierArgument<T>) -> Unit
    ) {
        val argument = commands.argument(alias, type)
        val brigadierArgument = BrigadierArgument(
            alias = alias,
            type = type,
            clazz = clazz
        )
        argument.block(brigadierArgument)
        this.then(argument)
    }

    inline fun <reified T : Any> RequiredArgumentBuilder<CommandSourceStack, *>.argument(
        alias: String,
        type: ArgumentType<T>,
        noinline block: RequiredArgumentBuilder<CommandSourceStack, T>.(BrigadierArgument<T>) -> Unit
    ) = argument(
        alias = alias,
        type = type,
        clazz = T::class.java,
        block = block
    )

    fun <T : Any> RequiredArgumentBuilder<CommandSourceStack, *>.argument(
        alias: String,
        type: ArgumentType<T>,
        clazz: Class<T>,
        block: RequiredArgumentBuilder<CommandSourceStack, T>.(BrigadierArgument<T>) -> Unit
    ) {
        val argument = commands.argument(alias, type)
        val brigadierArgument = BrigadierArgument(
            alias = alias,
            type = type,
            clazz = clazz
        )
        argument.block(brigadierArgument)
        this.then(argument)
    }

    fun RequiredArgumentBuilder<CommandSourceStack, *>.runs(
        onFailure: (CommandContext<CommandSourceStack>, Throwable) -> Unit = { _, _ -> },
        block: (RequiredArgumentBuilder<CommandSourceStack, *>.(CommandContext<CommandSourceStack>) -> Unit)
    ) {
        executes { ctx ->
            runCatching { block.invoke(this, ctx) }
                .onFailure { onFailure.invoke(ctx, it) }
            Command.SINGLE_SUCCESS
        }
    }

    fun LiteralArgumentBuilder<CommandSourceStack>.runs(
        onFailure: (CommandContext<CommandSourceStack>, Throwable) -> Unit = { _, _ -> },
        block: LiteralArgumentBuilder<CommandSourceStack>.(CommandContext<CommandSourceStack>) -> Unit
    ) {
        executes { ctx ->
            runCatching { block.invoke(this, ctx) }
                .onFailure { onFailure.invoke(ctx, it) }
            Command.SINGLE_SUCCESS
        }
    }

    fun RequiredArgumentBuilder<CommandSourceStack, *>.hints(block: (CommandContext<CommandSourceStack>) -> List<String>) {
        suggests { context, builder ->
            block.invoke(context).forEach(builder::suggest)
            builder.buildFuture()
        }
    }

    @Throws(IllegalArgumentException::class)
    fun <T : Any> CommandContext<CommandSourceStack>.requireArgument(bArgument: BrigadierArgument<T>): T {
        return getArgument(bArgument.alias, bArgument.clazz)
    }

    @Throws(ArgumentConverterException::class)
    fun <T : Any> CommandContext<CommandSourceStack>.requireArgument(
        bArgument: BrigadierArgument<String>,
        converter: ArgumentConverter<T>
    ): T {
        val string = getArgument(bArgument.alias, bArgument.clazz)
        return converter.transform(string)
    }
}

Command Context problem

However, we can't use CommandContext here

val command = MultiplatformCommand(PaperMultiplatformCommands)

fun multiplatform(command: MultiplatformCommand<*>) {
    with(command) {
        command("") {
            literal("") {
                argument("asd", StringArgumentType.string()) { strArg ->
                    runs { ctx ->
                        val str = ctx.requireArgument(strArg)
                        val custom = ctx.requireArgument(strArg, IntArgumentConverter)
                        // The problem
                        val source: Any? = ctx.source
                    }
                }
            }
        }
    }
}

Which means, we should create custom CommandContext wrapper

sealed interface MultiplatformCommandExecutor {
    data class Player(val player: MinecraftPlayer) : MultiplatformCommandExecutor
    data object Console : MultiplatformCommandExecutor
}

interface MultiplatformCommandContext {
    val executor: MultiplatformCommandExecutor
}
interface MultiplatformCommands<CommandSourceStack> {
    // ....

    fun CommandContext<CommandSourceStack>.toMultiplatformCommandContext(): MultiplatformCommandContext
}

class PaperMultiplatformCommandContext(
    ctx: CommandContext<CommandSourceStack>
) : MultiplatformCommandContext {
    override val executor: MultiplatformCommandExecutor = let {
        (ctx.source.sender as? ConsoleCommandSender)?.let {
            MultiplatformCommandExecutor.Console
        } ?: (ctx.source.sender as? Player)?.let {
            MultiplatformCommandExecutor.Player(it.asOnlineMinecraftPlayer())
        } ?: error("Unknown executor")
    }
}

val command = MultiplatformCommand(PaperMultiplatformCommands)

fun multiplatform(command: MultiplatformCommand<CommandSourceStack>) {
    with(command) {
        command("") {
            literal("") {
                argument("asd", StringArgumentType.string()) { strArg ->
                    runs { ctx ->
                        val str = ctx.requireArgument(strArg)
                        val custom = ctx.requireArgument(strArg, IntArgumentConverter)
                        // The problem
                        val source: Any? = ctx.source
                        val mppCtx = ctx.toMultiplatformCommandContext()
                        val executor: MultiplatformCommandExecutor = mppCtx.executor
                    }
                }
            }
        }
    }
}

Metadata

Metadata

Assignees

Labels

help wantedExtra attention is needed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions