Bukkit Plugin Structure Design - Handling Child Commands Using Annotations

A few words to introduce the situation.

Context:. To make my workflow easier when writing Bukkit plugins (basically the de facto API for the Minecraft server until Sponge starts to implement it), I decided to put together a “mini-framework” so I don’t have to repeat the same tasks again and again. (Also, I am trying to develop it so as not to depend too much on Bukkit, so I can continue to use it on Sponge just by changing my implementation)

Intention: Managing teams at Bukkit is, frankly, a mess. You have to define your root command (for example, you want to run / test ingame, "test" is the root directory) in the YML file (instead of calling some factory?), The processing of the child commands is nonexistent and implements the details are hidden, so getting 100 % reliable results are difficult. This is the only part of Bukkit that annoys me, and it was the main initiator of me, who decided to write a structure.

Purpose: Give up the nasty Bukkit management team and replace it with something that clears up.


Work with her:

This will be a long paragraph, where I will explain how Bukkit command processing is initially performed, as it will provide a deeper understanding of important command parameters, etc.

Any user connected to the Minecraft server can start a chat message using '/', which will lead to its analysis as a command.

To offer an example of the situation, any player in Minecraft has a life bar, which by default has a limit of 10 hearts and is depleted when dealing damage. The maximum and current "heart" (read: health) can be set by the server at any time.

Suppose we want to define a command like this:

/sethealth <current/maximum> <player or * for all> <value> 

To start implementing this ... oh boy. If you like clean code, I would say skip this ... I will comment to explain, and whenever I feel that Bukkit made a mistake.

Required plugin.yml:

 # Full name of the file extending JavaPlugin # My best guess? Makes lazy-loading the plugin possible # (aka: just load classes that are actually used by replacing classloader methods) main: com.gmail.zkfreddit.sampleplugin.SampleJavaPlugin # Name of the plugin. # Why not have this as an annotation on the plugin class? name: SamplePlugin # Version of the plugin. Why is this even required? Default could be 1.0. # And again, could be an annotation on the plugin class... version: 1.0 # Command section. Instead of calling some sort of factory method... commands: # Our '/sethealth' command, which we want to have registered. sethealth: # The command description to appear in Help Topics # (available via '/help' on almost any Bukkit implementation) description: Set the maximum or current health of the player # Usage of the command (will explain later) usage: /sethealth <current/maximum> <player/* for all> <newValue> # Bukkit has a simple string-based permission system, # this will be the command permission # (and as no default is specified, # will default to "everybody has it") permission: sampleplugin.sethealth 

