Resource Command Handler for your Plugins!

Discussion in 'Spigot Plugin Development' started by KKosyfarinis, Feb 9, 2018.

  1. (* Before reading, this is not something where I need help with something, but I was not sure where to post this. It's a command handler that could help with plugin development. If you know where to post this, please leave a reply or a DM! Thanks! *

    Command Handler
    Many of you when creating a plugin, might not be sure as to a "good" command handler, and instead of adding each and every command when the plugin starts, you can just create a new command class and start coding the command right away!

    There are a lot of ways that you can go for doing that, but the one that's in this post is using Java Reflections.

    For example, you can create the command "/help" with the alias commands "/h" and "/helpme". It will automatically find the class "Commandhelp.java" and run the methods from that class. it will also have some default methods, that can run even if a method doesn't exist inside of "Commandhelp.java".

    First of all, create the class "CommandHandler". You will need those two static Strings as well in that class:
    Code (Text):
    private static final String PACKAGE_PATH = "com.example.commands";

    private static final String COMMAND_PREFIX = "Command";
    After that, let's create the constructor and some private variables for the class that we will be using later.
    Code (Text):
    private Server server;
    private CommandSender sender;
    private String label;
    private String[] args;

    public CommandHandler(final Server server, final CommandSender sender, final String commandLabel, final String label, final String[] args) {
        this.args = args;
        this.label = label;
        this.sender = sender;
        this.server = server;
    }
    Now for the fun part. We'll create a method "execute" that will run the command (You can add the execute() inside of the command handler, but I like to manually call it)

    Those lines of code will also need a few catches with try-catch. They will be mentioned after the main functionality of the method "execute" is finished.

    First of all, we will have to create a Class variable, for the class that has the command.
    Code (Text):
    Class<?> cmdClass= Class.forName(PACKAGE_PATH + "." + COMMAND_PREFIX + label);
    For example, if you execute the command "testcmd", and the Class for it ("testpckg.commands.Commandtestcmd")

    Next, we'll be instancing the class, we can do this by doing this:
    Code (Text):
    Object classInstance = cmdClass.newInstance();
    Now, let's create a Method variable, that we will be creating the functionality of it later on. To do this:
    Code (Text):
    Method method;
    Let's "assign" to this method the "setParams(...)" method that will be setting up the Command Sender, Label, etc for our command to use. Before we do that, let's assign the parameters that the method will accept.
    Code (Text):
    Class[] params = new Class[] {Server.class, CommandSender.class, String.class, String[].class}; //You can add/modify what it can accept as parameters.
    method = cmdClass.getSuperClass().getDeclaredMethod("setParams", params);
    Next, let's call that method, and pass it the arguments we have (that we got from the constructor).
    Code (Text):
    method.invoke(classInstance, server, sender, label, args);
    (We will be creating the method setParams later)
    Now, we can go ahead and call each action that a command can do. Let's call those methods Run and RunConsole. Both of those will have a method for running without arguments and one with arguments.

    To do this, it's just a few if-statements. Here, you can perform the actions you would, to check if there are arguments passed by the player. If it actually is a player, and if it is a console, etc.

    Code (Text):
        if(args.length != 0) {
                    if(sender instanceof Player) {
                        method = cmdClass.getDeclaredMethod("Run");
                        if(method == null) {
                            method = cmdClass.getSuperclass().getDeclaredMethod("Run");
                        }
                    } else {
                        method = cmdClass.getDeclaredMethod("RunConsole");
                        if(method == null) {
                            method = cmdClass.getSuperclass().getDeclaredMethod("RunConsole");
                        }
                 
                        method.invoke(cmdObj);
                    }
                } else {
             
                    params = new Class[] {String[].class};
             
                    if(sender instanceof Player) {
                        method = cmdClass.getDeclaredMethod("Run", params);
                        if(method == null) {
                            method = cmdClass.getSuperclass().getDeclaredMethod("Run", params);
                        }
                    } else {
                        method = cmdClass.getDeclaredMethod("RunConsole", params);
                        if(method == null) {
                            method = cmdClass.getSuperclass().getDeclaredMethod("RunConsole", params);
                        }
                 
                        method.invoke(cmdObj, args);
                    }
                }
     
    Let's break it down what this does.
    IF no arguments were sent, check IF the sender is a player and run either "Run" or "RunConsole if it's not a player. We also check IF there is a method inside of the Command class and IF there is not, we run the default method for a command, which we will create later. Also, IF there are arguments, it's the same thing but we use the (Class[] variable) "params" that we created later, to add a parameter to the "Run" and "RunConsole". Basically, it's a few If statements.

    Lastly, everything inside of the "execute" method, should be around try-catch with

    Code (Text):
    try {
        //Everything we wrote in the execute class
    } catch( IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException | InstantiationException | ClassNotFoundException e ) {
        //It is up to you on how you would want to handle those exceptions!
    }
    So, basically, the entire class should look something like this: https://hastebin.com/xivuguhupi.vbs

    Next up, let's create the base class of all of our commands. Let's call this "CommandInfo" (Not sure what to name it).

    So, first of all, let's create the setParams method in our class, and also the variables that we can access from the commands that we create later on.

    Code (Text):
    public class CommandInfo {

        protected Server server;
        protected CommandSender sender;
        protected String label;
        protected String[] args;
     
        public void setParams(Server server, CommandSender sender, String label, String[] args) {
            this.server = server;
            this.sender = sender;
            this.label = label;
            this.args = args;
        }

     
    Then, let's create the default methods that every command will run, if no other method is found.
    Code (Text):

     
        void Run() {
            sender.sendMessage("You have run the command /" + label);
        }
     
        void Run(String[] args) {
            sender.sendMessage("You have run the command /" + label + " (includes args)");
        }
     
        void RunConsole() {
            sender.sendMessage("You have run the command /" + label);
        }
     
        void RunConsole(String[] args) {
            sender.sendMessage("You have run the command /" + label + " (includes args)");
        }
     
    I've chosen to simply send a message to the sender, saying that they have run a command, and that it includes arguments (if it does).

    And, that's about it for this class.

    Next up let's create a command (finally, right?).

    Now, all you need to do to create a new command is simply create a class that has the COMMAND_PREFIX prefix, and then the name of the command that you have given inside of "plugin.yml" and extend the CommandInfo class! Let's create the command /heal [player].

    Code (Text):

    import org.bukkit.Bukkit;
    import org.bukkit.entity.Player;
    public class Commandheal extends BaseCommand {
     
        void Run() {
            Player player = Bukkit.getPlayer(sender.getName());
     
            player.setHealth(20.0);
            player.sendMessage("You have been healed");
        }
     
        void Run(String[] args) {
            //Assume it's a valid player after the /heal command.
            Player player = Bukkit.getPlayer(args[0]);
     
            player.setHealth(20.0);
            player.sendMessage("You have been healed");
        }
     
        void RunConsole(String[] args) {
            //Assume it's a valid player after the /heal command.
            Player player = Bukkit.getPlayer(args[0]);
     
            player.setHealth(20.0);
            player.sendMessage("You have been healed");
        }
     
     
    }
     
    As you can see the no argument RunConsole method does not exist, that means that when you run the command, it will default to the RunConsole method from the CommandInfo class that we made. It's that easy to create a new command. Extend and create a few methods and before you know it, you've made a new command!

    Now, the way you "add" this command handler to your plugin, is that you go to the onCommand(...) method that you have, at the class that extends JavaPlugin (if you don't, just add that method) and do this
    Code (Text):

        public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
            CommandHandler command = new CommandHandler(getServer(), sender, label, args);
            command.execute();
        }
     
    We are almost done. As you might of have noticed till now, we have not added a command to get the "original" command label, and not just an alias of it. Let's create a class named "CommandHandlerGetAlternative"

    We will need a way to store each alias in the "plugins.yml" file and get their "parent" command name. So, since it's been rather long into this tutorial, I will paste the code for it. If any of you need a tutorial on this, I will post it in a separate thread)
    Code (Text):

    public class CommandHandlerGetAlternative {
        public CommandHandlerGetAlternative(Plugin plugin)
        {
            if(!alternatives.isEmpty()) {
                return;
            }
     
            setAlt(plugin);
        }
     
        private static Map<String, String> alternatives = new HashMap<>();
     
        private static void setAlt(Plugin plugin)
        {
            Map<String, Map<String, Object>> commands = plugin.getDescription().getCommands();
            for(String command : commands.keySet()) {
         
                Object aliases = commands.get(command).get("aliases");
                for(String alias : String.valueOf(aliases).split(",")) {
                    alternatives.put(alias.replace("[", "").replace("]", "").replace(" ", ""), command);
                }
                alternatives.put(command, command);
            }
        }
     
        public static String getAlt(String label)
        {
            String commandLabel = alternatives.getOrDefault(label, label).toLowerCase();
     
            return commandLabel;
        }
    }

     
    So, to "initialize" this, all you have to do is simply, in your onEnable() method, add this line of code:
    Code (Text):

    new CommandHandlerGetAlt(this);
     
    and last but not least, instead of passing directly the "label" variable from onCommand(...), you do this:

    Code (Text):

        public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
            label = CommandHandlerGetAlt.getAlt(label); //Added this line of code
            CommandHandler command = new CommandHandler(getServer(), sender, label, args);
            command.execute();
        }
     
    If everything goes according to plan, you should be able to create new commands, just by adding a new class and extending from the CommandInfo class!

    If any of you have any questions, and if you've made it this far, leave a reply bellow!
    Also, if something doesn't work, please post it bellow and I'll try to solve it!

    Thanks a lot for your time!


     
    #1 KKosyfarinis, Feb 9, 2018
    Last edited: Feb 9, 2018
  2. Mas

    Mas

    You're using reflection poorly - you should cache any methods/fields you fetch with reflection, else it will just mean ezcess overhead for no gain. Creating a new instance of the command class each time it's executed also is really unnecesary.

    I'm not really a fan of how you're trying to go about things - you assume that each command class has no explicit constructor declared when you create an instance of it, destroying the ability to use dependnecy injection in your codebase. If you were to use a proper dependency injection library such as Google Guice, you'd be able to solve this.
     
    • Like Like x 1
  3. It's a decent resource (add resource tag to title) nevertheless.
     

  4. Not that good with reflections since I just started using them and such, but thanks for the reply. Anyways, it's inspired from Unity's Scrips, and you've probably seen how much you can do with those. I'm trying to make it better at the moment. Any suggestions other than caching?
     
  5. Use methodhandle and varhandle(required java 9)
     
  6. I would suggest using this util file for caching reflection. You can obviously write it yourself, but it will help you if you have no idea how to do it. https://github.com/jkcclemens/Stora...age/shaded/net/amoebaman/util/Reflection.java

    (btw it depends on this file aswell https://github.com/jkcclemens/Stora...e/shaded/net/amoebaman/util/ArrayWrapper.java)
     

Share This Page