Skip to content

Command Framework

Petrus Pradella edited this page Jul 31, 2026 · 9 revisions

Command Framework

What this page covers: the annotation-driven command framework (@FinalCMD). You write a plain class, annotate a method (or the class) with @FinalCMD, annotate more methods with @FinalCMD.SubCMD, and register it once. The framework builds the command, wires sub-commands, parses and tab-completes arguments, checks permissions, renders an automatic help screen, and works identically on Bukkit and Hytale.

A command is not limited to two levels: @FinalCMD.Node adds a branch of any depth, and a branch can eat tokens of its own - /arena duel Colosseum spawn set 2 is one command, not a naming convention. See the command tree.

Argument details live on Argument Parsing; the localized @FCLocale messages you send from a command are on Localization.


The 30-second version

import br.com.finalcraft.evernifecore.api.common.commandsender.FCommandSender;
import br.com.finalcraft.evernifecore.commands.finalcmd.annotations.Arg;
import br.com.finalcraft.evernifecore.commands.finalcmd.annotations.FinalCMD;

@FinalCMD(
        aliases = {"template", "tpl"},
        permission = "myplugin.command"
)
public class TemplateCommand {

    @FinalCMD.SubCMD(subcmd = "info")
    public void info(FCommandSender sender) {
        sender.sendMessage("&aHello from /template info");
    }

    @FinalCMD.SubCMD(subcmd = "coins")
    public void coins(FCommandSender sender, @Arg(name = "<amount>") Integer amount) {
        sender.sendMessage("&aYou asked for &6" + amount + " &acoins.");
    }
}

Register it once, from your plugin bootstrap:

import br.com.finalcraft.evernifecore.commands.finalcmd.FinalCMDManager;

FinalCMDManager.registerCommand(ecPluginData, TemplateCommand.class);

That's a working /template (alias /tpl) with two sub-commands, typed argument parsing on <amount>, tab-completion, permission checks, and a /template help screen - none of which you wrote by hand.


@FinalCMD - the command

Put @FinalCMD on the class (as above) or on a single method. Its attributes:

Attribute Type Default Purpose
aliases String[] (required) Command names. First is primary, the rest are aliases.
permission String "" Permission node required to run/see the command. Empty = no permission.
usage String "" Usage line shown in help, but only used when the method has no @Arg (see Automatic help).
helpHeader String "" Header text for the auto-help screen (rendered centered with a rule).
useDefaultHelp CMDHelpType FULL Controls the automatic help sub-command (see below).
validation Class<? extends CMDAccessValidation>[] {} Access gates evaluated before the command runs (see Access validation).
locales FCLocale[] {} Inline localized description for this command - the only declarative way to describe a command; there is no desc attribute. See Localization.

The sender parameter is a portable FCommandSender - it works on both platforms. On Bukkit you may also declare a Player or CommandSender parameter directly; the framework injects the right one. See Argument Parsing for the full list of auto-injected (contextual) parameter types.


@FinalCMD.SubCMD - the sub-commands

Each method annotated with @FinalCMD.SubCMD becomes a sub-command of the class-level @FinalCMD.

@FinalCMD.SubCMD(
        subcmd = {"coins"},
        permission = "myplugin.command.coins",
        validation = {MustBePlayerValidation.class}
)
public void coins(FCommandSender sender, @Arg(name = "<amount>") Integer amount) { ... }

@FinalCMD.SubCMD carries subcmd (its names/aliases), plus usage, permission, validation, and locales with the same meaning as on @FinalCMD. A sub-command with no permission of its own falls back to the parent command's permission check only.

📌 Note - @FinalCMD.Ignore on a method tells the scanner to skip a method that was inherited from a super-class (the scanner walks the class hierarchy up to Object, so an annotated method on a parent class is picked up unless you ignore it).

Two shapes of a command class

The scanner supports two layouts, and they behave differently:

  • One @FinalCMD + N @FinalCMD.SubCMD (the common case): @FinalCMD is on the class or on a single method, and the @SubCMD methods become its sub-commands. This is what gives you /template info, /template coins, etc.
  • N @FinalCMD methods on one class: each annotated method becomes its own stand-alone command with no sub-commands. @FinalCMD.SubCMD is not allowed in this layout and is ignored with a warning.

Deeper than two levels: the command tree

A @FinalCMD.SubCMD is a leaf. When a segment needs children of its own, it is a node:

EverNifeCore's own /ecstorage is the smallest real one - a leaf next to a branch that eats nothing:

