-
-
Notifications
You must be signed in to change notification settings - Fork 7
Creating Commands Using Annotations
Commands built through CommandNodeBuilder
s 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.6.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 <player>|> <players> <messages>
. Suggestions are provided for each argument. For<player>
and<players>
, it's the > names of visible, online players. For<messages>
, it'sHello World!
. Lastly, the message,Hello darkness my old > friend
is displayed if no arguments are given.
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)
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 {
}
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));
}
}
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
}
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();
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();
It's the same as getting started.
For those interested, the generated Commands
class looks something like:
TODO