Fix no argument and multiple argument handling
- Added a bunch more command tests - Fixed no arg command handling - Fixed multiarg command handling (with data propagation) - Fixed exception reporting in the command system
This commit is contained in:
parent
19362cfe5f
commit
82d43e5b09
7 changed files with 134 additions and 31 deletions
|
@ -81,6 +81,22 @@ object ChromaUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws the exception directly during testing but only reports it when running on the server.
|
||||||
|
* This way exceptions get reported properly when running, but they can be checked during testing.
|
||||||
|
*
|
||||||
|
* Useful in code blocks scheduled using the Bukkit API.
|
||||||
|
*/
|
||||||
|
fun throwWhenTested(exception: Throwable, message: String) {
|
||||||
|
if (isTest) {
|
||||||
|
// Propagate exception back to the tests
|
||||||
|
throw exception
|
||||||
|
} else {
|
||||||
|
// Otherwise we don't run the code directly, so we need to handle this here
|
||||||
|
TBMCCoreAPI.SendException(message, exception, MainPlugin.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true while unit testing.
|
* Returns true while unit testing.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,7 +2,6 @@ package buttondevteam.lib.chat
|
||||||
|
|
||||||
import buttondevteam.core.MainPlugin
|
import buttondevteam.core.MainPlugin
|
||||||
import buttondevteam.lib.ChromaUtils
|
import buttondevteam.lib.ChromaUtils
|
||||||
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.coreArgument
|
||||||
import buttondevteam.lib.chat.commands.CommandUtils.coreCommand
|
import buttondevteam.lib.chat.commands.CommandUtils.coreCommand
|
||||||
|
@ -113,11 +112,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
} catch (e: CommandSyntaxException) {
|
} catch (e: CommandSyntaxException) {
|
||||||
sender.sendMessage(e.message)
|
sender.sendMessage(e.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
TBMCCoreAPI.SendException(
|
ChromaUtils.throwWhenTested(e, "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) {
|
if (ChromaUtils.isTest) {
|
||||||
|
@ -201,21 +196,26 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
method
|
method
|
||||||
).executes(this::executeHelpText)
|
).executes(this::executeHelpText)
|
||||||
|
|
||||||
fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>) {
|
fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>, executable: Boolean) {
|
||||||
// TODO: Implement optional arguments here by making the last non-optional parameter also executable
|
// TODO: Implement optional arguments here by making the last non-optional parameter also executable
|
||||||
val param = params.removeLast()
|
val param = params.removeFirst()
|
||||||
val argType = getArgumentType(param)
|
val argType = getArgumentType(param)
|
||||||
val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
|
val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
|
||||||
if (params.isEmpty()) {
|
if (params.isEmpty() || executable) {
|
||||||
arg.executes { context: CommandContext<TP> -> executeCommand(context) }
|
arg.executes { context: CommandContext<TP> -> executeCommand(context) }
|
||||||
} else {
|
} else {
|
||||||
arg.executes(::executeHelpText)
|
arg.executes(::executeHelpText)
|
||||||
getArgNodes(arg, params)
|
}
|
||||||
|
if (params.isNotEmpty()) {
|
||||||
|
getArgNodes(arg, params, param.optional)
|
||||||
}
|
}
|
||||||
parent.then(arg)
|
parent.then(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.isNotEmpty()) {
|
if (params.isNotEmpty()) {
|
||||||
getArgNodes(node, params.toMutableList())
|
getArgNodes(node, params.toMutableList(), false)
|
||||||
|
} else {
|
||||||
|
node.executes(::executeCommand)
|
||||||
}
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
@ -329,12 +329,13 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
* @return Vanilla command success level (0)
|
* @return Vanilla command success level (0)
|
||||||
*/
|
*/
|
||||||
protected open fun executeCommand(context: CommandContext<TP>): Int {
|
protected open fun executeCommand(context: CommandContext<TP>): Int {
|
||||||
assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments?
|
@Suppress("UNCHECKED_CAST")
|
||||||
val node = context.nodes.last().node.coreArgument()!!
|
val sd = context.nodes.lastOrNull()?.node?.let {
|
||||||
|
it.coreArgument()?.commandData as SubcommandData<TC, TP>?
|
||||||
|
?: it.coreCommand<_, SubcommandData<TC, TP>>()?.data
|
||||||
|
} ?: throw IllegalStateException("Could not find suitable command node for command ${context.input}")
|
||||||
val sender = context.source
|
val sender = context.source
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val sd = node.commandData as SubcommandData<TC, TP>
|
|
||||||
if (!sd.hasPermission(sender)) {
|
if (!sd.hasPermission(sender)) {
|
||||||
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
|
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
|
||||||
return 1
|
return 1
|
||||||
|
@ -398,9 +399,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
||||||
} else if (ret != null)
|
} else if (ret != null)
|
||||||
throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret")
|
throw Exception("Wrong return type! Must return a boolean or void. Return value: $ret")
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
TBMCCoreAPI.SendException("An error occurred in a command handler for ${sd.fullPath}!", e.cause ?: e, MainPlugin.instance)
|
ChromaUtils.throwWhenTested(e.cause ?: e, "An error occurred in a command handler for ${sd.fullPath}!")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
TBMCCoreAPI.SendException("Command handling failed for sender $sender and subcommand ${sd.fullPath}", e, MainPlugin.instance)
|
ChromaUtils.throwWhenTested(e, "Command handling failed for sender $sender and subcommand ${sd.fullPath}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (runOnPrimaryThread && !ChromaUtils.isTest)
|
if (runOnPrimaryThread && !ChromaUtils.isTest)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build(): CoreArgumentCommandNode<S, T> {
|
override fun build(): CoreArgumentCommandNode<S, T> {
|
||||||
return CoreArgumentCommandNode(
|
val result = CoreArgumentCommandNode(
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
command,
|
command,
|
||||||
|
@ -31,6 +31,10 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
||||||
suggestionsProvider,
|
suggestionsProvider,
|
||||||
optional
|
optional
|
||||||
)
|
)
|
||||||
|
for (node in arguments) {
|
||||||
|
result.addChild(node)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {
|
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package buttondevteam.lib.chat
|
package buttondevteam.lib.chat
|
||||||
|
|
||||||
|
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
|
||||||
import buttondevteam.lib.chat.commands.SubcommandData
|
import buttondevteam.lib.chat.commands.SubcommandData
|
||||||
import com.mojang.brigadier.Command
|
import com.mojang.brigadier.Command
|
||||||
import com.mojang.brigadier.RedirectModifier
|
import com.mojang.brigadier.RedirectModifier
|
||||||
|
@ -15,8 +16,16 @@ class CoreArgumentCommandNode<S : Command2Sender, T>(
|
||||||
val optional: Boolean
|
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>
|
private var _commandData: SubcommandData<*, S>? = null
|
||||||
internal set // TODO: This should propagate to other arguments
|
var commandData: SubcommandData<*, S>
|
||||||
|
get() {
|
||||||
|
return _commandData
|
||||||
|
?: throw UninitializedPropertyAccessException("Command data has not been initialized")
|
||||||
|
}
|
||||||
|
internal set(value) {
|
||||||
|
_commandData = value
|
||||||
|
children.forEach { it.coreArgument()?.commandData = value }
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUsageText(): String {
|
override fun getUsageText(): String {
|
||||||
return if (optional) "[$name]" else "<$name>"
|
return if (optional) "[$name]" else "<$name>"
|
||||||
|
|
|
@ -3,8 +3,6 @@ package buttondevteam.lib.chat
|
||||||
import buttondevteam.lib.chat.Command2.Subcommand
|
import buttondevteam.lib.chat.Command2.Subcommand
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
import java.util.*
|
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used as a base class for all the specific command implementations.
|
* This class is used as a base class for all the specific command implementations.
|
||||||
|
@ -20,7 +18,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
|
||||||
* @param sender The sender which ran the command
|
* @param sender The sender which ran the command
|
||||||
* @return The success of the command
|
* @return The success of the command
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
open fun def(sender: TP): Boolean {
|
open fun def(sender: TP): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -76,10 +73,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
|
||||||
private fun getcmdpath(): String {
|
private fun getcmdpath(): String {
|
||||||
if (!javaClass.isAnnotationPresent(CommandClass::class.java))
|
if (!javaClass.isAnnotationPresent(CommandClass::class.java))
|
||||||
throw RuntimeException("No @CommandClass annotation on command class ${javaClass.simpleName}!")
|
throw RuntimeException("No @CommandClass annotation on command class ${javaClass.simpleName}!")
|
||||||
val getFromClass = Function { cl: Class<*> ->
|
|
||||||
cl.simpleName.lowercase(Locale.getDefault()).replace("commandbase", "") // <-- ...
|
|
||||||
.replace("command", "")
|
|
||||||
}
|
|
||||||
val classList = mutableListOf<Class<*>>(javaClass)
|
val classList = mutableListOf<Class<*>>(javaClass)
|
||||||
while (true) {
|
while (true) {
|
||||||
val superClass = classList.last().superclass
|
val superClass = classList.last().superclass
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.TestMethodOrder
|
import org.junit.jupiter.api.TestMethodOrder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
||||||
class Command2MCTest {
|
class Command2MCTest {
|
||||||
|
@ -45,6 +46,9 @@ class Command2MCTest {
|
||||||
assertEquals("test", coreExecutable?.data?.argumentsInOrder?.firstOrNull()?.name, "Failed to get correct argument name")
|
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(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")
|
assertEquals(Command2MCSender::class.java, coreExecutable?.data?.senderType, "The sender's type doesn't seem to be stored correctly")
|
||||||
|
MainPlugin.instance.registerCommand(NoArgTestCommand)
|
||||||
|
assertEquals("No sender parameter for method '${ErroringTestCommand::class.java.getMethod("def")}'", assertFails { MainPlugin.instance.registerCommand(ErroringTestCommand) }.message)
|
||||||
|
MainPlugin.instance.registerCommand(MultiArgTestCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -66,6 +70,7 @@ 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)
|
||||||
|
user.playerName = "TestPlayer"
|
||||||
val sender = object : Command2MCSender(user, Channel.globalChat, user) {
|
val sender = object : Command2MCSender(user, Channel.globalChat, user) {
|
||||||
override fun sendMessage(message: String) {
|
override fun sendMessage(message: String) {
|
||||||
error(message)
|
error(message)
|
||||||
|
@ -75,20 +80,77 @@ class Command2MCTest {
|
||||||
error(message.joinToString("\n"))
|
error(message.joinToString("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm"))
|
runCommand(sender, "/test hmm", TestCommand, "hmm")
|
||||||
assertEquals("hmm", testCommandReceived)
|
runCommand(sender, "/noargtest", NoArgTestCommand, "TestPlayer")
|
||||||
|
assertFails { ButtonPlugin.command2MC.handleCommand(sender, "/noargtest failing") }
|
||||||
|
runFailingCommand(sender, "/erroringtest")
|
||||||
|
runCommand(sender, "/multiargtest hmm mhm", MultiArgTestCommand, "hmmmhm")
|
||||||
|
runCommand(sender, "/multiargtest test2 true 19", MultiArgTestCommand, "true 19")
|
||||||
|
// TODO: Add expected failed param conversions and missing params
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommand(sender: Command2MCSender, command: String, obj: ITestCommand2MC, expected: String) {
|
||||||
|
assert(ButtonPlugin.command2MC.handleCommand(sender, command)) { "Could not find command $command" }
|
||||||
|
assertEquals(expected, obj.testCommandReceived)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runFailingCommand(sender: Command2MCSender, command: String) {
|
||||||
|
assert(!ButtonPlugin.command2MC.handleCommand(sender, command)) { "Could execute command $command that shouldn't work" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@CommandClass
|
@CommandClass
|
||||||
object TestCommand : ICommand2MC() {
|
object TestCommand : ICommand2MC(), ITestCommand2MC {
|
||||||
|
override var testCommandReceived: String? = null
|
||||||
|
|
||||||
@Command2.Subcommand
|
@Command2.Subcommand
|
||||||
fun def(sender: Command2MCSender, test: String) {
|
fun def(sender: Command2MCSender, test: String) {
|
||||||
testCommandReceived = test
|
testCommandReceived = test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CommandClass
|
||||||
|
object NoArgTestCommand : ICommand2MC(), ITestCommand2MC {
|
||||||
|
override var testCommandReceived: String? = null
|
||||||
|
|
||||||
|
@Command2.Subcommand
|
||||||
|
override fun def(sender: Command2MCSender): Boolean {
|
||||||
|
testCommandReceived = sender.name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand
|
||||||
|
fun failing(sender: Command2MCSender): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandClass
|
||||||
|
object ErroringTestCommand : ICommand2MC() {
|
||||||
|
@Command2.Subcommand
|
||||||
|
fun def() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandClass
|
||||||
|
object MultiArgTestCommand : ICommand2MC(), ITestCommand2MC {
|
||||||
|
override var testCommandReceived: String? = null
|
||||||
|
|
||||||
|
@Command2.Subcommand
|
||||||
|
fun def(sender: Command2MCSender, test: String, test2: String) {
|
||||||
|
testCommandReceived = test + test2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command2.Subcommand
|
||||||
|
fun test2(sender: Command2MCSender, btest: Boolean, ntest: Int) {
|
||||||
|
testCommandReceived = "$btest $ntest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var initialized = false
|
private var initialized = false
|
||||||
private var testCommandReceived: String? = null
|
}
|
||||||
|
|
||||||
|
interface ITestCommand2MC {
|
||||||
|
var testCommandReceived: String?
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,6 +7,24 @@ buttondevteam:
|
||||||
def:
|
def:
|
||||||
method: def()
|
method: def()
|
||||||
params: 'test'
|
params: 'test'
|
||||||
|
NoArgTestCommand:
|
||||||
|
def:
|
||||||
|
method: def()
|
||||||
|
params: ''
|
||||||
|
failing:
|
||||||
|
method: failing()
|
||||||
|
params: ''
|
||||||
|
ErroringTestCommand:
|
||||||
|
def:
|
||||||
|
method: def()
|
||||||
|
params: ''
|
||||||
|
MultiArgTestCommand:
|
||||||
|
def:
|
||||||
|
method: def()
|
||||||
|
params: "test test2"
|
||||||
|
test2:
|
||||||
|
method: test2()
|
||||||
|
params: "btest ntest"
|
||||||
core:
|
core:
|
||||||
ComponentCommand:
|
ComponentCommand:
|
||||||
def:
|
def:
|
||||||
|
|
Loading…
Reference in a new issue