diff --git a/Chroma-Core/pom.xml b/Chroma-Core/pom.xml index a5ea55a..f57a4b9 100755 --- a/Chroma-Core/pom.xml +++ b/Chroma-Core/pom.xml @@ -255,6 +255,12 @@ 2.29.0 test + + + org.jetbrains.kotlin + kotlin-test-junit + 1.9.0 + TBMCPlugins 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 e62b3ce..13d94dd 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt @@ -1,6 +1,7 @@ package buttondevteam.lib.chat import buttondevteam.core.MainPlugin +import buttondevteam.lib.ChromaUtils import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.CommandUtils.coreCommand @@ -101,20 +102,25 @@ abstract class Command2, TP : Command2Sender>( if (results.reader.canRead()) { return false // Unknown command } - //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread - Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance) { _ -> + val executeCommand: () -> Unit = { try { dispatcher.execute(results) } catch (e: CommandSyntaxException) { sender.sendMessage(e.message) } catch (e: Exception) { TBMCCoreAPI.SendException( - "Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline, + "Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline", e, MainPlugin.instance ) } } + if (ChromaUtils.isTest) { + executeCommand() + } else { + //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread + Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance, executeCommand) + } return true //We found a method } @@ -145,8 +151,10 @@ abstract class Command2, TP : Command2Sender>( val ann = meth.getAnnotation(Subcommand::class.java) ?: continue val fullPath = command.commandPath + CommandUtils.getCommandPath(meth.name, ' ') assert(fullPath.isNotBlank()) { "No path found for command class ${command.javaClass.name} and method ${meth.name}" } - val (lastNode, mainNode, remainingPath) = registerNodeFromPath(fullPath) - lastNode.addChild(getExecutableNode(meth, command, ann, remainingPath, CommandArgumentHelpManager(command), fullPath)) + val (lastNode, mainNodeMaybe, remainingPath) = registerNodeFromPath(fullPath) + val execNode = getExecutableNode(meth, command, ann, remainingPath, CommandArgumentHelpManager(command), fullPath) + lastNode.addChild(execNode) + val mainNode = mainNodeMaybe ?: execNode if (mainCommandNode == null) mainCommandNode = mainNode else if (mainNode.name != mainCommandNode.name) { MainPlugin.instance.logger.warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.javaClass.simpleName) @@ -169,8 +177,10 @@ abstract class Command2, TP : Command2Sender>( * @param fullPath The full command path as registered * @return The executable node */ - private fun getExecutableNode(method: Method, command: TC, ann: Subcommand, remainingPath: String, - argHelpManager: CommandArgumentHelpManager, fullPath: String): LiteralCommandNode { + private fun getExecutableNode( + method: Method, command: TC, ann: Subcommand, remainingPath: String, + argHelpManager: CommandArgumentHelpManager, fullPath: String + ): CoreExecutableNode { val (params, senderType) = getCommandParametersAndSender(method, argHelpManager) // Param order is important val paramMap = HashMap() for (param in params) { @@ -190,7 +200,7 @@ abstract class Command2, TP : Command2Sender>( val argType = getArgumentType(param) parent.then(CoreArgumentBuilder.argument(param.name, argType, param.optional).also { parent = it }) } - return node.build() + return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath") } /** @@ -200,11 +210,11 @@ abstract class Command2, TP : Command2Sender>( * @return The last no-op node that can be used to register the executable node, * the main command node and the last part of the command path (that isn't registered yet) */ - private fun registerNodeFromPath(path: String): Triple, CoreCommandNode, String> { + private fun registerNodeFromPath(path: String): Triple, CoreCommandNode?, String> { val split = path.split(" ") var parent: CommandNode = dispatcher.root var mainCommand: CoreCommandNode? = null - split.forEachIndexed { i, part -> + split.dropLast(1).forEachIndexed { i, part -> val child = parent.getChild(part) if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp(part, getSubcommandList()) .executes(::executeHelpText).build().also { parent = it }) @@ -212,7 +222,7 @@ abstract class Command2, TP : Command2Sender>( if (i == 0) mainCommand = parent as CoreCommandNode // Has to be our own literal node, if not, well, error } - return Triple(parent, mainCommand!!, split.last()) + return Triple(parent, mainCommand, split.last()) } private fun getSubcommandList(): (Any) -> Array { @@ -235,10 +245,10 @@ abstract class Command2, TP : Command2Sender>( ): Pair, Class<*>> { val parameters = method.parameters if (parameters.isEmpty()) throw RuntimeException("No sender parameter for method '$method'") - val usage = argHelpManager.getParameterHelpForMethod(method) + val usage = argHelpManager.getParameterHelpForMethod(method)?.ifEmpty { null } val paramNames = usage?.split(" ") return Pair( - parameters.zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" }) + parameters.drop(1).zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" }) .map { (param, name) -> val numAnn = param.getAnnotation(NumberArg::class.java) CommandArgument( @@ -398,18 +408,22 @@ abstract class Command2, TP : Command2Sender>( * @param condition The condition for removing a given command */ fun unregisterCommandIf(condition: Predicate>>, nested: Boolean) { - dispatcher.root.children.removeIf { node -> node.coreExecutable()?.let { condition.test(it) } ?: false } - if (nested) for (child in dispatcher.root.children) child.coreCommand<_, NoOpSubcommandData>()?.let { unregisterCommandIf(condition, it) } + unregisterCommandIf(condition, dispatcher.root, nested) } private fun unregisterCommandIf( condition: Predicate>>, - root: CoreCommandNode + root: CommandNode, + nested: Boolean ) { - // TODO: Remvoe no-op nodes without children // Can't use getCoreChildren() here because the collection needs to be modifiable - root.children.removeIf { node -> node.coreExecutable()?.let { condition.test(it) } ?: false } - for (child in root.children) child.coreCommand<_, NoOpSubcommandData>()?.let { unregisterCommandIf(condition, it) } + if (nested) for (child in root.children) + child.coreCommand<_, NoOpSubcommandData>()?.let { unregisterCommandIf(condition, it, true) } + root.children.removeIf { node -> + node.coreExecutable() + ?.let { condition.test(it) } + ?: node.children.isEmpty() + } } /** diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt index 99dc49d..dc9a121 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt @@ -29,22 +29,45 @@ object CommandUtils { /** * Casts the node to whatever you say if it's a command node. Use responsibly. Returns null if an argument node. + * + * Command nodes are nodes that are subcommands. They may do something or just print their help text. + * They are definitely not argument nodes. */ @Suppress("UNCHECKED_CAST") fun CommandNode.coreCommand(): CoreCommandNode? { - return if (this is CoreCommandNode<*, *>) this as CoreCommandNode + return if (this.isCommand()) this as CoreCommandNode else null } /** * Returns the node as an executable core command node or returns null if it's a no-op node. + * + * Executable nodes are valid command nodes that do something other than printing help text. */ fun > CommandNode.coreExecutable(): CoreExecutableNode? { - val ret = this.coreCommand() - return if (ret?.data is SubcommandData<*, *>) ret.coreCommand() else null + return if (isExecutable()) coreCommand() else null } + /** + * Returns the node as an argument node or returns null if it's not one. + * + * Argument nodes are children of executable command nodes. + */ fun CommandNode.coreArgument(): CoreArgumentCommandNode? { return if (this is CoreArgumentCommandNode<*, *>) this as CoreArgumentCommandNode else null } + + /** + * Returns whether the current node is an executable or help text command node. + */ + fun CommandNode.isCommand(): Boolean { + return this is CoreCommandNode<*, *> + } + + /** + * Returns whether the current node is an executable command node. + */ + fun CommandNode.isExecutable(): Boolean { + return coreCommand()?.data is SubcommandData<*, *> + } } \ 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 b4c3db6..f0d2ec5 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 @@ -8,6 +8,7 @@ import buttondevteam.lib.chat.Command2 import buttondevteam.lib.chat.Command2MCSender import buttondevteam.lib.chat.CommandClass import buttondevteam.lib.chat.ICommand2MC +import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable import buttondevteam.lib.player.ChromaGamerBase import buttondevteam.lib.player.TBMCPlayer import org.junit.jupiter.api.MethodOrderer @@ -15,6 +16,7 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import java.util.* +import kotlin.test.assertEquals @TestMethodOrder(MethodOrderer.OrderAnnotation::class) class Command2MCTest { @@ -27,17 +29,22 @@ class Command2MCTest { throw RuntimeException("Failed to init tests! Something in here fails to initialize. Check the first test case.", e) } MockBukkit.load(MainPlugin::class.java, true) - ButtonPlugin.command2MC.unregisterCommands(MainPlugin.instance) // FIXME should have the init code separate of the plugin init code initialized = true } } @Test - @Order(1) + @Order(2) fun testRegisterCommand() { MainPlugin.instance.registerCommand(TestCommand) - assert(ButtonPlugin.command2MC.commandNodes.size == 1) - assert(ButtonPlugin.command2MC.commandNodes.first().literal == "test") + val nodes = ButtonPlugin.command2MC.commandNodes + assert(nodes.size == 1) + assert(nodes.first().literal == "test") + val coreExecutable = nodes.first().coreExecutable() + assertEquals(TestCommand::class.qualifiedName, coreExecutable?.data?.command?.let { it::class.qualifiedName }, "The command class name doesn't match or command is null") + assertEquals("test", coreExecutable?.data?.argumentsInOrder?.firstOrNull()?.name, "Failed to get correct argument name") + assertEquals(String::class.java, coreExecutable?.data?.arguments?.get("test")?.type, "The argument could not be found or type doesn't match") + assertEquals(Command2MCSender::class.java, coreExecutable?.data?.senderType, "The sender's type doesn't seem to be stored correctly") } @Test @@ -49,11 +56,14 @@ class Command2MCTest { } @Test + @Order(1) fun testUnregisterCommands() { + ButtonPlugin.command2MC.unregisterCommands(MainPlugin.instance) // FIXME should have the init code separate of the plugin init code + assert(ButtonPlugin.command2MC.commandNodes.isEmpty()) } @Test - @Order(2) + @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")) diff --git a/Chroma-Core/src/test/resources/commands.yml b/Chroma-Core/src/test/resources/commands.yml index 4d3ff60..bbf8960 100644 --- a/Chroma-Core/src/test/resources/commands.yml +++ b/Chroma-Core/src/test/resources/commands.yml @@ -6,7 +6,7 @@ buttondevteam: TestCommand: def: method: def() - params: '' + params: 'test' core: ComponentCommand: def: