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.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<TC : ICommand2<TP>, TP : Command2Sender> {
*/
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 paramMap = HashMap<String, CommandArgument?>()
val paramMap = HashMap<String, CommandArgument>()
for (param in params) {
paramMap[param.name] = param
}
val node = CoreCommandBuilder.literal<TP, TC>(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<TP> -> executeCommand(context) }
var parent: ArgumentBuilder<TP, *> = node
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)
*/
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 mainCommand: LiteralCommandNode<TP>? = 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<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
}
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
* @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
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" })
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,
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),
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<TC : ICommand2<TP>, TP : Command2Sender> {
private fun executeHelpText(context: CommandContext<TP>): Int {
println("""
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())
return 0
}
@ -333,8 +350,10 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
*
* @return A set of command node objects containing the commands
*/
val commandNodes: Set<CoreCommandNode<TP, TC>>
get() = dispatcher.root.children.stream().map { node: CommandNode<TP> -> node.core<TP, TC>() }.collect(Collectors.toUnmodifiableSet())
val commandNodes: Set<CoreCommandNode<TP, TC, NoOpSubcommandData>>
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.
@ -342,7 +361,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* @param command The exact name of the command
* @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()
}
@ -352,7 +371,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender> {
* @param command The command class (object) to unregister
*/
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
*/
fun unregisterCommandIf(condition: Predicate<CoreCommandNode<TP, TC>>, nested: Boolean) {
dispatcher.root.children.removeIf { node: CommandNode<TP> -> condition.test(node.core()) }
fun unregisterCommandIf(condition: Predicate<CoreCommandNode<TP, TC, SubcommandData<TC, TP>>>, nested: Boolean) {
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())
}
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
root.children.removeIf { node: CommandNode<TP> -> condition.test(node.core()) }
for (child in root.coreChildren) unregisterCommandIf(condition, child)
root.children.removeIf { node -> node.coreExecutable<TP, TC>()?.let { condition.test(it) } ?: false }
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.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<S : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData> private constructor(
literal: String,
val data: TSD
) : LiteralArgumentBuilder<S>(literal) {
public class CoreCommandBuilder<S extends Command2Sender, TC extends ICommand2<?>> extends LiteralArgumentBuilder<S> {
private final SubcommandData.SubcommandDataBuilder<TC, S> dataBuilder;
protected CoreCommandBuilder(String literal, Class<?> senderType, Map<String, CommandArgument> arguments, CommandArgument[] argumentsInOrder, TC command) {
super(literal);
dataBuilder = SubcommandData.<TC, S>builder().senderType(senderType).arguments(arguments)
.argumentsInOrder(argumentsInOrder).command(command);
override fun getThis(): CoreCommandBuilder<S, TC, TSD> {
return this
}
@Override
protected CoreCommandBuilder<S, TC> getThis() {
return this;
override fun build(): CoreCommandNode<S, TC, TSD> {
val result = CoreCommandNode<_, TC, _>(
literal,
command,
requirement,
this.redirect,
this.redirectModifier,
this.isFork,
data
)
for (node in arguments) {
result.addChild(node)
}
return result
}
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);
}
public static <S extends Command2Sender, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literalNoOp(String name) {
return literal(name, Command2Sender.class, Map.of(), new CommandArgument[0], null);
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 <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)
)
}
/**
* Static help text added through annotations. May be overwritten with the getter.
* Start building a no-op command node.
*
* @param helpText Help text shown to the user
* @return This instance
* @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.
*/
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;
fun <S : Command2Sender, TC : ICommand2<*>> literalNoOp(
name: String,
helpTextGetter: (Any) -> Array<String>,
): CoreCommandBuilder<S, TC, NoOpSubcommandData> {
return CoreCommandBuilder(name, NoOpSubcommandData(helpTextGetter))
}
}
}

View file

@ -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<T extends Command2Sender, TC extends ICommand2<?>> extends LiteralCommandNode<T> {
@Getter
private final SubcommandData<TC, T> data;
public CoreCommandNode(String literal, Command<T> command, Predicate<T> requirement, CommandNode<T> redirect, RedirectModifier<T> modifier, boolean forks, SubcommandData<TC, T> data) {
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);
}
}
class CoreCommandNode<T : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData>(
literal: String,
command: Command<T>,
requirement: Predicate<T>,
redirect: CommandNode<T>,
modifier: RedirectModifier<T>,
forks: Boolean,
val data: TSD
) : LiteralCommandNode<T>(literal, command, requirement, redirect, modifier, forks)

View file

@ -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 <TP : Command2Sender, TC : ICommand2<*>> CommandNode<TP>.core(): CoreCommandNode<TP, TC> {
return this as CoreCommandNode<TP, TC>
fun <TP : Command2Sender, TC : ICommand2<*>, TSD : NoOpSubcommandData> CommandNode<TP>.core(): CoreCommandNode<TP, TC, TSD> {
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,67 +1,48 @@
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 <TC> Command class type
* @param TC Command class type
* @param TP Command sender type
*/
@Builder
@RequiredArgsConstructor
public final class SubcommandData<TC extends ICommand2<?>, TP extends Command2Sender> {
class SubcommandData<TC : ICommand2<*>, 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.
*/
public final Class<?> senderType;
val senderType: Class<*>,
/**
* 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;
val arguments: Map<String, CommandArgument>,
/**
* 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;
val argumentsInOrder: List<CommandArgument>,
/**
* Static help text added through annotations. May be overwritten with the getter.
* The original command class that this data belongs to.
*/
private final String[] staticHelpText;
val command: TC,
/**
* 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.
* The function receives the sender as the command itself receives it.
*/
private final Function<Object, String[]> helpTextGetter;
helpTextGetter: (Any) -> Array<String>,
/**
* 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.
*
* @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;
}
private val permissionCheck: (TP) -> Boolean
) : NoOpSubcommandData(helpTextGetter) {
/**
* Check if the user has permission to execute this subcommand.
@ -69,7 +50,7 @@ public final class SubcommandData<TC extends ICommand2<?>, TP extends Command2Se
* @param sender The sender running the command
* @return Whether the user has permission
*/
public boolean hasPermission(TP sender) {
return hasPermission.apply(sender);
fun hasPermission(sender: TP): Boolean {
return permissionCheck(sender)
}
}