Skip to content

Creating Commands Using Annotations

Matthias Ngeo edited this page Jun 28, 2020 · 26 revisions

Overview

Commands built through CommandNodeBuilders can sometimes become a mess, especially if a command is complex. Command annotations aim to solve that issue by automatically binding fields and methods to commands, thereby eliminating the need to do so by hand. Command annotations were first introduced in 4.1.0 and subsequently overhauled in 4.7.0 to support ahead-of-time compilation. Annotations are parsed and then used to generate a corresponding Java source file at compile-time. Since everything is resolved at compile-time, there is zero reliance on reflection and hence minimal runtime overhead.

This guide provides a walkthrough of the creation of the same tell command in getting started but using annotations instead.

To quote the description of the tell command in getting started,

Our tell command allows a player to send a message to one or more other players. It's syntax is /tell|t <players> <messages>. Suggestions are provided for each argument. For <players>, it's the > names of visible, online players. For <messages>, it's Hello World!. Lastly, the message, Hello darkness my old > friend is displayed if no arguments are given.


Command Markup Language

Command annotations contains an extremely simple markup language used to declare commands within annotations.

A command is a sequence of arguments and literals that should start with a literal. An argument is declared by surrounding its name in < and >, e.g. <argument_name>. A literal is declared using its name, with aliases separated by |, e.g. command|alias_1|alias_2. All names and aliases should not contain whitespaces.

That's all, it wasn't too bad was it? In addition, the library will also catch syntax errors during compilation and provide explanations to help you fix the mistake.

For programming language nerds, the ABNF for the markup language is:

command  = literal *(argument / literal)
argument = "<" name ">"
literal  = name *("|" name)
name     = 1*(%x21 - %x59 / %x3D / %x3F - %7B / %x7D - %x7E) 

Declaring the tell command

The command annotations library is built around binding the fields and methods of a class to commands. However, a field or method cannot be bound to just any command. We must first bring a command into a classes' scope by annotating the class with @Command and declaring the commands in the annotation.

No global namespace exists. Commands declared within the scope of one class cannot be used in another class without first bringing that command into the scope of the other class.

import com.karuslabs.commons.command.aot.annotations.Command;

@Command({"tell|t <player> <message>"})
public class TellCommand {

}

Binding fields and methods to a command

Fields and methods that represent an ArgumentType<T>, Command<CommandSender>, Execution<CommanSender>, Predicate<CommandSender> or SuggestionProvider<CommandSender> can all be bound to a command. To bind a field or method to a command, we must annotated the field or method with @Bind and declare the commands in the annotation.

Fields annotated with @Bind must be public and final whereas methods annotated with @Bind must be public. Please see the FAQ for more information.

Binding the ArgumentTypes to <players> and <messages>:

import com.karuslabs.commons.command.aot.annotations.Bind;
import com.karuslabs.commons.command.aot.annotations.Command;
import com.karuslabs.commons.command.types.PlayersType;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;

@Command({"tell|t <players> <message>"})
public class TellCommand {

    public static final @Bind({"<message>"}) ArgumentType<String> message = StringArgumentType.string();
    public final @Bind({"<players>"}) PlayersType players = new PlayersType();

}

Editing TellCommand to bind a SuggestionProvider<CommandSender> to <message>:

import com.karuslabs.commons.command.aot.annotations.Bind;
import com.karuslabs.commons.command.aot.annotations.Command;
import com.karuslabs.commons.command.types.PlayersType;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.concurrent.CompletableFuture;
import org.bukkit.command.CommandSender;

@Command({"tell|t <players> <message>"})
public class TellCommand {

    // Other fields omitted for brevity 

    // We can either bind a field
    public static final @Bind({"<message>"}) SuggestionProvider<CommandSender> suggestions = (context, builder) -> builder.suggest("\"Hello World!\"").buildFuture();
    
    // Or bind a method (in subsequent examples we will use the field instead since it is more concise)
    @Bind({"<message>"})
    public CompletableFuture<Suggestions> suggest(CommandContext<CommandSender> context, SuggestionsBuilder builder) {
        return builder.suggest("\"Hello World!\"").buildFuture();
    }

}

Editing TellCommand again to bind behaviour to tell and <messaages>:

import com.karuslabs.commons.command.OptionalContext;
import com.karuslabs.commons.command.aot.annotations.Bind;
import com.karuslabs.commons.command.aot.annotations.Command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import java.util.List;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;


@Command({"tell|t <players> <message>"})
public class TellCommand {

    // Fields omitted for brevity
    
    // Binds a Command<CommandSender> to tell
    @Bind("tell") 
    public static int tell(CommandContext<CommandSender> context) {
        context.getSource().sendMessage("Hello darkness my old friend");
        return 1;
    }
    
    
    // Binds a Execution<CommandSender> to <messages>
    @Bind("<message>")
    public void send(CommandSender source, OptionalContext<CommandSender> context) {
        List<Player> recipents = context.getArgument("players", List.class);
        String entered = context.getArgument("message", String.class);
        
        recipents.forEach(p -> p.sendMessage(source.getName() + " says: " + entered));
    }
    
}

Specifying the location of the generated source

Commands created through annotations are parsed and used to generate a single Java source file at compile-time. To specify the location of the generated source file, we need to annotate a class with the @Source annotation. By default, if no location is declared in the @Source annotation, the source file will be generated in the same package as the annotated class as Commands.java.

Annotating TellCommand:

import com.karuslabs.commons.command.aot.annotations.Command;
import com.karuslabs.commons.command.aot.annotations.Source;

@Source
@Command({"tell|t <players> <message>"})
public class TellCommand {
    // Contents omitted for brevity
}

Registering the command

The final step after creating the command is to register the generated tell command. Each class annotated with @Command will have a corresponding overloaded of(...) method that accepts an instance of the annotated class in the generated source file. All that needs to be done is import the generated source file and register the map returned by the corresponding method to Dispatcher

Similarly, commands not registered in either JavaPlugin.onLoad() or JavaPlugin.onEnable() will only be available after Dispatcher.update() is called.

import com.karuslabs.commons.command.dispatcher.Dispatcher;
import my.fancy.plugin.Commands;
import my.fancy.plugin.TellCommand;

Dispatcher dispatcher = Dispatcher.of(yourPlugin);
dispatcher.register(Commands.of(new TellCommand()));
// Only needed if called outside JavaPlugin.onLoad() or JavaPlugin.onEnable()
dispatcher.update(); 

Putting it together

In the end, our tell command should look like:

import com.karuslabs.commons.command.OptionalContext;
import com.karuslabs.commons.command.aot.annotations.Command;
import com.karuslabs.commons.command.aot.annotations.Bind;
import com.karuslabs.commons.command.aot.annotations.Source;
import com.karuslabs.commons.command.dispatcher.Dispatcher;
import com.karuslabs.commons.command.types.PlayersType;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import java.util.List;
import my.fancy.plugin.Commands;
import my.fancy.plugin.TellCommand;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;


@Source
@Command({"tell|t <players> <message>"})
public class TellCommand {

    public static final @Bind({"<message>"}) ArgumentType<String> message = StringArgumentType.string();
    public static final @Bind({"<message>"}) SuggestionProvider<CommandSender> suggestions = (context, builder) -> builder.suggest("\"Hello World!\"").buildFuture();
    public final @Bind({"<players>"}) PlayersType players = new PlayersType();
    
    // Binds a Command<CommandSender> to tell
    @Bind("tell") 
    public static int tell(CommandContext<CommandSender> context) {
        context.getSource().sendMessage("Hello darkness my old friend");
        return 1;
    }
    
    
    // Binds a Execution<CommandSender> to <messages>
    @Bind("<message>")
    public void send(CommandSender source, OptionalContext<CommandSender> context) {
        List<Player> recipents = context.getArgument("players", List.class);
        String entered = context.getArgument("message", String.class);
        
        recipents.forEach(p -> p.sendMessage(source.getName() + " says: " + entered));
    }
    
}
Dispatcher dispatcher = Dispatcher.of(yourPlugin);
dispatcher.register(Commands.of(new TellCommand()));
// Only needed if called outside JavaPlugin.onLoad() or JavaPlugin.onEnable()
dispatcher.update(); 

Taking the tell command for a spin

The results are the same as the one demonstrated in getting started.

Generated Source

For those interested, the generated Commands class looks something like:

import com.karuslabs.commons.command.tree.nodes.*;

import com.mojang.brigadier.tree.CommandNode;

import java.util.*;
import java.util.function.Predicate;

import org.bukkit.command.CommandSender;


/**
 * This file was generated at 2020-06-03T13:06:03.378589600 using Chimera 4.7.0
 */
public class Commands {

    private static final Predicate<CommandSender> REQUIREMENT = s -> true;

    public static Map<String, CommandNode<CommandSender>> of(com.karusmc.raster.TellCommand source) {
        var commands = new HashMap<String, CommandNode<CommandSender>>();

        var command = new Argument<>("message", com.karusmc.raster.TellCommand.message, source::send, REQUIREMENT, com.karusmc.raster.TellCommand.suggestions);

        var command0 = new Argument<>("players", source.players, null, REQUIREMENT, null);
        command0.addChild(command);

        var command1 = new Literal<>("tell", com.karusmc.raster.TellCommand::tell, REQUIREMENT);
        Literal.alias(command1, "t");
        command1.addChild(command0);

        commands.put(command1.getName(), command1);
        return commands;
    }

}

Where to next?

Clone this wiki locally