/ecstorage                                  ROOT      @FinalCMD          (class or method)
├── status                                  LEAF      @FinalCMD.SubCMD   (method)
└── transfer                                NODE      @FinalCMD.Node     (class or FIELD)
    ├── section <plugin:section> <backend>  LEAF
    └── network <backend>                   LEAF

A branch that eats a token of its own goes one step further. This is TheDuel's arena command, and it is the shape the rest of this section builds on:

/arena                                 ROOT      @FinalCMD          (class or method)
├── list                               LEAF      @FinalCMD.SubCMD   (method)
└── duel                               NODE      @FinalCMD.Node     (class or FIELD)
    │  eats <arena>                              @FinalCMD.Capture  (method of the node)
    │  runnable on its own                       @FinalCMD.Execute  (method of the node, optional)
    ├── info                           LEAF
    └── spawn                          NODE      (no capture)
        └── set <index>                LEAF

Three annotations carry it, and one more reads it back:

Annotation Where What it does
@FinalCMD.Node class or field declares a branch: subcmd, permission, context, validation, locales. No usage.
@FinalCMD.Capture method of a node class the tokens the branch eats right after its own label
@FinalCMD.Execute method of a node class makes the branch itself runnable
@FinalCMD.Captured parameter of any descendant hands that branch's captured value to a leaf
@FinalCMD(aliases = {"arena"}, permission = "theduel.use")
public class CMDArena {

    @FinalCMD.SubCMD(subcmd = "list")
    public void list(FCommandSender sender) { }

    @FinalCMD.Node(subcmd = {"duel", "d"}, permission = "theduel.duel")
    public static class DuelNode {

        @FinalCMD.Capture
        public DuelArena capture(FCommandSender sender, @Arg(name = "<arena>") DuelArena arena) {
            return arena;
        }

        @FinalCMD.Execute   // /arena duel Colosseum - the node IS the label, it has no name of its own
        public void show(FCommandSender sender, @FinalCMD.Captured DuelArena arena) { }

        @FinalCMD.SubCMD(subcmd = "info")
        public void info(FCommandSender sender, @FinalCMD.Captured DuelArena arena) { }

        @FinalCMD.Node(subcmd = "spawn")
        public static class SpawnNode {
            @FinalCMD.SubCMD(subcmd = "set")
            public void set(FCommandSender sender,
                            @FinalCMD.Captured DuelArena arena,
                            @Arg(name = "<index>") Integer index,
                            @Arg(name = "[force]", def = "true") Boolean force) { }
        }
    }
}

Two ways to mount a node, one meaning

@FinalCMD.Node targets a type or a field. The mount point declares the segment (labels, permission, validation, locales); the class declares the content.

  • Inner class - the tree in the code is the tree in the chat. Best for a small stateless branch.
  • Field - the field's type is the branch (there is no delegate = X.class), so the same class can be mounted more than once, each mount with its own labels and permission, already built with the state and generics you gave it:
@FinalCMD(aliases = {"arena"})
public class CMDArena {

    @FinalCMD.Node(subcmd = {"duel"}, permission = "theduel.duel")
    private final CMDBaseArenaSettings<DuelArena> duel = new CMDBaseArenaSettings<>(DUEL_MODULE);

    @FinalCMD.Node(subcmd = {"ffa"}, permission = "theduel.ffa")
    private final CMDBaseArenaSettings<FfaArena> ffa = new CMDBaseArenaSettings<>(FFA_MODULE);
}

A null field is instantiated through the type's no-arg constructor. The class a field mounts must not carry @FinalCMD.Node itself - the segment would be declared twice, and that is refused.

The capture eats a fixed number of tokens

@FinalCMD.Capture marks at most one method per node class. Its @Arg parameters are the tokens the branch consumes, in order, and it consumes all of them, always: k declared is k eaten, which is what keeps the traversal a single pass with no lookahead and no backtracking. So [optional], fromSender and the variadic <tail...> are all refused inside a capture.

The return value is the branch's context, filed under the branch's node path. Returning null aborts the execution in silence - whoever refuses is whoever warns. void is legal only for a branch that exists to declare flags.

More than one token is fine:

@FinalCMD.Capture
public ArenaInWorld capture(@Arg(name = "<world>") String world,
                            @Arg(name = "<arena>") DuelArena arena) {
    return new ArenaInWorld(world, arena);
}
// /arena duel nether Colosseum spawn set 2

@FinalCMD.Captured - reading it back

The framework's rule is that an unannotated parameter is the caller and an annotated one comes from outside. So the captured value is explicit, and no wrapper type has to exist just to tell the target apart from the sender:

