Library Command (and language) System

Discussion in 'Resources' started by I Al Istannen, Aug 18, 2016.

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

    I Al Istannen

    Hey,

    now, @timtower, partly sole mod of this poor forum, I am terrible sorry if the post from the WIP should be moved, but I couldn't find any reference on that.

    And excuse my english, I am sure I mixed up some words or threw commas all over the place. German is a bit more strict with them :p


    So, I got fed up with making commands for plugins. Everytime you perform the same steps, make a handler, create a abstract Preset class and add the stuff. Or you just don't care and add the CommandExecutors directly. Both of these require far more code than I wanted to look and work in a nice way.

    And then you would have commands, which have subcommands. If you have a music plugin, you may only have ONE command, but lots of subcommands (/music skip, /music play, /music pause, ...). Doing this with a CommandExecutor would mean checking through each of the args and then reacting accordingly. Not nice, and a lot of boilerplate code.

    To top that though, you can have a command, with lots of subcommands, who each have their own subcommands. See where this is going? Yes, you have a problem (or make your own system :p).

    Now, I stumbled across that a few times and made a system in the past, but using it was nearly as much of a pain as not using any.
    So I said to myself: "Hey, you have holidays. And no life. Code one."
    And I eventually did.

    To save myself from adding the name, keyword, usage and other stuff for every command, it depends on a simple language system based on Javas I18N (ResourceBundle). I would encourage you to use it, as otherwise you would need to modify the system and have to hardcode everything. Currently it allows for a quite extensive localisation, being able to change the names of commands, their usages and descriptions.


    But enough of the idea behind, let's get on to the system ;)

    How does it (basically) work?
    Well, this is a rough explanation on this matter. I think a more in-depth one can be gathered more easily from the code or I can add it if you want.

    The system is basically a tree, consisting of commands. These commands are represented by the AbstractCommandNode class.
    This class provides functions to find children based on the patterns (keywords) of the commands.

    This works in the following way:
    The command the user entered is passed to the system as an array, split at " " (the same as the default commands).
    Then it gets processed by the Root of the tree. This root just asks all of it's children to do something with it. Every child checks if the first argument matches it's pattern. If yes, it removes the first argument and asks all of it's children. This goes recursive for as long, as arguments are present.
    Using this method the closest matching command is extracted and returned.

    This allows for having an unlimited amount of subcommands, each of which have their one subcommands. A tree, you know.

    Great. Now, how do I use it?
    Well, this is actually not that hard. You will need an instannce of the MessageProvider (language) first though. I would use the I18N class in the language system. To effectively use it, you will probably have to have a look at the official trail here. I am currently doing it in the following way:
    Language_Project_Structure.png
    This package just contains the language files. "Messages" is the name of the resource bundle, "_en" says that this file contains the english translations.

    In your onEnable you do something like this:
    Code:
    // get the directory to save them to
    Path directory = getDataFolder().toPath().resolve("language");
    if (Files.notExists(directory)) {
      try {
        // create it
        Files.createDirectories(directory);
      } catch (IOException e) {
        getLogger().log(Level.WARNING, "Can't create language folder", e);
      }
    }
    // copy the default files from the language package to the directory, not overwriting any
    I18N.copyDefaultFiles("language", directory, false, getClass());
    // create the language. The javadoc is probably okay for understanding that.
    // The categories ("Messages" in this case) are the names for the resource bundles, case sensitive.
    I18N language = new I18N("language", directory, Locale.ENGLISH,
                                         getLogger(), getClassLoader(), "Messages");
    
    "language" should be the name of the package the files are under. This will copy the files to a directory on the disk, in your plugin folder, where the user can modify them. The modified files from the disk will be used whenever possible.

    Now you have your language, which you can use throughout your plugin, to make your life easier and the users happy.

    Once this is done, you can start caring for the Command System.

    There are two ways currently.

    Use the ability to dynamically add top level commands:
    PHP:
    // create the command tree
    CommandTree commandTree = new CommandTree(language);
    // create the command executor. This one will handle finding the command, executing and
    // and sending error messages
    // If you pass true (or nothing as it is the default), the name of the command ("kor" in the example
    // above) will be added to the args the
    // user entered. This means that anything the user entered will be passed on through the "kor" command
    // If you use the addTopLevelChild method, set this to true.
    DefaultCommandExecutor commandExecutor = new DefaultCommandExecutor(commandTreelanguagetrue);
    // The same for the tabcompleter. Complets what you are able and allowed to use.
    DefaultTabCompleter tabCompleter = new DefaultTabCompleter(commandTreetrue);

    // A relay command is a command which just returns it's usage and shows it's children on
    // tab complete. The "command_kor" is the key for the language. You need to supply the
    // keys via the language. They are these:
    // command_kor_name
    // command_kor_keyword
    // command_kor_pattern
    // ~ command_kor_permission ==> Not used, but you should. May implement that though.
    // namecommand_kor_usage
    // namecommand_kor_description

    // The "none" is the permission
    // The (sender -> true) is the preset senders must follow. This way you can restrict a command
    // to console, players or BlockCommandSenders
    RelayCommandNode kor = new RelayCommandNode(language"command_kor""none", (sender -> true));

    // now we add our top level child. This child's keyword will be dynamically registered as a command.
    // If it is "kor", now "/kor" is a valid bukkit command.
    // Pass true if you want to add the default help command to this node. So "/kor help" would now be
    // valid
    commandTree.addTopLevelChild(kortruethiscommandExecutortabCompleter);
    // add some children as you please
    commandTree.addChild(kor, new CommandAge());
    commandTree.addChild(kor, new CommandRelto());
    commandTree.addChild(kor, new CommandPrisonBook());
    // done.
    Without comments not that much.

    Without dynamically adding commands:

    PHP:
    // same
    CommandTree commandTree = new CommandTree(language);
    // same, but we pass "false"
    DefaultCommandExecutor commandExecutor = new DefaultCommandExecutor(commandTreelanguagefalse);
    // same, but we pass "false", to ignore the command name in the search attempts
    DefaultTabCompleter tabCompleter = new DefaultTabCompleter(commandTreefalse);

    // now, no more "kor" command node. We add the children directly to the root.
    commandTree.addChild(new CommandAge());
    commandTree.addChild(new CommandRelto());
    commandTree.addChild(new CommandPrisonBook());
    // we set the help ourselves, as it isn't done automatically.
    commandTree.setHelpCommand(new DefaultHelpCommand(languagecommandTree"command_help"), commandTree.getRoot());

    // we use the normal methods for registering.
    getCommand("kor").setExecutor(commandExecutor);
    getCommand("kor").setTabCompleter(tabCompleter);
    This method requires a new tree for every top level command, but might be safer to use (no reflection).

    Your commands:
    Your commands must just extend AbstractCommandNode, or if you want it easier, DefaultCommandNode. Implementing the required methods should be quite self-explanatory.

    Help command:
    This system also offers a free, default help command. If using the first method it gets applied if you pass true, with the second you must add it by yourself.
    The help command just displays the node. It could look like this:
    Default_Help_Overview.png
    The numbers in the brackets are the amount of children.
    The help command can be made more specifc, by adding the name of the node you want to see the help for:
    Default_Help_One_Command.png

    And there are a few options for the help command:
    --depth=<number>
    --page=<number>
    --entriesPerPage=<number>
    --showUsage=<true | false>
    The depth says how many level of children it should show. The first picture with depth = 1:
    Default_Help_Depth.png

    The "--page=<number>" just goes to a different page.
    The "--entriesPerPage=<number>" says how many entries should be shown per page.
    And show usage? Well look at this:
    Default_Help_Depth_ShowUsage.png
    Explains it, doesn't it?

    Why the heck do I need this?
    Well, there is no real reason. One could be that you want to just add your commands without worrying for subcommands and how to add what, handle permissions and CommandSender type and trying to extract anything useful from the command arguments the user entered.
    The other could be that you want that help command. I know I would.


    Finally the end
    Enough wall of text for you. Sadly I am no great writer :/

    Now, I don't think there is much more to say. Currently the libary acts a plugin (like ProtocolLib), so you need to compile it and throw it in the plugins folder, as well as depend on it. If you have any suggestions, I can change that, or upload a pre-compiled version of it.

    If you want I can also make an example plugin and link the sources.

    But my guess is that nobody will use it. I probably wouldn't if I stumbled upon it, so I don't blame you :p

    Have a nice day though and consider leaving anything you have on your mind about that system right now :)

    Code:
    Github.
     
    Last edited: Aug 18, 2016
    ArsenArsen likes this.
  2. Offline

    ArsenArsen

  3. Offline

    I Al Istannen

    @ArsenArsen
    Well, I don't know how it works for others yet :p
    It surely has some quirks I am not aware of ;)

    Thanks though :)
     
  4. Offline

    Zombie_Striker

    These quotes really sums up most of the threads here in the Resources forum.

    Good job. I don't see a link to your project, so I don't think anyone can use it ATM.
     
  5. Offline

    I Al Istannen

    @Zombie_Striker
    God, I am dumb :confused:
    Here and I fixed it...

    Sadly the quotes do :/ So many projects, so little uses... :p
     
    Zombie_Striker likes this.
  6. Well at least I have something to study now. I'm still wandering around the dangerous desert of Java, and hell, my todo list is bigger then the desert itself.
     
    I Al Istannen likes this.
  7. @I Al Istannen could you please make an example plugin and link the sources, i'm quite confused on what I am supposed to be doing.
     
  8. Offline

    I Al Istannen

    @ibc2244
    I haven't used this library in quite some time, I created large chunks of PerceiveCore (I am probably the only one who does something with it, so I will most likely make my own version of it)

    I will most likely not provide an example plugin for this, I have practically abandoned it. I ported both utilities that are in this library to perceivecore, and made them better and easier to use. Feel free to have a look at that :)
     
    ibc2244 likes this.
  9. @I Al Istannen okay, thanks! One question, what MC version is it?
     
  10. Offline

    I Al Istannen

    @ibc2244
    It uses some things from 1.9+, so I would use that. I have tested it on 1.10 and some other people on 1.11, and it works for these. It should work for 1.9 too, I think.
     
  11. @I Al Istannen sick cheers. I'm using the latest MC version, 1.11.2 I think. Also, do you have any documentation for it or is it almost the same as this?
     
  12. @ibc2244 Exact same code, not that much changes per version.
     
  13. @ibc2244 You asked if it's the same I answered. What are you confused about?
     
  14. @bwfcwalshy are you on about the PerceiveCore or this command + language system?
     
  15. @ibc2244 This which makes me thing your talking about the former
     
  16. @bwfcwalshy yeah, I Ai Istannen pointed me towards the PercieveCore because he has basically abandoned this project and has put these two utils into it along with updating them.
     
  17. Offline

    I Al Istannen

    @ibc2244
    I am terrible with docs, but there weren't any major changes ;)
    PerceiveCore contains WAY more (a nice Gui system based on Swing, some config utilites, particle effects, Reflection utils, packet listeners (not as good as Protocollib, but usable), some updaters (I made spiget and curse API today), and some misc utilities.

    And obviously the Gui and command system :)

    The command system was made a bit more versatile and clever (it supports argument mapping based on annotations), but I think it is explained better in the wiki pages I made here.

    The language system is now a bit easier to use and supports nested keys to some extend. Wiki page.

    The wiki is really incomplete for Guis, but commands should be up to date and the language mostly too.

    I used it for a few plugins, you can find one here and one it kotlin here.
     
  18. @I Al Istannen yeah, i found the wiki after i posted my last comment. Thanks for linking plugins in which you used it as I was super confused on how to do the language system.
    Also, if you do create your own version of PerceiveCore would you mind messaging me on Github or on here if you don't post it on the forums?

    EDIT Am I able to override base bukkit commands like /ban?
    EDIT 2Can I have per player languages? The plugin i'm making for a friend needs to have English and Dutch as a language per player.
     
    Last edited: Jan 2, 2017
  19. Offline

    I Al Istannen

    @ibc2244
    I wouldn't mind, I am currently doing it in the i-al-istannen branch of the repo.
    No, per player languages are not a thing currently.

    If you have any idea how that would look like from the user side (i.e. yours :p), I will implement it though :)
     
  20. @I Al Istannen It could work where you specify where all your per player configs are, like you've done with the languages package, and the user using it sets a string in the config called "language" and they create a command saying what language they want, or use https://bukkit.org/threads/get-a-players-minecraft-language.172468/ on the player join. Then as that player runs the command it sends them a mesage in their language set in the configs.

    EDIT with the command system am I able to have per command help, because when I currently do one command in the cmd tree then help after it, it says the same thing for help. i.e:
    Code:
    Ban:
      Help: Shows the help.
    Unban
      Help: Shows the help.
    is how it currently shows if you've added that command to the help thing in the CommandTree class.
     
  21. Offline

    I Al Istannen

    @ibc2244
    So the PcCore plugin would serve each player a different language, reading it from some cache the user set with a command or the owner set in a file.

    While that shouldn't be too hard to implement (without saving between server restarts or with it), it would be a complete change to all methods in MessageProvider, as they currently do not take a Player object. You would need a MessageProvider instance per language and let it serve the players that selected it.
    This should be quite easy for you to implement yourself in your plugin, but a centralized alternative would require a method change.

    This will lead it to be incompatible with all older versions of PcCore.

    I could add a default method "translateForPlayer(String key, Player player, Objects... formattingObjects" to MessageProvider, which you could use.
     
  22. tbh that is what I was trying to say but I went off on a tangent.
     
    Last edited: Jan 3, 2017
  23. Offline

    I Al Istannen

    @ibc2244
    I will see if I can get around to it tomorrow, it will require a command and some storing mechanism.

    Have a nice day, it is midnight for me here ;)
     
  24. you to, off to bed myself. :D
     
    Last edited: Jan 3, 2017
Thread Status:
Not open for further replies.

Share This Page