Giter Club home page Giter Club logo

cloud's Introduction


cloud command framework

license central build docs

Note

Cloud 2 is a major update with many significant changes. Cloud 2 is not compatible with version 1. You can find the changelog here.

Cloud is a general-purpose Java command dispatcher & framework. It allows programmers to define command chains that are then parsed and invoked from user-supplied string inputs, to execute pre-defined actions.

Cloud commands consist out of deterministic chains of strongly typed arguments. When you define a command, you know exactly what type of data you're going to be working with, and you know that there will be no ambiguity at runtime. Cloud promotes writing reusable code, making it very easy to define new commands.

Cloud allows you to build commands in many different ways, according to your preferences:

Cloud is built to be very customisable, in order to fit your needs. You can inject handlers and processors along the entire command chain. If the pre-existing command parsers aren't enough for your needs, you're easily able to create your own parsers. If you use the annotation parsing system, you can also define your own annotations and register them to further customise the behaviour of the library.

Cloud by default ships with implementations and mappings for the most common Minecraft server platforms, Discord4J, JDA4, JDA5, Kord and Javacord for Discord bots, PircBotX for IRC and cloud-spring for Spring Shell. The core module allows you to use Cloud anywhere, simply by implementing the CommandManager for the platform of your choice.

links

repositories

develop & build

To clone the repository, use git clone https://github.com/Incendo/cloud.git.

To then build it, use ./gradlew clean build. If you want to build the examples as well, use ./gradlew clean build -Pcompile-examples.

attributions, links & acknowledgements

This library is licensed under the MIT license, and the code copyright belongs to Alexander Söderberg. The implementation is based on a paper written by the copyright holder, and this paper exists under the CC Attribution 4 license.

The Cloud icon was created by Thanga Vignesh P on Iconscout and Digital rights were purchased under a premium plan.

cloud's People

Contributors

0utplay avatar aurorasmiles avatar bergerkiller avatar broccolai avatar citymonstret avatar dependabot[bot] avatar derklaro avatar electronicboy avatar frankheijden avatar i0xhex avatar jacoballen1 avatar jarfiles avatar jpenilla avatar kezz avatar konicai avatar lucko avatar machine-maker avatar masmc05 avatar p5nbtgip0r avatar pablete1234 avatar phinner avatar piggypiglet avatar portlek avatar renovate[bot] avatar solonovamax avatar sparky983 avatar tadhgboyle avatar tehbrian avatar webcrawls avatar zml2008 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

cloud's Issues

Greedy parsers cannot have functioning suggestions

There's a hard limitation that a parser can only provide suggestions for one argument being typed. This is because as input to suggestions, only a single String can be provided. When there are two or more parts in the command queue, it is unclear what part should be sent to the suggestions provider.

A greedy string parser might want to suggest words with spaces in them, while a string[] parser might want to suggest a word for every element.

Because suggestions aren't even asked right now (because it is not clear what should happen), there is no workable solution.

https://github.com/Incendo/cloud/blob/1.4.0-dev/cloud-core/src/main/java/cloud/commandframework/CommandTree.java#L602

CompoundArgument has some special logic tailored to it, but also assumes the last-typed 'element' is what needs suggestions. For greedy parsers that is not valid.

Changing String to Queue<String> for suggestions would make it in parity with parsers, but would also break api compatibility. I know no good solution for this.

Open Question: Why is greedy not part of the command context?

I'm starting to run into a situation where I would like to use @greedy in custom argument parsers. I find that greedy is instead deeply rooted in the standard parsers, and you need to pass some properties to the ArgumentParser object you supply.

In other words, to simply accept an argument with spaces in it, I need to change the argument parser itself, how it is registered, and I'm not sure if what I want even works properly.

Why isn't there a isGreedy() property on the CommandContext itself? Then parsers can check if its greedy and if so, consume the entire queue as part of parsing, and otherwise, consume only one element. This would greatly simplify this api.

My current use case involves double values with units. For example, users can specify:

/command 0.5m/s
/command 20km/h

But there's no reason why they shouldn't be able to:

/command 20 mi/h
/command 20.5 mph

I see no real reason why accepting input with spaces in them should be so strict. Even nicer would be @greedy / setGreedy() squashing the inputs into one separated by spaces, so argument parsers don't even have to take greedy into account.

P.s. I might have trouble with this because of #139

Argument parser suggestions function is always called with a terminal command sender context

When registering a custom argument parser. the CommandContext.getSender() always returns the console sender, even when a player is typing the commands. This only happens when using the brigadier manager

On paper, this is:

com.destroystokyo.paper.console.TerminalConsoleCommandSender

This is currently rendering personalized suggestions ('suggestions based on what the player can see') impossible.

edit*

