Skip to content

Commit

Permalink
Merge pull request #73 from MineInAbyss/develop
Browse files Browse the repository at this point in the history
feat(commands): Brigadier command DSL
  • Loading branch information
0ffz committed Jun 6, 2024
2 parents 4c74d28 + 85e9f2c commit edc9f76
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
build
target
out/
.kotlin
kotlin-js-store

.classpath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package com.mineinabyss.idofront
import org.bukkit.plugin.java.JavaPlugin

class IdofrontPlugin : JavaPlugin() {
}
}
3 changes: 3 additions & 0 deletions idofront-commands/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ plugins {

dependencies {
implementation(project(":idofront-logging"))
implementation(project(":idofront-text-components"))
implementation(libs.minecraft.mccoroutine)
implementation(libs.kotlinx.coroutines)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.mineinabyss.idofront.commands.brigadier

@DslMarker
annotation class Annotations
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@file:Suppress("UnstableApiUsage")

package com.mineinabyss.idofront.commands.brigadier

import io.papermc.paper.command.brigadier.Commands
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents
import org.bukkit.plugin.Plugin

/**
* Idofront brigader DSL entrypoint.
*/
inline fun Commands.commands(plugin: Plugin, init: RootIdoCommands.() -> Unit) {
RootIdoCommands(this, plugin).apply(init).buildEach()
}

/**
* Idofront brigader DSL entrypoint.
*
* Must be registered in the plugin's onEnable or onLoad as it hooks into Paper's plugin lifecycle.
*/
inline fun Plugin.commands(crossinline init: RootIdoCommands.() -> Unit) {
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event ->
event.registrar().commands(this, init)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mineinabyss.idofront.commands.brigadier

import kotlin.reflect.KProperty

class IdoArgument<T>(
val name: String,
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): IdoArgument<T> {
return this
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mojang.brigadier.arguments.ArgumentType

data class IdoArgumentBuilder<T>(
val type: ArgumentType<out T>,
val suggestions: (suspend IdoSuggestionsContext.() -> Unit)? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.mineinabyss.idofront.commands.brigadier

import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher
import com.mineinabyss.idofront.commands.execution.CommandExecutionFailedException
import com.mineinabyss.idofront.textcomponents.miniMsg
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.suggestion.SuggestionProvider
import com.mojang.brigadier.tree.LiteralCommandNode
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import kotlin.reflect.KProperty

@Suppress("UnstableApiUsage")
@Annotations
open class IdoCommand(
internal val initial: LiteralArgumentBuilder<CommandSourceStack>,
val name: String,
val plugin: Plugin,
) {
private val renderSteps = mutableListOf<RenderStep>()

fun <T> ArgumentType<T>.suggests(suggestions: suspend IdoSuggestionsContext.() -> Unit): IdoArgumentBuilder<T> {
return IdoArgumentBuilder(this, suggestions)
}

fun <T> ArgumentType<T>.suggests(provider: SuggestionProvider<CommandSourceStack>): IdoArgumentBuilder<T> {
return IdoArgumentBuilder(this) { provider.getSuggestions(context, suggestions) }
}

operator fun <T> ArgumentType<T>.provideDelegate(t: T, property: KProperty<*>): IdoArgument<T> {
add(RenderStep.Builder(Commands.argument(property.name, this)))
return IdoArgument(property.name)
}

operator fun <T> IdoArgumentBuilder<T>.provideDelegate(thisRef: Any?, property: KProperty<*>): IdoArgument<T?> {
add(RenderStep.Builder(Commands.argument(property.name, type).apply {
if (this@provideDelegate.suggestions != null)
suggests { context, builder ->
CoroutineScope(plugin.asyncDispatcher).async {
this@provideDelegate.suggestions.invoke(IdoSuggestionsContext(context, builder))
builder.build()
}.asCompletableFuture()
}
}))
return IdoArgument(property.name)
}

/** Creates a subcommand using [Commands.literal]. */
inline operator fun String.invoke(init: IdoCommand.() -> Unit) {
add(RenderStep.Command(IdoCommand(Commands.literal(this), this, plugin).apply(init)))
}

/** Specifies a predicate for the command to execute further, may be calculated more than once. */
inline fun requires(crossinline init: CommandSourceStack.() -> Boolean) = edit {
requires { init(it) }
}

/** Specifies an end node for the command that runs something, only one executes block can run per command execution. */
inline fun executes(crossinline run: IdoCommandContext.() -> Unit) = edit {
executes { context ->
try {
run(IdoCommandContext(context))
} catch (e: CommandExecutionFailedException) {
e.replyWith?.let { context.source.sender.sendMessage(it) }
}
com.mojang.brigadier.Command.SINGLE_SUCCESS
}
}

/** [executes], ensuring the executor is a player. */
inline fun playerExecutes(crossinline run: IdoPlayerCommandContext.() -> Unit) {
executes {
if (executor !is Player) commandException("<red>This command can only be run by a player.".miniMsg())
run(IdoPlayerCommandContext(context))
}
}

@PublishedApi
internal fun add(step: RenderStep) {
renderSteps += step
}

/** Directly edit the command in Brigadier. */
inline fun edit(crossinline apply: IdoArgBuilder.() -> ArgumentBuilder<*, *>) {
add(RenderStep.Apply { apply() as IdoArgBuilder })
}

internal fun render(): List<RenderedCommand> {
return renderSteps.foldRight(listOf()) { step, acc ->
step.reduce(acc)
}
}

internal fun build(): LiteralCommandNode<CommandSourceStack> {
render().fold(initial as IdoArgBuilder) { acc, curr ->
curr.foldLeft(acc)
}
return initial.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mineinabyss.idofront.commands.execution.CommandExecutionFailedException
import com.mineinabyss.idofront.textcomponents.miniMsg
import com.mojang.brigadier.context.CommandContext
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver
import net.kyori.adventure.text.Component
import org.bukkit.Location
import org.bukkit.command.CommandSender
import org.bukkit.entity.Entity

@Annotations
@Suppress("UnstableApiUsage")
open class IdoCommandContext(
val context: CommandContext<CommandSourceStack>,
) {
/** Stops the command, sending a [message] formatted with MiniMessage to its [sender]. */
fun commandException(message: String): Nothing = throw CommandExecutionFailedException(message.miniMsg())

/** Stops the command, sending a [message] to its [sender]. */
fun commandException(message: Component): Nothing = throw CommandExecutionFailedException(message)

/** The sender that ran this command. */
val sender: CommandSender = context.source.sender

/** An entity representing the [sender] on the server. */
val executor: Entity? = context.source.executor

val location: Location = context.source.location

@JvmName("invoke1")
inline operator fun <reified T> IdoArgument<out ArgumentResolver<T>>.invoke(): T {
@Suppress("UNCHECKED_CAST") // getArgument logic ensures this cast always succeeds if the argument was registered
return ((this as IdoArgument<Any?>).invoke() as ArgumentResolver<T>)
.resolve(context.source)
}

@JvmName("invoke2")
inline operator fun <reified T> IdoArgument<T>.invoke(): T {
return context.getArgumentOrNull<T>(name)
?: commandException("<red>Argument $name not found".miniMsg())
}

@PublishedApi
internal inline fun <reified T> CommandContext<CommandSourceStack>.getArgumentOrNull(name: String): T? = runCatching {
context.getArgument(name, T::class.java)
}.getOrNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mojang.brigadier.context.CommandContext
import io.papermc.paper.command.brigadier.CommandSourceStack
import org.bukkit.entity.Player

@Annotations
@Suppress("UnstableApiUsage")
class IdoPlayerCommandContext(
context: CommandContext<CommandSourceStack>,
): IdoCommandContext(context) {
val player = executor as Player
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mojang.brigadier.builder.LiteralArgumentBuilder
import io.papermc.paper.command.brigadier.CommandSourceStack
import org.bukkit.plugin.Plugin

@Annotations
@Suppress("UnstableApiUsage")
class IdoRootCommand(
initial: LiteralArgumentBuilder<CommandSourceStack>,
name: String,
val description: String?,
val aliases: List<String>,
plugin: Plugin,
) : IdoCommand(initial, name, plugin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.SuggestionsBuilder
import io.papermc.paper.command.brigadier.CommandSourceStack

@Suppress("UnstableApiUsage")
data class IdoSuggestionsContext(
val context: CommandContext<CommandSourceStack>,
val suggestions: SuggestionsBuilder,
) {
/** Add a suggestion, filtering it as the user types. */
fun suggestFiltering(name: String) {
if (name.startsWith(suggestions.remaining, ignoreCase = true))
suggestions.suggest(name)
}

/** Add a list of suggestions, filtering them as the user types. */
fun suggest(list: List<String>) {
list.forEach { suggestFiltering(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mineinabyss.idofront.commands.brigadier

/**
* These are emitted line by line to reflect what a user specifies in the DSL.
*/
@Suppress("UnstableApiUsage")
sealed interface RenderStep {
fun reduce(rightAcc: List<RenderedCommand>): List<RenderedCommand>

class Builder(val builder: IdoArgBuilder) : RenderStep {
override fun reduce(rightAcc: List<RenderedCommand>): List<RenderedCommand> {
return listOf(RenderedCommand.ThenFold(builder, rightAcc))
}
}

class Command(val command: IdoCommand) : RenderStep {
override fun reduce(rightAcc: List<RenderedCommand>): List<RenderedCommand> {
return listOf(RenderedCommand.ThenFold(command.initial, command.render())) + rightAcc
}
}

class Apply(val apply: IdoArgBuilder.() -> Unit) : RenderStep {
override fun reduce(rightAcc: List<RenderedCommand>): List<RenderedCommand> {
return listOf(RenderedCommand.Apply(apply)) + rightAcc
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mineinabyss.idofront.commands.brigadier

/**
* [RenderStep]s get reduced into a list of commands that more directly represent Brigadier's builder structure.
*
* This lets us write more complex nodes more easily.
*/
sealed interface RenderedCommand {
fun foldLeft(acc: IdoArgBuilder): IdoArgBuilder

data class Apply(val apply: IdoArgBuilder.() -> Unit) : RenderedCommand {
override fun foldLeft(acc: IdoArgBuilder) = acc.apply(apply)
}

data class ThenFold(val initial: IdoArgBuilder, val list: List<RenderedCommand>) : RenderedCommand {
override fun foldLeft(acc: IdoArgBuilder) = acc.apply {
then(list.fold(initial) { acc, next -> next.foldLeft(acc) })
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.mineinabyss.idofront.commands.brigadier

import io.papermc.paper.command.brigadier.Commands
import org.bukkit.plugin.Plugin

@Suppress("UnstableApiUsage")
class RootIdoCommands(
val commands: Commands,
val plugin: Plugin,
) {
@PublishedApi
internal val rootCommands = mutableListOf<IdoRootCommand>()

/** Creates a new subcommand via a [Commands.literal] argument. */
inline operator fun String.invoke(aliases: List<String> = emptyList(), description: String? = null, init: IdoRootCommand.() -> Unit) {
rootCommands += IdoRootCommand(
Commands.literal(this),
this,
description,
aliases,
plugin,
).apply(init)
}

/** Creates a new subcommand with aliases via a [Commands.literal] argument. */
inline operator fun List<String>.invoke(description: String? = null, init: IdoRootCommand.() -> Unit) =
firstOrNull()?.invoke(aliases = drop(1), description = description, init = init)

/** Builder for commands with aliases. */
operator fun String.div(other: String) = listOf(this, other)

/** Builder for commands with aliases. */
operator fun List<String>.div(other: String) = listOf(this) + other

/** Builds and registers each root level command defined in the DSL. */
@PublishedApi
internal fun buildEach() {
rootCommands.forEach { command ->
commands.register(
command.build(),
command.description,
command.aliases
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mineinabyss.idofront.commands.brigadier

import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.tree.CommandNode
import io.papermc.paper.command.brigadier.CommandSourceStack

@Suppress("UnstableApiUsage")
internal typealias IdoArgBuilder = ArgumentBuilder<CommandSourceStack, *>
internal typealias IdoCommandNode = CommandNode<CommandSourceStack>

fun IdoArgBuilder.thenCast(other: IdoArgBuilder): IdoArgBuilder = then(other) as IdoArgBuilder

fun IdoArgBuilder.thenCast(other: IdoCommandNode): IdoArgBuilder = then(other) as IdoArgBuilder
Loading

0 comments on commit edc9f76

Please sign in to comment.