The main class of the plugin:

 package com.gmail.zkfreddit.sampleplugin; import org.bukkit.command.PluginCommand; import org.bukkit.plugin.java.JavaPlugin; public class SampleJavaPlugin extends JavaPlugin { //Called when the server enables our plugin @Override public void onEnable() { //Get the command object for our "sethealth" command. //This basically ties code to configuration, and I'm pretty sure is considered bad practice... PluginCommand command = getCommand("sethealth"); //Set the executor of that command to our executor. command.setExecutor(new SampleCommandExecutor()); } } 

Executor:

 package com.gmail.zkfreddit.sampleplugin; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; public class SampleCommandExecutor implements CommandExecutor { private static enum HealthOperationType { CURRENT, MAXIMUM; public void executeOn(Player player, double newHealth) { switch (this) { case CURRENT: player.setHealth(newHealth); break; case MAXIMUM: player.setMaxHealth(newHealth); break; } } } @Override public boolean onCommand( //The sender of the command - may be a player, but might also be the console CommandSender commandSender, //The command object representing this command //Why is this included? We know this is our SetHealth executor, //so why add this as another parameter? Command command, //This is the "label" of the command - when a command gets registered, //it name may have already been taken, so it gets prefixed with the plugin name //(example: 'sethealth' unavailable, our command will be registered as 'SamplePlugin:sethealth') String label, //The command arguments - everything after the command name gets split by spaces. //If somebody would run "/sethealth acb", this would be {"a", "c", "b"}. String[] args) { if (args.length != 3) { //Our command does not match the requested form {"<current/maximum>", "<player>", "<value>"}, //returning false will, ladies and gentleman... //display the usage message defined in plugin.yml. Hooray for some documented code /s return false; } HealthOperationType operationType; double newHealth; try { //First argument: <current/maximum> operationType = HealthOperationType.valueOf(args[0].toUpperCase()); } catch (IllegalArgumentException e) { return false; } try { //Third argument: The new health value newHealth = Double.parseDouble(args[2]); } catch (NumberFormatException e) { return false; } //Second argument: Player to operate on (or all) if (args[1].equalsIgnoreCase("*")) { //Run for all players for (Player player : Bukkit.getOnlinePlayers()) { operationType.executeOn(player, newHealth); } } else { //Run for a specific player Player player = Bukkit.getPlayerExact(args[1]); if (player == null) { //Player offline return false; } operationType.executeOn(player, newHealth); } //Handled successfully, return true to not display usage message return true; } } 

Now you can understand why I decided to abstract the processing of commands within my framework. I do not think that I am lonely, thinking that this method is not self-documenting and processing child commands in this way does not seem to be correct .


My intention:

How Bukkit event system works , I want to develop a framework / API to distract it.

My idea annotates the command methods with the corresponding annotation, which includes all unclassified information, and use some kind of registrar to register the command (in case of an event: Bukkit.getPluginManager().registerEvents(Listener, Plugin) ).

Again similar to the event API, command methods will have a specific signature. Since considering several parameters is annoying, I decided to pack it all in the context interface (also, thus, I do not violate all the previous code if I need to add something to the context!). However, I also need a return type if I want to quickly display usage (but I'm not going to choose a boolean, that's for sure!) Or do some other things. So, the signature of my idea comes down to CommandResult <anyMethodName>(CommandContext) .

Then, command registration created command instances for annotated methods and registered them.

My basic plan has taken shape. Please note that I have not come to writing JavaDoc yet, I added some quick comments to non-self-documenting code.

Team Registration:

 package com.gmail.zkfreddit.pluginframework.api.command; public interface CommandRegistration { public static enum ResultType { REGISTERED, RENAMED_AND_REGISTERED, FAILURE } public static interface Result { ResultType getType(); //For RENAMED_AND_REGISTERED Command getConflictCommand(); //For FAILURE Throwable getException(); //If the command got registered in some way boolean registered(); } Result register(Object commandObject); } 

Listing the result of a command:

 package com.gmail.zkfreddit.pluginframework.api.command; public enum CommandResult { //Command executed and handlded HANDLED, //Show the usage for this command as some parameter is wrong SHOW_USAGE, //Possibly more? } 

Command Context:

 package com.gmail.zkfreddit.pluginframework.api.command; import org.bukkit.command.CommandSender; import java.util.List; public interface CommandContext { CommandSender getSender(); List<Object> getArguments(); @Deprecated String getLabel(); @Deprecated //Get the command annotation of the executed command Command getCommand(); } 

The main annotation of a command that should be placed in command methods:

 package com.gmail.zkfreddit.pluginframework.api.command; import org.bukkit.permissions.PermissionDefault; public @interface Command { public static final String DEFAULT_STRING = ""; String name(); String description() default DEFAULT_STRING; String usageMessage() default DEFAULT_STRING; String permission() default DEFAULT_STRING; PermissionDefault permissionDefault() default PermissionDefault.TRUE; Class[] autoParse() default {}; } 

The goal of autoParse is that I can determine something quickly, and if the parsing fails, it just displays a message about using the command.

Now, as soon as my implementation is written down, I can rewrite the sethealth command executor mentioned above to something like this:

 package com.gmail.zkfreddit.sampleplugin; import de.web.paulschwandes.pluginframework.api.command.Command; import de.web.paulschwandes.pluginframework.api.command.CommandContext; import org.bukkit.entity.Player; import org.bukkit.permissions.PermissionDefault; public class BetterCommandExecutor { public static enum HealthOperationType { CURRENT, MAXIMUM; public void executeOn(Player player, double newHealth) { switch (this) { case CURRENT: player.setHealth(newHealth); break; case MAXIMUM: player.setMaxHealth(newHealth); break; } } } @Command( name = "sethealth", description = "Set health values for any or all players", usageMessage = "/sethealth <current/maximum> <player/* for all> <newHealth>", permission = "sampleplugin.sethealth", autoParse = {HealthOperationType.class, Player[].class, Double.class} //Player[] as there may be multiple players matched ) public CommandResult setHealth(CommandContext context) { HealthOperationType operationType = (HealthOperationType) context.getArguments().get(0); Player[] matchedPlayers = (Player[]) context.getArguments().get(1); double newHealth = (Double) context.getArguments().get(2); for (Player player : matchedPlayers) { operationType.executeOn(player, newHealth); } return CommandResult.HANDLED; } } 

I believe that what I most say is that this method feels cleaner.

So where do I ask the question here?

Where am i stuck

Handling child commands.

In this example, I managed to get away with a simple enumeration based on two cases for the first argument.

There may be times when I have to create many child commands, similar to "current / maximum". A good example is that which allows you to bring players together in a team - I need:

 /team create ... /team delete ... /team addmember/join ... /team removemember/leave ... 

etc .. - I want to be able to create separate classes for these child commands.

How exactly am I going to introduce a clean way of saying, “Hey, when the first argument of this matches something, do this and that!” - hell, the "agreed" part doesn't even have to be a hard-coded string, I might need something like

 /team [player] info 

at the same time, while retaining all previous child commands.

Not only do I need to bind child command methods, I also need to somehow bind the required object - in the end, my (future) command registration will take an instance of the object (in the example BetterCommandExecutor example) and register This. As I say, "Use this instance of the child command!" to register when passing the facility?

I was thinking about saying "**** everything, a reference to the child command class and just instantiating the no-args constructor", but while this will probably produce the smallest code, it won’t give much insight into how exactly instances of child teams. If I decide to go this way, I just define the childs parameter in my Command annotation and make some @ChildCommand annotation list for it (annotations in the annotation? Yo dawk, why not?).


So, after all this, the question arises: with this setting, can I clearly define the child commands, or do I need to completely change my base? I was thinking of expanding from a kind of abstract BaseCommand (with the abstract getChildCommands () method), but the annotation method has the advantage of being able to process multiple commands from the same class. Besides, as far as I still understood the open source code, I get the impression that extends is 2011 and implements is the taste of the year, so I should not force myself to renew something every time I create some kind of command handler.

I apologize for the long post. This went on longer than I expected: /


Edit # 1:

I just realized that what I basically create is some kind of ... tree? teams. However, just using any CommandTreeBuilder is not necessary, because it contradicts one of the things I wanted from this idea: Ability to define several command handlers in one class. Back to the brainstorming session.

+6
source share
2 answers

The only thing I can think of is to share your annotations. You will have one class in which the base command will be an annotation, and then methods in this class with various subcommands:

 @Command("/test") class TestCommands { @Command("sub1"// + more parameters and stuff) public Result sub1Command(...) { // do stuff } @Command("sub2"// + more parameters and stuff) public Result sub2Command(...) { // do stuff } } 

If you want more flexibility, you can also take the inheritance hierarchy, but I'm not sure that there will be self-documentation then (as part of the commands will be hidden in the parent classes).

This solution, however, does not solve your /team [player] info example, but I think this is an insignificant thing. In any case, it would be difficult to enter subcommands into different parameters of your team.

+1
source

The standard Bukkit API for processing commands is pretty good in my opinion, so why not use it? I think you are just embarrassed, then you avoid it. This is how I do it.

Register a team

Create a new section called commands , where you put them all as child nodes.

 commands: sethealth: 

Avoid using the permission key: we will check this later. Avoid using the usage key: it is difficult to write a large error message that is valid in each case. In general, I hate these helper keys, so leave the parent node blank.

Handle it in class

Use a separate class that implements the CommandExecutor interface.

 public class Sethealth implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) { // ... return true; } } 

Add the following class under the onEnable() method in the main class.

 getCommand("sethealth").setExecutor(new Sethealth()); 

You do not need to check command.getName() if you use this class only for this command. Make the return method true anyway: you did not detect the error message, so why do you get it?

Make safe

You no longer have to worry about processing sender in the first line. In addition, you can check all common permissions here.

 if (!(sender instanceof Player)) { sender.sendMessage("You must be an in-game player."); return true; } Player player = (Player)sender; if (!player.hasPermission("sethealth.use")) { player.sendMessage(ChatColor.RED + "Insufficient permissions."); return true; } // ... 

You can use colors to make messages more readable.

Work with arguments

Simple production of 100% reliable results. This is just an incomplete example of how you should work.

 if (args.length == 0) { player.sendMessage(ChatColor.YELLOW + "Please specify the target."); return true; } Player target = Server.getPlayer(args[0]); if (target == null) { player.sendMessage(ChatColor.RED + "Target not found."); return true; } if (args.length == 1) { player.sendMessage(ChatColor.YELLOW + "Please specify the new health."); return true; } try { double value = Double.parseDouble(args[1]); if (value < 0D || value > 20D) { player.sendMessage(ChatColor.RED + "Invalid value."); return true; } target.setHealth(value); player.sendMessage(ChatColor.GREEN + target.getName() + " health set to " + value + "."); } catch (NumberFormatException numberFormat) { player.sendMessage(ChatColor.RED + "Invalid number."); } 

Plan your code with security points , and if you want subcommands, always check them with String.equalsIgnoreCase(String) .

+1
source

Source: https://habr.com/ru/post/982585/


All Articles