JorelAli / CommandAPI

A Bukkit/Spigot API for the command UI introduced in Minecraft 1.13

Home Page:https://commandapi.jorel.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.