Basic command execution implemented and fixed!

- Added check for errors that are sent for the sender and other test checks
- Fixed getting argument nodes
- Changed setting subcommand data for arguments so that the order of the registration allows finalising a node before adding it to another (that's why I needed to swap the order)
- Implemented basic command execution (invoking the method)
This commit is contained in:
Norbi Peti 2023-07-22 01:52:39 +02:00
parent 84062fee7c
commit 19362cfe5f
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
8 changed files with 151 additions and 78 deletions

View file

@ -4,8 +4,11 @@ import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.*
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
import buttondevteam.lib.chat.commands.CommandUtils.coreCommand import buttondevteam.lib.chat.commands.CommandUtils.coreCommand
import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable
import com.google.common.base.Defaults
import com.google.common.primitives.Primitives
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.* import com.mojang.brigadier.arguments.*
import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.ArgumentBuilder
@ -14,6 +17,8 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode import com.mojang.brigadier.tree.LiteralCommandNode
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.ChatColor
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.function.Function import java.util.function.Function
import java.util.function.Predicate import java.util.function.Predicate
@ -192,13 +197,25 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
{ helpText }, // TODO: Help text getter support { helpText }, // TODO: Help text getter support
{ sender: TP, data: SubcommandData<TC, TP> -> hasPermission(sender, data) }, { sender: TP, data: SubcommandData<TC, TP> -> hasPermission(sender, data) },
method.annotations.filterNot { it is Subcommand }.toTypedArray(), method.annotations.filterNot { it is Subcommand }.toTypedArray(),
fullPath fullPath,
) method
.executes { context: CommandContext<TP> -> executeCommand(context) } ).executes(this::executeHelpText)
var parent: ArgumentBuilder<TP, *> = node
for (param in params) { // Register parameters in the right order fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>) {
// TODO: Implement optional arguments here by making the last non-optional parameter also executable
val param = params.removeLast()
val argType = getArgumentType(param) val argType = getArgumentType(param)
parent.then(CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional).also { parent = it }) val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
if (params.isEmpty()) {
arg.executes { context: CommandContext<TP> -> executeCommand(context) }
} else {
arg.executes(::executeHelpText)
getArgNodes(arg, params)
}
parent.then(arg)
}
if (params.isNotEmpty()) {
getArgNodes(node, params.toMutableList())
} }
return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath") return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath")
} }
@ -311,65 +328,85 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
* @param context The command context * @param context The command context
* @return Vanilla command success level (0) * @return Vanilla command success level (0)
*/ */
private fun executeCommand(context: CommandContext<TP>): Int { protected open fun executeCommand(context: CommandContext<TP>): Int {
println("Execute command") assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments?
println("Should be running sync: $runOnPrimaryThread") val node = context.nodes.last().node.coreArgument()!!
val sender = context.source
/*if (!hasPermission(sender, sd.command, sd.method)) { @Suppress("UNCHECKED_CAST")
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command"); val sd = node.commandData as SubcommandData<TC, TP>
return; if (!sd.hasPermission(sender)) {
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
return 1
} }
// TODO: WIP // TODO: WIP
val type = sendertype.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch } val convertedSender = convertSenderType(sender, sd.senderType)
if (convertedSender == null) {
//TODO: Should have a prettier display of Command2 classes here
val type = sd.senderType.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch }
sender.sendMessage("${ChatColor.RED}You need to be a $type to use this command.") sender.sendMessage("${ChatColor.RED}You need to be a $type to use this command.")
sender.sendMessage(sd.getHelpText(sender)) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only sender.sendMessage(sd.getHelpText(sender)) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only
return 0
if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type
val args = parsed.getContext().getArguments();
for (var arg : sd.arguments.entrySet()) {*/
// TODO: Invoke using custom method
/*if (pj == commandline.length() + 1) { //No param given
if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) {
if (cl.isPrimitive())
params.add(Defaults.defaultValue(cl));
else if (Number.class.isAssignableFrom(cl)
|| Number.class.isAssignableFrom(cl))
params.add(Defaults.defaultValue(Primitives.unwrap(cl)));
else
params.add(null);
continue; //Fill the remaining params with nulls
} else {
sender.sendMessage(sd.helpText); //Required param missing
return;
} }
}*/
/*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?) val params = executeGetArguments(sd, context) ?: return executeHelpText(context)
params.add(commandline.substring(j + 1).split(" +"));
continue; // TODO: Invoke using custom method
}*/ // TODO: Varargs support? (colors?)
// TODO: Character handling (strlen) // TODO: Character handling (strlen)
// TODO: Param converter // TODO: Param converter
/*}
Runnable invokeCommand = () -> { executeInvokeCommand(sd, sender, convertedSender, params)
try { return 0
sd.method.setAccessible(true); //It may be part of a private class
val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time)
if (ret instanceof Boolean) {
if (!(boolean) ret) //Show usage
sender.sendMessage(sd.helpText);
} else if (ret != null)
throw new Exception("Wrong return type! Must return a boolean or void. Return value: " + ret);
} catch (InvocationTargetException e) {
TBMCCoreAPI.SendException("An error occurred in a command handler for " + subcommand + "!", e.getCause(), MainPlugin.Instance);
} catch (Exception e) {
TBMCCoreAPI.SendException("Command handling failed for sender " + sender + " and subcommand " + subcommand, e, MainPlugin.Instance);
} }
};
if (sync) private fun executeGetArguments(sd: SubcommandData<TC, TP>, context: CommandContext<TP>): MutableList<Any?>? {
Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand); val params = mutableListOf<Any?>()
for (argument in sd.argumentsInOrder) {
try {
val userArgument = context.getArgument(argument.name, argument.type)
params.add(userArgument)
} catch (e: IllegalArgumentException) {
// TODO: This probably only works with primitive types (argument.type)
if (argument.optional) {
if (argument.type.isPrimitive) {
params.add(Defaults.defaultValue(argument.type))
} else if (Number::class.java.isAssignableFrom(argument.type)) {
params.add(Defaults.defaultValue(Primitives.unwrap(argument.type)))
} else {
params.add(null)
}
} else {
return null
}
}
}
return params
}
/**
* Invokes the command method with the given sender and parameters.
*/
private fun executeInvokeCommand(sd: SubcommandData<TC, TP>, sender: TP, actualSender: Any, params: List<Any?>) {
val invokeCommand = {
try {
val ret = sd.executeCommand(actualSender, *params.toTypedArray())
if (ret is Boolean) {
if (!ret) //Show usage
sd.sendHelpText(sender)
} else if (ret != null)
throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret")
} catch (e: InvocationTargetException) {
TBMCCoreAPI.SendException("An error occurred in a command handler for ${sd.fullPath}!", e.cause ?: e, MainPlugin.instance)
} catch (e: Exception) {
TBMCCoreAPI.SendException("Command handling failed for sender $sender and subcommand ${sd.fullPath}", e, MainPlugin.instance)
}
}
if (runOnPrimaryThread && !ChromaUtils.isTest)
Bukkit.getScheduler().runTask(MainPlugin.instance, invokeCommand)
else else
invokeCommand.run();*/return 0 invokeCommand()
} }
abstract fun hasPermission(sender: TP, data: SubcommandData<TC, TP>): Boolean abstract fun hasPermission(sender: TP, data: SubcommandData<TC, TP>): Boolean

View file

@ -16,7 +16,6 @@ import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
import com.mojang.brigadier.builder.RequiredArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode import com.mojang.brigadier.tree.LiteralCommandNode
import me.lucko.commodore.Commodore import me.lucko.commodore.Commodore
import me.lucko.commodore.CommodoreProvider import me.lucko.commodore.CommodoreProvider
@ -40,8 +39,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
override fun registerCommand(command: ICommand2MC) { override fun registerCommand(command: ICommand2MC) {
val commandNode = super.registerCommandSuper(command) val commandNode = super.registerCommandSuper(command)
val bcmd = registerOfficially(command, commandNode) val bcmd = registerOfficially(command, commandNode)
if (bcmd != null) // TODO: Support aliases // TODO: Support aliases
super.registerCommandSuper(command)
val permPrefix = "chroma.command." val permPrefix = "chroma.command."
//Allow commands by default, it will check mod-only //Allow commands by default, it will check mod-only
val nodes = commandNode.coreExecutable<Command2MCSender, ICommand2MC>() val nodes = commandNode.coreExecutable<Command2MCSender, ICommand2MC>()
@ -60,7 +58,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
} }
override fun hasPermission(sender: Command2MCSender, data: SubcommandData<ICommand2MC, Command2MCSender>): Boolean { override fun hasPermission(sender: Command2MCSender, data: SubcommandData<ICommand2MC, Command2MCSender>): Boolean {
val defWorld = Bukkit.getWorlds().first().name val defWorld = if (ChromaUtils.isTest) "TestWorld" else Bukkit.getWorlds().first().name
val check = if (sender.permCheck !is TBMCPlayerBase) ({ val check = if (sender.permCheck !is TBMCPlayerBase) ({
MainPlugin.permission.groupHas( MainPlugin.permission.groupHas(
defWorld, defWorld,

View file

@ -3,7 +3,7 @@ package buttondevteam.lib.chat
import buttondevteam.core.component.channel.Channel import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.player.ChromaGamerBase import buttondevteam.lib.player.ChromaGamerBase
class Command2MCSender(val sender: ChromaGamerBase, val channel: Channel, val permCheck: ChromaGamerBase) : Command2Sender { open class Command2MCSender(val sender: ChromaGamerBase, val channel: Channel, val permCheck: ChromaGamerBase) : Command2Sender {
// TODO: Remove this class and only use the user classes. // TODO: Remove this class and only use the user classes.
// TODO: The command context should be stored separately. // TODO: The command context should be stored separately.
override fun sendMessage(message: String) { override fun sendMessage(message: String) {

View file

@ -1,6 +1,5 @@
package buttondevteam.lib.chat package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.arguments.ArgumentType import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.suggestion.SuggestionProvider import com.mojang.brigadier.suggestion.SuggestionProvider
@ -11,7 +10,6 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
private val optional: Boolean private val optional: Boolean
) : ArgumentBuilder<S, CoreArgumentBuilder<S, T>>() { ) : ArgumentBuilder<S, CoreArgumentBuilder<S, T>>() {
private var suggestionsProvider: SuggestionProvider<S>? = null private var suggestionsProvider: SuggestionProvider<S>? = null
internal lateinit var data: SubcommandData<*, S>
fun suggests(provider: SuggestionProvider<S>): CoreArgumentBuilder<S, T> { fun suggests(provider: SuggestionProvider<S>): CoreArgumentBuilder<S, T> {
suggestionsProvider = provider suggestionsProvider = provider
return this return this
@ -31,15 +29,11 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
redirectModifier, redirectModifier,
isFork, isFork,
suggestionsProvider, suggestionsProvider,
optional, optional
data
) )
} }
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> { override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {
if (argument is CoreArgumentBuilder<*, *>) {
(argument as CoreArgumentBuilder<S, *>).data = data
}
return super.then(argument) return super.then(argument)
} }

View file

@ -12,9 +12,12 @@ import java.util.function.Predicate
class CoreArgumentCommandNode<S : Command2Sender, T>( class CoreArgumentCommandNode<S : Command2Sender, T>(
name: String?, type: ArgumentType<T>?, command: Command<S>?, requirement: Predicate<S>?, redirect: CommandNode<S>?, modifier: RedirectModifier<S>?, forks: Boolean, customSuggestions: SuggestionProvider<S>?, name: String?, type: ArgumentType<T>?, command: Command<S>?, requirement: Predicate<S>?, redirect: CommandNode<S>?, modifier: RedirectModifier<S>?, forks: Boolean, customSuggestions: SuggestionProvider<S>?,
val optional: Boolean, val commandData: SubcommandData<*, S> val optional: Boolean
) : ) :
ArgumentCommandNode<S, T>(name, type, command, requirement, redirect, modifier, forks, customSuggestions) { ArgumentCommandNode<S, T>(name, type, command, requirement, redirect, modifier, forks, customSuggestions) {
lateinit var commandData: SubcommandData<*, S>
internal set // TODO: This should propagate to other arguments
override fun getUsageText(): String { override fun getUsageText(): String {
return if (optional) "[$name]" else "<$name>" return if (optional) "[$name]" else "<$name>"
} }

View file

@ -1,10 +1,12 @@
package buttondevteam.lib.chat package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.CommandArgument import buttondevteam.lib.chat.commands.CommandArgument
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
import buttondevteam.lib.chat.commands.NoOpSubcommandData import buttondevteam.lib.chat.commands.NoOpSubcommandData
import buttondevteam.lib.chat.commands.SubcommandData import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder
import java.lang.reflect.Method
class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcommandData> private constructor( class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcommandData> private constructor(
literal: String, literal: String,
@ -32,11 +34,12 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcom
} }
override fun then(argument: ArgumentBuilder<S, *>): LiteralArgumentBuilder<S> { override fun then(argument: ArgumentBuilder<S, *>): LiteralArgumentBuilder<S> {
if (argument is CoreArgumentBuilder<*, *> && data is SubcommandData<*, *>) { super.then(argument)
if (data is SubcommandData<*, *>) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(argument as CoreArgumentBuilder<S, *>).data = data as SubcommandData<*, S> arguments.forEach { it.coreArgument()?.commandData = data as SubcommandData<*, S> }
} }
return super.then(argument) return this
} }
companion object { companion object {
@ -62,11 +65,12 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcom
helpTextGetter: (Any) -> Array<String>, helpTextGetter: (Any) -> Array<String>,
hasPermission: (S, SubcommandData<TC, S>) -> Boolean, hasPermission: (S, SubcommandData<TC, S>) -> Boolean,
annotations: Array<Annotation>, annotations: Array<Annotation>,
fullPath: String fullPath: String,
method: Method
): CoreCommandBuilder<S, TC, SubcommandData<TC, S>> { ): CoreCommandBuilder<S, TC, SubcommandData<TC, S>> {
return CoreCommandBuilder( return CoreCommandBuilder(
name, name,
SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath) SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath, method)
) )
} }

View file

@ -2,6 +2,7 @@ package buttondevteam.lib.chat.commands
import buttondevteam.lib.chat.Command2Sender import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.ICommand2 import buttondevteam.lib.chat.ICommand2
import java.lang.reflect.Method
/** /**
* Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command. * Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command.
@ -42,14 +43,21 @@ class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
* A function that determines whether the user has permission to run this subcommand. * A function that determines whether the user has permission to run this subcommand.
*/ */
private val permissionCheck: (TP, SubcommandData<TC, TP>) -> Boolean, private val permissionCheck: (TP, SubcommandData<TC, TP>) -> Boolean,
/** /**
* All annotations implemented by the method that executes the command. Can be used to add custom metadata when implementing a platform. * All annotations implemented by the method that executes the command. Can be used to add custom metadata when implementing a platform.
*/ */
val annotations: Array<Annotation>, val annotations: Array<Annotation>,
/** /**
* The space-separated full command path of this subcommand. * The space-separated full command path of this subcommand.
*/ */
val fullPath: String val fullPath: String,
/**
* The method to run when executing the command.
*/
private val method: Method
) : NoOpSubcommandData(helpTextGetter) { ) : NoOpSubcommandData(helpTextGetter) {
/** /**
@ -61,4 +69,22 @@ class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
fun hasPermission(sender: TP): Boolean { fun hasPermission(sender: TP): Boolean {
return permissionCheck(sender, this) return permissionCheck(sender, this)
} }
/**
* Execute the command and return the result. Doesn't perform any checks.
*
* @param sender The actual sender as expected by the method
* @param args The rest of the method args
*/
fun executeCommand(sender: Any, vararg args: Any?): Any? {
method.isAccessible = true
return method.invoke(command, sender, *args)
}
/**
* Send the help text to the specified sender.
*/
fun sendHelpText(sender: TP) {
sender.sendMessage(getHelpText(sender))
}
} }

View file

@ -66,18 +66,29 @@ class Command2MCTest {
@Order(3) @Order(3)
fun testHandleCommand() { fun testHandleCommand() {
val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java) val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java)
assert(ButtonPlugin.command2MC.handleCommand(Command2MCSender(user, Channel.globalChat, user), "/test hmm")) val sender = object : Command2MCSender(user, Channel.globalChat, user) {
override fun sendMessage(message: String) {
error(message)
}
override fun sendMessage(message: Array<String>) {
error(message.joinToString("\n"))
}
}
assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm"))
assertEquals("hmm", testCommandReceived)
} }
@CommandClass @CommandClass
object TestCommand : ICommand2MC() { object TestCommand : ICommand2MC() {
@Command2.Subcommand @Command2.Subcommand
fun def(sender: Command2MCSender, test: String) { fun def(sender: Command2MCSender, test: String) {
println(test) testCommandReceived = test
} }
} }
companion object { companion object {
private var initialized = false private var initialized = false
private var testCommandReceived: String? = null
} }
} }