Greatly improve Brigardier support

This commit is contained in:
Jakob K
2021-09-06 17:40:55 +02:00
parent 8d460c514f
commit 91d025d9fb
13 changed files with 439 additions and 131 deletions

View File

@@ -19,7 +19,6 @@ plugins {
}
repositories {
maven("https://libraries.minecraft.net")
mavenCentral()
mavenLocal() // to get the locally available binaries of spigot (use the BuildTools)
}
@@ -31,8 +30,6 @@ dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.1")
api("me.lucko:commodore:1.10")
}
tasks {

View File

@@ -0,0 +1,40 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import net.axay.kspigot.commands.internal.ArgumentTypeUtils
import net.axay.kspigot.commands.internal.ServerCommandSource
/**
* Adds an argument.
*
* @param name the name of the argument
* @param type the type of the argument - e.g. IntegerArgumentType.integer() or StringArgumentType.string()
*/
inline fun <T> ArgumentBuilder<ServerCommandSource, *>.argument(
name: String,
type: ArgumentType<T>,
builder: RequiredArgumentBuilder<ServerCommandSource, T>.() -> Unit = {}
): RequiredArgumentBuilder<ServerCommandSource, T> =
RequiredArgumentBuilder.argument<ServerCommandSource, T>(name, type).apply(builder).also { then(it) }
/**
* Adds an argument. The argument type will be resolved via the reified
* type [T].
*
* @param name the name of the argument
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> ArgumentBuilder<ServerCommandSource, *>.argument(
name: String,
builder: RequiredArgumentBuilder<ServerCommandSource, T>.() -> Unit = {}
): RequiredArgumentBuilder<ServerCommandSource, T> =
RequiredArgumentBuilder.argument<ServerCommandSource, T>(name, ArgumentTypeUtils.fromReifiedType<T>()).apply(builder).also { then(it) }
/**
* Get the value of this argument.
*/
inline fun <reified T> CommandContext<ServerCommandSource>.getArgument(name: String): T =
getArgument(name, T::class.java)

View File

@@ -1,28 +0,0 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package net.axay.kspigot.commands
import com.mojang.brigadier.tree.LiteralCommandNode
import me.lucko.commodore.CommodoreProvider
import net.axay.kspigot.main.PluginInstance
/**
* This class provides Brigardier support. It does that
* by using reflection once. Additionally, this class is
* using some obfuscated functions.
*/
object BrigardierSupport {
private val provider = if (CommodoreProvider.isSupported()) CommodoreProvider.getCommodore(PluginInstance) else kotlin.run {
PluginInstance.logger.severe("Could not initialize Brigardier support on the current Minecraft version! (Requested by ${PluginInstance.name})")
null
}
fun register(name: String, brigardierCommand: LiteralCommandNode<*>) {
val command = PluginInstance.getCommand(name)
if (command == null) {
PluginInstance.logger.severe("Could not register command '$name' of plugin ${PluginInstance.name}! Maybe it is missing from the plugin.yml?")
return
}
provider?.register(command, brigardierCommand)
}
}

View File

@@ -1,97 +0,0 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.tree.LiteralCommandNode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import net.minecraft.commands.CommandListenerWrapper
/**
* Create a new command.
*
* @param name the name of the root command
* @param register if true, the command will be automatically registered
* when the plugin is fully enabled
*/
inline fun command(
name: String,
register: Boolean = true,
builder: LiteralArgumentBuilder<CommandListenerWrapper>.() -> Unit,
): LiteralCommandNode<CommandListenerWrapper> =
LiteralArgumentBuilder.literal<CommandListenerWrapper>(name).apply(builder).build().apply {
if (register)
BrigardierSupport.register(name, this)
}
/**
* Add custom execution logic for this command.
*/
inline fun ArgumentBuilder<CommandListenerWrapper, *>.simpleExecutes(
crossinline executor: CommandContext.() -> Unit,
) {
executes wrapped@{
executor.invoke(CommandContext(it))
return@wrapped 1
}
}
/**
* Add a new literal to this command.
*
* @param name the name of the literal
*/
inline fun ArgumentBuilder<CommandListenerWrapper, *>.literal(
name: String,
builder: LiteralArgumentBuilder<CommandListenerWrapper>.() -> Unit,
) = command(name, false, builder).also { then(it) }
/**
* Add an argument.
*
* @param name the name of the argument
* @param type the type of the argument - e.g. IntegerArgumentType.integer() or StringArgumentType.string()
*/
inline fun <T> ArgumentBuilder<CommandListenerWrapper, *>.argument(
name: String,
type: ArgumentType<T>,
builder: RequiredArgumentBuilder<CommandListenerWrapper, T>.() -> Unit,
): RequiredArgumentBuilder<CommandListenerWrapper, T> =
RequiredArgumentBuilder.argument<CommandListenerWrapper, T>(name, type).apply(builder).also { then(it) }
/**
* Add an argument.
*
* @param name the name of the argument
*/
inline fun <reified T> ArgumentBuilder<CommandListenerWrapper, *>.argument(
name: String,
builder: RequiredArgumentBuilder<CommandListenerWrapper, T>.() -> Unit,
): RequiredArgumentBuilder<CommandListenerWrapper, T> =
RequiredArgumentBuilder.argument<CommandListenerWrapper, T>(name, ArgumentTypeUtils.fromReifiedType<T>()).apply(builder).also { then(it) }
private val argumentCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
/**
* Add custom suspending suggestion logic for an argument.
*/
fun RequiredArgumentBuilder<CommandListenerWrapper, *>.simpleSuggests(
suggestionBuilder: suspend CommandContext.() -> Iterable<Any?>?,
) {
suggests { context, builder ->
argumentCoroutineScope.async {
suggestionBuilder.invoke(CommandContext(context))?.forEach {
if (it is Int)
builder.suggest(it)
else
builder.suggest(it.toString())
}
builder.build()
}.asCompletableFuture()
}
}

View File

@@ -2,14 +2,14 @@ package net.axay.kspigot.commands
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
import net.minecraft.commands.CommandListenerWrapper
import net.axay.kspigot.commands.internal.ServerCommandSource
import net.minecraft.network.chat.ChatMessage
import org.bukkit.Location
import org.bukkit.Server
import org.bukkit.World
import org.bukkit.entity.Player
class CommandContext(val nmsContext: CommandContext<CommandListenerWrapper>) {
class CommandContext(val nmsContext: CommandContext<ServerCommandSource>) {
companion object {
private val REQUIRES_PLAYER_EXCEPTION = SimpleCommandExceptionType(ChatMessage("permissions.requires.player"))
}

View File

@@ -0,0 +1,32 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import net.axay.kspigot.commands.internal.BrigardierSupport
import net.axay.kspigot.commands.internal.ServerCommandSource
/**
* Creates a new command.
*
* @param name the name of the root command
* @param register if true, the command will automatically be registered
*/
inline fun command(
name: String,
register: Boolean = true,
builder: LiteralArgumentBuilder<ServerCommandSource>.() -> Unit
): LiteralArgumentBuilder<ServerCommandSource> =
LiteralArgumentBuilder.literal<ServerCommandSource>(name).apply(builder).apply {
if (register)
BrigardierSupport.commands += this
}
/**
* Adds a new literal to this command.
*
* @param name the name of the literal
*/
inline fun ArgumentBuilder<ServerCommandSource, *>.literal(
name: String,
builder: LiteralArgumentBuilder<ServerCommandSource>.() -> Unit = {}
) = command(name, false, builder).also { then(it) }

View File

@@ -0,0 +1,44 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.Command
import com.mojang.brigadier.builder.ArgumentBuilder
import net.axay.kspigot.commands.internal.ServerCommandSource
/**
* Adds execution logic to this command. The place where this function
* is called matters, as this defines for which path in the command tree
* this executor should be called.
*
* @see com.mojang.brigadier.builder.ArgumentBuilder.executes
*/
inline infix fun ArgumentBuilder<ServerCommandSource, *>.runs(
crossinline executor: CommandContext.() -> Unit,
) = this.apply {
executes wrapped@{
executor.invoke(CommandContext(it))
return@wrapped 1
}
}
/**
* Adds execution logic to this command. The place where this function
* is called matters, as this defines for which path in the command tree
* this executor should be called.
*
* @see com.mojang.brigadier.builder.ArgumentBuilder.executes
*/
infix fun <S> ArgumentBuilder<S, *>.runs(executor: Command<S>) =
this.apply {
executes(executor)
}
/**
* Add custom execution logic for this command.
*/
@Deprecated(
"The name 'simpleExecutes' has been superseded by 'runs'.",
ReplaceWith("runs { executor.invoke() }")
)
inline infix fun ArgumentBuilder<ServerCommandSource, *>.simpleExecutes(
crossinline executor: CommandContext.() -> Unit,
) = runs(executor)

View File

@@ -0,0 +1,25 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import net.axay.kspigot.annotations.NMS_General
import net.axay.kspigot.commands.internal.BrigardierSupport
import net.minecraft.commands.CommandListenerWrapper
/**
* Registers this command at the [CommandDispatcher] of the server.
*
* @param sendToPlayers whether the new command tree should be send to
* all players, this is true by default, but you can disable it if you are
* calling this function as the server is starting
*/
@NMS_General
fun LiteralArgumentBuilder<CommandListenerWrapper>.register(sendToPlayers: Boolean = true) {
if (!BrigardierSupport.executedDefaultRegistration)
BrigardierSupport.commands += this
else {
BrigardierSupport.commandDispatcher.register(this)
if (sendToPlayers)
BrigardierSupport.updateCommandTree()
}
}

View File

@@ -0,0 +1,25 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.builder.ArgumentBuilder
import net.axay.kspigot.commands.internal.ServerCommandSource
import org.bukkit.permissions.Permission
/**
* Defines that the given [permission] is required to interact with this
* path of the command.
*/
fun ArgumentBuilder<ServerCommandSource, *>.requiresPermission(permission: String) {
requires {
it.bukkitSender.hasPermission(permission)
}
}
/**
* Defines that the given [permission] is required to interact with this
* path of the command.
*/
fun ArgumentBuilder<ServerCommandSource, *>.requiresPermission(permission: Permission) {
requires {
it.bukkitSender.hasPermission(permission)
}
}

View File

@@ -0,0 +1,185 @@
package net.axay.kspigot.commands
import com.mojang.brigadier.Message
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.SuggestionsBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
@PublishedApi
internal val argumentCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
/**
* Suggest the value which is the result of the [suggestionBuilder].
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestSingle(
crossinline suggestionBuilder: (CommandContext<S>) -> Any?
) {
suggests { context, builder ->
builder.applyAny(suggestionBuilder(context))
builder.buildFuture()
}
}
/**
* Suggest the value which is the result of the [suggestionBuilder].
* Additionaly, a separate tooltip associated with the suggestion
* will be shown as well.
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestSingleWithTooltip(
crossinline suggestionBuilder: (CommandContext<S>) -> Pair<Any, Message>?
) {
suggests { context, builder ->
builder.applyAnyWithTooltip(suggestionBuilder(context))
builder.buildFuture()
}
}
/**
* Suggest the value which is the result of the [suggestionBuilder].
*
* @param coroutineScope the [CoroutineScope] where the suggestion should be built in - an async scope by default
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestSingleSuspending(
coroutineScope: CoroutineScope = argumentCoroutineScope,
crossinline suggestionBuilder: suspend (CommandContext<S>) -> Any?
) {
suggests { context, builder ->
coroutineScope.async {
builder.applyAny(suggestionBuilder(context))
builder.build()
}.asCompletableFuture()
}
}
/**
* Suggest the value which is the result of the [suggestionBuilder].
* Additionaly, a separate tooltip associated with the suggestion
* will be shown as well.
*
* @param coroutineScope the [CoroutineScope] where the suggestion should be built in - an async scope by default
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestSingleWithTooltipSuspending(
coroutineScope: CoroutineScope = argumentCoroutineScope,
crossinline suggestionBuilder: suspend (CommandContext<S>) -> Pair<Any?, Message>?
) {
suggests { context, builder ->
coroutineScope.async {
builder.applyAnyWithTooltip(suggestionBuilder(context))
builder.build()
}.asCompletableFuture()
}
}
/**
* Suggest the entries of the iterable which is the result of the
* [suggestionsBuilder].
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestList(
crossinline suggestionsBuilder: (CommandContext<S>) -> Iterable<Any?>?
) {
suggests { context, builder ->
builder.applyIterable(suggestionsBuilder(context))
builder.buildFuture()
}
}
/**
* Suggest the entries of the iterable which is the result of the
* [suggestionsBuilder].
* Additionaly, a separate tooltip associated with each suggestion
* will be shown as well.
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestListWithTooltips(
crossinline suggestionsBuilder: (CommandContext<S>) -> Iterable<Pair<Any?, Message>?>?
) {
suggests { context, builder ->
builder.applyIterableWithTooltips(suggestionsBuilder(context))
builder.buildFuture()
}
}
/**
* Suggest the entries of the iterable which is the result of the
* [suggestionsBuilder].
*
* @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestListSuspending(
coroutineScope: CoroutineScope = argumentCoroutineScope,
crossinline suggestionsBuilder: suspend (CommandContext<S>) -> Iterable<Any?>?
) {
suggests { context, builder ->
coroutineScope.async {
builder.applyIterable(suggestionsBuilder(context))
builder.build()
}.asCompletableFuture()
}
}
/**
* Suggest the entries of the iterable which is the result of the
* [suggestionsBuilder].
* Additionaly, a separate tooltip associated with each suggestion
* will be shown as well.
*
* @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default
*/
inline fun <S> RequiredArgumentBuilder<S, *>.suggestListWithTooltipsSuspending(
coroutineScope: CoroutineScope = argumentCoroutineScope,
crossinline suggestionsBuilder: (CommandContext<S>) -> Iterable<Pair<Any?, Message>?>?
) {
suggests { context, builder ->
coroutineScope.async {
builder.applyIterableWithTooltips(suggestionsBuilder(context))
builder.build()
}.asCompletableFuture()
}
}
@PublishedApi
internal fun SuggestionsBuilder.applyAny(any: Any?) {
when (any) {
is Int -> suggest(any)
is String -> suggest(any)
else -> suggest(any.toString())
}
}
@PublishedApi
internal fun SuggestionsBuilder.applyAnyWithTooltip(pair: Pair<Any?, Message>?) {
if (pair == null) return
val (any, message) = pair
when (any) {
is Int -> suggest(any, message)
is String -> suggest(any, message)
else -> suggest(any.toString(), message)
}
}
@PublishedApi
internal fun SuggestionsBuilder.applyIterable(iterable: Iterable<Any?>?) =
iterable?.forEach(::applyAny)
@PublishedApi
internal fun SuggestionsBuilder.applyIterableWithTooltips(iterable: Iterable<Pair<Any?, Message>?>?) =
iterable?.forEach(::applyAnyWithTooltip)
/**
* Adds custom suspending suggestion logic for an argument.
*
* @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default
*/
@Deprecated(
"The name 'simpleSuggests' has been superseded by 'suggestListSuspending'",
ReplaceWith("suggestListSuspending(coroutineScope, suggestionBuilder)")
)
fun <S> RequiredArgumentBuilder<S, *>.simpleSuggests(
coroutineScope: CoroutineScope = argumentCoroutineScope,
suggestionBuilder: suspend (CommandContext<S>) -> Iterable<Any?>?
) = suggestListSuspending(coroutineScope, suggestionBuilder)

View File

@@ -1,4 +1,4 @@
package net.axay.kspigot.commands
package net.axay.kspigot.commands.internal
import com.mojang.brigadier.arguments.*

View File

@@ -0,0 +1,82 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package net.axay.kspigot.commands.internal
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import net.axay.kspigot.annotations.NMS_1_17
import net.axay.kspigot.annotations.NMS_General
import net.axay.kspigot.event.listen
import net.axay.kspigot.extensions.onlinePlayers
import net.axay.kspigot.extensions.server
import net.axay.kspigot.main.KSpigotMainInstance
import net.axay.kspigot.utils.reflectField
import net.minecraft.commands.CommandListenerWrapper
import org.bukkit.event.player.PlayerJoinEvent
typealias ServerCommandSource = CommandListenerWrapper
/**
* This class provides Brigardier support. It does that
* by using reflection once. Additionally, this class is
* using some obfuscated functions.
*/
object BrigardierSupport {
@PublishedApi
internal val commands = LinkedHashSet<LiteralArgumentBuilder<CommandListenerWrapper>>()
internal var executedDefaultRegistration = false
private set
init {
listen<PlayerJoinEvent> { event ->
val player = event.player
val permAttachment = player.addAttachment(KSpigotMainInstance)
commands.forEach {
permAttachment.setPermission("minecraft.command.${it.literal}", true)
}
}
}
/**
* The command manager is used to hold the command dispatcher,
* and to manage and dispatch the brigardier commands for
* all players on the server.
*/
@Suppress("HasPlatformType") // do not refer non-lazily to the type in this class
@NMS_General
val commandManager by lazy {
(server as org.bukkit.craftbukkit.v1_17_R1.CraftServer).server.vanillaCommandDispatcher
}
/**
* The command dispatcher is used to register brigardier commands.
*/
@NMS_1_17
val commandDispatcher by lazy {
// g = the command dispatcher
commandManager.reflectField<CommandDispatcher<CommandListenerWrapper>>("g")
}
@NMS_General
internal fun registerAll() {
executedDefaultRegistration = true
// TODO unregister commands which are now missing due to a possible reload
if (commands.isNotEmpty()) {
commands.forEach {
commandDispatcher.register(it)
}
if (onlinePlayers.isNotEmpty())
updateCommandTree()
}
}
@NMS_General
fun updateCommandTree() {
onlinePlayers.forEach {
// send the command tree
commandManager.a((it as org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer).handle)
}
}
}

View File

@@ -1,5 +1,6 @@
package net.axay.kspigot.main
import net.axay.kspigot.commands.internal.BrigardierSupport
import net.axay.kspigot.extensions.bukkit.warn
import net.axay.kspigot.extensions.console
import net.axay.kspigot.gui.GUIHolder
@@ -64,6 +65,8 @@ abstract class KSpigot : JavaPlugin() {
final override fun onEnable() {
startup()
BrigardierSupport.registerAll()
}
final override fun onDisable() {