public void info(FCommandSender sender,                   // contextual: whoever typed it
                 @FinalCMD.Captured DuelArena arena) { }  // captured: what it is about

Left empty, it resolves to the only compatible capture on the path. When more than one fits, naming the branch is mandatory, and the registration error says exactly which names exist:

public void diff(FCommandSender sender,
                 @FinalCMD.Captured("duel")         DuelArena a,
                 @FinalCMD.Captured("duel.against") DuelArena b) { }

The name is a node path - primary labels, dot joined - and it hands over what that node's @Capture returned. To take a single @Arg of a multi-token capture instead, append :<argName>, spelled exactly as the capture declared it:

@FinalCMD.Capture
public ArenaInWorld capture(@Arg(name = "<world>") String world,
                            @Arg(name = "<arena>") DuelArena arena) { ... }

@FinalCMD.SubCMD(subcmd = "four")
public void four(FCommandSender sender,
                 @FinalCMD.Captured("duel")         ArenaInWorld ctx,    // the @Capture's return
                 @FinalCMD.Captured("duel:<world>") String world) { }    // one token of it

Both forms live in the same method, and the second works for a one-token capture too. The consequence is worth knowing: the name of a capture's @Arg is public contract. Renaming <world> is refused at boot, listing the names that do exist - never silently, in a leaf three levels away.

An executable node

@FinalCMD.Execute makes the branch itself runnable: /arena duel Colosseum runs the method instead of printing the branch's help. It cannot declare @Arg - only contextual parameters, @FinalCMD.Captured and @FlagArg. That restriction is the whole reason the traversal stays unambiguous: with no positional of its own, every token after a node still has to be a child's label.

Without an @Execute, typing the branch prints its help.

What eats the next token

Situation at a node Rule
reserved word (help, ?, ajuda) beats everything, at any depth
node with a capture of k tokens eats the next k tokens, always
node without a capture the next token has to match a child's label, otherwise the node's help

Because the capture eats before any literal is matched, a target whose name happens to equal a child's label is still reachable.

The five phases of a dispatch

Fixed order, and it exists to break a circle - finding the leaf would need the arity of the flags, and the arity of the flags needs the leaf:

1. WALK      match literals, COUNT capture tokens, resolve the leaf.
             No @Capture runs. No parser runs.
             A flag token before the path ends is an error (see Flags, rule L).
2. EXTRACT   pull every declared flag out of what is left, with the bindings accumulated root..leaf.
3. CAPTURE   run each @Capture, root -> leaf, each parsing its own @Arg and receiving its @FlagArg.
4. PARSE     parse the leaf's positionals, in the window the path left behind.
5. INVOKE    call the method.

Tab-completion runs only phase 1 - it counts tokens and never resolves a value, because it runs once per keystroke. See Argument Parsing for what a parser is handed while completing.

Depth costs nothing at registration, and everything is refused early

A malformed tree is refused while the server is still opening, with a message that names the class, the member, the node path and the call that fixes it. The command is then simply not registered - the reason is in the log and the rest of the plugin loads. What is refused: a node with no children, two captures on one node, a capture that eats nothing, an optional/variadic/fromSender capture argument, an ambiguous or unresolvable @FinalCMD.Captured, two children claiming one word, a label written with brackets, a node class also carrying @FinalCMD, a mount cycle, an @Execute with an @Arg, and a flag spelling an ancestor already claims.

📌 The evernifecore-common-tests artifact publishes that whole battery as CommandShapeErrors.check(harness), plus a four-level Commands.referenceTree() and harness.tree(cmd) structural assertions - so a plugin building trees tests them without copying a fixture.


Registering commands

Registration is a static call. It needs your plugin's ECPluginData (the handle every ECPlugin gets) and either the command class (the framework instantiates it via its no-arg constructor) or an already-built instance:

public static void registerCommands(ECPluginData ecPluginData) {
    FinalCMDManager.registerCommand(ecPluginData, TemplateCommand.class);
    // or, if you need a pre-built instance:
    FinalCMDManager.registerCommand(ecPluginData, new TemplateCommand());
}

Call this from your plugin's enable/reload path (the same method typically runs on both the Bukkit and Hytale bootstrap). Registration also scans the class for static LocaleMessage fields and loads them, so your @FCLocale messages are ready by the time the command runs.

To remove a command at runtime:

FinalCMDManager.unregisterCommand("template");

⚠️ Gotcha - the command class must have a public no-arg constructor when you register it by .class. If it doesn't, registration logs a warning and the command is skipped.