This example reproduces the problem using Cloud v1.2.0 branch:

Code
    private BukkitCommandManager<CommandSender> manager;
    private AnnotationParser<CommandSender> annotationParser;

    public void enable(Plugin plugin) {
        try {
            this.manager = new PaperCommandManager<>(
                    /* Owning plugin */ plugin,
                    /* Coordinator function */ CommandExecutionCoordinator.simpleCoordinator(),
                    /* Command Sender -> C */ Function.identity(),
                    /* C -> Command Sender */ Function.identity()
            );
        } catch (final Exception e) {
            throw new IllegalStateException("Failed to initialize the command manager", e);
        }

        // Register Brigadier mappings
        if (manager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) {
            manager.registerBrigadier();

            CloudBrigadierManager<?, ?> brig = manager.brigadierManager();
            brig.setNativeNumberSuggestions(false);
        }

        // Just a simple class type to register with an argument parser
        class TestClass {
            public String name;

            public TestClass(String name) {
                this.name = name;
            }

            @Override
            public String toString() {
                return this.name;
            }
        }

        // parser supplier will parse TestClass and provide suggestions
        manager.getParserRegistry().registerParserSupplier(TypeToken.get(TestClass.class), (params) -> {
            return new ArgumentParser<CommandSender, TestClass>() {
                @Override
                public ArgumentParseResult<TestClass> parse(
                        CommandContext<CommandSender> commandContext,
                        Queue<String> inputQueue
                ) {
                    final String input = inputQueue.peek();
                    if (input == null) {
                        return ArgumentParseResult.failure(new NoInputProvidedException(
                                this.getClass(),
                                commandContext
                        ));
                    }

                    return ArgumentParseResult.success(new TestClass(inputQueue.poll()));
                }

                @Override
                public List<String> suggestions(
                        final CommandContext<CommandSender> commandContext,
                        final String input
                ) {
                    for (Player player : Bukkit.getOnlinePlayers()) {
                        player.sendMessage("You are " + commandContext.getSender().getClass().getName());
                    }
                    return Arrays.asList("a", "b", "c");
                }
            };
        });

        // register the actual command which uses the parser (/command <test>)
        manager.command(
                manager.commandBuilder("command", Description.of("Test cloud command using a builder"))
                        .argument(TestClass.class, "test", (a) -> {
                        })
                        .handler(context -> {
                            context.getSender().sendMessage("Input: " + context.get("test"));
                        })
        );
    }

On Spigot:
ScreenShot_2020-11-21 18-54-18

On paperspigot:
ScreenShot_2020-11-21 18-56-15

When not using brigadier manager it works as expected:
ScreenShot_2020-11-21 19-04-29

Allow passing a supplier to `CommandContext#getOrDefault`

While it isn't crucial, pretty much trivial, I reckon it would be nice to have something like CommandContext#getOrDefault(String, Supplier<? extends T>) and get the fallback value be evaluated only when needed. Would also allow for a "controlled" "default trigger detection/handling".
Just a thought 🙂 Could PR this myself but you peeps have your own workflow over this!

Exceptions thrown by Command handlers are not handled properly

When exceptions are thrown in execution handlers, they are currently not handled properly. The asynchronous handler doesn't catch exceptions at all, in fact:

https://github.com/Incendo/cloud/blob/1.2.0-dev/cloud-core/src/main/java/cloud/commandframework/execution/AsynchronousCommandExecutionCoordinator.java#L95

The annotation handler merely prints the stack trace. No message is sent to the player informing him an internal error occurred:
https://github.com/Incendo/cloud/blob/1.2.0-dev/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java#L119

At the very least some sort of message should be displayed to the player when command execution fails. Ideally, the exception propagates to the Command Execution Coordinator, where it could possibly be handled by custom implementations.

The exception could be wrapped into a special 'Command Execution Exception'.

I made some changes in a personal fork, but based on discussion in Discord it is probably not sufficient.
1.2.0-dev...bergerhealer:1.2.0-dev

Generic Argument Builders

In 2.0.0, the CommandArgument.Builder should take in a generic builder argument to properly forward the builder type. The declaration would be

public static final class Builder<C, T, B extends Builder<C, T>>

Currently the argument specific builder methods need to be called before the inherited methods. This is very annoying.

Issues registering Argument Pairs following Argument Triplets to Brigadier

Description:

When registering a command in the form of .. <doubles triplet> <floats pair>, the pair isn't registered on Brig.

Environment:

  • cloud module: cloud-paper
  • cloud version: 1.1.0
  • Server version: Paper 1.16.3 b248
  • Command manager: PaperCommandManager
  • Coordinator function whatever: SimpleCoordinator
  • Brigadier hooking: yes
  • Paper async tab completions hooking: yes

Sample code:

// onEnable
final PaperCommandManager<CommandSender> manager;
manager = new PaperCommandManager<>(this, CommandExecutionCoordinator.SimpleCoordinator.simpleCoordinator(),
                                    Function.identity(), Function.identity())

if (manager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) {
  manager.registerBrigadier();
}

if (manager.queryCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
  manager.registerAsynchronousCompletions();
}

final Command.Builder<CommandSender> base = manager.commandBuilder("playerheads", "pheads", "ph");
manager.command(base.literal("spawnnpc", Description.empty())
                    .argumentTriplet("location", TypeToken.get(Vector.class),
                                     Triplet.of("x", "y", "z"), Triplet.of(Double.class, Double.class, Double.class),
                                     (sender, triplet) -> fromDoubleTriplet(triplet),
                                     Description.empty())
                    .argumentPair("rotation",
                                  Pair.of("yaw", "pitch"), Pair.of(Float.class, Float.class),
                                  Description.empty()));

Resulting registration:

Improve support for advanced Kotlin features

Kotlin users are usually satisfied with the JVM interoperability that already exists, but sometimes that gets in the way when using some advanced features like Coroutines.

I'd love to see support for some kotlin specific features such as the following:

  • Suspending functions with the help of coroutines
  • Default values provided on the method signatures (for annotated methods)
  • DSL builder for commands

For the supending fun/coroutine support, it would be very appreciated if we could pass an instance of a CoroutineScope into the builder/annotation parser/command executor (don't know which one is more appropriate here) and have a way to use it easily inside the suspend fun. Support should also be added for builders by addding Extension overloads that made use of CoroutineScope.(...) -> ... code blocks

Allow for named suggestion providers

Keep a registry of them, similar to how the parser registry works. This would allow for named suggestion providers for annoted methods, and not just the builders.

Discussion: Support multiple variables

Context:

Description:
Cloud currently supports having multiple string literals OR a single variable the children of any parent node. This means that Cloud allows /command literal1|literal2|literal3 and /command <argument> but not /command literal|<argument> or /command <argument1>|<argument2>

This was done to guarantee that no ambiguous cases can exist in a command tree, which means that we get completely deterministic input evaluation.

This is however a quite major limitation and this decision has been somewhat controversial. The decision is not final and I think it's about time we come to a conclusion.

Possible Solutions:

  1. We can do nothing and keep the current limitations.

  2. We can support multiple literals and a single argument. This has been attempted in pull request #142. This requires that all string literals are parsed before any variable arguments and this restriction should be appended to the library contracts.

  3. We can support multiple literals and multiple arguments. This would be the less restrictive path, but has some major problems: how do we handle error feedback when invalid command input is provided? what order do we evaluate the arguments in? how do we deal with platform integrations (most notably, Brigadier)?

The first option is obviously the easiest but appears to be less favourable. The second option is possible as has been demonstrated by @bergerkiller. The third option would be the least restrictive but would probably require a major rework of the library internals.

All opinions are very welcome!

Make CloudBrigadierManager accessible

There's no way to get the brigadier manager from the classes that makes use of it. This should probably be solved by adding an interface that Brigadier enabled managers can implement, so that it is possible to get the brigadier instance.

The method shoudld look something like:

/**
* Get the Brigadier manager instance used by this manager. This method being present
* in a command manager means that the manager has the capability to register commands
* to Brigadier, but does not necessarily mean that it does.
* <p>
* In the case that Brigadier isn't used, this method should always return {@code null}.
* 
* @return The Brigadier manager instance, if commands are being registered to Brigadier.
          Else, {@code null}
*/          
@Nullable CloudBrigadierManager<C> brigadierManager();

Command pipeline does not use the right argument parser when multiple commands are used

When using a custom argument parser, it looks like parameters configured using annotations do not get used. What I do:

  1. Register the annotation so that it is translated to a single parser parameter (unique key + bool true as value)
  2. Register a factory for the argument parser mapped to type, where parser parameters are read
  3. Use the value read using parameters in the argument parser

I have a custom parser annotation, parameter and argument parser declared:

Parser annotation and parameter
    /**
     * Declares that the command will require write access to the saved train.
     * If the sender has no such access, declines the argument.
     */
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SavedTrainRequiresAccess {
        public static final ParserParameter<Boolean> PARAM = new ParserParameter<Boolean>(
                "savedtrain.requiresaccess", TypeToken.get(Boolean.class));
    }

    public class SavedTrainPropertiesParser implements ArgumentParser<CommandSender, SavedTrainProperties> {
        private final boolean mustHaveAccess;

        public SavedTrainPropertiesParser(boolean mustHaveAccess) {
            this.mustHaveAccess = mustHaveAccess;
        }

        public boolean isMustHaveAccess() {
            return this.mustHaveAccess;
        }

        @Override
        public ArgumentParseResult<SavedTrainProperties> parse(CommandContext<CommandSender> commandContext, Queue<String> inputQueue) {
            commandContext.getSender().sendMessage("I am parsing with mustHaveAccess=" + mustHaveAccess);
            inputQueue.remove();
            return ArgumentParseResult.success(SavedTrainProperties.none() /*not shown*/);
        }
    }

Which are registered as follows:

manager.getParserRegistry().registerAnnotationMapper(SavedTrainRequiresAccess.class, (a, typeToken) -> {
    return ParserParameters.single(SavedTrainRequiresAccess.PARAM, Boolean.TRUE);
});

manager.getParserRegistry().registerParserSupplier(TypeToken.get(SavedTrainProperties.class), (parameters) -> {
    boolean mustHaveAccess = parameters.get(SavedTrainRequiresAccess.PARAM, Boolean.FALSE);
    System.out.println("MustHaveAccess: " + mustHaveAccess);
    return new SavedTrainPropertiesParser(mustHaveAccess);
});

When then declaring a command using the annotations method:

@CommandMethod("savedtrain <savedtrainname> rename <newsavedtrainname>")
private void commandRename(
        final CommandSender sender,
        final @SavedTrainRequiresAccess @Argument("savedtrainname") SavedTrainProperties savedTrain,
        final @Argument("newsavedtrainname") String newSavedTrainName
) {
    sender.sendMessage("Command executed!");
}

Note that there are several commands like this, some without @SavedTrainRequiresAccess annotated!

Other commands
    @CommandMethod("savedtrain <savedtrainname> info")
    @CommandDescription("Shows detailed information about a saved train")
    private void commandShowInfo(
            final CommandSender sender,
            final @Argument("savedtrainname") SavedTrainProperties savedTrain
    ) {
        sender.sendMessage("Other command info");
    }

    @CommandMethod("savedtrain <savedtrainname> export")
    @CommandDescription("Exports detailed information about a saved train to a network location")
    private void commandShowInfo(
            final CommandSender sender,
            final @Argument("savedtrainname") SavedTrainProperties savedTrain
    ) {
        sender.sendMessage("Other command export");
    }

On startup I see many messages flow by with 'MustHaveAccess: false' and then once 'MustHaveAccess: true', as expected, because I have several commands registered that parse SavedTrainProperties as arguments. Only one has this access annotation, so, so far so good.

The problem is when it comes time to parse user-provided input. I find that the mustHaveAccess flag is suddenly false. That is, I read for /savedtrain name rename newname:

I am parsing with mustHaveAccess=false

Further testing showed that it is using a parser from an entirely different command, one that uses different parameters. This is wrong.


I can use registerCommandPostProcessor, check whether the individual argument uses the SavedTrainPropertiesParser, and if so, perform post processing on it. In that situation the SavedTrainPropertiesParser isMustHaveAccess() returns true, as expected.

Sample code
manager.registerCommandPostProcessor(context -> {
    for (CommandArgument<CommandSender, ?> arg : context.getCommand().getArguments())
    {
        if (arg.getParser() instanceof SavedTrainPropertiesParser
                && ((SavedTrainPropertiesParser) arg.getParser()).isMustHaveAccess())
        {
            // This indeed gets printed!
            context.getCommandContext().getSender().sendMessage("isMustHaveAccess=true!");
        }
    }
});

My guess is that during parsing it has some internal cache purely mapped to type, which it uses instead of the command-declared parser. Or, maybe the other commands get matched first and that parser is used, even though there is another command that matches more perfectly.

Misbehaving permissions

With two commands like these, the first command mycommand will not allow a user with the test.permission permission to execute the command. In order to execute mycommand, the user must also have the test.permission.2 permission (the permission for mycommand [string]). Expected behavior is that each command uses it's correct permission node.

In the help menu/tab completions permission seem to work correctly for such commands, but when trying to execute them you will get a no permission error.

mgr.command(mgr.commandBuilder("mycommand")
        .permission("test.permission")
        .handler(c -> c.getSender().sendMessage("ok")));

mgr.command(mgr.commandBuilder("mycommand")
        .argument(StringArgument.single("string"))
        .permission("test.permission.2")
        .handler(c -> c.getSender().sendMessage("test: " + c.get("string"))));

Suggestions don't work for command aliases

Description:

The suggestions for the main command work properly, but for the aliases it isn't handled at all (server fallbacks to online players' names)

Environment:

  • cloud module: cloud-paper
  • cloud version: 1.1.0
  • Server version: Paper 1.16.3 b248
  • Command manager: PaperCommandManager<CommandSender>
  • Coordinator function whatever: SimpleCoordinator
  • Brigadier hooking: no
  • Paper async tab completions hooking: yes

Sample code:

// onEnable
final PaperCommandManager<CommandSender> manager;
manager = new PaperCommandManager<>(this, CommandExecutionCoordinator.SimpleCoordinator.simpleCoordinator(),
                                    Function.identity(), Function.identity())

// Possibly irrelevant?
if (manager.queryCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
  manager.registerAsynchronousCompletions();
}

final Command.Builder<CommandSender> base = manager.commandBuilder("playerheads", "pheads", "ph");
manager.command(base.literal("spawnnpc", Description.empty()));

Resulting suggestions:

gif

Permission Predicate

Hello!

It would be awesome to have a method to filter commands in a customized way.

For instance, in our server we do not make use of permission nodes, so the Command.Builder#permission(String) method is not suitable for us.

I am talking about something like this:

manager.commandBuilder("command", Description.of("This command requires ADMIN rank."))
        .senderType(Player.class)
        .permissionPredicate(sender -> /* Example */ RanksManager.getPlayerRank(sender) == Rank.ADMIN)
        .handler(handler -> ...

If the predicate returns false, the message sent is the generic one of not having the required permissions.
It basically works the same way as the Command.Builder#permission(String) method, and it gives more flexibility to the command builder.

Thanks!

Better flags

  • Allow joining presence alias, like -a -b -c <=> -abc
  • Look into feasibility of creating literal chains for flags (when under some threshold, as this adds up to n! + (n - 1)! + (n - 2) + ... + 1! literals when using Brigadier <-- Check how Sponge does it

Forcing flags to the end of the command is a little annoying, but because of the way Brigadier works, we're quite limited in how we do this, if we want the same exact syntax for brig vs. no brig.

Move argument requirement into the tree

Instead of having the requirement be a property of the argument, have it be stored in the command tree together with the argument.

Argument registration would then look something like:

command.required(StringArgument.greedy("potato"));

(String) argument suggestions do not show up because argument succeeds parsing

I didn't know whether to put this in the PR or as a separate issue, so I'm putting it out here.

Note: no brigadier is used

https://github.com/Incendo/cloud/blob/1.2.0-dev/cloud-core/src/main/java/cloud/commandframework/CommandTree.java#L508

In the CommandTree suggestions code, the following is done when providing suggestions for non-static arguments:

            // START: Preprocessing
            final ArgumentParseResult<Boolean> preParseResult = child.getValue().preprocess(
                    commandContext,
                    commandQueue
            );
            if (preParseResult.getFailure().isPresent() || !preParseResult.getParsedValue().orElse(false)) {
                final String value = stringOrEmpty(commandQueue.peek());
                commandContext.setCurrentArgument(child.getValue());
                return child.getValue().getSuggestionsProvider().apply(commandContext, value);
            }
            // END: Preprocessing

            // START: Parsing
            commandContext.setCurrentArgument(child.getValue());
            final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue);
            if (result.getParsedValue().isPresent()) {
                commandContext.store(child.getValue().getName(), result.getParsedValue().get());
                return this.getSuggestions(commandContext, commandQueue, child);
            } else if (result.getFailure().isPresent()) {
                final String value = stringOrEmpty(commandQueue.peek());
                commandContext.setCurrentArgument(child.getValue());
                return child.getValue().getSuggestionsProvider().apply(commandContext, value);
            }

It is difficult to see the problem, until you start using suggestion providers. The problem is that when you add a StringArgument with a custom suggestion provider, the parsing will succeed even when the value being parsed isn't one of the suggestions. As a result, players can start typing and see the full list, but the moment they type one more character, all suggestions disappear.

I will probably fix this in my PR #142 but need to know whether this was intentional behavior.

My proposed fix will be to not call the deeper getSuggestions() function when the number of remaining elements in the command queue is 0, and instead do the (parse) failure case.

The only workaround right now is to not use the suggestion provider, but make a custom argument parser which rejects values that aren't part of the enumeration. That's not a solution for people that want to accept non-suggested input.

Add methods to Command.Builder to pre- or post-execute a handler

Currently you can set one handler on a command (builder) to be executed. Since there is no getter for the current handler, it is not possible to execute something else before or after the already set command executor.

One solution that I'm using for the moment is to simply make a new handler that calls the previous one and the newly added one, but this does not follow proper pipeline architecture:

        /**
         * Specify a command execution handler to execute before the current handler
         *
         * @param commandExecutionHandler New execution handler
         * @return New builder instance using the command execution handler
         */
        public @NonNull Builder<C> prependHandler(final @NonNull CommandExecutionHandler<C> commandExecutionHandler) {
            final CommandExecutionHandler<C> handlerAfter = this.commandExecutionHandler;
            return handler((context) -> {
                commandExecutionHandler.execute(context);
                handlerAfter.execute(context);
            });
        }

        /**
         * Specify a command execution handler to execute after the current handler
         *
         * @param commandExecutionHandler New execution handler
         * @return New builder instance using the command execution handler
         */
        public @NonNull Builder<C> appendHandler(final @NonNull CommandExecutionHandler<C> commandExecutionHandler) {
            final CommandExecutionHandler<C> handlerBefore = this.commandExecutionHandler;
            return handler((context) -> {
                handlerBefore.execute(context);
                commandExecutionHandler.execute(context);
            });
        }

Use case

My use case for this currently is so that I can do pre-validation before executing a command. This can be permissions or some sort of "is the player in the right state". In that case I can throw an exception in the pre-process handler.

This was done using lots of code spammed as such:

    @CommandMethod("cart breakblocks|break clear")
    @CommandDescription("Clears the list of blocks broken by the cart, disabling it")
    private void setPropertyClear(
            final CommandSender sender,
            final CartProperties properties
    ) {
        handlePermission(sender, "breakblocks");

        // <actual code>
    }

But now that annotations offer a way to modify a builder using custom-registered annotations, it becomes much cleaner. I can specify a custom annotation that 'injects' additional logic before/after executing the method

    @PropertyHandlePermissions("breakblocks")
    @CommandMethod("cart breakblocks|break clear")
    @CommandDescription("Clears the list of blocks broken by the cart, disabling it")
    private void setPropertyClear(
            final CommandSender sender,
            final CartProperties properties
    ) {
        // <actual code>
    }

This does however require a way for modifying the builder in a way that this is possible.

Add @SuggestionsProvider and @ArgumentParser annotated methods

@SuggestionsProvider("cow")
public List<String> cowSuggestions(CommandContext<C> context, String input) {
  return Collections.singletonList("cow");
}

This would only work for named suggestions, which I feel is a fair limitation. We could also allow for an optional target class, which would bind it to a parsed argument type. However, I am still not sure about that.

@ArgumentParser("cow")
public Cow cowParser(CommandContext<C> context, Queue<String> input) {
  return new Cow();
}

The ArgumentParser could return a parse result, but it would then be very hard to extract the generic type. I'd also prefer to just listen for exceptions and wrap them in a parse failure. Though, I am open for suggestions.

These methods would be a lot easier to add support to as they have (mostly) fixed signatures.

Quoted string parser does not support escapes or empty strings

There are a couple of issues with the way we are currently parsing quoted strings.

  • We don't handle escaped quote characters (ie "string with \" escaped quote" or 'test \'test\'')
    • Brig does properly support and highlights for quoted strings with escapes, making this especially confusing for users
  • Empty quotes are returned as invalid, ie "" or '', but should be a successful parse with an empty string result
  • A string with no spaces when quoted results in a failed parse, ie 'cat' or "dog" will fail
  • When more than one space character is placed sequentially in a quoted string, it is reduced to one. ie 'a______b' -> a_b
    • This is also an issue with vanilla minecraft brig commands and we probably don't need to worry about it.
    • /minecraft:tell @s "______test______" -> "_test_" (replace _ with )

