Basic command execution implemented and fixed!
- Added check for errors that are sent for the sender and other test checks - Fixed getting argument nodes - Changed setting subcommand data for arguments so that the order of the registration allows finalising a node before adding it to another (that's why I needed to swap the order) - Implemented basic command execution (invoking the method)
This commit is contained in:
parent
84062fee7c
commit
19362cfe5f
8 changed files with 151 additions and 78 deletions
|
@ -4,8 +4,11 @@ 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
|
||||
import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable
|
||||
import com.google.common.base.Defaults
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.mojang.brigadier.CommandDispatcher
|
||||
import com.mojang.brigadier.arguments.*
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder
|
||||
|
@ -14,6 +17,8 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException
|
|||
import com.mojang.brigadier.tree.CommandNode
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.ChatColor
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.util.function.Function
|
||||
import java.util.function.Predicate
|
||||
|
@ -192,13 +197,25 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
{ helpText }, // TODO: Help text getter support
|
||||
{ sender: TP, data: SubcommandData<TC, TP> -> hasPermission(sender, data) },
|
||||
method.annotations.filterNot { it is Subcommand }.toTypedArray(),
|
||||
fullPath
|
||||
)
|
||||
.executes { context: CommandContext<TP> -> executeCommand(context) }
|
||||
var parent: ArgumentBuilder<TP, *> = node
|
||||
for (param in params) { // Register parameters in the right order
|
||||
fullPath,
|
||||
method
|
||||
).executes(this::executeHelpText)
|
||||
|
||||
fun getArgNodes(parent: ArgumentBuilder<TP, *>, params: MutableList<CommandArgument>) {
|
||||
// TODO: Implement optional arguments here by making the last non-optional parameter also executable
|
||||
val param = params.removeLast()
|
||||
val argType = getArgumentType(param)
|
||||
parent.then(CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional).also { parent = it })
|
||||
val arg = CoreArgumentBuilder.argument<TP, _>(param.name, argType, param.optional)
|
||||
if (params.isEmpty()) {
|
||||
arg.executes { context: CommandContext<TP> -> executeCommand(context) }
|
||||
} else {
|
||||
arg.executes(::executeHelpText)
|
||||
getArgNodes(arg, params)
|
||||
}
|
||||
parent.then(arg)
|
||||
}
|
||||
if (params.isNotEmpty()) {
|
||||
getArgNodes(node, params.toMutableList())
|
||||
}
|
||||
return node.build().coreExecutable() ?: throw IllegalStateException("Command node should be executable but isn't: $fullPath")
|
||||
}
|
||||
|
@ -311,65 +328,85 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
|
|||
* @param context The command context
|
||||
* @return Vanilla command success level (0)
|
||||
*/
|
||||
private fun executeCommand(context: CommandContext<TP>): Int {
|
||||
println("Execute command")
|
||||
println("Should be running sync: $runOnPrimaryThread")
|
||||
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()!!
|
||||
val sender = context.source
|
||||
|
||||
/*if (!hasPermission(sender, sd.command, sd.method)) {
|
||||
sender.sendMessage("${ChatColor.RED}You don't have permission to use this command");
|
||||
return;
|
||||
@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
|
||||
}
|
||||
// TODO: WIP
|
||||
|
||||
val type = sendertype.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch }
|
||||
val convertedSender = convertSenderType(sender, sd.senderType)
|
||||
if (convertedSender == null) {
|
||||
//TODO: Should have a prettier display of Command2 classes here
|
||||
val type = sd.senderType.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch }
|
||||
sender.sendMessage("${ChatColor.RED}You need to be a $type to use this command.")
|
||||
sender.sendMessage(sd.getHelpText(sender)) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only
|
||||
|
||||
if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type
|
||||
val args = parsed.getContext().getArguments();
|
||||
for (var arg : sd.arguments.entrySet()) {*/
|
||||
// TODO: Invoke using custom method
|
||||
/*if (pj == commandline.length() + 1) { //No param given
|
||||
if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) {
|
||||
if (cl.isPrimitive())
|
||||
params.add(Defaults.defaultValue(cl));
|
||||
else if (Number.class.isAssignableFrom(cl)
|
||||
|| Number.class.isAssignableFrom(cl))
|
||||
params.add(Defaults.defaultValue(Primitives.unwrap(cl)));
|
||||
else
|
||||
params.add(null);
|
||||
continue; //Fill the remaining params with nulls
|
||||
} else {
|
||||
sender.sendMessage(sd.helpText); //Required param missing
|
||||
return;
|
||||
return 0
|
||||
}
|
||||
}*/
|
||||
/*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?)
|
||||
params.add(commandline.substring(j + 1).split(" +"));
|
||||
continue;
|
||||
}*/
|
||||
|
||||
val params = executeGetArguments(sd, context) ?: return executeHelpText(context)
|
||||
|
||||
// TODO: Invoke using custom method
|
||||
// TODO: Varargs support? (colors?)
|
||||
// TODO: Character handling (strlen)
|
||||
// TODO: Param converter
|
||||
/*}
|
||||
Runnable invokeCommand = () -> {
|
||||
try {
|
||||
sd.method.setAccessible(true); //It may be part of a private class
|
||||
val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time)
|
||||
if (ret instanceof Boolean) {
|
||||
if (!(boolean) ret) //Show usage
|
||||
sender.sendMessage(sd.helpText);
|
||||
} else if (ret != null)
|
||||
throw new Exception("Wrong return type! Must return a boolean or void. Return value: " + ret);
|
||||
} catch (InvocationTargetException e) {
|
||||
TBMCCoreAPI.SendException("An error occurred in a command handler for " + subcommand + "!", e.getCause(), MainPlugin.Instance);
|
||||
} catch (Exception e) {
|
||||
TBMCCoreAPI.SendException("Command handling failed for sender " + sender + " and subcommand " + subcommand, e, MainPlugin.Instance);
|
||||
|
||||
executeInvokeCommand(sd, sender, convertedSender, params)
|
||||
return 0
|
||||
}
|
||||
};
|
||||
if (sync)
|
||||
Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand);
|
||||
|
||||
private fun executeGetArguments(sd: SubcommandData<TC, TP>, context: CommandContext<TP>): MutableList<Any?>? {
|
||||
val params = mutableListOf<Any?>()
|
||||
for (argument in sd.argumentsInOrder) {
|
||||
try {
|
||||
val userArgument = context.getArgument(argument.name, argument.type)
|
||||
params.add(userArgument)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// TODO: This probably only works with primitive types (argument.type)
|
||||
if (argument.optional) {
|
||||
if (argument.type.isPrimitive) {
|
||||
params.add(Defaults.defaultValue(argument.type))
|
||||
} else if (Number::class.java.isAssignableFrom(argument.type)) {
|
||||
params.add(Defaults.defaultValue(Primitives.unwrap(argument.type)))
|
||||
} else {
|
||||
params.add(null)
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the command method with the given sender and parameters.
|
||||
*/
|
||||
private fun executeInvokeCommand(sd: SubcommandData<TC, TP>, sender: TP, actualSender: Any, params: List<Any?>) {
|
||||
val invokeCommand = {
|
||||
try {
|
||||
val ret = sd.executeCommand(actualSender, *params.toTypedArray())
|
||||
if (ret is Boolean) {
|
||||
if (!ret) //Show usage
|
||||
sd.sendHelpText(sender)
|
||||
} 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)
|
||||
} catch (e: Exception) {
|
||||
TBMCCoreAPI.SendException("Command handling failed for sender $sender and subcommand ${sd.fullPath}", e, MainPlugin.instance)
|
||||
}
|
||||
}
|
||||
if (runOnPrimaryThread && !ChromaUtils.isTest)
|
||||
Bukkit.getScheduler().runTask(MainPlugin.instance, invokeCommand)
|
||||
else
|
||||
invokeCommand.run();*/return 0
|
||||
invokeCommand()
|
||||
}
|
||||
|
||||
abstract fun hasPermission(sender: TP, data: SubcommandData<TC, TP>): Boolean
|
||||
|
|
|
@ -16,7 +16,6 @@ import com.mojang.brigadier.arguments.StringArgumentType
|
|||
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
|
||||
import com.mojang.brigadier.tree.CommandNode
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode
|
||||
import me.lucko.commodore.Commodore
|
||||
import me.lucko.commodore.CommodoreProvider
|
||||
|
@ -40,8 +39,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
|
|||
override fun registerCommand(command: ICommand2MC) {
|
||||
val commandNode = super.registerCommandSuper(command)
|
||||
val bcmd = registerOfficially(command, commandNode)
|
||||
if (bcmd != null) // TODO: Support aliases
|
||||
super.registerCommandSuper(command)
|
||||
// TODO: Support aliases
|
||||
val permPrefix = "chroma.command."
|
||||
//Allow commands by default, it will check mod-only
|
||||
val nodes = commandNode.coreExecutable<Command2MCSender, ICommand2MC>()
|
||||
|
@ -60,7 +58,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
|
|||
}
|
||||
|
||||
override fun hasPermission(sender: Command2MCSender, data: SubcommandData<ICommand2MC, Command2MCSender>): Boolean {
|
||||
val defWorld = Bukkit.getWorlds().first().name
|
||||
val defWorld = if (ChromaUtils.isTest) "TestWorld" else Bukkit.getWorlds().first().name
|
||||
val check = if (sender.permCheck !is TBMCPlayerBase) ({
|
||||
MainPlugin.permission.groupHas(
|
||||
defWorld,
|
||||
|
|
|
@ -3,7 +3,7 @@ package buttondevteam.lib.chat
|
|||
import buttondevteam.core.component.channel.Channel
|
||||
import buttondevteam.lib.player.ChromaGamerBase
|
||||
|
||||
class Command2MCSender(val sender: ChromaGamerBase, val channel: Channel, val permCheck: ChromaGamerBase) : Command2Sender {
|
||||
open class Command2MCSender(val sender: ChromaGamerBase, val channel: Channel, val permCheck: ChromaGamerBase) : Command2Sender {
|
||||
// TODO: Remove this class and only use the user classes.
|
||||
// TODO: The command context should be stored separately.
|
||||
override fun sendMessage(message: String) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package buttondevteam.lib.chat
|
||||
|
||||
import buttondevteam.lib.chat.commands.SubcommandData
|
||||
import com.mojang.brigadier.arguments.ArgumentType
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider
|
||||
|
@ -11,7 +10,6 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
|||
private val optional: Boolean
|
||||
) : ArgumentBuilder<S, CoreArgumentBuilder<S, T>>() {
|
||||
private var suggestionsProvider: SuggestionProvider<S>? = null
|
||||
internal lateinit var data: SubcommandData<*, S>
|
||||
fun suggests(provider: SuggestionProvider<S>): CoreArgumentBuilder<S, T> {
|
||||
suggestionsProvider = provider
|
||||
return this
|
||||
|
@ -31,15 +29,11 @@ class CoreArgumentBuilder<S : Command2Sender, T>(
|
|||
redirectModifier,
|
||||
isFork,
|
||||
suggestionsProvider,
|
||||
optional,
|
||||
data
|
||||
optional
|
||||
)
|
||||
}
|
||||
|
||||
override fun then(argument: ArgumentBuilder<S, *>?): CoreArgumentBuilder<S, T> {
|
||||
if (argument is CoreArgumentBuilder<*, *>) {
|
||||
(argument as CoreArgumentBuilder<S, *>).data = data
|
||||
}
|
||||
return super.then(argument)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,12 @@ import java.util.function.Predicate
|
|||
|
||||
class CoreArgumentCommandNode<S : Command2Sender, T>(
|
||||
name: String?, type: ArgumentType<T>?, command: Command<S>?, requirement: Predicate<S>?, redirect: CommandNode<S>?, modifier: RedirectModifier<S>?, forks: Boolean, customSuggestions: SuggestionProvider<S>?,
|
||||
val optional: Boolean, val commandData: SubcommandData<*, S>
|
||||
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
|
||||
|
||||
override fun getUsageText(): String {
|
||||
return if (optional) "[$name]" else "<$name>"
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package buttondevteam.lib.chat
|
||||
|
||||
import buttondevteam.lib.chat.commands.CommandArgument
|
||||
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
|
||||
import buttondevteam.lib.chat.commands.NoOpSubcommandData
|
||||
import buttondevteam.lib.chat.commands.SubcommandData
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcommandData> private constructor(
|
||||
literal: String,
|
||||
|
@ -32,11 +34,12 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcom
|
|||
}
|
||||
|
||||
override fun then(argument: ArgumentBuilder<S, *>): LiteralArgumentBuilder<S> {
|
||||
if (argument is CoreArgumentBuilder<*, *> && data is SubcommandData<*, *>) {
|
||||
super.then(argument)
|
||||
if (data is SubcommandData<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(argument as CoreArgumentBuilder<S, *>).data = data as SubcommandData<*, S>
|
||||
arguments.forEach { it.coreArgument()?.commandData = data as SubcommandData<*, S> }
|
||||
}
|
||||
return super.then(argument)
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -62,11 +65,12 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcom
|
|||
helpTextGetter: (Any) -> Array<String>,
|
||||
hasPermission: (S, SubcommandData<TC, S>) -> Boolean,
|
||||
annotations: Array<Annotation>,
|
||||
fullPath: String
|
||||
fullPath: String,
|
||||
method: Method
|
||||
): CoreCommandBuilder<S, TC, SubcommandData<TC, S>> {
|
||||
return CoreCommandBuilder(
|
||||
name,
|
||||
SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath)
|
||||
SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission, annotations, fullPath, method)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package buttondevteam.lib.chat.commands
|
|||
|
||||
import buttondevteam.lib.chat.Command2Sender
|
||||
import buttondevteam.lib.chat.ICommand2
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command.
|
||||
|
@ -42,14 +43,21 @@ class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
|
|||
* A function that determines whether the user has permission to run this subcommand.
|
||||
*/
|
||||
private val permissionCheck: (TP, SubcommandData<TC, TP>) -> Boolean,
|
||||
|
||||
/**
|
||||
* All annotations implemented by the method that executes the command. Can be used to add custom metadata when implementing a platform.
|
||||
*/
|
||||
val annotations: Array<Annotation>,
|
||||
|
||||
/**
|
||||
* The space-separated full command path of this subcommand.
|
||||
*/
|
||||
val fullPath: String
|
||||
val fullPath: String,
|
||||
|
||||
/**
|
||||
* The method to run when executing the command.
|
||||
*/
|
||||
private val method: Method
|
||||
) : NoOpSubcommandData(helpTextGetter) {
|
||||
|
||||
/**
|
||||
|
@ -61,4 +69,22 @@ class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
|
|||
fun hasPermission(sender: TP): Boolean {
|
||||
return permissionCheck(sender, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command and return the result. Doesn't perform any checks.
|
||||
*
|
||||
* @param sender The actual sender as expected by the method
|
||||
* @param args The rest of the method args
|
||||
*/
|
||||
fun executeCommand(sender: Any, vararg args: Any?): Any? {
|
||||
method.isAccessible = true
|
||||
return method.invoke(command, sender, *args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the help text to the specified sender.
|
||||
*/
|
||||
fun sendHelpText(sender: TP) {
|
||||
sender.sendMessage(getHelpText(sender))
|
||||
}
|
||||
}
|
|
@ -66,18 +66,29 @@ class Command2MCTest {
|
|||
@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"))
|
||||
val sender = object : Command2MCSender(user, Channel.globalChat, user) {
|
||||
override fun sendMessage(message: String) {
|
||||
error(message)
|
||||
}
|
||||
|
||||
override fun sendMessage(message: Array<String>) {
|
||||
error(message.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
assert(ButtonPlugin.command2MC.handleCommand(sender, "/test hmm"))
|
||||
assertEquals("hmm", testCommandReceived)
|
||||
}
|
||||
|
||||
@CommandClass
|
||||
object TestCommand : ICommand2MC() {
|
||||
@Command2.Subcommand
|
||||
fun def(sender: Command2MCSender, test: String) {
|
||||
println(test)
|
||||
testCommandReceived = test
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var initialized = false
|
||||
private var testCommandReceived: String? = null
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue