Skip to content

Creating Commands Using Annotations

Matthias Ngeo edited this page Aug 6, 2022 · 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 most recently overhauled in 5.0.0. 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.

⚠️ Do not define commands in the plugin.yml: Due to changes in Spigot 1.19, doing so will cause commands to be unregistered

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.


Settings up the project

To create commands using annotations, include the Typist library in the project's pom.xml

releases-maven snapshots-maven javadoc

<repositories>         
    <repository>
        <id>chimera-releases</id>
        <url>https://repo.karuslabs.com/repository/chimera-releases/</url>
    </repository>

    <repository>
        <id>minecraft-libraries</id>
        <url>https://libraries.minecraft.net</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.mojang</groupId>
        <artifactId>brigadier</artifactId>
        <version>1.0.17</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.karuslabs</groupId>
        <artifactId>commons</artifactId>
        <version>5.1.0</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>com.karuslabs</groupId>
        <artifactId>typist</artifactId>
        <version>5.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

As the more keen-eyed among the readers may have noticed, we declared Typist as a provided dependency. This is due to the code generated by Typist not relying on Typist and all annotations being discarded after compilation. In essence, Typist is only needed at compile-time.

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. In most cases, the feedback is almost instantaneous.

Example 1

Example 2

Example 3

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.

Fields and methods can be bound to commands that match a given pattern or specific commands. A pattern is defined as a sequence of literals and arguments. The annotated target will be bound to the last argument or literal in said sequence. For example, given the command, a <b> c <d>, an annotated target with the pattern, <b> c, will be bound to c.

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

import com.karuslabs.typist.annotations.Bind;
import com.karuslabs.typist.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(pattern = {"<message>"}) ArgumentType<String> message = StringArgumentType.string();
    public final @Bind({"tell <players>"}) PlayersType players = new PlayersType();

}

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

import com.karuslabs.typist.annotations.Bind;
import com.karuslabs.typist.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({pattern = "<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(pattern = {"<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.typist.annotations.Bind;
import com.karuslabs.typist.annotations.Command;
import com.karuslabs.typist.annotations.Let;
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(pattern = {"<message>"})
    public void send(CommandSender source, OptionalContext<CommandSender> context, @Let("<players>") List<Player> recipients, @Let String message) {
        recipients.forEach(p -> p.sendMessage(source.getName() + " says: " + message));
    }
    
}

What are those @Let thingamajigs? Annotating a method parameter with @Let is syntax-sugar for retrieving an argument's value. In the above code snippet, recipients retrieves the value of tell|t <players> which we declared earlier to be a list of players. If the annotated method parameter's name matches the retrieved argument, the argument name in @Let can be omitted. Since the message method parameter matches both the name and type of tell|t <players> <message>, we don't have to explicitly specify an argument for @Let.


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 @Pack annotation, the source file will be generated in the same package as the annotated class as Commands.java.

Annotating TellCommand:

import com.karuslabs.typist.annotations.Command;
import com.karuslabs.typist.annotations.Pack;

@Pack(age = "my.fancy.pack")
@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.pack.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.types.PlayersType;

import com.karuslabs.typist.annotations.Bind;
import com.karuslabs.typist.annotations.Command;
import com.karuslabs.typist.annotations.Let;
import com.karuslabs.typist.annotations.Pack;

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 org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

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

    public static final @Bind({pattern = "<message>"}) ArgumentType<String> message = StringArgumentType.string();
    public static final @Bind(pattern = {"<message>"}) SuggestionProvider<CommandSender> suggestions = (context, builder) -> builder.suggest("\"Hello World!\"").buildFuture();
    public final @Bind({"tell <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(pattern = "<message>")
    public void send(CommandSender source, OptionalContext<CommandSender> context, @Let("<players>") List<Player> recipents, @Let String message) {
        recipents.forEach(p -> p.sendMessage(source.getName() + " says: " + message));
    }
    
}
Dispatcher dispatcher = Dispatcher.of(yourPlugin);
dispatcher.register(Commands.of(new TellCommand()));
// Only needed if called outside JavaPlugin.onLoad() or JavaPlugin.onEnable()
dispatcher.update(); 

The same fully working example is provided in Typist Example Plugin

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 (this sample differs a little since it's taken from the example plugin):

package com.karuslabs.example.plugin.commands;

// CHECKSTYLE:OFF

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

import com.mojang.brigadier.Command;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;

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

import org.bukkit.command.CommandSender;

/**
 * This file was generated at 2021-07-04T21:24:56.749689600 using Chimera 5.0.0-SNAPSHOT
 */
public class Commands {
    private static final Predicate<CommandSender> REQUIREMENT = s -> true;

    public static Map<String, CommandNode<CommandSender>> of(com.karuslabs.typist.example.plugin.TellCommand source) {
        var commands = new HashMap<String, CommandNode<CommandSender>>();
        var command0 = new Argument<>(
            "message",
            com.karuslabs.typist.example.plugin.TellCommand.message,
            "",
            (sender, context) -> {
                var players1 = context.getArgument("players", java.util.List.class);
                var message2 = context.getArgument("message", java.lang.String.class);
                source.send(sender, context, players1, message2);
            },
            REQUIREMENT,
            com.karuslabs.typist.example.plugin.TellCommand.suggestions
        );

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

        var command4 = new Literal<>(
            "typist:tell",
            "",
            (context) -> {
                return com.karuslabs.typist.example.plugin.TellCommand.tell(context);
            },
            REQUIREMENT
        );
        Literal.alias(command4, "typist:t");
        command4.addChild(command3);
        commands.put(command4.getName(), command4);

        return commands;
    }
}

Where to next?

Clone this wiki locally