The command registry

Every command registered through FinalCMDManager is tracked on its owning plugin's ECPluginData, so a plugin can inspect or tear down what it registered without keeping its own bookkeeping:

List<FinalCMDPluginCommand> mine = ecPluginData.getRegisteredCommands();  // immutable snapshot
ecPluginData.findRegisteredCommand("template");                          // Optional<...>, by any label

FinalCMDManager.unregisterAllCommands(ecPluginData);  // tear down everything this plugin registered
// or, for a single command instance you're holding:
command.unregister();                                 // idempotent - safe to call twice

A plugin implementing IECPluginBootstrap gets this for free: the default onECPluginShutdownPre() already calls FinalCMDManager.unregisterAllCommands(getPluginData()) (alongside ECListener.unregisterAll(...)) before the rest of shutdown runs, so a well-behaved ECPlugin cleans up its commands automatically. Override the hook only if you need different ordering, and call the default (or its two calls) yourself if you still want the cleanup.

The command registry files - one per plugin, and off by default

An admin can disable any command (or any branch of one) and rename its aliases from a file, without touching the plugin. The feature is opt-in and starts off: while it is off nothing is read, nothing is written, no folder is created, and the annotations are the only source of truth.

Turn it on in EverNifeCore's config.yml:

Settings:
  Commands:
    REGISTRY_FILES_ENABLED: true

From the next boot, every plugin that registers a command gets its own file under EverNifeCore's data folder, seeded with that plugin's whole tree - leaves included, so the paths are discoverable without reading any code:

plugins/EverNifeCore/commands/MyPlugin.yml
plugins/EverNifeCore/commands/TheDuel.yml
plugins/EverNifeCore/commands/EverNifeCore.yml

The file nests exactly like the tree: each node's key is its primary label, and its children live under nodes:.

commands:
  arena:
    enabled: true
    aliases: [arenas, ar]
    nodes:
      duel:
        enabled: true
        aliases: [d]
        nodes:
          spawn:
            enabled: false          # kills /arena duel spawn and everything under it
      ffa:
        enabled: true
  • enabled: false removes that node and its whole subtree - from dispatch, from tab-complete and from help at once, because the node is dropped before the command reaches the platform. On the root it means the command is never registered at all.
  • aliases: replaces the extra labels only. The key of the entry is the primary label: it is the entry's identity and can never be changed here, and it always keeps answering.
  • An entry you delete simply goes back to what the annotation says - the file never has to be complete.
  • permission is deliberately not configurable here. It is the plugin's contract, and a typo in a yaml file is not an acceptable way to turn one into a security hole.
  • Changes apply on the owning plugin's next boot or reload - there is no hot rebind.

A parent that is off wins over a child that is explicitly on: reviving the child would leave a path nobody can type. When the file changed anything, the console gets one line naming the plugin and the file; the path-by-path detail sits behind the COMMAND_REGISTRY debug module.

A plugin can ask what survived, over the tree that is live right now:

ecPluginData.isCommandPathEnabled(path);   // false once the file pruned it; true while it is active

With the feature off it answers true for everything - nothing was ever pruned.


Automatic help

Every command with sub-commands gets a generated help screen for free. useDefaultHelp on @FinalCMD chooses the policy:

CMDHelpType Behaviour
FULL Always provide the help sub-command and list every sub-command. (default)
EXCEPT_EMPTY Provide help, but omit sub-commands that have no description/usage.
NONE No automatic help sub-command.

Each help line is built from the sub-command's name, its @Arg names (e.g. <amount>), and its description. helpHeader sets a centered title.

help, ? and ajuda are reserved at every depth, so /arena duel Colosseum spawn help prints that branch's help and nothing else can claim those words. A branch's help lists only its own children (one level, not the whole subtree - four levels rendered at once overflow a chat screen), and every line carries the whole path the sender has to type, click-suggest included. A branch the sender cannot reach never shows up, and neither does one whose every leaf is denied.

A sub-command that declares a HelpLine parameter can also render its own single help line on demand - handy for "wrong arguments, show usage":

@FinalCMD.SubCMD(subcmd = "set", usage = "<PluginName> <LocaleName>")
public void set(FCommandSender sender, MultiArgumentos argumentos, HelpLine helpLine) {
    if (argumentos.emptyArgs(0, 1)) {
        helpLine.sendTo(sender); // prints this sub-command's usage line
        return;
    }
    // ...
}

⚠️ usage is only legal when the method declares NO @Arg and no @FlagArg. The help line of an annotated method is always built from those names, so a usage written next to them would be dead text - declaring both is refused at registration. Whatever you put in usage is rendered exactly as written: nothing inside it is a placeholder and nothing is stripped out of it. The framework already prints ▶ /${label} ${path} in front of it (e.g. ▶ /eccooldown set), so the sub-command's own name does not belong in the string.


Flags

Beyond positional @Arg parameters, a method can declare @FlagArg parameters for --name value style options that can appear anywhere on the command line (/eccooldown set myid 5m --network):

@FinalCMD.SubCMD(subcmd = "set")
public void set(FCommandSender sender,
                 @Arg(name = "<CooldownID>") String cooldownId,
                 @Arg(name = "<duration>") String duration,
                 @FlagArg(name = "--network", aliases = "-n", def = "false") Boolean network) {
    Cooldown cooldown = network ? Cooldown.network(cooldownId) : Cooldown.of(cooldownId);
    // ...
}

A visible flag gets a compact [--network | -n] token on the usage line and its own hover block. Full syntax rules (quoting, -- end-of-flags, negative numbers), the @FlagArg attributes, permission-per- flag, and the manual MultiArgumentos.getFlags() escape hatch are on the dedicated Flags page.


Permissions

Permission checks run over the whole path at dispatch time, root first, and every segment gets a say:

  • A @FinalCMD, @FinalCMD.Node, @FinalCMD.SubCMD or @FinalCMD.Execute with a non-empty permission requires that node. Declaring it once on a branch covers everything under it, which is why /ecstorage transfer carries the node and its two leaves declare nothing.
  • The check also gates tab-completion, at every level: a sender without the branch's permission never sees the branch, and never completes anything inside it either.
  • An empty permission means "no node required".

Permission nodes are ordinary strings; keep them in a PermissionNodes constants class per plugin so they stay consistent between the annotation and your plugin.yml.


Access validation

For gates richer than a single permission node - "must be a player", "must be a clan leader" - extend CMDAccessValidation and list it in validation = { ... }. It runs before arguments are parsed:

public class MustBePlayerValidation extends CMDAccessValidation {

    @FCLocale(lang = LocaleType.EN_US, text = "&c Only players can run this command.")
    @FCLocale(lang = LocaleType.PT_BR, text = "&c Apenas jogadores podem usar este comando.")
    public static LocaleMessage ONLY_PLAYERS;

    @Override
    public boolean onPreCommandValidation(AccessContext accessContext) {
        if (!accessContext.isPlayer()) {
            ONLY_PLAYERS.send(accessContext.getSender()); // you MAY warn here
            return false;                                 // false denies execution
        }
        return true;
    }

    @Override
    public boolean onPreTabValidation(AccessContext accessContext) {
        return accessContext.isPlayer(); // hides the sub-command from tab-completion; do NOT warn here
    }
}

AccessContext exposes getSender(), isPlayer(), getPlayerData(), getPDSection(...), and hasProperPermission(). Returning false from onPreCommandValidation denies the command (and lets you send an explanation); returning false from onPreTabValidation simply hides the entry from tab-completion. A validation class with its own @FCLocale fields has them loaded automatically.


Locales in commands

Commands are the primary place localized messages are used. Two patterns coexist:

  • Inline description - the locales = { @FCLocale(...) } on @FinalCMD/@SubCMD provides the localized help description for that command.
  • Static message fields - declare public static LocaleMessage FOO; on the command class with @FCLocale annotations; the framework populates them when the command is registered. Send them with FOO.addPlaceholder("x", value).send(sender) - the key is declared bare, and the text cites ${x}. The scan walks up the class hierarchy, so a LocaleMessage declared on a base command class is loaded too.

Where an entry lands in the language file follows the tree, not the class name. A help-line entry is keyed by the command path - lp.user.permission.SET - and a static field by the chain of enclosing simple names. Two inner classes both called PermissionNode, under different parents, are two different commands, and a simple name cannot tell them apart.

Full details, language files, and the ${placeholder} mechanism are on Localization.


See also

  • Argument Parsing - @Arg, <required> vs [nullable], built-in types, def(), custom ArgParsers.
  • Flags - @FlagArg, the --name value syntax, help/tab rendering, the manual flag API.
  • Localization - @FCLocale, LocaleMessage, language files, placeholders.
  • FancyText - hover/click text you can build inside a command (as /eclocale list does).
  • Quick Start - a minimal plugin wiring a command, a config, and a locale end to end.
  • Platform Abstraction - FCommandSender/FPlayer, the portable sender types.

Clone this wiki locally