Add a `FlagsArgument`
willkroboth opened this issue · comments
Description
This suggestion comes from a common question on the CommandAPI Discord. Most recently, a comment from Lear reminded me of this, but there have also been ideas from TheZequex, Skepter, NextdoorPsycho and probably lots more.
In Vanilla Minecraft, the execute
command is very powerful. It has many subcommands that a user can type out in any order, with each subcommand having its own arguments that define how the command's context should be changed. For example, you can type commands like this:
execute as @e if block ~ ~ ~ minecraft:air ...
execute if block ~ ~ ~ minecraft:air as @e ...
Here, the as
and if block
subcommands can go in any order. as
has an EntitySelectorArgument before it loops back, and if block
has a LocationArgument followed by a BlockPredicateArgument. You can keep going, adding new subcommands until the run
subcommand, which allows you to exit the loop and run any other command.
The Brigadier system handles this by allowing redirects to any other node in the CommandDispatch tree. In /execute
's case, each subcommand redirects back to the base execute
node to continue the loop.
The CommandAPI can help a little with writing commands like this, as shown by this example in the documentation. However, you still need to import Brigadier
and deal with its classes directly. Additionally, the Brigadier source when using Bukkit is the NMS class CommandListenerWrapper
, which means you have to deal with raw types when building the commands. I think it's possible to give a bit more help here.
Expected code
I'll show what I'm thinking with Lear's usecase. Lear wanted to implement filtering for a custom object with syntax like /findCustomObject sort=random limit=1 distance=..3
, similar to EntitySelector filtering. Using the FlagsArgument
as I'm calling it, something similar could be possible like so:
new CommandAPICommand("findCustomObject")
.withArguments(
new FlagsArgument("filters")
.loopingBranch(
new LiteralArgument("filter", "sort").setListed(true),
new MultiLiteralArgument("sortType", "furthest", "nearest", "random")
)
.loopingBranch(
new LiteralArgument("filter", "limit").setListed(true),
new IntegerArgument("limitAmount", 0)
)
.loopingBranch(
new LiteralArgument("filter", "distance").setListed(true),
new IntegerRangeArgument("distanceRange")
)
)
.executes((sender, args) -> {
List<Object> candidateObjects = getCandiates();
for (CommandArguments branch : args.<List<CommandArguments>>getUnchecked("filters")) {
String filterType = branch.getUnchecked("filter");
switch (filterType) {
case "sort" -> {
String sortType = branch.getUnchecked("sortType");
switch (sortType) {
case "furthest" -> candidateObjects.sort((a, b) -> -Integer.compare(a.getDistance(), b.getDistance()));
case "closest" -> candidateObjects.sort((a, b) -> Integer.compare(a.getDistance(), b.getDistance()));
case "random" -> Collections.shuffle(candidateObjects);
}
}
case "limit" -> {
int limit = branch.getUnchecked("limitAmount");
candidateObjects = candidateObjects.subList(0, limit);
}
case "distance" -> {
IntegerRange range = branch.getUnchecked("distanceRange");
candidateObjects.stream().filter(o -> range.isInRange(o.getDistance()));
}
}
}
processCandidates(candidateObjects);
})
.register();
With this code, commands like these should be executable:
/findcustomobject filters sort random
/findcustomobject filters limit 1
/findcustomobject filters distance 4..10
/findcustomobject filters distance ..10 sort random limit 1
Something like the execute command might look like this:
.withArguments(
new FlagsArgument("execute")
.loopingBranch(
new LiteralArgument("subcommand", "as").setListed(true),
new EntitySelectorArgument.ManyEntities("targets")
)
.loopingBranch(
new FlagsArgument("if")
.terminalBranch(
new LiteralArgument("ifType", "block").setListed(true),
new BlockPredicateArgument("predicate")
)
.terminalBranch(
new LiteralArgument("ifType", "entity").setListed(true),
new EntitySelectorArgument.ManyEntities("predicate")
)
)
.terminalBranch(
new LiteralArgument("run")
),
new CommandArgument("command")
)
A loopingBranch
would redirect to the base node of the FlagsArgument
, while a terminalBranch
could be used to continue down the command tree. There could also be a FlagsArgument
inside a branch of another FlagsArgument
.
The argument above would create a CommandDispatch tree with an abstract structure like this:
execute Node:
as Node:
targets Node
redirect to execute
if Node:
block Node:
predicate Node
same termination as if
entity Node:
predicate Node
same termination as if
redirect to execute
run Node:
same termination as execute
continue to next Node
command Node
Extra details
I'm not sure the FlagsArgument
makes sense exactly as I describe, so the API presented here is subject to change.
Implementing anything like the FlagsArgument
would also likely require rewriting the CommandAPI's command building system. For one, the CommandAPI doesn't include Brigadier redirects at all. The FlagsArgument
also isn't a typical Argument since it's made up of many nodes. I see this as an opportunity though. Currently, the CommandAPI flattens everything into a CommandAPICommand
. However, Brigadier natively works with tree structures. Certain features like MultiLiteralArgument
, CommandTree
, and optional arguments could benefit from building commands directly as trees.