-
-
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 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.
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'sHello World!
. Lastly, the message,Hello darkness my old > friend
is displayed if no arguments are given.
To create commands using annotations, include the Typist
library in the project's pom.xml
<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.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.karuslabs</groupId>
<artifactId>typist</artifactId>
<version>5.0.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 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.
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.
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
.
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
}
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();
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
The results are the same as the one demonstrated in getting started.
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;
}
}