Command System

Discussion in 'Plugin Development' started by I Al Istannen, Jul 1, 2016.

Thread Status:
Not open for further replies.
  1. Offline

    I Al Istannen

    Hey,
    I recently thought a command system would be a nice thing to develop, as I always find myself creating a bad one for every new plugin.
    After a bit of arguing with myself, I thought I would create it in the fashion of a tree, as that allows child commands quite easily.

    And now I am standing here, having developed a system, which can certainly be improved by a lot. (Noticed the emphasis? It is the main reason why I am posting here and not somewhere in the Resource section. I wouldn't want to post a half-baked, inefficient and maybe overcomplicated or not working resource there. Which is hopefully understandable :p I also don't think it belongs in WIP, so I chose this forum... :))

    As I have absolutly no idea how to deal with git, you will find the source (Zip archive, the two exported eclipse projects inside) and a compiled version (also with source, so the IDE should be intelligent enough to show the Javadoc...) later.

    The basic System is as follows:
    You have an abstract class called "CommandNode", which is a node in a Tree. This node offers a few methods, most of whom are private or protected, so you won't really need to deal with them :p
    It has a few basic attributrs (name, keyword, Pattern, permission) a parent, a list with children and a Predicate. The Predicate checks if the commandSender is accpeted. I will say more about that later.

    For the most things I said are public getters or validators (matchesPattern, hasPermission, acceptsCommandSender) available.

    Then there are two more main methods.
    One is "onTabComplete" which traverses all children recursively and tries to pick out the most fitting one. It also respects the permission and the acceptsCommandSender method.
    The other is "execute". It does exactly this. It finds the most fitting node and lets it handle the execute method.


    I also made 3 CommandNode classes, which are a bit more specialized. They are PlayerCommandNode, ConsoleCommandNode and BlockCommandNode. They will only allow their specific sender to execute or tab complete them and provide new abstract methods, supplying the correct sender.
    So, in the PlayerCommandNode class you won't have an "execute(CommandSender", but an "execute(Player". This makes casting redundant.

    One of the nicer things is that you can have multiple commands with the same keyword and/or pattern. This means, you can have two (or more) classes for the command "test" with the pattern "test". One of them extends ConsoleCommandNode, the other PlayerCommandNode.
    Now, if a player executes "/basecommand test", it will execute the one extending PlayerCommandNode. If the console does it, it will execute the one extending ConsoleCommandNode.
    This means, you can have two versions of one command, each in their own class, but one for players and one (possibly also taking a location) for the console.

    The other main thing are subcommands. You can have "test" and two children "crash" and "dummy". Each of them is quite complex, which makes a if clause on the first argument quite messy.
    With this system, you register "crash" and "dummy" as children of "test", and they will be availlable for tabcompletion and can also be executed just like test. They will be passed the correct arguments too. So "/basecommand test dummy hey" will execute "dummy" with the arguments "hey" and "/basecommand test crash" will just execute "crash".

    Another thing is the help command. How would you do that? Well, the default implementation of the CommandListener thinks of that. You can add the "@HelpCommandNode" annotation to a CommandNode class, and it will be used as the main help command, if no valid command was detected. This happens for example, when the user just writes the baseCommand, without any additional parameters.

    There are a few other useful classes. One is TreeCommandManager, which provides some useful checks for nodes, and is able to access package bound methods. It can handle most of the tasks you need.

    The other ones are TreeCommandCommandListener and TreeCommandTabCompleteListener. They provide default implementations for handeling tab completes and commands, so you need to do nothing to make it work.
    They each take a TreeCommandManager as constructor parameter, whose commands they will manage.
    The CommandListener also takes a MessageProvider which is an interface in another (I18N) project. It just delivers the "no help node specified" and "no permission message". It may be better to put this in the same package and simplify it.

    But enough to how it works. Here is how you basically add it:
    Create the commands you want, extending CommandNode or a subclass of it.
    Then you need to do some things in your onEnable or where you register the command:
    Code:
    // create the tree manager
    CommandTreeManager treeManager = new CommandTreeManager();
    
    // register your commands
    treeManager.registerChild(treeManager.getRoot(), new CommandExampleTestBlock());
    
    // Currently: Create the mesage provider with the keys "no help node" and "no permission", as the constructor of CommandTreeCommandListener says.
    // I used an I18N instance in my main plugin, as I use it everywhere else, too.
    // Here is the important part of a dummy:
    MessageProvider messageProvider = new MessageProvider() {
           
        @Override
        public String tr(String key, Object... formattingObjects) {
            if(key.equals("no help node")) {
                return ChatColor.RED + "No help node specified.";
            }
            else if(key.equals("no permission")) {
                return ChatColor.RED + "No permission!";
            }
            else {
                return "Unknown key: '" + key + "'";
            }
        }
    // and so on. Just make some dummy methods or pass a real instance of a message provider. For a short test, a dummy is okay ;)
    
    // register the executor and Listener
    getCommand("exampleTest").setExecutor(new CommandTreeCommandListener(treeManager, messageProvider));
    getCommand("exampleTest").setTabCompleter(new CommandTreeTabCompleteListener(treeManager, true));
    
    And that was it. It *should* now work for you as good or bad as for me :p


    I would absolutly love to hear some criticism, as long as it adds to the conversation! I think the term is constructive :p

    Nice you managed to read until here, have a nice day ;)

    Link: Compiled jar with packaged source: Dropbox
    Link: Zip with exported eclipse projects: Dropbox too.
    (Hope I didn't mess them up somehow :p)
     
    Lordloss, ChipDev and mine-care like this.
  2. Offline

    I Al Istannen

    Bump (as a few days have passed).

    I apologize for the wall of text, but I don't know how to explain it more concise.

    The whole thing is a bit (too?) complicated, which is why I posted it here, so I could get help condensing it or improving the code quality :p
     
  3. Offline

    ArsenArsen

    Sounds nice! Such a coincidence that I developed the same thing a few days ago. My way. Very different. Still WIP.

    Anyways, great utility.
     
  4. Offline

    Ligachamp

    Good job so far! But I don't see any reason to criticise this API. You've thought of everything, implemented it already. I still need to dig deeper into it, but just looking at it I'd say it's already finished. ;)

    Anyways it could be helpful to add this to a git repo, since that would make collaboration a LOT easier ^^" If you allow me to, I could do that for you and give you some basics about working with git ;) (ok, as far as I've got some spare time...). You'd just have to tell me under which licence you want it to be published? (And should add that to the zip files you share either).
     
  5. Offline

    I Al Istannen

    @Ligachamp
    Thanks :)

    I would absolutly love that! Thank you :)
    Though I may be able to learn the basics myself, don't want to stress you :) And there are some tutorials out there :p


    Hmm, I didn't even think of that :p
    I frankly don't care if anybody modifies it, maybe MIT? But you can suggest another one if you want :)
     
  6. Offline

    Ligachamp

    I just found this link, looking through my bookmarks, it might help you already ;)

    I agree, the MIT licence would be fine :) So I'm looking forward to bring a repo up and share the link with you ;) You might want to create a github account for yourself, so I can give you all neccessary rights as soon as I upload it?
     
  7. Offline

    I Al Istannen

    @Ligachamp
    I will look into it, thanks!

    The second thing might take half a day or so, don't know how long I will stay on the computer for now and I really hate making user names. Never know what to use :p
     
    Ligachamp likes this.
  8. Offline

    Ligachamp

    I'm sorry I still haven't uploaded anything, I didn't have the time till now :/
    I'll create the repo now and upload everything (hopefully) tomorrow. Otherwise you could upload it yourself, if you already have the github account? ;) I'd just have to transfer the ownership to you, which I'll do anyways when you join :)

    EDIT: Here is the link ;)
     
    Last edited: Jul 5, 2016
  9. Offline

    I Al Istannen

    @Ligachamp
    I was extremly creative! Hope I configured my profile correctly :p
    I pushed something, don't know if it worked correctly though :p Never did it before ;)
     
    Ligachamp likes this.
  10. Offline

    ArsenArsen

    Its easy, and I've seen your past work, I'm sure you got it right ;)
    PS: Welcome to Git and GitHub! Where the open source lives!
     
    I Al Istannen likes this.
  11. Offline

    I Al Istannen

  12. Come on @I Al Istannen you should know this belongs in resources. Tut tut tut

    EDIT: Just actually read your post.. Still belongs there :p Just put [WIP]
     
  13. Offline

    ArsenArsen

    @bwfcwalshy I was kindof sad too, but he has a strong reason:
     
    I Al Istannen likes this.
  14. @ArsenArsen And I have a strong edit but a resource is a resource despite it's current state.
     
    ArsenArsen likes this.
  15. Offline

    I Al Istannen

    @bwfcwalshy
    Seems like your definition of ressource and mine differ slightly. I wouldn't consider it a WIP-resource, as I don't actively work on it - simply because i don't know what to change. Hence this thread ;)

    If you insist, feel free to report the thread (I can't move it anyways).
    Good night :)

    Sry, German autocorrect....

    EDIT: Fixed worst typos. German and english is similar enough for the phone autocorrect to screw it up complety :/
     
    Last edited: Jul 6, 2016
  16. Offline

    Ligachamp

    Yep, looks like you did everything fine, so I guess I have to delete the repo I created for you :p

    Anyways, happy to hear you learned how to deal with git ;)

    EDIT: Just beeing curious, what does that Language System you pushed to the repo?
     
  17. Offline

    I Al Istannen

    @Ligachamp
    Sorry :/ I appreciate the help though!

    I have only read the complaints about it :p
    So I hope I won't experience that xD


    It is currently needed for the CommandSystem, as two messages (yes, two...) are fetched from an MessageProvider (which is in the language system package). It was made this way to easily allow translations.

    The LanguageSystem package currently only holds a simple implementation of Javas I18N, where parts are inspired by essentials system :p THIS is something I have just written in 10 minutes, as I thought translations woud be nice :p

    I used javas I18N, as it supports unicode and all sorts of other stuff and I found it a bit (LOT) more customizable and working (Syntax isn't that strict) than the Bukkit YML. Just wanted to spare me the nightmare of writing a yml language system :p

    You are more than invited to make a pull request for a bettr version (If I find out how to merge it, I will do it xD)
     
Thread Status:
Not open for further replies.

Share This Page