Skip to content

New command API#330

Merged
astei merged 71 commits into
PaperMC:dev/1.1.0from
hugmanrique:feat/new-command-api
Jul 29, 2020
Merged

New command API#330
astei merged 71 commits into
PaperMC:dev/1.1.0from
hugmanrique:feat/new-command-api

Conversation

@hugmanrique
Copy link
Copy Markdown
Contributor

@hugmanrique hugmanrique commented Jul 16, 2020

Introduces a new command framework that supports both legacy String[] args commands and 1.13+ Brigadier commands.

As discussed in #329 (comment) and #232, the current command framework lacks support for new command features. This PR adds 3 subinterfaces to Command: BrigadierCommand, LegacyCommand and RawCommand. All execution-related methods such as execute and hasPermission now take a single CommandInvocation argument that includes the command source and additional execution metadata such as the String[] args for legacy commands.

TODO:

  • Decide on command execution context name. Candidates so far are CommandExecutionContext, CommandContext and CommandInvocation
  • Move built-in commands to Brigadier
  • Test all the things!!1!

@astei astei added the type: feature New feature or request label Jul 16, 2020
@hugmanrique hugmanrique marked this pull request as draft July 16, 2020 12:51
Comment thread api/src/main/java/com/velocitypowered/api/newcommand/BrigadierCommand.java Outdated
Comment thread api/src/main/java/com/velocitypowered/api/newcommand/Command.java Outdated
Comment thread api/src/main/java/com/velocitypowered/api/newcommand/CommandManager.java Outdated
Comment thread build.gradle Outdated
@hugmanrique
Copy link
Copy Markdown
Contributor Author

hugmanrique commented Jul 16, 2020

Alright, here's some usage examples:

import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument;
import static com.velocitypowered.api.command.BrigadierCommand.argumentBuilder;

import com.google.inject.Inject;
import com.mojang.brigadier.CommandDispatcher;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.LegacyCommand;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import net.kyori.adventure.text.TextComponent;

@Plugin(id = "test-plugin")
public class TestPlugin {

    private final ProxyServer server;

    @Inject
    public TestPlugin(final ProxyServer server) {
        this.server = server;
    }

    @Subscribe
    public void onProxyInitialization(final ProxyInitializeEvent event) {
        CommandManager commandManager = server.getCommandManager();

        BrigadierCommand command = commandManager.brigadierBuilder().register(
            argumentBuilder("foo")
                .then(argument("bar", integer()))
                    .executes(c -> {
                        System.out.println("Bar is " + getInteger(c, "bar"));
                        return 1;
                    })
                .executes(c -> {
                    System.out.println("Called foo with no arguments");
                    return 1;
                })
        );

        LegacyCommand legacyCommand = commandManager.legacyBuilder().register(context -> {
            CommandSource source = context.source();
            String[] arguments = context.arguments();

            source.sendMessage(TextComponent.of("foo"));
        });
    }
}

Comment thread api/src/main/java/com/velocitypowered/api/command/LegacyCommand.java Outdated
@astei astei added this to the Velocity 2.0.0 milestone Jul 16, 2020
The main concern is the context type parameter added to the Command
interface. According to
https://wiki.eclipse.org/Evolving_Java-based_APIs_2#Evolving_API_interfaces_-_API_methods
this is binary compatible due to Java's handling of raw types.

Also adds back the CommandManager#register methods.
@hugmanrique
Copy link
Copy Markdown
Contributor Author

I reached a middle ground so current plugins can still directly register commands while adding a fluent command builder some may prefer (not many? do we even need it?).

@astei astei modified the milestones: Velocity 2.0.0, Velocity 1.1.0 Jul 16, 2020
@astei
Copy link
Copy Markdown
Contributor

astei commented Jul 16, 2020

My API compatibility concerns are assuaged for now. In 2.0.0 we'll just outright force the explicit use of LegacyCommand or BrigadierCommand.

Is there a better name for this?
@alexstaeding
Copy link
Copy Markdown
Contributor

CommandExecutionContext is a bit too wordy in my opinion. Why not just CommandContext?

@alexstaeding
Copy link
Copy Markdown
Contributor

For the sake of conventions and consistency with the rest of the API, methods should start with a verb. I think CommandSource source(); should rather be CommandSource getSource();.

@hugmanrique
Copy link
Copy Markdown
Contributor Author

It's common for immutable classes with getter accessors. See https://stackoverflow.com/a/1680515/4514467 for a more in depth answer.

Also fixes the redirect issue by cloning the entire children tree.
Compression should make the increased packet size negligible.
@hugmanrique
Copy link
Copy Markdown
Contributor Author

hugmanrique commented Jul 21, 2020

Alright, tested execution, tab complete and permission checks on 1.12.2, 1.16.1 and the console and everything looks fine. This is now ready for review.

One note that should be added to the docs: Brigadier commands should not use #requires to perform permission checks. This leads to the command being omitted from the parse tree and #execute throwing dispatcherUnknownCommand, meaning the execute is redirected to the backend server. Instead, they should use BrigadierCommand.Builder#permission. This also has the advantage of passing the entire CommandContext instead of just the CommandSource.

We now clone the entire children tree on redirect, no matter what. LiteralArgumentBuilder#redirect breaks the argument-less command even if it is manually added to the builder. LiteralCommandNode#createBuilder performs a shallow clone with the same alias. Instead, we use our own builder and add the destination children nodes to the builder. This avoids expensive copying on build, and the Available Commands packet size will be almost the same (if not better) thanks to compression.

@hugmanrique hugmanrique marked this pull request as ready for review July 21, 2020 12:53
@astei
Copy link
Copy Markdown
Contributor

astei commented Jul 21, 2020

Just to clarify your comment opening this to further review, the Brigadier requires behavior is very similar to the current behavior with hasPermission.

Comment thread api/src/main/java/com/velocitypowered/api/command/BrigadierCommand.java Outdated
Comment thread build.gradle Outdated
- Fixes CommandManager API-breakage (leftover from the Velocity 2.0
merge intention)
- Clarify BrigadierCommand.Builder#permission javadoc (still subject to
be removed)
- Correct execute return when the source doesn't have permission or a
syntax error is made. This follows the #execute javadoc. Under this
definition, a Brigadier command with a syntax error is not executed.
The legacy behavior is preserved.
- Revert git executable path
Avoids the horrible overengineered wrapping logic.
Also preserves the previous logic of forwarding a #requires false return
to the backend server. Also fixes sending suggestions the client doesn't have
permission for.
Hinting allows non-Brigadier commands to provide LiteralCommandNodes and
ArgumentCommandNodes that are only used to provide additional argument
metadata and tab-complete suggestions. Note that the Brigadier Command of a hinting command
node or any of its children is ignored, and will instead call the
regular "legacy wrapper" arguments command (see
BrigadierUtils#buildRawArgumentsLiteral).

While introducing this feature I also noticed the manager doesn't keep
track of registered Command instances. As noted before, this is correct
as the Command interface is now a dummy transformer. However, hinting
requires modifying **all** the aliases of a Command, so I introduced the
CommandMeta interface, which keeps track of the aliases and hinting data
for now. This commit also introduces new CommandManager#register methods
that accept a CommandMeta object. The Command.Builder and BrigadierCommand.Builder interfaces became useless with this introduction, so I also removed them and made BrigadierCommand a final class that accepts the LiteralCommandNode instead.
@hugmanrique hugmanrique requested a review from astei July 22, 2020 22:52
Copy link
Copy Markdown
Contributor

@astei astei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to look for some more eyes on this but LGTM

Copy link
Copy Markdown
Contributor

@Proximyst Proximyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks terrific!

@astei
Copy link
Copy Markdown
Contributor

astei commented Jul 29, 2020

Without any further comments left to make here, I will merge this in. Thanks for your contribution @hugmanrique, it is greatly appreciated.

@astei astei merged commit 6cc6e0f into PaperMC:dev/1.1.0 Jul 29, 2020
@WesternPine
Copy link
Copy Markdown

That's huge, congrats @hugmanrique. Looks like a great implementation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants