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: