-
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.
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.
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("<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 legal when the method declares no @Arg and no @Arg.Flag (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("<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.
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 | produces: the tokens the branch eats right after its own label |
@FinalCMD.Execute |
method of a node class | makes the branch itself runnable |
@Arg.NodeCaptured |
parameter of any descendant | consumes: hands that branch's captured value to a leaf |
@FinalCMD.Capture produces and @Arg.NodeCaptured consumes - that pairing is why the consumer
carries Node in its name. They sit in different annotations because they describe different things:
Capture declares a node, so it stays on @FinalCMD; NodeCaptured is one of the four sources of a
parameter's value, so it lives with the other three under @Arg (see Argument Parsing).
@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("<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, @Arg.NodeCaptured DuelArena arena) { }
@FinalCMD.SubCMD(subcmd = "info")
public void info(FCommandSender sender, @Arg.NodeCaptured DuelArena arena) { }
@FinalCMD.Node(subcmd = "spawn")
public static class SpawnNode {
@FinalCMD.SubCMD(subcmd = "set")
public void set(FCommandSender sender,
@Arg.NodeCaptured DuelArena arena,
@Arg("<index>") Integer index,
@Arg(value = "[force]", def = "true") Boolean force) { }
}
}
}@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.
@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. void is legal only for a
branch that exists to declare flags.
⚠️ Returningnullaborts the whole dispatch and the framework says nothing. That is the contract, not an oversight: the capture is the only code that knows why its segment cannot be entered ("no such arena", "you do not own it"), so whoever answersnullis whoever has already told the sender. A capture that returnsnullwithout sending a message leaves the sender staring at an unchanged screen. On the consuming end the mirror holds: an@Arg.NodeCapturedparameter is nevernull, so a null check there can only be dead code.
More than one token is fine:
@FinalCMD.Capture
public ArenaInWorld capture(@Arg("<world>") String world,
@Arg("<arena>") DuelArena arena) {
return new ArenaInWorld(world, arena);
}
// /arena duel nether Colosseum spawn set 2The 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
@Arg.NodeCaptured DuelArena arena) { } // captured: what it is aboutLeft 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,
@Arg.NodeCaptured("duel") DuelArena a,
@Arg.NodeCaptured("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("<world>") String world,
@Arg("<arena>") DuelArena arena) { ... }
@FinalCMD.SubCMD(subcmd = "four")
public void four(FCommandSender sender,
@Arg.NodeCaptured("duel") ArenaInWorld ctx, // the @Capture's return
@Arg.NodeCaptured("duel:<world>") String world) { } // one token of itBoth 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.
@FinalCMD.Execute makes the branch itself runnable: /arena duel Colosseum runs the method instead of
printing the branch's help. It cannot declare a positional @Arg, and that restriction is the whole
reason the traversal stays unambiguous: with no positional of its own, every bare token after a node
still has to be a child's label. The other three sources are fine, precisely because none of them
claims a bare token - a contextual parameter and an @Arg.NodeCaptured read no token at all, and an
@Arg.Flag is addressed by its own --name.
Without an @Execute, typing the branch prints its help.
| Situation at a node | Rule |
|---|---|
node with a capture of k tokens |
eats the next k tokens, always |
| the token names a child | that child, always - a declaration beats every convention below |
| the token is a flag the node already reads | the path ends here; the token belongs to the window |
help word (help, ?, ajuda) nothing claims |
the node's help |
| anything else | unknown sub-command, naming the children that do exist |
Because the capture eats before any literal is matched, a target whose name happens to equal a child's
label is still reachable - and, for the same reason, a target named help is too.
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 the Flags page).
2. EXTRACT pull every declared flag out of what is left, with the bindings accumulated root..leaf.
Runs on every command, and stops where a variadic tail begins.
3. CAPTURE run each @Capture, root -> leaf, each parsing its own @Arg and receiving its @Arg.Flag.
4. PARSE resolve the leaf's parameters, in the window the path left behind.
5. INVOKE call the method.
Phase 4 is itself ordered, and the order is what lets a parser read a value the invocation already
produced: captures, then early contextuals, then flags, then positionals, then late contextuals.
Captures come first because phase 3 already resolved them - making anything wait would only hide them
from whoever runs first. The five steps and the ResolutionPhase lever that moves a parameter between
the second and the last are on Argument Parsing.
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.
Nothing thrown by phase 5 escapes the framework: an exception out of your method's body is logged with its stack on the owning plugin's log, the sender gets a generic localized "something went wrong running this command", and the platform never sees it. Handle what you can explain; the rest is a bug report, not a message.
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 @Arg.NodeCaptured, two children claiming one word, a label written with brackets, a node
class also carrying @FinalCMD, a mount cycle, an @Execute with an @Arg, a flag spelling an
ancestor already claims, a parameter carrying two of the four @Arg... families, and a primitive
parameter on an argument that may resolve to nothing.
Every one of them arrives as an ArgMountException, including the ones a parser raises about its own
declaration - the parser's sentence travels, with the parameter it was about added to it. One type for
the whole catalog, so a caller can tell "this declaration is wrong" from "somebody typed something
wrong" by the exception alone.
📌 The
evernifecore-common-testsartifact publishes that whole battery asCommandShapeErrors.check(harness), plus a four-levelCommands.referenceTree()andharness.tree(cmd)structural assertions - so a plugin building trees tests them without copying a fixture.
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 the failure at SEVERE on your plugin's log, with the cause, and the command is skipped - the boot never stops for it.
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.
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: trueFrom 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: falseremoves 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.
-
permissionis 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 activeWith the feature off it answers true for everything - nothing was ever pruned.
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 open the help at every depth, so /arena duel Colosseum spawn help prints
that branch's help. They are an interceptor, not a reservation: a child labelled help, or a capture
standing where the word was typed, claims the token first, so @FinalCMD.SubCMD(subcmd = "help") is
reachable and a player named Ajuda is addressable. The word means help wherever nothing else claims
it, which is everywhere by default. The list itself is a server setting -
Settings.Commands.HELP_WORDS in EverNifeCore's config.yml - so a Spanish or French server adds
ayuda/aide without touching a single command; emptying it does not turn the help off, the shipped
words come back.
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 child the sender cannot reach never shows up - its own permission, its own
validations and its own playerOnly all filter the line out, and a sender left with no line at all is
told which of the two it was. It is the child's own declaration that decides: walking the subtree to
ask whether anything further down is still reachable is what the tab and the mistyped-word list do, and
the help stops at the level it prints.
A branch with more children than Settings.Commands.HELP_PAGE_SIZE (8 by default) is paged: the
page is the token right after the help word (/arena duel help 2), a footer says where the sender is
and spells out the command for the next page, and asking for a page past the end lands on the last one.
A help that fits in a single page is printed exactly as it always was, with no page indicator at all.
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;
}
// ...
}
⚠️ usageis only legal when the method declares NO@Argand no@Arg.Flag. The help line of an annotated method is always built from those names, so ausagewritten next to them would be dead text - declaring both is refused at registration. Whatever you put inusageis 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.
Beyond positional @Arg parameters, a method can declare @Arg.Flag 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("<CooldownID>") String cooldownId,
@Arg("<duration>") String duration,
@Arg.Flag(value = "--network", aliases = "-n", def = "false") Boolean network) {
Cooldown cooldown = network ? Cooldown.network(cooldownId) : Cooldown.of(cooldownId);
// ...
}A visible flag gets a compact [--network] token on the usage line - one spelling, the one usageName()
picked - and its own hover block, which is where every spelling is listed. Full syntax rules (quoting,
-- end-of-flags, negative numbers), the @Arg.Flag attributes, permission-per-flag, and the manual
MultiArgumentos.getFlags() escape hatch are on the dedicated Flags page.
Permission checks run over the whole path at dispatch time, root first, and every segment gets a say:
- A
@FinalCMD,@FinalCMD.Node,@FinalCMD.SubCMDor@FinalCMD.Executewith a non-emptypermissionrequires that node. Declaring it once on a branch covers everything under it, which is why/ecstorage transfercarries 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
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.
Permission and validation are evaluated by the same code for dispatch, tab, help and the "available subcommands" list a mistyped word gets - the only difference is that a dispatch speaks (permission message, or the validation's own warning) and a listing stays silent. The chain is always the whole one:
-
every node from the root down to the target, whether or not it has a method of its own. A
@FinalCMD.Nodethat exists only to hold children still validates the paths that pass through it, and a validation declared on the@FinalCMDroot guards the entire tree, not only its own method; -
plus the
@FinalCMD.Executedeclaration of the node that runs, which carries a permission and validations the node it hangs on knows nothing about. A@SubCMDleaf and its node are one declaration, so there is nothing extra to ask there - and nothing runs twice.
That is what makes "hidden" mean hidden: a subcommand the tab does not offer is a subcommand the dispatch does not run, and a typo does not list it either.
playerOnly is decided separately, by the parsers a method declares, and it gets its own sentence:
a console that runs (or asks the help of) a player-only command is told "Only a player can use
this", never "you do not have the permission". No permission node would have opened it, and sending
an admin to look for one is worse than saying nothing - which is what this used to do.
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)- the key is declared bare, and the text cites${x}. The scan walks up the class hierarchy, so aLocaleMessagedeclared 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.
-
Argument Parsing -
@Arg,<required>vs[nullable], built-in types,def(), customArgParsers. -
Flags -
@Arg.Flag, 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