Thread: Class based commands

Results 1 to 7 of 7
  1. #1 Class based commands 
    Registered Member Frostcore's Avatar
    Join Date
    Jul 2014
    Age
    27
    Posts
    6
    Thanks given
    0
    Thanks received
    1
    Rep Power
    0
    Would have posted in snippets, however the guidelines said considerable amounts of code go here, so uh here goes. If somebody wants to move it then please do.

    Before we get started: being a plugin developer for Bukkit I'm obviously used to rather different code style than most RSPS sources, what with plugins being loaded on startup/reload, etc. I admire how the command/plugin system works in Bukkit so I wondered "why not make something similar for an RSPS?" so here it is.

    If anyone wishes to be critical of this post then I only request you try do it as constructively as possible being my first post here, as soon as I've made more then feel free to tear this thread apart.

    Requirements
    • Matrix 718 source (I haven't used other bases, this may work for them, it may not.)
    • Patience
    • Knowledge of how to add .jar files as libraries in your preferred IDE.


    What are the results of this tutorial?

    The idea of this tutorial is to separate commands from being cluttered in a single file and to be able to add a command class and have the game automatically acknowledge that it exists.

    How does this work?

    The Reflections library is used to seek out every class that implements the RSCommand interface, a class called CommandManager then registers and processes commands whenever they're called.

    Why should I use this?

    Honestly, you don't have to. However I just prefer dealing with code in this sort of manner, some people may view it as messy but I'd much rather have a lot of class files lying around than a single class where you have to flick all over the place just to find what you want.

    Spoiler for Media:

    Command line view:


    Example of output, handled entirely by CommandManager minus the Args output which is a command I setup to ensure arguments were received properly from WorldPacketsDecoder


    Output when a user is denied access to a command or it returns CommandResult.SUCCESS





    Let's get started!

    First up add the Reflections library to your project - I use Maven with IntelliJ - links to the relevant files are available at the Reflections link above.

    You're going to have to create a package to store the code, I used "com.rs.commands" however I expect that some of the programming veterans here will have a more appropriate to put this

    * There is some manual problem solving you will have to do, for anyone with a brain it should be easy.

    IF anybody is interested then I've included the Javadoc comments so that people can read how I intended it to work and laugh at how it actually works.

    Once you've created that package create a new class called CommandManager and paste the following code into it.

    Spoiler for CommandManager.java:
    Code:
    package com.rs.commands;
     
    import com.rs.Settings;
    import com.rs.game.player.Player;
    import com.rs.utils.Logger;
    import org.reflections.Reflections;
     
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Set;
     
    import static com.rs.commands.CommandResult.EXCEPTION;
    import static com.rs.commands.CommandResult.NO_RIGHTS;
     
    /**
     * One class to rule them all.
     * This class is responsible for loading and registration of
     * all classes that implement the {@link RSCommand} interface.
     *
     * @author Connor Spencer Harries
     */
    public final class CommandManager {
        private static HashMap<String, Class<? extends RSCommand>> commandMap = new HashMap<>();
     
        /**
         * Register a new command and its associated class.
         * @param name command name.
         * @param command command class.
         * @return true if the command was registered.
         */
        public static boolean registerCommand(String name, Class<? extends RSCommand> command) {
            if(commandExists(name))
                return false;
            Method method = null;
            try {
                method = command.getMethod("run", Player.class, String[].class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            if(method != null) {
                if (!method.isAnnotationPresent(Command.class)) {
                    Logger.handle(new Exception("\"" + command.getSimpleName() + ".run()\" is Missing \"@Command\" Annotation."));
                    return false;
                }
                commandMap.put(name, command);
            }
            return commandMap.containsKey(name);
        }
     
        /**
         * Test to see whether a command exists or not, useful for using before you call things.
         * @param name
         * @return
         */
        public static boolean commandExists(String name) {
            return commandMap.containsKey(name);
        }
     
        /**
         * Try and process the command, very experimental right now as Reflection is
         * heavily used.
         *
         * @param sender command sender.
         * @param command command sent.
         * @param args args sent.
         * @return
         */
        public static boolean processCommand(Player sender, String command, String[] args) {
            if(commandExists(command)) {
                Class<? extends RSCommand> commandClass = getCommand(command);
                try {
                    Object commandInstance = commandClass.newInstance();
                    Method method = commandClass.getMethod("run", Player.class, String[].class);
                    Command commandParams = method.getAnnotation(Command.class);
                    if(sender.hasRights(commandParams.rights())) {
                        CommandResult result = (CommandResult) method.invoke(commandInstance, sender, args);
                        switch (result) {
                            case SUCCESS:
                                if(Settings.DEBUG) {
                                    Logger.log(String.format("Player \"%s\" executed command \"%s\" successfully.", sender.getUsername(), command));
                                }
                                return true;
                            case INVALID_ARGS:
                                sender.sendMessage(String.format(result.getMessage(), commandParams.usage()));
                                break;
                            case EXCEPTION:
                                sender.sendMessage(result);
                                break;
                            default:
                                break;
                        }
                    } else {
                        sender.sendMessage(NO_RIGHTS);
                        if(Settings.DEBUG) {
                            Logger.log("\"" + sender.getUsername() + "\" was denied access to command: \"" + command + "\"");
                        }
                    }
                } catch (Exception ex) {
                    sender.sendMessage(EXCEPTION);
                    if(sender.hasRights(Rights.DEVELOPER)) {
                        sender.sendMessage("<col=ff0000>See the console for more details.");
                    }
                    Logger.handle(ex);
                }
            } else {
                sender.sendMessage("<col=ff0000>Sorry, that command does not exist.");
            }
            return false;
        }
     
        /**
         * Get the class associated with a command.
         * @param name command to lookup.
         * @return null if the command has not yet been registered.
         */
        private static Class<? extends RSCommand> getCommand(String name) {
            return (commandMap.get(name) != null ? commandMap.get(name) : null);
        }
     
        /**
         * Register a command alias to allow more than one command
         * to trigger the code behind said command.
         *
         * @param existing name of command.
         * @param alias alias of command.
         * @return true if the command was registered.
         */
        public static boolean registerAlias(String existing, String alias) {
            Class<? extends RSCommand> command = getCommand(existing);
            if(command != null && !commandMap.containsKey(alias)) {
                commandMap.put(alias, command);
            } else {
                return false;
            }
            return (commandMap.get(alias) != null);
        }
     
        /**
         * Replacement for the constructor.
         */
        public static synchronized void init() {
            Reflections ref = new Reflections(Settings.COMMAND_ROOT);
            Set<Class<? extends RSCommand>> classes = ref.getSubTypesOf(RSCommand.class);
            for(Class<? extends RSCommand> commandClass : classes) {
                try {
                    Method run = commandClass.getMethod("run", Player.class, String[].class);
                    if(run.isAnnotationPresent(Command.class)) {
                        Command command = run.getAnnotation(Command.class);
                        String name = command.name();
                        if(registerCommand(name, commandClass)) {
                            int aliasCount = 0;
                            if (command.aliases().length > 0) {
                                String[] aliases = command.aliases();
                                for (String alias : aliases) {
                                    if(registerAlias(name, alias))
                                        aliasCount++;
                                }
                            }
                            if(aliasCount > 0) {
                                Logger.log("Registered command: \"" + name + "\" with " + aliasCount + " " + (aliasCount > 1 ? "aliases." : "alias."));
                            } else {
                                Logger.log("Registered command: \"" + name + "\" with no aliases.");
                            }
                        }
                    }
                } catch (Exception e) {
                    Logger.handle(e);
                }
            }
        }
    }


    If you go into your Launcher.java file add "CommandManager.init();" to the bottom of the main method.

    Next up is the Command interface, all the command information comes from this so uh, try not to screw up. (There'll be an explanation of how everything works - including this - at the bottom of the thread)

    Spoiler for Command.java:
    Code:
    package com.rs.commands;
     
    import java.lang.annotation.*;
     
     
    /**
     * Annotation to mark methods as command handling methods.
     */
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Command {
        /**
         * Minimum rights required to use the command.
         * Valid rights:
         * <ol>
         *     <li>PLAYER</li>
         *     <li>MOD</li>
         *     <li>ADMIN</li>
         * </ol>
         *
         * @return Required rights.
         */
        Rights rights() default Rights.PLAYER;
     
        /**
         * @return command name.
         */
        String name();
     
        /**
         * @return command usage.
         */
        String usage();
     
        /**
         * @return list of aliases used to execute the command.
         */
        String[] aliases() default {};
    }


    Up next is the enum that is responsible for sending response messages to players, it's possible to omit this however I like how simplistic this is and how practical it is.

    Spoiler for CommandResult.java:
    Code:
    package com.rs.commands;
     
    /**
     * Simple enum that contains possible command execution results.
     *
     * @author Connor Spencer Harries
     */
    public enum CommandResult {
        SUCCESS,
        NO_RIGHTS("<col=ff0000>You do not have permission to use that command."),
        INVALID_ARGS("<col=ff3636>Invalid command usage, try \"%s\""),
        EXCEPTION("<col=ff3636>An unknown exception occurred whilst executing the command.");
     
        /**
         * Message storage.
         */
        private final String message;
     
        /**
         * Assign no value to message.
         */
        private CommandResult() { message = null; }
     
        /**
         * Assign a custom message to the enum.
         * @param message message to be displayed when toString() is called.
         */
        private CommandResult(final String message) {
            this.message = message;
        }
     
        public String getMessage() {
            return this.message;
        }
     
        /**
         * Lazy way to use the enum values.
         * @return appropriate message.
         */
        @Override
        public String toString() {
            return this.message;
        }
    }


    This is the interface that all commands will have to implement in order to be recognised by the CommandManager init method.

    Spoiler for RSCommand.java:

    Code:
    package com.rs.commands;
     
    import com.rs.game.player.Player;
     
    /**
     * Superclass for all text based commands.
     *
     * @see com.rs.commands.CommandResult
     * @author Connor Spencer Harries
     */
    public interface RSCommand {
        /**
         * Code that gets called when the command is executed.
         * @param args Arguments to pass.
         * @return true if the command executed correctly.
         */
        public CommandResult run(Player player, String[] args);
    }


    Finally another practical enum, you can easily alter this and have it update in numerous places at once, no more annoying tapping numbers in everywhere to check!

    Spoiler for Rights.java:

    Code:
    package com.rs.commands;
     
    /**
     * Possible replacement for all right-based actions.
     * @author Connor Spencer Harries
     */
    public enum Rights {
        PLAYER(0),
        MOD(1),
        ADMIN(2),
        DEVELOPER(1337);
     
        private final int rights;
     
        private Rights(final int rights) {
            this.rights = rights;
        }
     
        public Integer get() {
            return this.rights;
        }
     
        public static Rights rightsFromInteger(Integer integer) {
            for(Rights rights : values()) {
                if(rights.get().equals(integer))
                    return rights;
            }
            return PLAYER;
        }
    }


    That's it for the "com.rs.commands" package, now comes the problem solving.

    BONUS:
    Here is an example of what your commands will now look like, I went with a simple one just to give off the general idea. (This file goes in the "com.rs.commands.executors" package - that's the package I used for my COMMAND_ROOT)

    Spoiler for Teleport.java:

    Code:
    package com.rs.commands.executors;
     
    import com.rs.commands.Command;
    import com.rs.commands.CommandResult;
    import com.rs.commands.RSCommand;
    import com.rs.commands.Rights;
    import com.rs.game.WorldTile;
    import com.rs.game.player.Player;
     
    import static com.rs.commands.CommandResult.*;
     
    /**
     * Information:
     * Created at: 13:22 on 19/07/2014
     * Project: Runescape - Server
     * Created by Connor Harries.
     */
    public class Teleport implements RSCommand {
        @Command(
                name = "tele",
                usage = "tele X Y",
                rights = Rights.MOD,
                aliases = { "tp", "tpto", "teleport" }
        )
        @Override
        public CommandResult run(Player player, String[] args) {
            try {
                player.resetWalkSteps();
                if (args.length < 2)
                    return INVALID_ARGS;
                player.setNextWorldTile(new WorldTile(Integer.valueOf(args[0]), Integer.valueOf(args[1]), player.getPlane()));
            } catch (NumberFormatException ex) {
                return INVALID_ARGS;
            } catch (Exception e) {
                return EXCEPTION;
            }
            return SUCCESS;
        }
    }


    Now that I've wasted enough of your time I feel like some explanation is due!

    1. Scan Settings.COMMAND_ROOT for any classes implementing RSCommand
    2. Test class one-by-one for the @Command annotation
    3. Get name from @Command and call #registerCommand(), passing the #name() and class as arguments.
    4. Check @Command for command aliases & register them if any are specified
    5. Wait for the command to be called


    When a command is called #get() is called on the HashMap and the #run(Player, String[]) method is called.




    You can now replace the default code at line 1365 (for me at least) with this code in order to tell the CommandManager class to handle commands, there's another place which you will have to edit in order to make it handle console commands, however I currently have no implementation for differentiating between chat and console commands.

    Spoiler for WorldPacketsDecoder.java:

    Code:
    if (message.startsWith("::")) {
        String[] split = message.replace("::", "").split(" ");
        String command = split[0];
        if (command.length() == 0)
            return;
        String[] args = new String[split.length - 1];
        if(split.length > 1) {
            System.arraycopy(split, 1, args, 0, split.length - 1);
        }
        CommandManager.processCommand(player, command, args);
        return;
    }


    All code used in this tutorial is available on GitHub as a Gist. You can also click here to download the latest version as a zip archive.




    Fancy nerding on? Keep reading.

    Here's yet another example of a command using this setup:
    Code:
    package com.rs.commands.executors;
     
    import com.rs.commands.Command;
    import com.rs.commands.CommandResult;
    import com.rs.commands.RSCommand;
    import com.rs.commands.Rights;
    import com.rs.game.World;
    import com.rs.game.player.Player;
    import com.rs.utils.SerializableFilesManager;
    import com.rs.utils.Utils;
     
    import static com.rs.commands.CommandResult.*;
     
    /**
     * Simple command to promote users.
     * @author Connor Spencer Harries.
     */
    public class Promote implements RSCommand {
        @Command(
                name = "promote",
                usage = "promote username",
                rights = Rights.ADMIN,
                aliases = { "rankup" }
        )
        @Override
        public CommandResult run(Player player, String[] args) {
            if(args.length < 1)
                return INVALID_ARGS;
            String target = args[0];
            Player worker = World.getPlayerByDisplayName(target);
            if(worker == null) {
                worker = SerializableFilesManager.loadPlayer(Utils.formatPlayerNameForProtocol(target));
            }
            if(worker == null) {
                return EXCEPTION;
            }
            worker.setRights(Rights.MOD);
            SerializableFilesManager.savePlayer(worker);
            return SUCCESS;
        }
    }
    The Command annotation
    You may or may not have seen annotations like this before, if you haven't I'll be surprised honestly unless you're a leech. Standard Java annotations include @Deprecated and @Override, these are the two I see most commonly at least.

    But they don't have parameters?!

    No, they don't require parameters to do their job. My Command annotation has parameters because without them the CommandManager class doesn't know how to deal with the command itself.

    What data can the annotation contain?

    The annotation has four fields, two of which have the most basic defaults available, the other two (name & usage) MUST be assigned to - though I'm sure you clever people can figure out how to make it so only the name is required.

    Fields:
    • name - the main command itself
    • rights - the minimum rights required to actually trigger the run method
    • aliases - a String[] containing other commands which can be used to trigger the run method
    • usage - how to use the command, eg: "tele X Y"
    Last edited by Frostcore; 07-20-2014 at 07:11 PM. Reason: Updated information.
    Reply With Quote  
     

  2. #2  
    Banned

    Join Date
    Nov 2013
    Posts
    270
    Thanks given
    107
    Thanks received
    76
    Rep Power
    0
    Way more complicated than it needs to be.
    Reply With Quote  
     

  3. #3  
    Registered Member
    Zivik's Avatar
    Join Date
    Oct 2007
    Age
    28
    Posts
    4,421
    Thanks given
    891
    Thanks received
    1,527
    Rep Power
    3285
    Little much in my opinion. Good job though.
    Reply With Quote  
     

  4. #4  
    Registered Member Frostcore's Avatar
    Join Date
    Jul 2014
    Age
    27
    Posts
    6
    Thanks given
    0
    Thanks received
    1
    Rep Power
    0
    Quote Originally Posted by ub3r View Post
    Way more complicated than it needs to be.
    Eh, I tried at least
    I think it could be because I wasn't sure about how to do it in the most efficient manner so I just fell back on what I already knew to do it.
    I'm more than happy accepting edits and corrections

    Quote Originally Posted by Zivik View Post
    Little much in my opinion. Good job though.
    Thanks
    Reply With Quote  
     

  5. #5  
    Banned Class based commands Market Banned


    Join Date
    Jan 2011
    Age
    26
    Posts
    3,112
    Thanks given
    1,198
    Thanks received
    1,479
    Rep Power
    0
    Quote Originally Posted by Frostcore View Post
    I admire how the command/plugin system works in Bukkit so I wondered "why not make something similar for an RSPS?" so here it is.
    already exists pretty much, look into apollo
    Reply With Quote  
     

  6. #6  
    Registered Member Frostcore's Avatar
    Join Date
    Jul 2014
    Age
    27
    Posts
    6
    Thanks given
    0
    Thanks received
    1
    Rep Power
    0
    Quote Originally Posted by lare96 View Post
    already exists pretty much, look into apollo
    Ah, my bad for not looking around more.
    Specifically this was targeting Matrix though so I met my expectations at least
    Reply With Quote  
     

  7. #7  
    Banned Class based commands Market Banned


    Join Date
    Jan 2011
    Age
    26
    Posts
    3,112
    Thanks given
    1,198
    Thanks received
    1,479
    Rep Power
    0
    Quote Originally Posted by Frostcore View Post
    Ah, my bad for not looking around more.
    Specifically this was targeting Matrix though so I met my expectations at least
    no worries man, always nice to see progressive thinkers in the community
    Reply With Quote  
     

  8. Thankful user:



Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Similar Threads

  1. [614]:: HashMap Based Commands[614]
    By `Eclipse™ in forum Tutorials
    Replies: 15
    Last Post: 12-28-2010, 02:19 AM
  2. Replies: 7
    Last Post: 09-26-2010, 07:43 PM
  3. Teleporting Class, Base.
    By Greyfield in forum Snippets
    Replies: 14
    Last Post: 02-27-2010, 04:29 PM
  4. Simple Commands For Delta based servers.
    By pvp pker in forum Tutorials
    Replies: 6
    Last Post: 03-28-2009, 07:35 AM
  5. Making the lil base on magic into a Full class file
    By `Lubricant in forum Configuration
    Replies: 7
    Last Post: 09-14-2008, 09:33 PM
Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •