-
Notifications
You must be signed in to change notification settings - Fork 7
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.
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.
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.
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.Ignoreon a method tells the scanner to skip a method that was inherited from a super-class (the scanner walks the class hierarchy up toObject, so an annotated method on a parent class is picked up unless you ignore it).
The scanner supports two layouts, and they behave differently:
-
One
@FinalCMD+ N@FinalCMD.SubCMD(the common case):@FinalCMDis on the class or on a single method, and the@SubCMDmethods become its sub-commands. This is what gives you/template info,/template coins, etc. -
N
@FinalCMDmethods on one class: each annotated method becomes its own stand-alone command with no sub-commands.@FinalCMD.SubCMDis not allowed in this layout and is ignored with a warning.
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.
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 twiceA 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.
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: falseskips the platform registration entirely (the command never reaches the server, tab-complete, orgetRegisteredCommands()) and logs why. -
aliases:overrides the command's extra labels only - the primary label (templateabove) 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.
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;
}
// ...
}
⚠️ usageis only read when the method has NO@Argparameter. As soon as a method declares one@Arg, the help line is built entirely from the@Argnames instead andusageis ignored - never write both on the same method.%name%and%label%typed insideusageare legacy tokens that get stripped to""(not substituted with anything) before the text is shown; newusagestrings 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 inusage.)
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.
Permission checks are per-command and per-sub-command, evaluated at dispatch time against the sender:
- A
@FinalCMD/@FinalCMD.SubCMDwith a non-emptypermissionrequires that node. - The check also gates tab-completion: a sender without the permission won't see the sub-command suggested.
- An empty
permissionmeans "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.
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.
Commands are the primary place localized messages are used. Two patterns coexist:
-
Inline description - the
locales = { @FCLocale(...) }on@FinalCMD/@SubCMDprovides the localized help description for that command. -
Static message fields - declare
public static LocaleMessage FOO;on the command class with@FCLocaleannotations; the framework populates them when the command is registered. Send them withFOO.addPlaceholder("%x%", value).send(sender).
Full details, language files, and the %placeholder% mechanism are on Localization.
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.
-
Argument Parsing -
@Arg,<required>vs[nullable], built-in types,def(), customArgParsers. -
Flags -
@FlagArg, the--name valuesyntax, 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 listdoes). - Quick Start - a minimal plugin wiring a command, a config, and a locale end to end.
-
Platform Abstraction -
FCommandSender/FPlayer, the portable sender types.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference