Resource (ACF - BETA) Annotation Command Framework

Discussion in 'Spigot Plugin Development' started by Aikar, Apr 19, 2017.

  1. ACF (Annotation Command Framework)
    If ACF, Timings, or any of my development libraries have helped you, please consider donating as a thank you.
    ACF is an extremely powerful command framework that takes nearly every concept of boilerplate code commonly found in command handlers, and abstracts them away behind annotations.

    ACF redefines how you build your command handlers, allowing things such as Dependency Injection, Validation, Tab Completion, Help Documentation, Syntax Advice, and Stateful Conditions to all be behind Annotations that you place on methods.

    Clean up your command handlers and unleash rich command experiences that would be too burdensome to pull off manually.

    Using ACF Wiki Page:


    Examples of Command Definitions:

    Code (Java):

    public class ResidenceCommand extends co.aikar.commands.BaseCommand {

        @CommandCompletion("@allplayers:30 @flags @flagstates")
        public void onResFlagPSet(Player player, @Flags("admin") Residence res, EmpireUser[] users, String flag, @Values("@flagstates") String state) {
            res.getPermissions().setPlayerFlag(player, Stream.of(users).map(EmpireUser::getName).collect(Collectors.joining(",")), flag, state, resadmin, true);

        @Subcommand("area replace")
        public void onResAreaReplace(Player player, CuboidSelection selection, @Flags("verbose") Residence res, @Default("main") @Single String area) {
                new CuboidArea(selection),

    public class GroupCommand extends co.aikar.commands.BaseCommand {
        public GroupCommand() {
        @Syntax("[radius=32] &e- Invite Nearby Players to the group.")
        public void onInviteNear(Player player, @Default("32") Integer radius) {
            int maxRadius = UserUtil.isModerator(player) ? 256 : 64;
            radius = !UserUtil.isSrStaff(player) ? Math.min(maxRadius, radius) : radius;
            List<String> names = player.getNearbyEntities(radius, Math.min(128, radius), radius)
                .stream().filter((e) -> e instanceof Player && !UserUtil.isVanished((Player) e))
            Groups.invitePlayers(player, names);

        @Syntax("<name> [name2] [name3] &e- Invite Players to the group.")
        public void onInvite(Player player, String[] names) {
            Groups.invitePlayers(player, names);

        @Syntax("<player> &e- Kick Player from the group.")
        public void onKick(Player player, @Flags("leader") Group group, OnlinePlayer toKick) {
            group.kickPlayer(player, toKick.getPlayer());

    Why you should want to use this

    How many times have you wrote code that converts what the player typed into a number, complete with error feedback? How many places is that code duplicated?

    With ACF, you only write it once (and numbers are already done for you!), and then simply build your command format as a method signature.

    All of the users input is mapped into the method parameters.

    Flags let you easily define restrictions and other validations quick and easy.

    ACF is designed to make creating complex command architectures painlessly.

    Oh and did I mention you no longer need to use plugin.yml to define commands?

    All commands registered with ACF are 100% programmatic. No plugin.yml definitions needed.

    Annotation Driven
    Everything about ACF is Annotation driven.

    You can define a @CommandPermission at the root node, and require everything in that command class to need that permission, or you can break it down to the per subcommand level, and put them on that.

    Want multiple ways to invoke a command? The syntax for @Subcommand and @CommandAlias supports a pipe format to easily create tens to hundreds of combinations to create a command

    Code (Java):
    @CommandAlias("foobarcommand|foo|f bar|b baz")
    This will result in all of the following commands working:
    Code (Text):

    foobarcommand bar baz
    foobarcommand b baz
    foo bar baz
    foo b baz
    f bar baz
    f b baz
    Want a a subcommand of one of your primary commands to also have its own root command? That works too!

    Code (Java):

    public class GroupCommand extends BaseCommand {
        public void onGroupInvite(Player player, OnlinePlayer playerToInvite) { }
    In this example, both /group invite Aikar and /ginvite Aikar would do the same thing!
    Command Completion? Yep it's smart enough to work from both of those commands.

    Support / Disclaimer
    Documentation is very light right now. Please examine the source code and just poke around. I will try to document things as I can.


    Also note that this is still beta, at version 0.5.0.

    Semantic Versioning for Pre 1.0.0 releases are treated as minor (0.X) are API breakages, and patch updates would be safe.

    I welcome early testers!

    PSST, Also check my other libraries!
    #1 Aikar, Apr 19, 2017
    Last edited: Feb 26, 2019
    • Winner x 37
    • Like x 24
    • Useful x 9
    • Friendly x 5
    • Agree x 1
    • Creative x 1
  2. thanks, this has really sped up my development.
    #2 Synapz, Apr 19, 2017
    Last edited: Jul 27, 2017
  3. FrostedSnowman

    Resource Staff

    i've always highly respected u because of your use of the latest Java 8 techniques and functions, thanks for encouraging it in all your stuff, i myself use it as well, and would want the rest of the community to do so as well!
    • Agree Agree x 4
  4. WAS


    Well this is sure interesting. Really like this concept.
  5. simpleauthority


    Love it. Integrating immediately. Can't wait to see where I can help with the development. Woooo! Pumped.
  6. @Aikar
    You need to like annotations ;)
    The flags seem like a nice concept though, and the resolvers look nice.

    I do have a question though:
    Are all messages defined in the source code?

    What I like about my own bad command thingy is the following:
    1. You select a language
    2. All command keywords and messages change to respect that.
      I can have "/friend" (english/fallback) and "/freunde" (german) refer to the exact same command, depending on the chosen language.

    Is that possible with yours?

    Thank you for open-sourcing your work though, I appreciate it!
  7. Let's ditch onCommand! :)
    • Funny Funny x 1
  8. Just add them all as aliases.
    Code (Java):

    public void onFriend(){}
    What the code does and the command used is up to you, but there is a field exposed to access the raw command used

    Overall I18N work will be up to you though, but def possible.

    EDIT: 1/6/2018 - The work discussed here has been implemented!
    #8 Aikar, Apr 19, 2017
    Last edited: Jan 6, 2018
  9. Though for the hard coded messages, I can see supporting Language Tables for that.
    • Like Like x 1
  10. Why you use "|" to separate aliases, but not comma?
  11. Pipe is a pretty standard separator that is unlikely to conflict with natural language.

    Sure for aliases a comma could of worked too, but consistency is more important.

    For example, for separating natural language you may do

    Hello, Bob|Hello, Sally
    • Like Like x 2
    • Agree Agree x 1
    • Useful Useful x 1
  12. @Aikar
    Of course I could just add them as aliases. But only in the source code, as the value of the annotations is not evaluated at runtime.

    If the user decides he now wants arabic, I would need to add an arabic alias. By resolving the aliases at runtime he can just change a language file.

    Do you see what I mean? Or have I misunderstood your response?
  13. As said above, I am for I18N work to provide language .yml files to it.

    We could do something like @CommandAlias("")

    That would replace from example YML:
    Code (Text):

      - foo: "bar"

    EDIT: 1/6/2018 - The work discussed here has been implemented!
    #13 Aikar, Apr 19, 2017
    Last edited: Jan 6, 2018
    • Like Like x 1
  14. I'll try to work on some i18n stuff over the next few days. I got some ideas already.
  15. Ok 2 things happened tonight.
    I changed the Maven Artifact ID and bumped to 0.2.0
    it is now
    Code (Text):
    GROUP: co.aikar.commands
    ARTIFACT: acf
    I then wrote an example plugin and found 0.1.0 wasn't working, and fixed the bug.

    So please bump up to 0.2.0 (the code package didn't change, so a simple pom/gradle file change)

    Also note, its very important you add Package Relocation Instructions to your plugin to avoid clashes with other plugins using ACF.

    Additionally, it is recommended to add -parameters compile flag

    Both of these are covered in the setup guides:

    Maven Setup:
    Gradle Setup:

    Example Plugin POM with Relocation and parameters:

    Example Plugin Source:

    Basic stuff is working on the Example Plugin now!
    #15 Aikar, Apr 21, 2017
    Last edited: Apr 24, 2017
    • Creative Creative x 1
  16. Some more bugs fixed! But heads up, im likely going to change the group ID again in 0.3.0, as i realized I did use co.aikar for TaskChain and i want to keep it consistent.

    and to future proof it, i think im going to do

    similar to taskchain. But i'll let everyone know when that happens, as I'll prob wait until the I18N stuff comes in for that.
    • Informative Informative x 1
  17. Ok group/artifact ID updated

    GROUP ID: co.aikar
    ARTIFACT: acf-core

    Just exposed ACFUtil which has a ton of misc util methods, not really related to the framework itself, but good stuff.
    • Like Like x 1
    • Useful Useful x 1
  18. [​IMG]
    Posting in memory of the dinosaurs.

    Anyway, nice work, Aikar. This completely redefines the command system!
    #18 dinosaur, Apr 24, 2017
    Last edited: Apr 24, 2017
    • Winner Winner x 2
  19. MiniDigger


    dinosaurs have timemachines? :O
    • Funny Funny x 1
  20. Gasp! A dinosaur! Quick, give it some Java 8!
    • Funny Funny x 1