Converted and reworked command builder, node, data

This commit is contained in:
Norbi Peti 2023-02-22 00:32:33 +01:00
parent 00852dd868
commit 8cf01f1137
6 changed files with 212 additions and 197 deletions

View file

@ -4,11 +4,11 @@ import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.*
import buttondevteam.lib.chat.commands.CommandUtils.core import buttondevteam.lib.chat.commands.CommandUtils.core
import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.* import com.mojang.brigadier.arguments.*
import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.context.CommandContext import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.context.ParsedCommandNode
import com.mojang.brigadier.exceptions.CommandSyntaxException import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode import com.mojang.brigadier.tree.LiteralCommandNode
@ -157,12 +157,14 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
*/ */
private fun getExecutableNode(method: Method, command: TC, ann: Subcommand, path: String, argHelpManager: CommandArgumentHelpManager<TC, TP>): LiteralCommandNode<TP> { private fun getExecutableNode(method: Method, command: TC, ann: Subcommand, path: String, argHelpManager: CommandArgumentHelpManager<TC, TP>): LiteralCommandNode<TP> {
val (params, _) = getCommandParametersAndSender(method, argHelpManager) // Param order is important val (params, _) = getCommandParametersAndSender(method, argHelpManager) // Param order is important
val paramMap = HashMap<String, CommandArgument?>() val paramMap = HashMap<String, CommandArgument>()
for (param in params) { for (param in params) {
paramMap[param.name] = param paramMap[param.name] = param
} }
val node = CoreCommandBuilder.literal<TP, TC>(path, params[0].type, paramMap, params, command) val helpText = command.getHelpText(method, ann)
.helps(command.getHelpText(method, ann)).permits { sender: TP -> hasPermission(sender, command, method) } val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, params, command,
{ helpText }, // TODO: Help text getter support
{ sender: TP -> hasPermission(sender, command, method) })
.executes { context: CommandContext<TP> -> executeCommand(context) } .executes { context: CommandContext<TP> -> executeCommand(context) }
var parent: ArgumentBuilder<TP, *> = node var parent: ArgumentBuilder<TP, *> = node
for (param in params) { // Register parameters in the right order for (param in params) { // Register parameters in the right order
@ -180,16 +182,23 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* the main command node and the last part of the command path (that isn't registered yet) * 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>, LiteralCommandNode<TP>?, String> { private fun registerNodeFromPath(path: String): Triple<CommandNode<TP>, LiteralCommandNode<TP>?, String> {
val split = path.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val split = path.split(" ")
var parent: CommandNode<TP> = dispatcher.root var parent: CommandNode<TP> = dispatcher.root
var mainCommand: LiteralCommandNode<TP>? = null var mainCommand: LiteralCommandNode<TP>? = null
for (i in 0 until split.size - 1) { split.forEachIndexed { i, part ->
val part = split[i]
val child = parent.getChild(part) val child = parent.getChild(part)
if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp<TP, TC>(part).executes { context: CommandContext<TP> -> executeHelpText(context) }.build().also { parent = it }) else parent = child if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp<TP, TC>(part, getSubcommandList())
.executes(::executeHelpText).build().also { parent = it })
else parent = child
if (i == 0) mainCommand = parent as LiteralCommandNode<TP> // Has to be a literal, if not, well, error if (i == 0) mainCommand = parent as LiteralCommandNode<TP> // Has to be a literal, if not, well, error
} }
return Triple(parent, mainCommand, split[split.size - 1]) return Triple(parent, mainCommand, split.last())
}
private fun getSubcommandList(): (Any) -> Array<String> {
return {
arrayOf("TODO") // TODO: Subcommand list
}
} }
/** /**
@ -200,17 +209,25 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* @return Parameter data objects and the sender type * @return Parameter data objects and the sender type
* @throws RuntimeException If there is no sender parameter declared in the method * @throws RuntimeException If there is no sender parameter declared in the method
*/ */
private fun getCommandParametersAndSender(method: Method, argHelpManager: CommandArgumentHelpManager<TC, TP>): Pair<List<CommandArgument>, Class<*>> { private fun getCommandParametersAndSender(
method: Method,
argHelpManager: CommandArgumentHelpManager<TC, TP>
): Pair<List<CommandArgument>, Class<*>> {
val parameters = method.parameters val parameters = method.parameters
if (parameters.isEmpty()) throw RuntimeException("No sender parameter for method '$method'") if (parameters.isEmpty()) throw RuntimeException("No sender parameter for method '$method'")
val usage = argHelpManager.getParameterHelpForMethod(method) val usage = argHelpManager.getParameterHelpForMethod(method)
val paramNames = usage?.split(" ") val paramNames = usage?.split(" ")
return Pair(parameters.zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" }) return Pair(
.map { (param, name) -> parameters.zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" })
val numAnn = param.getAnnotation(NumberArg::class.java) .map { (param, name) ->
CommandArgument(name, param.type, val numAnn = param.getAnnotation(NumberArg::class.java)
param.isVarArgs || param.isAnnotationPresent(TextArg::class.java), CommandArgument(
if (numAnn == null) Pair(Double.MIN_VALUE, Double.MAX_VALUE) else Pair(numAnn.lowerLimit, numAnn.upperLimit), name, param.type,
param.isVarArgs || param.isAnnotationPresent(TextArg::class.java),
if (numAnn == null) Pair(Double.MIN_VALUE, Double.MAX_VALUE) else Pair(
numAnn.lowerLimit,
numAnn.upperLimit
),
param.isAnnotationPresent(OptionalArg::class.java), param.isAnnotationPresent(OptionalArg::class.java),
name) name)
}, parameters[0].type) }, parameters[0].type)
@ -253,7 +270,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
private fun executeHelpText(context: CommandContext<TP>): Int { private fun executeHelpText(context: CommandContext<TP>): Int {
println(""" println("""
Nodes: Nodes:
${context.nodes.stream().map { node: ParsedCommandNode<TP> -> node.node.name + "@" + node.range }.collect(Collectors.joining("\n"))} ${context.nodes.stream().map { node -> node.node.name + "@" + node.range }.collect(Collectors.joining("\n"))}
""".trimIndent()) """.trimIndent())
return 0 return 0
} }
@ -333,8 +350,10 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* *
* @return A set of command node objects containing the commands * @return A set of command node objects containing the commands
*/ */
val commandNodes: Set<CoreCommandNode<TP, TC>> val commandNodes: Set<CoreCommandNode<TP, TC, NoOpSubcommandData>>
get() = dispatcher.root.children.stream().map { node: CommandNode<TP> -> node.core<TP, TC>() }.collect(Collectors.toUnmodifiableSet()) get() = dispatcher.root.children.stream()
.map { node: CommandNode<TP> -> node.core<TP, TC, NoOpSubcommandData>() }
.collect(Collectors.toUnmodifiableSet())
/** /**
* Get a node that belongs to the given command. * Get a node that belongs to the given command.
@ -342,7 +361,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* @param command The exact name of the command * @param command The exact name of the command
* @return A command node * @return A command node
*/ */
fun getCommandNode(command: String?): CoreCommandNode<TP, TC> { fun getCommandNode(command: String): CoreCommandNode<TP, TC, NoOpSubcommandData> { // TODO: What should this return? No-op? Executable? What's the use case?
return dispatcher.root.getChild(command).core() return dispatcher.root.getChild(command).core()
} }
@ -352,7 +371,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* @param command The command class (object) to unregister * @param command The command class (object) to unregister
*/ */
fun unregisterCommand(command: ICommand2<TP>) { fun unregisterCommand(command: ICommand2<TP>) {
dispatcher.root.children.removeIf { node: CommandNode<TP> -> node.core<TP, TC>().data.command === command } dispatcher.root.children.removeIf { node: CommandNode<TP> -> node.coreExecutable<TP, TC>()?.data?.command === command }
} }
/** /**
@ -360,14 +379,18 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* *
* @param condition The condition for removing a given command * @param condition The condition for removing a given command
*/ */
fun unregisterCommandIf(condition: Predicate<CoreCommandNode<TP, TC>>, nested: Boolean) { fun unregisterCommandIf(condition: Predicate<CoreCommandNode<TP, TC, SubcommandData<TC, TP>>>, nested: Boolean) {
dispatcher.root.children.removeIf { node: CommandNode<TP> -> condition.test(node.core()) } dispatcher.root.children.removeIf { node -> node.coreExecutable<TP, TC>()?.let { condition.test(it) } ?: false }
if (nested) for (child in dispatcher.root.children) unregisterCommandIf(condition, child.core()) if (nested) for (child in dispatcher.root.children) unregisterCommandIf(condition, child.core())
} }
private fun unregisterCommandIf(condition: Predicate<CoreCommandNode<TP, TC>>, root: CoreCommandNode<TP, TC>) { private fun unregisterCommandIf(
condition: Predicate<CoreCommandNode<TP, TC, SubcommandData<TC, TP>>>,
root: CoreCommandNode<TP, TC, NoOpSubcommandData>
) {
// TODO: Remvoe no-op nodes without children
// Can't use getCoreChildren() here because the collection needs to be modifiable // Can't use getCoreChildren() here because the collection needs to be modifiable
root.children.removeIf { node: CommandNode<TP> -> condition.test(node.core()) } root.children.removeIf { node -> node.coreExecutable<TP, TC>()?.let { condition.test(it) } ?: false }
for (child in root.coreChildren) unregisterCommandIf(condition, child) for (child in root.children) unregisterCommandIf(condition, child.core())
} }
} }

