Param converter and command list with permission check

- Displaying and testing error message for param converters
- Improved crash testing
- Using has permission check for listing (sub)commands
- Implemented and tested command list
This commit is contained in:
Norbi Peti 2023-07-31 17:19:05 +02:00
parent 401a54b078
commit d16a6a742c
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
7 changed files with 107 additions and 65 deletions

1
.gitignore vendored
View file

@ -225,3 +225,4 @@ dependency-reduced-pom.xml
TBMC/
/.apt_generated/
.attach_pid*

View file

@ -1,38 +0,0 @@
package buttondevteam.core;
import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.ICommand2MC;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin;
import java.util.Arrays;
import java.util.Optional;
@CommandClass
public class ChromaCommand extends ICommand2MC {
public ChromaCommand() {
getManager().addParamConverter(ButtonPlugin.class, name ->
(ButtonPlugin) Optional.ofNullable(Bukkit.getPluginManager().getPlugin(name))
.filter(plugin -> plugin instanceof ButtonPlugin).orElse(null),
"No Chroma plugin found by that name.", () -> Arrays.stream(Bukkit.getPluginManager().getPlugins())
.filter(plugin -> plugin instanceof ButtonPlugin).map(Plugin::getName)::iterator);
}
@Command2.Subcommand
public void reload(CommandSender sender, @Command2.OptionalArg ButtonPlugin plugin) {
if (plugin == null)
plugin = getPlugin();
if (plugin.tryReloadConfig())
sender.sendMessage("${ChatColor.AQUA}" + plugin.getName() + " config reloaded.");
else
sender.sendMessage("${ChatColor.RED}Failed to reload config. Check console.");
}
@Command2.Subcommand
public void def(CommandSender sender) {
sender.sendMessage(ButtonPlugin.getCommand2MC().getCommandsText());
}
}

View file

@ -0,0 +1,47 @@
package buttondevteam.core
import buttondevteam.lib.architecture.ButtonPlugin
import buttondevteam.lib.architecture.ButtonPlugin.Companion.command2MC
import buttondevteam.lib.chat.Command2.OptionalArg
import buttondevteam.lib.chat.Command2.Subcommand
import buttondevteam.lib.chat.Command2MCSender
import buttondevteam.lib.chat.CommandClass
import buttondevteam.lib.chat.ICommand2MC
import org.bukkit.Bukkit
import org.bukkit.ChatColor
import org.bukkit.command.CommandSender
import java.util.*
@CommandClass
class ChromaCommand : ICommand2MC() {
init {
manager.addParamConverter(
ButtonPlugin::class.java, { name ->
Bukkit.getPluginManager().getPlugin(name)
?.let { if (it is ButtonPlugin) it else null }
},
"No Chroma plugin found by that name."
) {
Iterable {
Arrays.stream(Bukkit.getPluginManager().plugins)
.filter { it is ButtonPlugin }.map { it.name }.iterator()
}
}
}
@Subcommand
fun reload(sender: CommandSender, @OptionalArg plugin: ButtonPlugin?) {
val pl = plugin ?: this.plugin
if (pl.tryReloadConfig())
sender.sendMessage("${ChatColor.AQUA}${pl.name} config reloaded.")
else
sender.sendMessage("${ChatColor.RED}Failed to reload config. Check console.")
}
@Subcommand
override fun def(sender: Command2MCSender): Boolean {
sender.sendMessage("${ChatColor.GOLD}---- Commands ----")
sender.sendMessage(command2MC.getCommandList(sender))
return true
}
}

View file

@ -90,7 +90,7 @@ object ChromaUtils {
fun throwWhenTested(exception: Throwable, message: String) {
if (isTest) {
// Propagate exception back to the tests
throw exception
throw Exception(message, exception)
} else {
// Otherwise we don't run the code directly, so we need to handle this here
TBMCCoreAPI.SendException(message, exception, MainPlugin.instance)

View file

@ -76,7 +76,6 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
)
protected val paramConverters = HashMap<Class<*>, ParamConverter<*>>()
private val commandHelp = ArrayList<String>() //Mainly needed by Discord
private val dispatcher = CommandDispatcher<TP>()
/**
@ -106,14 +105,14 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
open fun handleCommand(sender: TP, commandline: String): Boolean {
val results = dispatcher.parse(commandline.removePrefix("/"), sender)
if (results.reader.canRead()) {
if (results.context.nodes.isNotEmpty()) {
return if (results.context.nodes.isNotEmpty()) {
for ((node, ex) in results.exceptions) {
sender.sendMessage("${ChatColor.RED}${ex.message}")
executeHelpText(results.context.build(results.reader.string))
}
return true
true
} else {
return false // Unknown command
false // Unknown command
}
}
val executeCommand: () -> Unit = {
@ -252,10 +251,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
return Triple(parent, mainCommand, split.last())
}
private fun getSubcommandList(): (Any) -> Array<String> {
return {
arrayOf("TODO") // TODO: Subcommand list
}
fun getCommandList(sender: TP): Array<String> {
return commandNodes.filter { it.data.hasPermission(sender) }
.map { commandChar + it.data.fullPath }.toTypedArray()
}
/**
@ -333,7 +331,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
val node = context.nodes.lastOrNull()?.node ?: error("No nodes found when executing help text for ${context.input}!")
val helpText = node.subcommandDataNoOp()?.getHelpText(context.source) ?: error("No subcommand data found when executing help text for ${context.input}")
if (node.isCommand()) {
val subs = getSubcommands(node.coreCommandNoOp()!!).map { commandChar + it.data.fullPath }.sorted()
val subs = getSubcommands(node.coreCommandNoOp()!!)
.filter { it.data.hasPermission(context.source) }
.map { commandChar + it.data.fullPath }.sorted()
val messages = if (subs.isNotEmpty()) {
helpText + "${ChatColor.GOLD}---- Subcommands ----" + subs
} else {
@ -369,7 +369,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
return 0
}
val params = executeGetArguments(sd, context) ?: return executeHelpText(context)
val params = executeGetArguments(sd, context, sender) ?: return executeHelpText(context)
// TODO: Varargs support? (colors?)
// TODO: Character handling (strlen)
@ -378,7 +378,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
return 0
}
private fun executeGetArguments(sd: SubcommandData<TC, TP>, context: CommandContext<TP>): MutableList<Any?>? {
private fun executeGetArguments(sd: SubcommandData<TC, TP>, context: CommandContext<TP>, sender: TP): MutableList<Any?>? {
val params = mutableListOf<Any?>()
for (argument in sd.argumentsInOrder) {
try {
@ -387,9 +387,14 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
params.add(userArgument)
} else {
val userArgument = context.getArgument(argument.name, String::class.java)
val converter = paramConverters[argument.type]?.converter
val converter = paramConverters[argument.type]
?: error("No suitable converter found for ${argument.type} ${argument.name}")
params.add(converter.apply(userArgument))
val result = converter.converter.apply(userArgument)
if (result == null) {
sender.sendMessage("${ChatColor.RED}Error: ${converter.errormsg}")
return null
}
params.add(result)
}
} catch (e: IllegalArgumentException) {
if (ChromaUtils.isTest && e.message?.contains("No such argument '${argument.name}' exists on this command") != true) {
@ -431,15 +436,14 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
}
abstract fun hasPermission(sender: TP, data: SubcommandData<TC, TP>): Boolean
val commandsText: Array<String> get() = commandHelp.toTypedArray()
/**
* Get all registered command nodes. This returns all registered Chroma commands with all the information about them.
*
* @return A set of command node objects containing the commands
*/
val commandNodes: Set<CoreCommandNode<TP, NoOpSubcommandData>>
get() = dispatcher.root.children.mapNotNull { it.coreCommand<TP, NoOpSubcommandData>() }.toSet()
val commandNodes: Set<CoreExecutableNode<TP, TC>>
get() = getSubcommands(true, dispatcher.root).toSet()
/**
* Get a node that belongs to the given command.
@ -499,7 +503,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
private fun getSubcommands(
deep: Boolean = true,
root: CoreNoOpNode<TP>
root: CommandNode<TP>
): List<CoreExecutableNode<TP, TC>> {
return root.children.mapNotNull { it.coreExecutable<TP, TC>() } +
if (deep) root.children.flatMap { child -> child.coreCommand<_, NoOpSubcommandData>()?.let { getSubcommands(deep, it) } ?: emptyList() } else emptyList()

View file

@ -26,6 +26,11 @@ abstract class Command2MCCommands {
@Command2.Subcommand
fun playerFail(sender: Command2MCSender, player: TBMCPlayer) {
}
@Command2.Subcommand
fun errorTest(sender: Command2MCSender) {
error("Hmm")
}
}
@CommandClass

View file

@ -38,8 +38,8 @@ class Command2MCTest {
fun testRegisterCommand() {
TestCommand.register()
val nodes = ButtonPlugin.command2MC.commandNodes
assert(nodes.size == 1)
assert(nodes.first().literal == "test")
assertEquals(4, nodes.size)
assertEquals("test", nodes.first().literal)
val coreExecutable = nodes.first().coreExecutable<Command2MCSender, TestCommand>()
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")
@ -108,11 +108,19 @@ class Command2MCTest {
fun runCommandWithReceive(command: String): String {
return withMessageReceive { ButtonPlugin.command2MC.handleCommand(this, command) }
}
fun runFailingCommand(command: String) {
assert(!ButtonPlugin.command2MC.handleCommand(this, command)) { "Could execute command $command that shouldn't work" }
}
fun runCrashingCommand(command: String, errorCheck: (Throwable) -> Boolean) {
assert(errorCheck(assertFails { ButtonPlugin.command2MC.handleCommand(this, command) })) { "Command exception failed test!" }
}
}
sender.runCommand("/test hmm", TestCommand, "hmm")
sender.runCommand("/noargtest", NoArgTestCommand, "TestPlayer")
assertFails { ButtonPlugin.command2MC.handleCommand(sender, "/noargtest failing") }
runFailingCommand(sender, "/erroringtest")
sender.runCrashingCommand("/noargtest failing") { it.cause?.cause is IllegalStateException }
sender.runFailingCommand("/erroringtest")
sender.runCommand("/multiargtest test hmm mhm", MultiArgTestCommand, "hmmmhm")
sender.runCommand("/multiargtest test2 true 19", MultiArgTestCommand, "true 19")
@ -123,11 +131,14 @@ class Command2MCTest {
sender.runCommand("/multiargtest testoptionalmulti", MultiArgTestCommand, "false null")
sender.runCommand("/test plugin Chroma-Core", TestCommand, "Chroma-Core")
assertFails { ButtonPlugin.command2MC.handleCommand(sender, "/test playerfail TestPlayer") }
sender.runCrashingCommand("/test playerfail TestPlayer") { it.cause?.message == "No suitable converter found for class buttondevteam.lib.player.TBMCPlayer param1" }
assertEquals("§cError: §cNo Chroma plugin found by that name.", sender.runCommandWithReceive("/test plugin asd"))
sender.runCrashingCommand("/test errortest") { it.cause?.cause?.message === "Hmm" }
assertEquals("Test command\n" +
"Used for testing\n" +
"§6---- Subcommands ----\n" +
"/test errortest\n" +
"/test playerfail\n" +
"/test plugin", sender.runCommandWithReceive("/test")
)
@ -143,16 +154,28 @@ class Command2MCTest {
sender.runCommand("/testparams 12 34 56 78", TestParamsCommand, "12 34 56.0 78.0 Player0")
assertEquals("§cExpected integer at position 11: ...estparams <--[HERE]", sender.runCommandWithReceive("/testparams asd 34 56 78"))
// TODO: Change test when usage help is added
assertEquals(
"/test\n" +
"/noargtest\n" +
"/testparams\n" +
"/test plugin\n" +
"/test playerfail\n" +
"/test errortest\n" +
"/noargtest failing\n" +
"/multiargtest test\n" +
"/multiargtest test2\n" +
"/multiargtest testoptional\n" +
"/multiargtest testoptionalmulti\n" +
"/some test cmd\n" +
"/some another cmd", ButtonPlugin.command2MC.getCommandList(sender).joinToString("\n")
)
}
private fun ICommand2MC.register() {
MainPlugin.instance.registerCommand(this)
}
private fun runFailingCommand(sender: Command2MCSender, command: String) {
assert(!ButtonPlugin.command2MC.handleCommand(sender, command)) { "Could execute command $command that shouldn't work" }
}
companion object {
private var initialized = false
}