Command with arguments in-between subcommands throws NPE

The command:

@CommandMethod("boosters type <identifier> canstack <value>")
fun boosterCanStack(sender: CommandSender, @Argument("identifier") identifier: String, @Argument("value") canStack: Boolean)

being called with /boosters type test canstack false

fails with a NullPointerException:

11:42:23 WARN]: java.lang.NullPointerException: No such object stored in the context: identifier
[11:42:23 WARN]:        at cloud.commandframework.context.CommandContext.get(CommandContext.java:190)
[11:42:23 WARN]:        at cloud.commandframework.annotations.MethodCommandExecutionHandler.execute(MethodCommandExecutionHandler.java:69)
[11:42:23 WARN]:        at cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator.lambda$coordinateExecution$0(AsynchronousCommandExecutionCoordinator.java:82)
[11:42:23 WARN]:        at cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator.lambda$coordinateExecution$2(AsynchronousCommandExecutionCoordinator.java:109)
[11:42:23 WARN]:        at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
[11:42:23 WARN]:        at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
[11:42:23 WARN]:        at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
[11:42:23 WARN]:        at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
[11:42:23 WARN]:        at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)

despite the fact that test is being passed for identifier

Presumably a bug in annotation parsing, but I'm not familiar enough with the codebase to tell.

Float/Double has wrong default ranges in StandardParserRegistry

