diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt index 13d94dd..1d18a8d 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt @@ -4,8 +4,11 @@ import buttondevteam.core.MainPlugin import buttondevteam.lib.ChromaUtils import buttondevteam.lib.TBMCCoreAPI 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.coreExecutable +import com.google.common.base.Defaults +import com.google.common.primitives.Primitives import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.arguments.* 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.LiteralCommandNode import org.bukkit.Bukkit +import org.bukkit.ChatColor +import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.util.function.Function import java.util.function.Predicate @@ -192,13 +197,25 @@ abstract class Command2, TP : Command2Sender>( { helpText }, // TODO: Help text getter support { sender: TP, data: SubcommandData -> hasPermission(sender, data) }, method.annotations.filterNot { it is Subcommand }.toTypedArray(), - fullPath - ) - .executes { context: CommandContext -> executeCommand(context) } - var parent: ArgumentBuilder = node - for (param in params) { // Register parameters in the right order + fullPath, + method + ).executes(this::executeHelpText) + + fun getArgNodes(parent: ArgumentBuilder, params: MutableList) { + // TODO: Implement optional arguments here by making the last non-optional parameter also executable + val param = params.removeLast() val argType = getArgumentType(param) - parent.then(CoreArgumentBuilder.argument(param.name, argType, param.optional).also { parent = it }) + val arg = CoreArgumentBuilder.argument(param.name, argType, param.optional) + if (params.isEmpty()) { + arg.executes { context: CommandContext -> 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") } @@ -311,65 +328,85 @@ abstract class Command2, TP : Command2Sender>( * @param context The command context * @return Vanilla command success level (0) */ - private fun executeCommand(context: CommandContext): Int { - println("Execute command") - println("Should be running sync: $runOnPrimaryThread") + protected open fun executeCommand(context: CommandContext): Int { + assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments? + val node = context.nodes.last().node.coreArgument()!! + val sender = context.source - /*if (!hasPermission(sender, sd.command, sd.method)) { - sender.sendMessage("${ChatColor.RED}You don't have permission to use this command"); - return; - } - // TODO: WIP + @Suppress("UNCHECKED_CAST") + val sd = node.commandData as SubcommandData + if (!sd.hasPermission(sender)) { + sender.sendMessage("${ChatColor.RED}You don't have permission to use this command") + return 1 + } + // 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(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 + } + + val params = executeGetArguments(sd, context) ?: return executeHelpText(context) - 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?) - params.add(commandline.substring(j + 1).split(" +")); - continue; - }*/ + // TODO: Varargs support? (colors?) // TODO: Character handling (strlen) // TODO: Param converter - /*} - Runnable invokeCommand = () -> { - try { - 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) - Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand); - else - invokeCommand.run();*/return 0 + + executeInvokeCommand(sd, sender, convertedSender, params) + return 0 + } + + private fun executeGetArguments(sd: SubcommandData, context: CommandContext): MutableList? { + val params = mutableListOf() + 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, sender: TP, actualSender: Any, params: List) { + 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 + invokeCommand() } abstract fun hasPermission(sender: TP, data: SubcommandData): Boolean diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt index 14abeea..5601ec4 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt @@ -16,7 +16,6 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal import com.mojang.brigadier.builder.RequiredArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument -import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.LiteralCommandNode import me.lucko.commodore.Commodore import me.lucko.commodore.CommodoreProvider @@ -40,8 +39,7 @@ class Command2MC : Command2('/', true), Listener override fun registerCommand(command: ICommand2MC) { val commandNode = super.registerCommandSuper(command) val bcmd = registerOfficially(command, commandNode) - if (bcmd != null) // TODO: Support aliases - super.registerCommandSuper(command) + // TODO: Support aliases val permPrefix = "chroma.command." //Allow commands by default, it will check mod-only val nodes = commandNode.coreExecutable() @@ -60,7 +58,7 @@ class Command2MC : Command2('/', true), Listener } override fun hasPermission(sender: Command2MCSender, data: SubcommandData): 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) ({ MainPlugin.permission.groupHas( defWorld, diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MCSender.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MCSender.kt index 1b93455..80a6dd2 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MCSender.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MCSender.kt @@ -3,7 +3,7 @@ package buttondevteam.lib.chat import buttondevteam.core.component.channel.Channel 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: The command context should be stored separately. override fun sendMessage(message: String) { diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt index 7d0df65..eadc65a 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentBuilder.kt @@ -1,6 +1,5 @@ package buttondevteam.lib.chat -import buttondevteam.lib.chat.commands.SubcommandData import com.mojang.brigadier.arguments.ArgumentType import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.suggestion.SuggestionProvider @@ -11,7 +10,6 @@ class CoreArgumentBuilder( private val optional: Boolean ) : ArgumentBuilder>() { private var suggestionsProvider: SuggestionProvider? = null - internal lateinit var data: SubcommandData<*, S> fun suggests(provider: SuggestionProvider): CoreArgumentBuilder { suggestionsProvider = provider return this @@ -31,15 +29,11 @@ class CoreArgumentBuilder( redirectModifier, isFork, suggestionsProvider, - optional, - data + optional ) } override fun then(argument: ArgumentBuilder?): CoreArgumentBuilder { - if (argument is CoreArgumentBuilder<*, *>) { - (argument as CoreArgumentBuilder).data = data - } return super.then(argument) } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt index 1c4f207..5e81498 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreArgumentCommandNode.kt @@ -12,9 +12,12 @@ import java.util.function.Predicate class CoreArgumentCommandNode( name: String?, type: ArgumentType?, command: Command?, requirement: Predicate?, redirect: CommandNode?, modifier: RedirectModifier?, forks: Boolean, customSuggestions: SuggestionProvider?, - val optional: Boolean, val commandData: SubcommandData<*, S> + val optional: Boolean ) : ArgumentCommandNode(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 { return if (optional) "[$name]" else "<$name>" } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt index fd9cf9c..8aeca7d 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt @@ -1,10 +1,12 @@ package buttondevteam.lib.chat import buttondevteam.lib.chat.commands.CommandArgument +import buttondevteam.lib.chat.commands.CommandUtils.coreArgument import buttondevteam.lib.chat.commands.NoOpSubcommandData import buttondevteam.lib.chat.commands.SubcommandData import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder +import java.lang.reflect.Method class CoreCommandBuilder, TSD : NoOpSubcommandData> private constructor( literal: String, @@ -32,11 +34,12 @@ class CoreCommandBuilder, TSD : NoOpSubcom } override fun then(argument: ArgumentBuilder): LiteralArgumentBuilder { - if (argument is CoreArgumentBuilder<*, *> && data is SubcommandData<*, *>) { + super.then(argument) + if (data is SubcommandData<*, *>) { @Suppress("UNCHECKED_CAST") - (argument as CoreArgumentBuilder).data = data as SubcommandData<*, S> + arguments.forEach { it.coreArgument()?.commandData = data as SubcommandData<*, S> } } - return super.then(argument) + return this } companion object { @@ -62,11 +65,12 @@ class CoreCommandBuilder, TSD : NoOpSubcom helpTextGetter: (Any) -> Array, hasPermission: (S, SubcommandData) -> Boolean, annotations: Array, - fullPath: String + fullPath: String, + method: Method ): CoreCommandBuilder> { return CoreCommandBuilder( name, - SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath) + SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath, method) ) } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt index 7f220d8..fb4c679 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt @@ -2,6 +2,7 @@ package buttondevteam.lib.chat.commands import buttondevteam.lib.chat.Command2Sender 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. @@ -42,14 +43,21 @@ class SubcommandData, TP : Command2Sender>( * A function that determines whether the user has permission to run this subcommand. */ private val permissionCheck: (TP, SubcommandData) -> Boolean, + /** * All annotations implemented by the method that executes the command. Can be used to add custom metadata when implementing a platform. */ val annotations: Array, + /** * 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) { /** @@ -61,4 +69,22 @@ class SubcommandData, TP : Command2Sender>( fun hasPermission(sender: TP): Boolean { 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)) + } } \ No newline at end of file diff --git a/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt b/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt index f0d2ec5..a40e2ce 100644 --- a/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt +++ b/Chroma-Core/src/test/kotlin/buttondevteam/lib/chat/test/Command2MCTest.kt @@ -66,18 +66,29 @@ class Command2MCTest { @Order(3) fun testHandleCommand() { 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) { + error(message.joinToString("\n")) + } + } + assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm")) + assertEquals("hmm", testCommandReceived) } @CommandClass object TestCommand : ICommand2MC() { @Command2.Subcommand fun def(sender: Command2MCSender, test: String) { - println(test) + testCommandReceived = test } } companion object { private var initialized = false + private var testCommandReceived: String? = null } } \ No newline at end of file