Skip to content

Command Framework

Petrus Pradella edited this page Jul 23, 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.

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.

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.

commands.yml - the central command registry file

Every FinalCMDPluginCommand.registerCommand() call - both FinalCMDManager.registerCommand(...) overloads, and dynamic registrations like CMDAlias - is gated by ONE commands.yml, in EverNifeCore's own data folder, seeded automatically on first registration:

Commands:
  MyPlugin:
    template:
      enabled: true
      aliases: [tpl]
  • enabled: false skips the platform registration entirely (the command never reaches the server, tab-complete, or getRegisteredCommands()) and logs why.
  • aliases: overrides the command's extra labels only - the primary label (template above) is the entry's identity and can never be changed here.
  • Changes apply on the owning plugin's next boot or reload - there is no hot rebind.

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. 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 = "%name% <PluginName> <LocaleName>")
public void set(FCommandSender sender, MultiArgumentos argumentos, HelpLine helpLine) {
    if (argumentos.emptyArgs(1, 2)) {
        helpLine.sendTo(sender); // prints this sub-command's usage line
        return;
    }
    // ...
}

⚠️ usage is only read when the method has NO @Arg parameter. As soon as a method declares one @Arg, the help line is built entirely from the @Arg names instead and usage is ignored - never write both on the same method. %name% and %label% typed inside usage are legacy tokens that get stripped to "" (not substituted with anything) before the text is shown; new usage strings don't need them. (The %label%/%subcmd% you see rendered in front of the line, e.g. ▶ /eccooldown set, come from the framework's own prefix template, not from anything you write in usage.)


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 are per-command and per-sub-command, evaluated at dispatch time against the sender:

  • A @FinalCMD / @FinalCMD.SubCMD with a non-empty permission requires that node.
  • The check also gates tab-completion: a sender without the permission won't see the sub-command suggested.
  • 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).

Full details, language files, and the %placeholder% mechanism are on Localization.


Migrating from 2.x

The command framework changed shape across several 3.x milestones. If you have plugin code written against 2.x, these are the breaking changes to walk through:

# 2.x 3.x Notes
1 @FinalCMD(desc = "...") / @SubCMD(desc = "...") locales = { @FCLocale(...) } desc was removed from both annotations; locales() is the only declarative description left. For dynamic commands built per-instance (see CMDAlias), CMDData.setDescriptionOverride(...) takes a LocaleMessageImp - typically a per-instance copy derived via LocaleMessageImp#derivePlaceholderResolved from a class-level @FCLocale template, so each instance keeps its own multi-language hover.
2 -nome:valor flag syntax --nome valor The old single-dash-colon syntax is gone entirely (: is no longer special). Multi-word values need quotes: --title 'A B'. See Flags.
3 MultiArgumentos.getFlag(...) matched the old syntax only getFlag(...) now resolves x/-x/--x the same way, case-insensitively No API change, but flag VALUES produced by the parser changed shape - re-check any code that reads a flag's raw string.
4 @FlagArg existed but crashed at dispatch if used @FlagArg fully works: declarative binding, help/hover/tab, permission-per-flag If you had @FlagArg parameters that never actually ran (they threw before), they now run for real - re-verify the behavior.
5 FinalCMDManager.registerCommand(...) returned boolean Returns List<FinalCMDPluginCommand> (empty = total failure) Check .isEmpty() instead of a boolean; a multi-@FinalCMD class can now return a partial list.
6 /account, /eccooldown setnetwork, /eccooldown setplayernetwork /ecaccount, /eccooldown set --network, /eccooldown setplayer --network Builtin command renames/collapses - see the table below.
7 evernifecore.command.storagestatus / .storagetransfer (CMDStorageStatus/CMDStorageTransfer, two separate commands) evernifecore.command.storage.status / .storage.transfer (CMDECStorage, one command with status/transfer sub-commands) Permission nodes and command classes both changed; /ecstorage status and /ecstorage transfer replace the two old commands.

Renamed/merged builtin commands, in detail:

Old New Permission node(s)
/account (alias of /ecaccount) info|link|unlink|migrate /ecaccount info|link|unlink|migrate (the generic /account alias is gone) evernifecore.command.account(.link) - unchanged
/ecorestoragestatus / /ecstoragestatus (CMDStorageStatus) /ecstorage status evernifecore.command.storage.status
/ecorestoragetransfer / /ecstoragetransfer (CMDStorageTransfer) /ecstorage transfer evernifecore.command.storage.transfer
/eccooldown setnetwork <id> <duration> /eccooldown set <id> <duration> --network evernifecore.command.cooldown - unchanged
/eccooldown setplayernetwork <player> <id> <duration> /eccooldown setplayer <player> <id> <duration> --network evernifecore.command.cooldown - unchanged

A downstream plugin still compiled against the old core API breaks loudly at load time rather than silently - for example, a plugin calling the old ECStorage.openBackend(...) shape against a new core jar fails with NoSuchMethodError on that call, not a corrupted read. This is expected for a major version bump: rebuild downstream plugins against 3.x.


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