this.registerParserSupplier(TypeToken.get(Float.class), options ->
        new FloatArgument.FloatParser<>(
                (float) options.get(StandardParameters.RANGE_MIN, Float.MIN_VALUE),
                (float) options.get(StandardParameters.RANGE_MAX, Float.MAX_VALUE)
        ));
this.registerParserSupplier(TypeToken.get(Double.class), options ->
        new DoubleArgument.DoubleParser<>(
                (double) options.get(StandardParameters.RANGE_MIN, Double.MIN_VALUE),
                (double) options.get(StandardParameters.RANGE_MAX, Double.MAX_VALUE)
        ));

MIN/MAX_VALUE -> NEGATIVE/POSITIVE_INFINITY

Bukkit uses description from last registered command for a node

When commands are displayed in help menu they get a description from the last registered subcommand.
Expected behaviour is to have description from the root module.

Cloud verison: 1.1.0

edit:
This is referring to the Bukkit help menu, which is using the description from the last registered command for a root node, as that command is the one whose Bukkit command instance is registered in the command map for that root node.

Expose some way to construct or access TaskFactory

right now, if i want to utilize the cloud-tasks dependency in any meaningful way, i need to either implement my own TaskFactory or use PaperCommandManager#taskRecipe. i think having to pass around the command manager is kinda sloppy, it would be nice if you publicized BukkitSynchronizer or whatever other classes are needed to make my own task factory

Command senders are suggested commands which they cannot use

If a specific sender type is set for a command, this is not taken into account for suggestions. For example, if the specific sender type is Player, our command will still be suggested in the console, even though console cannot execute this command. Command syntax strings have a similar issue with showing commands not usable by that user.

Static Arguments aren't working as intended

Example:

    commandManager.command(
        commandManager.commandBuilder("spigot")
            .argument(StaticArgument.optional("reconnect"))
            .handler(context -> {
              if (context.get("reconnect").isPresent()) {
                context.getSender().sendMessage("Reconnecting...");
                spigotComponent.connectAsync();
                return;
              }

              context.getSender().sendMessage("Api Status: " + MarkdownUtil.monospace(
                  spigotComponent.getApiStatus().name()
              ));
            }).build()
    );
  }

Expected behaviour:

  • Build a command with an optional parameter called "reconnect"
  • If the parameter is added when executing the command, the code in the if block should be executed
  • Otherwise, everything below

Current behaviour:

  • Parameter is required, not optional
  • Optional is always empty even if parameter is added when executing the command
  • Code below if gets executed

Improve toString() of classes that stringify TypeToken

There are some cases where the object responsible for something in cloud reports an error, where that object is printed. One such class is ServiceWrapper, which sadly does a TypeToken.toString(), which isn't implemented. This results in garbage being printed that is useless for a developer trying to fix such issues.

For example:

[17:31:16] [Server thread/ERROR]: [Train_Carts] Exception executing command handler
com.bergerkiller.bukkit.tc.dep.cloud.services.PipelineException: Failed to retrieve result from 
ServiceWrapper{
   type=com.bergerkiller.bukkit.tc.dep.cloud.annotations.injection.ParameterInjectorRegistry$1@c51fe3c,
   implementation=com.bergerkiller.bukkit.tc.dep.typetoken.TypeToken$1@294e3e87
 }
	at com.bergerkiller.bukkit.tc.dep.cloud.services.ServiceSpigot.getResult(ServiceSpigot.java:98) ~[?:?]
	at com.bergerkiller.bukkit.tc.dep.cloud.annotations.injection.ParameterInjectorRegistry.getInjectable(ParameterInjectorRegistry.java:136) ~[?:?]
	at com.bergerkiller.bukkit.tc.dep.cloud.annotations.MethodCommandExecutionHandler.execute(MethodCommandExecutionHandler.java:92) ~[?:?]
	at com.bergerkiller.bukkit.tc.dep.cloud.execution.CommandExecutionCoordinator$SimpleCoordinator.coordinateExecution(CommandExecutionCoordinator.java:121) ~[?:?]

Code should be reviewed and any place where TypeToken.toString() is used, a suitable alternative is chosen.

I kinda don't know how to fix the example error right now, because I have no clue what object is involved or even what 'injector handler' is responsible. And it doesn't lead into any of my own code either :/

Class annotations

Support annotations on the command class directly. Make method inherit these, and override where necessary.

Repeatable flags

I'd like to be able to mark command flags as repeatable.

In value flags: This would store a Collection of the value type. Entering repeatable mode would be done with some sort of method on the builder, maybe something like withRepeatableArgument.

In presence flags: The presence container would, instead of being an Object, hold a counter of the number of encounters. This enables the pattern of -v being normal verbosity, -vv being more verbose, and so on.

Pattern matching where literals are mixed with arguments

I don't know if this is a bug, something that is purposefully not implemented, or something I could work around using annotations I've missed in the documentation.

I have two commands declared:

    @CommandMethod("command literal")
    @CommandDescription("Performs the literal operation")
    private void commandLiteral(
              final CommandSender sender
    ) {
        sender.sendMessage("Performed command with a literal match");
    }

    @CommandMethod("command <argument>")
    @CommandDescription("Performs the operation with some variable")
    private void commandArgument(
              final CommandSender sender,
              final @Argument(value="argument") String argument
    ) {
        sender.sendMessage("Performed command using " + argument);
    }

But get the error:

com.bergerkiller.bukkit.tc.dep.cloud.exceptions.AmbiguousNodeException: Ambiguous Node: argument cannot be added as a child to command (All children: literal, argument)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.checkAmbiguity(CommandTree.java:741)
    at java.lang.Iterable.forEach(Iterable.java:75)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.checkAmbiguity(CommandTree.java:745)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.verifyAndRegister(CommandTree.java:672)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.insertCommand(CommandTree.java:617)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandManager.command(CommandManager.java:210)
    at com.bergerkiller.bukkit.tc.dep.cloud.annotations.AnnotationParser.parse(AnnotationParser.java:250)

I want /command literal to match one, and have /command anythingelse match the other. Obviously I can handle this by doing an equals check of the argument, but let's assume there are a lot of literals like this. Or that some literals have flags, while the variable one doesn't.

Is there a solution?

Feature request: register custom annotation that can mutate an annotated command being built

Sometimes you want to modify a large group of commands the same way, for example, adding a flag or generating certain descriptions. For this task it would be very useful to be able to modify the command built by the annotation parser, right before it is registered.

Pseudocode:

parser.registerCommandBuilder(MyAnnotation.class, (annotation, builder) -> {
    return builder.flag(CommandFlag.newBuilder("flag").build());
});

@MyAnnotation
@CommandMethod("command test")
private void commandTest(
   etc...
) {
}

TabCompleteEvent does not exist before 1.9

TabCompleteEvent does not exist before 1.9, so plugins using cloud on prior versions (1.8) will log an error and fail to register the CloudBukkitListener. Also, tab completions obviously do not work when the listener is not registered.

Ambiguous node error when really, it is not, given the constraints with flags

While playing around with the syntax, I've run into this problem several times, and it makes it difficult to define commands.

This involves the annotations library, I don't know whether the same happens with the builder.

I have two commands:

    @CommandMethod("savedtrain list modules")
    @CommandDescription("Lists all modules in which saved trains are saved")
    private void commandListModules(final CommandSender sender) {
        sender.sendMessage("List of modules");
    }

    @CommandMethod("savedtrain list")
    @CommandDescription("Lists all trains accessible to the player")
    private void commandListSavedTrains(
              final CommandSender sender,
              final @Flag(value="all", description="Show all trains on this server, not just those owned by the player") boolean showAll,
              final @Flag(value="module", suggestions="savedtrainmodules", description="Selects a module to list the saved trains of") String moduleName
    ) {
        sender.sendMessage("List of trains in " + moduleName + " all=" + showAll);
    }

One is the command:
/savedtrain list modules

The other is:

/savedtrain list
/savedtrain list --module something
/savedtrain list --all

Despite not being ambiguous, this fails to initialize.

cloud.exceptions.AmbiguousNodeException: Ambiguous Node: flags cannot be added as a child to list (All children: modules, flags)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.checkAmbiguity(CommandTree.java:741)
    at java.lang.Iterable.forEach(Iterable.java:75)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.checkAmbiguity(CommandTree.java:745)
    at java.lang.Iterable.forEach(Iterable.java:75)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.checkAmbiguity(CommandTree.java:745)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.verifyAndRegister(CommandTree.java:672)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandTree.insertCommand(CommandTree.java:617)
    at com.bergerkiller.bukkit.tc.dep.cloud.CommandManager.command(CommandManager.java:210)
    at com.bergerkiller.bukkit.tc.dep.cloud.annotations.AnnotationParser.parse(AnnotationParser.java:250)

I've tried mitigating it with [flags...] as was hinted at on the wiki to no avail.

Add a capability checking system and command unregistration

Command unregistration is a sorely needed feature. My use case is a simple text-command remembrance system that registers simple alias->output commands. There is another command which "forgets" these aliased commands, and I need to unregister in cloud in that instance.

As I understand that MC platforms do not typically react well to unregistration (brigadier) I suggest a capability system.

From @Proximyst, it would look like this:

commandManager.capability(CommandUnregistration.class).ifPresent(cap -> cap.unregister(command))

This seems perfectly reasonable. Please add. Thanks =))

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.