It compiles! Finished MC tab completion

- Custom tab complete methods are now case-sensitive
- Custom tab complete methods are also not supported for now
This commit is contained in:
Norbi Peti 2023-04-21 04:05:04 +02:00
parent ee7e531dc0
commit 5e1f378ec7
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
9 changed files with 137 additions and 214 deletions

View file

@ -62,7 +62,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>

View file

@ -17,6 +17,7 @@ abstract class TBMCChatEventBase(
*/
val groupID: String,
) : Event(true), Cancellable {
@JvmField
var isCancelled: Boolean = false
/**

View file

@ -139,18 +139,9 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
val ann = meth.getAnnotation(Subcommand::class.java) ?: continue
val fullPath = command.commandPath + CommandUtils.getCommandPath(meth.name, ' ')
val (lastNode, mainNode, remainingPath) = registerNodeFromPath(fullPath)
lastNode.addChild(
getExecutableNode(
meth,
command,
ann,
remainingPath,
CommandArgumentHelpManager(command),
fullPath
)
)
lastNode.addChild(getExecutableNode(meth, command, ann, remainingPath, CommandArgumentHelpManager(command), fullPath))
if (mainCommandNode == null) mainCommandNode = mainNode
else if (mainNode!!.name != mainCommandNode.name) {
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)
}
}
@ -202,7 +193,7 @@ abstract class Command2<TC : ICommand2<TP>, 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<CommandNode<TP>, CoreCommandNode<TP, *>?, String> {
private fun registerNodeFromPath(path: String): Triple<CommandNode<TP>, CoreCommandNode<TP, *>, String> {
val split = path.split(" ")
var parent: CommandNode<TP> = dispatcher.root
var mainCommand: CoreCommandNode<TP, *>? = null
@ -214,7 +205,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
if (i == 0) mainCommand =
parent as CoreCommandNode<TP, *> // 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<String> {
@ -425,17 +416,15 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
fun getSubcommands(
mainCommand: LiteralCommandNode<TP>,
deep: Boolean = true
): List<CoreCommandNode<TP, SubcommandData<TC, TP>>> {
return getSubcommands(mainCommand, deep, mainCommand.core())
): List<CoreExecutableNode<TP, TC>> {
return getSubcommands(deep, mainCommand.core())
}
private fun getSubcommands(
mainCommand: LiteralCommandNode<TP>,
deep: Boolean = true,
root: CoreCommandNode<TP, NoOpSubcommandData>
): List<CoreCommandNode<TP, SubcommandData<TC, TP>>> {
root: CoreNoOpNode<TP>
): List<CoreExecutableNode<TP, TC>> {
return root.children.mapNotNull { it.coreExecutable<TP, TC>() } +
if (deep) root.children.flatMap { getSubcommands(mainCommand, deep, it.core()) } else emptyList()
if (deep) root.children.flatMap { getSubcommands(deep, it.core()) } else emptyList()
}
}

View file

@ -5,19 +5,15 @@ import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ButtonPlugin
import buttondevteam.lib.architecture.Component
import buttondevteam.lib.chat.commands.CommandUtils
import buttondevteam.lib.chat.commands.CommandUtils.coreArgument
import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable
import buttondevteam.lib.chat.commands.MCCommandSettings
import buttondevteam.lib.chat.commands.SubcommandData
import buttondevteam.lib.player.ChromaGamerBase
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.Suggestion
import com.mojang.brigadier.suggestion.SuggestionProvider
import com.mojang.brigadier.suggestion.Suggestions
import com.mojang.brigadier.suggestion.SuggestionsBuilder
import com.mojang.brigadier.tree.ArgumentCommandNode
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode
import me.lucko.commodore.Commodore
@ -31,9 +27,7 @@ import org.bukkit.entity.Player
import org.bukkit.event.Listener
import org.bukkit.permissions.Permission
import org.bukkit.permissions.PermissionDefault
import java.lang.reflect.Parameter
import java.util.*
import java.util.function.BiConsumer
import java.util.function.Function
import java.util.function.Supplier
@ -179,7 +173,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
bukkitCommand = oldcmd
if (bukkitCommand is PluginCommand) bukkitCommand.setExecutor(this::executeCommand)
}
if (CommodoreProvider.isSupported()) TabcompleteHelper.registerTabcomplete(command, node, bukkitCommand)
TabcompleteHelper.registerTabcomplete(command, node, bukkitCommand)
bukkitCommand
} catch (e: Exception) {
if (command.component == null)
@ -220,12 +214,7 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
}
@Throws(IllegalArgumentException::class)
override fun tabComplete(
sender: CommandSender,
alias: String,
args: Array<out String>?,
location: Location?
): MutableList<String> {
override fun tabComplete(sender: CommandSender, alias: String, args: Array<out String>?, location: Location?): MutableList<String> {
return mutableListOf()
}
}
@ -234,151 +223,75 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
private val commodore: Commodore by lazy {
val commodore = CommodoreProvider.getCommodore(MainPlugin.instance) //Register all to the Core, it's easier
commodore.register(
LiteralArgumentBuilder.literal<Any?>("un") // TODO: This is a test
.redirect(
RequiredArgumentBuilder.argument<Any?, String>(
"unsomething",
StringArgumentType.word()
).suggests { _, builder ->
builder.suggest("untest").buildFuture()
}.build()
literal<Any?>("un") // TODO: This is a test
.redirect(argument<Any?, String>("unsomething", StringArgumentType.word())
.suggests { _, builder -> builder.suggest("untest").buildFuture() }.build()
)
)
commodore
}
fun registerTabcomplete(
command2MC: ICommand2MC,
commandNode: LiteralCommandNode<Command2MCSender>,
bukkitCommand: Command
) {
commodore.dispatcher.root.getChild(commandNode.name) // TODO: Probably unnecessary
val customTCmethods =
Arrays.stream(command2MC.javaClass.declaredMethods) //val doesn't recognize the type arguments
.flatMap { method ->
Optional.ofNullable(method.getAnnotation(CustomTabCompleteMethod::class.java)).stream()
.flatMap { ctcmAnn ->
val paths = Optional.of(ctcmAnn.subcommand).filter { s -> s.isNotEmpty() }
.orElseGet {
arrayOf(
CommandUtils.getCommandPath(method.name, ' ').trim { it <= ' ' })
}
Arrays.stream(paths).map { name: String? -> Triple(name, ctcmAnn, method) }
}
}.toList()
for (subcmd in subcmds) {
val subpathAsOne = CommandUtils.getCommandPath(subcmd.method.getName(), ' ').trim { it <= ' ' }
val subpath = subpathAsOne.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var scmd: CommandNode<Any> = cmd
if (subpath[0].isNotEmpty()) { //If the method is def, it will contain one empty string
for (s in subpath) {
scmd =
appendSubcommand(s, scmd, subcmd) //Add method name part of the path (could_be_multiple())
fun registerTabcomplete(command2MC: ICommand2MC, commandNode: CoreCommandNode<Command2MCSender, *>, bukkitCommand: Command) {
if (!CommodoreProvider.isSupported()) {
throw UnsupportedOperationException("Commodore is not supported! Please use 1.14 or higher. Server version: ${Bukkit.getVersion()}")
}
// TODO: Allow extending annotation processing for methods and parameters
val customTabCompleteMethods = command2MC.javaClass.declaredMethods
.flatMap { method ->
method.getAnnotation(CustomTabCompleteMethod::class.java)?.let { ctcmAnn ->
(ctcmAnn.subcommand.takeIf { it.isNotEmpty() }
?: arrayOf(CommandUtils.getCommandPath(method.name, ' ').trim { it <= ' ' }))
.map { name -> Triple(name, ctcmAnn, method) }
} ?: emptyList()
}
val mcNode = CommandUtils.mapSubcommands(commandNode) { node ->
val builder = node.createBuilder()
val argNode = node.coreArgument() ?: return@mapSubcommands builder
val subpath = "" // TODO: This needs the same processing as the command path to have the same flexibility
val argData = argNode.commandData.arguments[argNode.name] ?: return@mapSubcommands builder
val customTCTexts = argData.annotations.filterIsInstance<CustomTabComplete>().flatMap { it.value.asList() }
val customTCmethod = customTabCompleteMethods.firstOrNull { (name, ann, _) ->
name == subpath && argData.name.replace("[\\[\\]<>]".toRegex(), "") == ann.param
}
(builder as RequiredArgumentBuilder<Command2MCSender, *>).suggests { context, b ->
val sbuilder = if (argData.greedy) { //Do it before the builder is used
val nextTokenStart = context.input.lastIndexOf(' ') + 1
b.createOffset(nextTokenStart)
} else b
// Suggest custom tab complete texts
for (ctc in customTCTexts) {
sbuilder.suggest(ctc)
}
val ignoreCustomParamType = false // TODO: This should be set by the @CustomTabCompleteMethod annotation
// TODO: Custom tab complete method handling
if (!ignoreCustomParamType) {
val converter = getParamConverter(argData.type, command2MC)
if (converter != null) {
val suggestions = converter.allSupplier.get()
for (suggestion in suggestions) sbuilder.suggest(suggestion)
}
}
if (argData.type === Boolean::class.javaPrimitiveType || argData.type === Boolean::class.java)
sbuilder.suggest("true").suggest("false")
val loweredInput = sbuilder.remaining.lowercase(Locale.getDefault())
// The list is automatically ordered, so we need to put the <param> at the end after that
// We're also removing all suggestions that don't start with the input
sbuilder.suggest(argData.name).buildFuture().whenComplete { ss, _ ->
ss.list.add(ss.list.removeAt(0))
}.whenComplete { ss, _ ->
ss.list.removeIf { s ->
s.text.lowercase().let { !it.startsWith("<") && !it.startsWith("[") && !it.startsWith(loweredInput) }
}
}
}
val parameters: Array<Parameter> = subcmd.method.getParameters()
for (i in 1 until parameters.size) { //Skip sender
val parameter = parameters[i]
val customParamType: Boolean
// TODO: Arg type
val param: String = subcmd.parameters.get(i - 1)
val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete::class.java))
.map { obj -> obj.value }
val customTCmethod =
customTCmethods.stream().filter { t -> subpathAsOne.equals(t.first, ignoreCase = true) }
.filter { t -> param.replace("[\\[\\]<>]", "").equals(t.second.param, ignoreCase = true) }
.findAny()
val argb: RequiredArgumentBuilder<S, T> = RequiredArgumentBuilder.argument(param, type)
.suggests(SuggestionProvider<S?> { context: CommandContext<S?>, builder: SuggestionsBuilder ->
if (parameter.isVarArgs) { //Do it before the builder is used
val nextTokenStart = context.getInput().lastIndexOf(' ') + 1
builder = builder.createOffset(nextTokenStart)
}
if (customTC.isPresent) for (ctc in customTC.get()) builder.suggest(ctc)
var ignoreCustomParamType = false
if (customTCmethod.isPresent) {
val tr = customTCmethod.get()
if (tr.second.ignoreTypeCompletion) ignoreCustomParamType = true
val method = tr.third
val params = method.parameters
val args = arrayOfNulls<Any>(params.size)
var j = 0
var k = 0
while (j < args.size && k < subcmd.parameters.length) {
val paramObj = params[j]
if (CommandSender::class.java.isAssignableFrom(paramObj.type)) {
args[j] = commodore.getBukkitSender(context.getSource())
j++
continue
}
val paramValueString = context.getArgument(subcmd.parameters.get(k), String::class.java)
if (paramObj.type == String::class.java) {
args[j] = paramValueString
j++
continue
}
//Break if converter is not found or for example, the player provided an invalid plugin name
val converter = getParamConverter(params[j].type, command2MC) ?: break
val paramValue = converter.converter.apply(paramValueString) ?: break
args[j] = paramValue
k++ //Only increment if not CommandSender
j++
}
if (args.isEmpty() || args[args.size - 1] != null) { //Arguments filled entirely
try {
when (val suggestions = method.invoke(command2MC, *args)) {
is Iterable<*> -> {
for (suggestion in suggestions) if (suggestion is String) builder.suggest(
suggestion as String?
) else throw ClassCastException("Bad return type! It should return an Iterable<String> or a String[].")
}
is Array<*> -> for (suggestion in suggestions) builder.suggest(suggestion)
else -> throw ClassCastException("Bad return type! It should return a String[] or an Iterable<String>.")
}
} catch (e: Exception) {
val msg = "Failed to run tabcomplete method " + method.name + " for command " + command2MC.javaClass.simpleName
if (command2MC.component == null) TBMCCoreAPI.SendException(msg, e, command2MC.plugin) else TBMCCoreAPI.SendException(msg, e, command2MC.component)
}
}
}
if (!ignoreCustomParamType && customParamType) {
val converter = getParamConverter(ptype, command2MC)
if (converter != null) {
val suggestions = converter.allSupplier.get()
for (suggestion in suggestions) builder.suggest(suggestion)
}
}
if (ptype === Boolean::class.javaPrimitiveType || ptype === Boolean::class.java) builder.suggest("true").suggest("false")
val loweredInput = builder.remaining.lowercase(Locale.getDefault())
builder.suggest(param).buildFuture().whenComplete(BiConsumer<Suggestions, Throwable> { s: Suggestions, e: Throwable? -> //The list is automatically ordered
s.list.add(s.list.removeAt(0))
}) //So we need to put the <param> at the end after that
.whenComplete(BiConsumer<Suggestions, Throwable> { ss: Suggestions, e: Throwable? ->
ss.list.removeIf { s: Suggestion ->
val text = s.text
!text.startsWith("<") && !text.startsWith("[") && !text.lowercase(Locale.getDefault()).startsWith(loweredInput)
}
})
})
val arg: ArgumentCommandNode<S, T> = argb.build()
scmd.addChild(arg)
scmd = arg
}
builder
}
if (shouldRegister.get()) {
commodore.register(maincmd)
//MinecraftArgumentTypes.getByKey(NamespacedKey.minecraft(""))
val pluginName = command2MC.plugin.name.lowercase(Locale.getDefault())
val prefixedcmd = LiteralArgumentBuilder.literal<Any>(pluginName + ":" + path.get(0))
.redirect(maincmd).build()
commodore.register(prefixedcmd)
for (alias in bukkitCommand.aliases) {
commodore.register(LiteralArgumentBuilder.literal<Any>(alias).redirect(maincmd).build())
commodore.register(
LiteralArgumentBuilder.literal<Any>("$pluginName:$alias").redirect(maincmd).build()
)
}
commodore.register(mcNode as LiteralCommandNode<*>)
commodore.register(literal<Command2MCSender>("${command2MC.plugin.name.lowercase()}:${mcNode.name}").redirect(mcNode))
for (alias in bukkitCommand.aliases) {
commodore.register(literal<Command2MCSender>(alias).redirect(mcNode))
commodore.register(literal<Command2MCSender>("${command2MC.plugin.name.lowercase()}:${alias}").redirect(mcNode))
}
}
}
@ -396,3 +309,5 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
}
}
}
private typealias CNode = CommandNode<Command2MCSender>

View file

@ -1,15 +1,17 @@
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
class CoreArgumentBuilder<S, T>(
class CoreArgumentBuilder<S : Command2Sender, T>(
private val name: String,
private val type: ArgumentType<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
@ -29,12 +31,13 @@ class CoreArgumentBuilder<S, T>(
redirectModifier,
isFork,
suggestionsProvider,
optional
optional,
data
)
}
companion object {
fun <S, T> argument(name: String, type: ArgumentType<T>, optional: Boolean): CoreArgumentBuilder<S, T> {
fun <S : Command2Sender, T> argument(name: String, type: ArgumentType<T>, optional: Boolean): CoreArgumentBuilder<S, T> {
return CoreArgumentBuilder(name, type, optional)
}
}

View file

@ -1,30 +1,25 @@
package buttondevteam.lib.chat;
package buttondevteam.lib.chat
import com.mojang.brigadier.Command;
import com.mojang.brigadier.RedirectModifier;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.CommandNode;
import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.Command
import com.mojang.brigadier.RedirectModifier
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.suggestion.SuggestionProvider
import com.mojang.brigadier.tree.ArgumentCommandNode
import com.mojang.brigadier.tree.CommandNode
import java.util.function.Predicate
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>
) :
ArgumentCommandNode<S, T>(name, type, command, requirement, redirect, modifier, forks, customSuggestions) {
override fun getUsageText(): String {
return if (optional) "[$name]" else "<$name>"
}
public class CoreArgumentCommandNode<S, T> extends ArgumentCommandNode<S, T> {
private final boolean optional;
public CoreArgumentCommandNode(String name, ArgumentType<T> type, Command<S> command, Predicate<S> requirement, CommandNode<S> redirect, RedirectModifier<S> modifier, boolean forks, SuggestionProvider<S> customSuggestions, boolean optional) {
super(name, type, command, requirement, redirect, modifier, forks, customSuggestions);
this.optional = optional;
}
@Override
public String getUsageText() {
return optional ? "[" + getName() + "]" : "<" + getName() + ">";
}
@Override
public RequiredArgumentBuilder<S, T> createBuilder() {
return super.createBuilder();
}
override fun createBuilder(): RequiredArgumentBuilder<S, T> {
return super.createBuilder()
}
}

View file

@ -3,9 +3,10 @@ package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.CommandArgument
import buttondevteam.lib.chat.commands.NoOpSubcommandData
import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder
class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData> private constructor(
class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<S>, TSD : NoOpSubcommandData> private constructor(
literal: String,
val data: TSD
) : LiteralArgumentBuilder<S>(literal) {
@ -30,6 +31,14 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcom
return result
}
override fun then(argument: ArgumentBuilder<S, *>): LiteralArgumentBuilder<S> {
if (argument is CoreArgumentBuilder<*, *> && data is SubcommandData<*, *>) {
@Suppress("UNCHECKED_CAST")
(argument as CoreArgumentBuilder<S, *>).data = data as SubcommandData<*, S>
}
return super.then(argument)
}
companion object {
/**
* Start building an executable command node.
@ -44,7 +53,7 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcom
* @param annotations All annotations implemented by the method that executes the command
* @param fullPath The full command path of this subcommand.
*/
fun <S : Command2Sender, TC : ICommand2<*>> literal(
fun <S : Command2Sender, TC : ICommand2<S>> literal(
name: String,
senderType: Class<*>,
arguments: Map<String, CommandArgument>,
@ -67,7 +76,7 @@ class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcom
* @param name The subcommand name as written by the user
* @param helpTextGetter Custom help text that can depend on the context. The function receives the sender as the command itself receives it.
*/
fun <S : Command2Sender, TC : ICommand2<*>> literalNoOp(
fun <S : Command2Sender, TC : ICommand2<S>> literalNoOp(
name: String,
helpTextGetter: (Any) -> Array<String>,
): CoreCommandBuilder<S, TC, NoOpSubcommandData> {

View file

@ -4,11 +4,11 @@ package buttondevteam.lib.chat.commands
* A command argument's information to be used to construct the command.
*/
class CommandArgument(
val name: String,
val name: String, // TODO: Remove <> from name and add it where appropriate
val type: Class<*>,
val greedy: Boolean,
val limits: Pair<Double, Double>,
val optional: Boolean,
val description: String,
val annotations: Array<Annotation> // TODO: Annotations for parameters as well
val annotations: Array<Annotation>
)

View file

@ -1,9 +1,7 @@
package buttondevteam.lib.chat.commands
import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.CoreCommandNode
import buttondevteam.lib.chat.CoreExecutableNode
import buttondevteam.lib.chat.ICommand2
import buttondevteam.lib.chat.*
import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.tree.CommandNode
import java.util.*
@ -20,6 +18,15 @@ object CommandUtils {
.lowercase(Locale.getDefault())
}
/**
* Performs the given action on the given node and all of its nodes recursively and creates new nodes.
*/
fun <S : Command2Sender> mapSubcommands(node: CommandNode<S>, action: (CommandNode<S>) -> ArgumentBuilder<S, *>): CommandNode<S> {
val newNode = action(node)
node.children.map { mapSubcommands(it, action) }.forEach(newNode::then)
return newNode.build()
}
/**
* Casts the node to whatever you say. Use responsibly.
*/
@ -35,4 +42,8 @@ object CommandUtils {
val ret = core<TP, NoOpSubcommandData>()
return if (ret.data is SubcommandData<*, *>) ret.core() else null
}
fun <TP : Command2Sender> CommandNode<TP>.coreArgument(): CoreArgumentCommandNode<TP, *>? {
return if (this is CoreArgumentCommandNode<*, *>) this as CoreArgumentCommandNode<TP, *> else null
}
}