Listeners and commands¶
Event listeners and commands are where a plugin meets the server, and they are where manual
wiring usually breaks first: a listener that never gets registered fails silently, and a
command handler wired before its dependencies exist fails loudly. weftkit removes both problems.
A wired listener is registered for you, and a wired command is bound to its plugin.yml entry
for you, each once the graph is up and its dependencies exist.
Listeners¶
Any @Wired component that implements Listener is registered with the
server automatically during enable, by the bundled listener
module. Make it a @Singleton so it is registered as a single instance, and
weftkit's compile-time rule checks that each @EventHandler method is well formed. A listener
gated by a @FeatureFlag is only created and registered while its
flag is on.
@Wired
@Singleton
final class JoinListener implements Listener {
private final Greeter greeter;
JoinListener(Greeter greeter) {
this.greeter = greeter;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
event.getPlayer().sendMessage(greeter.greet(event.getPlayer().getName()));
}
}
There is no registration call to forget and none to clean up. When the plugin enables, the
bundled listener module collects every wired Listener from the graph and registers it with
the server; when the plugin disables, Bukkit drops the registrations along with the plugin.
Commands¶
Any @Wired component that implements CommandExecutor and carries a
@CommandHandler naming a plugin.yml command is bound to that command automatically during
enable, by the bundled command module. The annotation name
mirrors @EventHandler. Make it a @Singleton, and a component gated by a
@FeatureFlag is only created and bound while its flag is on. If the
component also implements TabCompleter, it is bound for completion too.
@Wired
@Singleton
@CommandHandler("hello")
final class HelloCommand implements CommandExecutor {
private final Greeter greeter;
HelloCommand(Greeter greeter) {
this.greeter = greeter;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
sender.sendMessage(greeter.greet(sender.getName()));
return true;
}
}
There is no setExecutor call to write in onWeftEnable and none to clean up. Declare the
command in plugin.yml as usual, and @CommandHandler binds the executor to it.
The command name in the annotation is the one declared in plugin.yml, without the leading
slash. A name that no plugin.yml entry declares is logged and left unbound rather than failing
the plugin. To wire a command by hand instead, leave off @CommandHandler and set the executor
yourself in onWeftEnable from the loader.
Paper commands¶
On Paper 1.21.1 or newer, commands can skip plugin.yml and register through Paper's Brigadier
API instead. That is also the only way for plugins with a paper-plugin.yml, which cannot
declare commands. Swap the adapter for weftkit-paper and its processor, which bring the Bukkit
adapter along:
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
implementation("org.weftkit:weftkit-paper:0.6.0")
annotationProcessor("org.weftkit:weftkit-processor:0.6.0")
annotationProcessor("org.weftkit:weftkit-paper-processor:0.6.0")
}
Any @Wired component that carries @PaperCommand is registered by the
bundled paper:commands module. It implements one of two interfaces:
BasicCommand, Paper's own, for string argumentsCommandTree, for a full Brigadier tree rooted at the label it is handed
@Wired
@Singleton
@PaperCommand(value = "warps", description = "List every warp", aliases = "warplist")
final class WarpsCommand implements BasicCommand {
private final WarpStore warps;
WarpsCommand(WarpStore warps) {
this.warps = warps;
}
@Override
public void execute(CommandSourceStack source, String[] args) {
source.getSender().sendMessage("Warps: " + String.join(", ", warps.names()));
}
}
@Wired
@Singleton
@PaperCommand("warp")
final class WarpCommand implements CommandTree {
private final WarpStore warps;
WarpCommand(WarpStore warps) {
this.warps = warps;
}
@Override
public LiteralArgumentBuilder<CommandSourceStack> tree(String label) {
return Commands.literal(label)
.requires(source -> source.getExecutor() instanceof Player)
.then(Commands.argument("name", StringArgumentType.word()).executes(this::teleport));
}
private int teleport(CommandContext<CommandSourceStack> context) {
Location warp = warps.find(StringArgumentType.getString(context, "name"));
if (warp == null) return 0;
((Player) context.getSource().getExecutor()).teleport(warp);
return Command.SINGLE_SUCCESS;
}
}
It must be a @Singleton, and a @FeatureFlag registers it only
while its flag is on. The processor checks that a component is a singleton, implements exactly
one of the two interfaces, names a lowercase label and aliases free of whitespace, : and a
leading / that no other command of the plugin claims, and does not also carry @CommandHandler.
Commands behind different feature flags may share a label. While both flags are on, the first by
class name registers and the other is left unregistered with a warning.
Paper has no way to unregister a command. When the plugin disables, its commands are hidden instead: they fail every permission check, so they drop out of completion and stay unusable until Paper rebuilds its commands.
To run on Folia too, add weftkit-folia to the build and set folia-supported: true in
paper-plugin.yml, see Folia. The same jar then runs on Paper and Folia.
Related¶
- join-leave-messages: a runnable plugin around one wired listener
- player-homes: binds two commands with
@CommandHandler - warps: registers a
CommandTreeand aBasicCommandwith@PaperCommand - Components: constructor injection and the rules behind
@Wired - Modules and feature flags: the machinery that registers listeners, and how to gate one behind a flag
- Lifecycle: what has already happened by the time
onWeftEnableruns