View file

@ -1,74 +1,72 @@
package buttondevteam.lib.chat; package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.CommandArgument; import buttondevteam.lib.chat.commands.CommandArgument
import buttondevteam.lib.chat.commands.SubcommandData; import buttondevteam.lib.chat.commands.NoOpSubcommandData
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import buttondevteam.lib.chat.commands.SubcommandData
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.builder.LiteralArgumentBuilder
import java.util.Map; class CoreCommandBuilder<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData> private constructor(
import java.util.function.Function; literal: String,
val data: TSD
) : LiteralArgumentBuilder<S>(literal) {
public class CoreCommandBuilder<S extends Command2Sender, TC extends ICommand2<?>> extends LiteralArgumentBuilder<S> { override fun getThis(): CoreCommandBuilder<S, TC, TSD> {
private final SubcommandData.SubcommandDataBuilder<TC, S> dataBuilder; return this
}
protected CoreCommandBuilder(String literal, Class<?> senderType, Map<String, CommandArgument> arguments, CommandArgument[] argumentsInOrder, TC command) { override fun build(): CoreCommandNode<S, TC, TSD> {
super(literal); val result = CoreCommandNode<_, TC, _>(
dataBuilder = SubcommandData.<TC, S>builder().senderType(senderType).arguments(arguments) literal,
.argumentsInOrder(argumentsInOrder).command(command); command,
} requirement,
this.redirect,
this.redirectModifier,
this.isFork,
data
)
for (node in arguments) {
result.addChild(node)
}
return result
}
@Override companion object {
protected CoreCommandBuilder<S, TC> getThis() { /**
return this; * Start building an executable command node.
} *
* @param name The subcommand name as written by the user
* @param senderType The expected command sender type based on the subcommand method
* @param arguments A map of the command arguments with their names as keys
* @param argumentsInOrder A list of the command arguments in the order they are expected
* @param command The command object that has this subcommand
* @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<*>> literal(
name: String,
senderType: Class<*>,
arguments: Map<String, CommandArgument>,
argumentsInOrder: List<CommandArgument>,
command: TC,
helpTextGetter: (Any) -> Array<String>,
hasPermission: (S) -> Boolean
): CoreCommandBuilder<S, TC, SubcommandData<TC, S>> {
return CoreCommandBuilder(
name,
SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission)
)
}
public static <S extends Command2Sender, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literal(String name, Class<?> senderType, Map<String, CommandArgument> arguments, CommandArgument[] argumentsInOrder, TC command) { /**
return new CoreCommandBuilder<>(name, senderType, arguments, argumentsInOrder, command); * Start building a no-op command node.
} *
* @param name The subcommand name as written by the user
public static <S extends Command2Sender, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literalNoOp(String name) { * @param helpTextGetter Custom help text that can depend on the context. The function receives the sender as the command itself receives it.
return literal(name, Command2Sender.class, Map.of(), new CommandArgument[0], null); */
} fun <S : Command2Sender, TC : ICommand2<*>> literalNoOp(
name: String,
/** helpTextGetter: (Any) -> Array<String>,
* Static help text added through annotations. May be overwritten with the getter. ): CoreCommandBuilder<S, TC, NoOpSubcommandData> {
* return CoreCommandBuilder(name, NoOpSubcommandData(helpTextGetter))
* @param helpText Help text shown to the user }
* @return This instance }
*/ }
public CoreCommandBuilder<S, TC> helps(String[] helpText) {
dataBuilder.staticHelpText(helpText);
return this;
}
/**
* Custom help text that depends on the context. Overwrites the static one.
* The function receives the sender but its type is not guaranteed to match the one at the subcommand.
* It will either match or be a Command2Sender, however.
*
* @param getter The getter function receiving the sender and returning the help text
* @return This instance
*/
public CoreCommandBuilder<S, TC> helps(Function<Object, String[]> getter) {
dataBuilder.helpTextGetter(getter);
return this;
}
public CoreCommandBuilder<S, TC> permits(Function<S, Boolean> permChecker) {
dataBuilder.hasPermission(permChecker);
return this;
}
@Override
public CoreCommandNode<S, TC> build() {
var result = new CoreCommandNode<S, TC>(this.getLiteral(), this.getCommand(), this.getRequirement(),
this.getRedirect(), this.getRedirectModifier(), this.isFork(),
dataBuilder.build());
for (CommandNode<S> node : this.getArguments()) {
result.addChild(node);
}
return result;
}
}

View file

@ -1,36 +1,18 @@
package buttondevteam.lib.chat; package buttondevteam.lib.chat
import buttondevteam.lib.chat.commands.SubcommandData; import buttondevteam.lib.chat.commands.NoOpSubcommandData
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command
import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.RedirectModifier
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode
import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode
import lombok.Getter; import java.util.function.Predicate
import java.util.Collection; class CoreCommandNode<T : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData>(
import java.util.function.Predicate; literal: String,
import java.util.stream.Collectors; command: Command<T>,
requirement: Predicate<T>,
public class CoreCommandNode<T extends Command2Sender, TC extends ICommand2<?>> extends LiteralCommandNode<T> { redirect: CommandNode<T>,
@Getter modifier: RedirectModifier<T>,
private final SubcommandData<TC, T> data; forks: Boolean,
val data: TSD
public CoreCommandNode(String literal, Command<T> command, Predicate<T> requirement, CommandNode<T> redirect, RedirectModifier<T> modifier, boolean forks, SubcommandData<TC, T> data) { ) : LiteralCommandNode<T>(literal, command, requirement, redirect, modifier, forks)
super(literal, command, requirement, redirect, modifier, forks);
this.data = data;
}
/**
* @see #getChildren()
*/
public Collection<CoreCommandNode<T, TC>> getCoreChildren() {
return super.getChildren().stream().map(node -> (CoreCommandNode<T, TC>) node).collect(Collectors.toUnmodifiableSet());
}
/**
* @see #getChild(String)
*/
public CoreCommandNode<T, TC> getCoreChild(String name) {
return (CoreCommandNode<T, TC>) super.getChild(name);
}
}

View file

@ -15,11 +15,23 @@ object CommandUtils {
* @return The command path starting with the replacement char. * @return The command path starting with the replacement char.
*/ */
fun getCommandPath(methodName: String, replaceChar: Char): String { fun getCommandPath(methodName: String, replaceChar: Char): String {
return if (methodName == "def") "" else replaceChar.toString() + methodName.replace('_', replaceChar).lowercase(Locale.getDefault()) return if (methodName == "def") "" else replaceChar.toString() + methodName.replace('_', replaceChar)
.lowercase(Locale.getDefault())
} }
/**
* Casts the node to whatever you say. Use responsibly.
*/
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <TP : Command2Sender, TC : ICommand2<*>> CommandNode<TP>.core(): CoreCommandNode<TP, TC> { fun <TP : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData> CommandNode<TP>.core(): CoreCommandNode<TP, TC, TSD> {
return this as CoreCommandNode<TP, TC> return this as CoreCommandNode<TP, TC, TSD>
}
/**
* Returns the node as an executable core command node or returns null if it's a no-op node.
*/
fun <TP : Command2Sender, TC : ICommand2<*>> CommandNode<TP>.coreExecutable(): CoreCommandNode<TP, TC, SubcommandData<TC, TP>>? {
val ret = core<TP, TC, NoOpSubcommandData>()
return if (ret.data is SubcommandData<*, *>) ret.core() else null
} }
} }

View file

@ -0,0 +1,19 @@
package buttondevteam.lib.chat.commands
open class NoOpSubcommandData(
/**
* Custom help text that depends on the context. Overwrites the static one.
* The function receives the sender as the command itself receives it.
*/
private val helpTextGetter: (Any) -> Array<String>
) {
/**
* Get help text for this subcommand. Returns an empty array if it's not specified.
*
* @param sender The sender running the command
* @return Help text shown to the user
*/
fun getHelpText(sender: Any): Array<String> {
return helpTextGetter(sender)
}
}

View file

@ -1,75 +1,56 @@
package buttondevteam.lib.chat.commands; package buttondevteam.lib.chat.commands
import buttondevteam.lib.chat.Command2Sender; import buttondevteam.lib.chat.Command2Sender
import buttondevteam.lib.chat.ICommand2; import buttondevteam.lib.chat.ICommand2
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.function.Function;
/** /**
* Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command. * Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command.
* *
* @param <TC> Command class type * @param TC Command class type
* @param TP Command sender type
*/ */
@Builder class SubcommandData<TC : ICommand2<*>, TP : Command2Sender>(
@RequiredArgsConstructor /**
public final class SubcommandData<TC extends ICommand2<?>, TP extends Command2Sender> { * The type of the sender running the command.
/** * The actual sender type may not be represented by Command2Sender (TP).
* The type of the sender running the command. * In that case it has to match the expected type.
* The actual sender type may not be represented by Command2Sender (TP). */
* In that case it has to match the expected type. val senderType: Class<*>,
*/
public final Class<?> senderType;
/**
* Command arguments collected from the subcommand method.
* Used to construct the arguments for Brigadier and to hold extra information.
*/
public final Map<String, CommandArgument> arguments;
/**
* Command arguments in the order they appear in code and in game.
*/
public final CommandArgument[] argumentsInOrder;
/**
* The original command class that this data belongs to. If null, that meaans only the help text can be used.
*/
@Nullable
public final TC command;
/** /**
* Static help text added through annotations. May be overwritten with the getter. * Command arguments collected from the subcommand method.
*/ * Used to construct the arguments for Brigadier and to hold extra information.
private final String[] staticHelpText; */
/** val arguments: Map<String, CommandArgument>,
* Custom help text that depends on the context. Overwrites the static one.
* The function receives the sender but its type is not guaranteed to match the one at the subcommand.
* It will either match or be a Command2Sender, however.
*/
private final Function<Object, String[]> helpTextGetter;
/**
* A function that determines whether the user has permission to run this subcommand.
*/
private final Function<TP, Boolean> hasPermission;
/** /**
* Get help text for this subcommand. * Command arguments in the order they appear in code and in game.
* */
* @param sender The sender running the command val argumentsInOrder: List<CommandArgument>,
* @return Help text shown to the user
*/
public String[] getHelpText(Object sender) {
return staticHelpText == null ? helpTextGetter.apply(sender) : staticHelpText;
}
/** /**
* Check if the user has permission to execute this subcommand. * The original command class that this data belongs to.
* */
* @param sender The sender running the command val command: TC,
* @return Whether the user has permission /**
*/ * Custom help text that depends on the context. Overwrites the static one.
public boolean hasPermission(TP sender) { * The function receives the sender as the command itself receives it.
return hasPermission.apply(sender); */
} helpTextGetter: (Any) -> Array<String>,
}
/**
* A function that determines whether the user has permission to run this subcommand.
*/
private val permissionCheck: (TP) -> Boolean
) : NoOpSubcommandData(helpTextGetter) {
/**
* Check if the user has permission to execute this subcommand.
*
* @param sender The sender running the command
* @return Whether the user has permission
*/
fun hasPermission(sender: TP): Boolean {
return permissionCheck(sender)
}
}