From 8cf01f1137ba2bf529c22487343f5bc6493c4e9c Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Wed, 22 Feb 2023 00:32:33 +0100 Subject: [PATCH] Converted and reworked command builder, node, data --- .../java/buttondevteam/lib/chat/Command2.kt | 75 ++++++---- .../lib/chat/CoreCommandBuilder.kt | 134 +++++++++--------- .../buttondevteam/lib/chat/CoreCommandNode.kt | 50 +++---- .../lib/chat/commands/CommandUtils.kt | 18 ++- .../lib/chat/commands/NoOpSubcommandData.kt | 19 +++ .../lib/chat/commands/SubcommandData.kt | 113 ++++++--------- 6 files changed, 212 insertions(+), 197 deletions(-) create mode 100644 Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NoOpSubcommandData.kt diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt index 6bcbdef..9a7a514 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt @@ -4,11 +4,11 @@ import buttondevteam.core.MainPlugin import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.chat.commands.* import buttondevteam.lib.chat.commands.CommandUtils.core +import buttondevteam.lib.chat.commands.CommandUtils.coreExecutable import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.arguments.* import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.context.CommandContext -import com.mojang.brigadier.context.ParsedCommandNode import com.mojang.brigadier.exceptions.CommandSyntaxException import com.mojang.brigadier.tree.CommandNode import com.mojang.brigadier.tree.LiteralCommandNode @@ -157,12 +157,14 @@ abstract class Command2, TP : Command2Sender> { */ private fun getExecutableNode(method: Method, command: TC, ann: Subcommand, path: String, argHelpManager: CommandArgumentHelpManager): LiteralCommandNode { val (params, _) = getCommandParametersAndSender(method, argHelpManager) // Param order is important - val paramMap = HashMap() + val paramMap = HashMap() for (param in params) { paramMap[param.name] = param } - val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, params, command) - .helps(command.getHelpText(method, ann)).permits { sender: TP -> hasPermission(sender, command, method) } + val helpText = command.getHelpText(method, ann) + 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 -> executeCommand(context) } var parent: ArgumentBuilder = node for (param in params) { // Register parameters in the right order @@ -180,16 +182,23 @@ abstract class Command2, TP : Command2Sender> { * the main command node and the last part of the command path (that isn't registered yet) */ private fun registerNodeFromPath(path: String): Triple, LiteralCommandNode?, String> { - val split = path.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val split = path.split(" ") var parent: CommandNode = dispatcher.root var mainCommand: LiteralCommandNode? = null - for (i in 0 until split.size - 1) { - val part = split[i] + split.forEachIndexed { i, part -> val child = parent.getChild(part) - if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp(part).executes { context: CommandContext -> executeHelpText(context) }.build().also { parent = it }) else parent = child + if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp(part, getSubcommandList()) + .executes(::executeHelpText).build().also { parent = it }) + else parent = child if (i == 0) mainCommand = parent as LiteralCommandNode // 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 { + return { + arrayOf("TODO") // TODO: Subcommand list + } } /** @@ -200,17 +209,25 @@ abstract class Command2, TP : Command2Sender> { * @return Parameter data objects and the sender type * @throws RuntimeException If there is no sender parameter declared in the method */ - private fun getCommandParametersAndSender(method: Method, argHelpManager: CommandArgumentHelpManager): Pair, Class<*>> { + private fun getCommandParametersAndSender( + method: Method, + argHelpManager: CommandArgumentHelpManager + ): Pair, Class<*>> { val parameters = method.parameters if (parameters.isEmpty()) throw RuntimeException("No sender parameter for method '$method'") val usage = argHelpManager.getParameterHelpForMethod(method) val paramNames = usage?.split(" ") - return Pair(parameters.zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" }) - .map { (param, name) -> - val numAnn = param.getAnnotation(NumberArg::class.java) - CommandArgument(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), + return Pair( + parameters.zip(paramNames ?: (1 until parameters.size).map { i -> "param$i" }) + .map { (param, name) -> + val numAnn = param.getAnnotation(NumberArg::class.java) + CommandArgument( + 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), name) }, parameters[0].type) @@ -253,7 +270,7 @@ abstract class Command2, TP : Command2Sender> { private fun executeHelpText(context: CommandContext): Int { println(""" Nodes: - ${context.nodes.stream().map { node: ParsedCommandNode -> node.node.name + "@" + node.range }.collect(Collectors.joining("\n"))} + ${context.nodes.stream().map { node -> node.node.name + "@" + node.range }.collect(Collectors.joining("\n"))} """.trimIndent()) return 0 } @@ -333,8 +350,10 @@ abstract class Command2, TP : Command2Sender> { * * @return A set of command node objects containing the commands */ - val commandNodes: Set> - get() = dispatcher.root.children.stream().map { node: CommandNode -> node.core() }.collect(Collectors.toUnmodifiableSet()) + val commandNodes: Set> + get() = dispatcher.root.children.stream() + .map { node: CommandNode -> node.core() } + .collect(Collectors.toUnmodifiableSet()) /** * Get a node that belongs to the given command. @@ -342,7 +361,7 @@ abstract class Command2, TP : Command2Sender> { * @param command The exact name of the command * @return A command node */ - fun getCommandNode(command: String?): CoreCommandNode { + fun getCommandNode(command: String): CoreCommandNode { // TODO: What should this return? No-op? Executable? What's the use case? return dispatcher.root.getChild(command).core() } @@ -352,7 +371,7 @@ abstract class Command2, TP : Command2Sender> { * @param command The command class (object) to unregister */ fun unregisterCommand(command: ICommand2) { - dispatcher.root.children.removeIf { node: CommandNode -> node.core().data.command === command } + dispatcher.root.children.removeIf { node: CommandNode -> node.coreExecutable()?.data?.command === command } } /** @@ -360,14 +379,18 @@ abstract class Command2, TP : Command2Sender> { * * @param condition The condition for removing a given command */ - fun unregisterCommandIf(condition: Predicate>, nested: Boolean) { - dispatcher.root.children.removeIf { node: CommandNode -> condition.test(node.core()) } + fun unregisterCommandIf(condition: Predicate>>, nested: Boolean) { + dispatcher.root.children.removeIf { node -> node.coreExecutable()?.let { condition.test(it) } ?: false } if (nested) for (child in dispatcher.root.children) unregisterCommandIf(condition, child.core()) } - private fun unregisterCommandIf(condition: Predicate>, root: CoreCommandNode) { + private fun unregisterCommandIf( + condition: Predicate>>, + root: CoreCommandNode + ) { + // TODO: Remvoe no-op nodes without children // Can't use getCoreChildren() here because the collection needs to be modifiable - root.children.removeIf { node: CommandNode -> condition.test(node.core()) } - for (child in root.coreChildren) unregisterCommandIf(condition, child) + root.children.removeIf { node -> node.coreExecutable()?.let { condition.test(it) } ?: false } + for (child in root.children) unregisterCommandIf(condition, child.core()) } } \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt index 5fa44c0..ca36e87 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.kt @@ -1,74 +1,72 @@ -package buttondevteam.lib.chat; +package buttondevteam.lib.chat -import buttondevteam.lib.chat.commands.CommandArgument; -import buttondevteam.lib.chat.commands.SubcommandData; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.tree.CommandNode; +import buttondevteam.lib.chat.commands.CommandArgument +import buttondevteam.lib.chat.commands.NoOpSubcommandData +import buttondevteam.lib.chat.commands.SubcommandData +import com.mojang.brigadier.builder.LiteralArgumentBuilder -import java.util.Map; -import java.util.function.Function; +class CoreCommandBuilder, TSD : NoOpSubcommandData> private constructor( + literal: String, + val data: TSD +) : LiteralArgumentBuilder(literal) { -public class CoreCommandBuilder> extends LiteralArgumentBuilder { - private final SubcommandData.SubcommandDataBuilder dataBuilder; + override fun getThis(): CoreCommandBuilder { + return this + } - protected CoreCommandBuilder(String literal, Class senderType, Map arguments, CommandArgument[] argumentsInOrder, TC command) { - super(literal); - dataBuilder = SubcommandData.builder().senderType(senderType).arguments(arguments) - .argumentsInOrder(argumentsInOrder).command(command); - } + override fun build(): CoreCommandNode { + val result = CoreCommandNode<_, TC, _>( + literal, + command, + requirement, + this.redirect, + this.redirectModifier, + this.isFork, + data + ) + for (node in arguments) { + result.addChild(node) + } + return result + } - @Override - protected CoreCommandBuilder getThis() { - return this; - } + companion object { + /** + * 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 > literal( + name: String, + senderType: Class<*>, + arguments: Map, + argumentsInOrder: List, + command: TC, + helpTextGetter: (Any) -> Array, + hasPermission: (S) -> Boolean + ): CoreCommandBuilder> { + return CoreCommandBuilder( + name, + SubcommandData(senderType, arguments, argumentsInOrder, command, helpTextGetter, hasPermission) + ) + } - public static > CoreCommandBuilder literal(String name, Class senderType, Map arguments, CommandArgument[] argumentsInOrder, TC command) { - return new CoreCommandBuilder<>(name, senderType, arguments, argumentsInOrder, command); - } - - public static > CoreCommandBuilder literalNoOp(String name) { - return literal(name, Command2Sender.class, Map.of(), new CommandArgument[0], null); - } - - /** - * Static help text added through annotations. May be overwritten with the getter. - * - * @param helpText Help text shown to the user - * @return This instance - */ - public CoreCommandBuilder 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 helps(Function getter) { - dataBuilder.helpTextGetter(getter); - return this; - } - - public CoreCommandBuilder permits(Function permChecker) { - dataBuilder.hasPermission(permChecker); - return this; - } - - @Override - public CoreCommandNode build() { - var result = new CoreCommandNode(this.getLiteral(), this.getCommand(), this.getRequirement(), - this.getRedirect(), this.getRedirectModifier(), this.isFork(), - dataBuilder.build()); - - for (CommandNode node : this.getArguments()) { - result.addChild(node); - } - - return result; - } -} + /** + * Start building a no-op command node. + * + * @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 > literalNoOp( + name: String, + helpTextGetter: (Any) -> Array, + ): CoreCommandBuilder { + return CoreCommandBuilder(name, NoOpSubcommandData(helpTextGetter)) + } + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandNode.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandNode.kt index 2f0639d..c8ebfbc 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandNode.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandNode.kt @@ -1,36 +1,18 @@ -package buttondevteam.lib.chat; +package buttondevteam.lib.chat -import buttondevteam.lib.chat.commands.SubcommandData; -import com.mojang.brigadier.Command; -import com.mojang.brigadier.RedirectModifier; -import com.mojang.brigadier.tree.CommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; -import lombok.Getter; +import buttondevteam.lib.chat.commands.NoOpSubcommandData +import com.mojang.brigadier.Command +import com.mojang.brigadier.RedirectModifier +import com.mojang.brigadier.tree.CommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import java.util.function.Predicate -import java.util.Collection; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class CoreCommandNode> extends LiteralCommandNode { - @Getter - private final SubcommandData data; - - public CoreCommandNode(String literal, Command command, Predicate requirement, CommandNode redirect, RedirectModifier modifier, boolean forks, SubcommandData data) { - super(literal, command, requirement, redirect, modifier, forks); - this.data = data; - } - - /** - * @see #getChildren() - */ - public Collection> getCoreChildren() { - return super.getChildren().stream().map(node -> (CoreCommandNode) node).collect(Collectors.toUnmodifiableSet()); - } - - /** - * @see #getChild(String) - */ - public CoreCommandNode getCoreChild(String name) { - return (CoreCommandNode) super.getChild(name); - } -} +class CoreCommandNode, TSD : NoOpSubcommandData>( + literal: String, + command: Command, + requirement: Predicate, + redirect: CommandNode, + modifier: RedirectModifier, + forks: Boolean, + val data: TSD +) : LiteralCommandNode(literal, command, requirement, redirect, modifier, forks) \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt index 518c25a..6b22671 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.kt @@ -15,11 +15,23 @@ object CommandUtils { * @return The command path starting with the replacement char. */ 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") - fun > CommandNode.core(): CoreCommandNode { - return this as CoreCommandNode + fun , TSD : NoOpSubcommandData> CommandNode.core(): CoreCommandNode { + return this as CoreCommandNode + } + + /** + * Returns the node as an executable core command node or returns null if it's a no-op node. + */ + fun > CommandNode.coreExecutable(): CoreCommandNode>? { + val ret = core() + return if (ret.data is SubcommandData<*, *>) ret.core() else null } } \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NoOpSubcommandData.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NoOpSubcommandData.kt new file mode 100644 index 0000000..9181c77 --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NoOpSubcommandData.kt @@ -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 +) { + /** + * 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 { + return helpTextGetter(sender) + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt index 178822d..00755f8 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.kt @@ -1,75 +1,56 @@ -package buttondevteam.lib.chat.commands; +package buttondevteam.lib.chat.commands -import buttondevteam.lib.chat.Command2Sender; -import buttondevteam.lib.chat.ICommand2; -import lombok.Builder; -import lombok.RequiredArgsConstructor; - -import javax.annotation.Nullable; -import java.util.Map; -import java.util.function.Function; +import buttondevteam.lib.chat.Command2Sender +import buttondevteam.lib.chat.ICommand2 /** * Stores information about the subcommand that can be used to construct the Brigadier setup and to get information while executing the command. * - * @param Command class type + * @param TC Command class type + * @param TP Command sender type */ -@Builder -@RequiredArgsConstructor -public final class SubcommandData, TP extends Command2Sender> { - /** - * The type of the sender running the command. - * The actual sender type may not be represented by Command2Sender (TP). - * In that case it has to match the expected type. - */ - 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 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; +class SubcommandData, TP : Command2Sender>( + /** + * The type of the sender running the command. + * The actual sender type may not be represented by Command2Sender (TP). + * In that case it has to match the expected type. + */ + val senderType: Class<*>, - /** - * Static help text added through annotations. May be overwritten with the getter. - */ - private final String[] staticHelpText; - /** - * 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 helpTextGetter; - /** - * A function that determines whether the user has permission to run this subcommand. - */ - private final Function hasPermission; + /** + * Command arguments collected from the subcommand method. + * Used to construct the arguments for Brigadier and to hold extra information. + */ + val arguments: Map, - /** - * Get help text for this subcommand. - * - * @param sender The sender running the command - * @return Help text shown to the user - */ - public String[] getHelpText(Object sender) { - return staticHelpText == null ? helpTextGetter.apply(sender) : staticHelpText; - } + /** + * Command arguments in the order they appear in code and in game. + */ + val argumentsInOrder: List, - /** - * Check if the user has permission to execute this subcommand. - * - * @param sender The sender running the command - * @return Whether the user has permission - */ - public boolean hasPermission(TP sender) { - return hasPermission.apply(sender); - } -} + /** + * The original command class that this data belongs to. + */ + val command: TC, + /** + * Custom help text that depends on the context. Overwrites the static one. + * The function receives the sender as the command itself receives it. + */ + helpTextGetter: (Any) -> Array, + + /** + * 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) + } +} \ No newline at end of file