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.
|
||||
*/
|
||||
|
|
|
@ -2,7 +2,6 @@ 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.coreArgument
|
||||
import buttondevteam.lib.chat.commands.CommandUtils.coreCommand
|
||||
|
@ -113,11 +112,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
} 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",
|
||||
e,
|
||||
MainPlugin.instance
|
||||
)
|
||||
ChromaUtils.throwWhenTested(e, "Command execution failed for sender ${sender.name}(${sender.javaClass.canonicalName}) and message $commandline")
|
||||
}
|
||||
}
|
||||
if (ChromaUtils.isTest) {
|
||||
|
@ -201,21 +196,26 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
method
|
||||
).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
|
||||
val param = params.removeLast()
|
||||
val param = params.removeFirst()
|
||||
val argType = getArgumentType(param)
|
||||
val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
|
||||
if (params.isEmpty()) {
|
||||
if (params.isEmpty() || executable) {
|
||||
arg.executes { context: CommandContext<TP> -> executeCommand(context) }
|
||||
} else {
|
||||
arg.executes(::executeHelpText)
|
||||
getArgNodes(arg, params)
|
||||
}
|
||||
if (params.isNotEmpty()) {
|
||||
getArgNodes(arg, params, param.optional)
|
||||
}
|
||||
parent.then(arg)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -329,12 +329,13 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
* @return Vanilla command success level (0)
|
||||
*/
|
||||
protected open fun executeCommand(context: CommandContext<TP>): Int {
|
||||
assert(context.nodes.lastOrNull()?.node?.coreArgument() != null) // TODO: What if there are no arguments?
|
||||
val node = context.nodes.last().node.coreArgument()!!
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val sd = node.commandData as SubcommandData<TC, TP>
|
||||
if (!sd.hasPermission(sender)) {
|
||||
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command")
|
||||
return 1
|
||||
|
@ -398,9 +399,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
} 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)
|
||||
ChromaUtils.throwWhenTested(e.cause ?: e, "An error occurred in a command handler for ${sd.fullPath}!")
|
||||
} 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)
|
||||
|
|
|
@ -20,7 +20,7 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
|||
}
|
||||
|
||||
override fun build(): CoreArgumentCommandNode<S, T> {
|
||||
return CoreArgumentCommandNode(
|
||||
val result = CoreArgumentCommandNode(
|
||||
name,
|
||||
type,
|
||||
command,
|
||||
|
@ -31,6 +31,10 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
|||
suggestionsProvider,
|
||||
optional
|
||||
)
|
||||
for (node in arguments) {
|
||||
result.addChild(node)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package buttondevteam.lib.chat
|
||||
|
||||
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
|
||||
import buttondevteam.lib.chat.commands.SubcommandData
|
||||
import com.mojang.brigadier.Command
|
||||
import com.mojang.brigadier.RedirectModifier
|
||||
|
@ -15,8 +16,16 @@ class CoreArgumentCommandNode<S : Command2Sender, T>(
|
|||
val optional: Boolean
|
||||
) :
|
||||
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
|
||||
private var _commandData: SubcommandData<*, S>? = null
|
||||
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 {
|
||||
return if (optional) "[$name]" else "<$name>"
|
||||
|
|
|
@ -3,8 +3,6 @@ package buttondevteam.lib.chat
|
|||
import buttondevteam.lib.chat.Command2.Subcommand
|
||||
import java.lang.reflect.Method
|
||||
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.
|
||||
|
@ -20,7 +18,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
|
|||
* @param sender The sender which ran the command
|
||||
* @return The success of the command
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
open fun def(sender: TP): Boolean {
|
||||
return false
|
||||
}
|
||||
|
@ -76,10 +73,6 @@ abstract class ICommand2<TP : Command2Sender>(val manager: Command2<*, TP>) {
|
|||
private fun getcmdpath(): String {
|
||||
if (!javaClass.isAnnotationPresent(CommandClass::class.java))
|
||||
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)
|
||||
while (true) {
|
||||
val superClass = classList.last().superclass
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test
|
|||
import org.junit.jupiter.api.TestMethodOrder
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
||||
class Command2MCTest {
|
||||
|
@ -45,6 +46,9 @@ class Command2MCTest {
|
|||
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")
|
||||
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
|
||||
|
@ -66,6 +70,7 @@ class Command2MCTest {
|
|||
@Order(3)
|
||||
fun testHandleCommand() {
|
||||
val user = ChromaGamerBase.getUser(UUID.randomUUID().toString(), TBMCPlayer::class.java)
|
||||
user.playerName = "TestPlayer"
|
||||
val sender = object : Command2MCSender(user, Channel.globalChat, user) {
|
||||
override fun sendMessage(message: String) {
|
||||
error(message)
|
||||
|
@ -75,20 +80,77 @@ class Command2MCTest {
|
|||
error(message.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm"))
|
||||
assertEquals("hmm", testCommandReceived)
|
||||
runCommand(sender, "/test hmm", TestCommand, "hmm")
|
||||
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
|
||||
object TestCommand : ICommand2MC() {
|
||||
object TestCommand : ICommand2MC(), ITestCommand2MC {
|
||||
override var testCommandReceived: String? = null
|
||||
|
||||
@Command2.Subcommand
|
||||
fun def(sender: Command2MCSender, test: String) {
|
||||
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 {
|
||||
private var initialized = false
|
||||
private var testCommandReceived: String? = null
|
||||
}
|
||||
|
||||
interface ITestCommand2MC {
|
||||
var testCommandReceived: String?
|
||||
}
|
||||
}
|
|
@ -7,6 +7,24 @@ buttondevteam:
|
|||
def:
|
||||
method: def()
|
||||
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:
|
||||
ComponentCommand:
|
||||
def:
|
||||
|
|
Loading…
Reference in a new issue