-
Notifications
You must be signed in to change notification settings - Fork 7
Argument Parsing
What this page covers: how a command method's parameters become typed, validated arguments. Where a value comes from is decided by its annotation, never by syntax buried in a string. This page is the companion to Command Framework.
@FinalCMD.SubCMD(subcmd = "coins")
public void coins(FCommandSender sender, @Arg("<amount>") Integer amount) { ... }Every source of a parameter's value is spelled @Arg..., and the nesting is ergonomics rather than a
hierarchy - four annotations, one place to look:
| Form | Provenance |
|---|---|
| no annotation | the invocation itself - the caller (sender, label, raw args, ...). It never consumes a typed token. Contextual, exactly like @Arg.Contextual, just unnamed. |
@Arg("<name>") |
a token of the line, in this executable's window |
@Arg.Flag("--name") |
a --name value flag, anywhere after the command path - see Flags
|
@Arg.Contextual("name") |
the invocation itself, with a name, a pinned parser or a chosen phase. Consumes no token. |
@Arg.NodeCaptured |
what an ancestor node captured - see Command Framework |
@Arg, @Arg.Flag and @Arg.Contextual name themselves with value(), so the short form
@Arg("<amount>") is the usual spelling. Java only allows that short form while value is the only
element: the moment you add context, def, parser or aliases, it has to be written out as
value = "...". (@Arg.NodeCaptured also carries a value(), but it names the ancestor to read
from, not the parameter, and it defaults to empty - see Command Framework.)
@Arg("<amount>") Integer amount // value alone: short form
@Arg(value = "[page]", context = "[1:*]", def = "1") // anything else: value = "..."So an unannotated FPlayer is always whoever typed the command, and a target is always annotated. That
is what spares the framework a wrapper type whose only job would be telling the two apart.
public @interface Arg {
String value(); // display name incl. brackets, e.g. "<amount>"
String context() default ""; // parser-specific constraint (see below)
boolean fromSender() default false; // resolve from the sender when no token was typed
Class<? extends ArgParser> parser() default ArgParser.class; // override the type-based parser
FCLocale[] locales() default {}; // localized error text for this arg
String def() default ""; // declarative default for an OPTIONAL arg (see below)
}The brackets around the name decide whether the argument is required. This is parsed from the
value string itself, and there are exactly two forms:
| Form | Meaning | If the player omits it |
|---|---|---|
<name> |
Required | Parsing fails, the arg's error message is shown, the command aborts. |
[name] |
Nullable / optional | The parameter is injected as null; your method handles it. |
// Required: "/heal <target>" - target must be given.
public void heal(FCommandSender s, @Arg("<target>") FPlayer target) { ... }
// Optional: "/heal [target]" - null target means "heal myself".
public void heal(FCommandSender s, @Arg("[target]") FPlayer target) {
FPlayer who = target != null ? target : (FPlayer) s;
// ...
}
⚠️ Gotcha - an optional[name]argument is injected asnullwhen omitted. Always null-check optional arguments before dereferencing them.
Where a value comes from is always an annotation, never syntax buried in the displayed name.
fromSender = true lets the parser answer from the sender's own state when the position was left
empty, so one method serves both /arena enable true (the arena the admin is standing in) and
/arena enable true myarena (the named one):
public void enable(FCommandSender s,
@Arg("<enabled>") Boolean enabled,
@Arg(value = "<arena>", fromSender = true) Arena arena) { ... }Two rules follow from it:
- the parser has to override
fromSender(ParseCall). One that does not is refused at registration - otherwise the argument would silently eat the next token instead of being inferred; - it fires only when that position is empty, so an inferred argument has to be the last positional (or optional). It does not hand the token back to the argument after it.
A ... suffix inside the brackets makes the argument the tail: it takes every token left. It has to
be the method's last @Arg, and there is at most one. Six parameter types are accepted, and they are
six ways of handing over the same tokens:
| Type | What you get |
|---|---|
String |
the tokens joined by a single space |
String[] |
one entry per token |
Argumento |
the joined text as the raw token type |
MultiArgumentos |
the tokens as an argument list |
List<String> |
one entry per token, as typed |
Set<String> |
a LinkedHashSet: typing order kept, repeats dropped |
The two collections carry the raw tokens, so their element type is always String - a List<Integer>
tail is refused at registration rather than handing the method strings under another type.
@FinalCMD.SubCMD(subcmd = "broadcast")
public void broadcast(FCommandSender s, @Arg("<message...>") String message) { ... }A required tail with zero tokens sends the help line. An optional [rest...] with zero tokens hands over
an empty value of its type - "", an empty array, an empty collection - never null: it is the one
optional argument that behaves this way, because "the rest of the line" always exists and may simply hold
nothing. Give it a def() to get something else there; unlike every other def(), a tail's is split on
whitespace, because a tail is a stretch of the line rather than one position on it.
The tail never goes through an ArgParser - choosing among six shapes of the same tokens is the
framework's job - but it still shows on the usage line and still tab-completes (the tail's parser keeps
answering for every position it covers). It does land in the resolved-arguments bag like any other
positional, so a late contextual can read it by name or by type.
def() is only legal on an [optional] argument - declaring it on a <required> one, or next to
fromSender = true (which answers even when nothing was typed, so the default could never be reached),
fails at registration, not at dispatch. When the player
omits the argument (including typing an explicit empty string), the def() text is parsed by the
exact same ArgParser the typed value would have gone through - context bounds and choices apply
to it too:
// Omitted -> parsed as if the player had typed "1" - so it still respects the [1:*] bound.
@Arg(value = "[page]", context = "[1:*]", def = "1") Integer pageA value the player actually types always wins over def().
A def() the parser cannot use is the command's bug, never the sender's - that text was written by
whoever declared the argument, so it does not take the silent degradation an optional argument gives a
token nobody recognized. Either way it can fail - unreadable for the type, or refused by a context
bound or choice - the dispatch aborts, the stack and the offending default go to the owning plugin's log,
and whoever typed the command gets a generic line instead of being told to fix a value they never typed.
def() also supports the %placeholder% mechanism ICustomFinalCMD.customize() resolves for the rest
of ArgData (name, context, locales).
context narrows what the parser accepts, and its meaning is parser-specific. Every built-in parser
shares the same small mini-DSL:
| Type | Form | Meaning |
|---|---|---|
| any number type |
"min:max" (e.g. "1:10", "[1:*]", "[*:10]") |
An inclusive numeric range. A bare * means "no limit on this side", and which side it is written on decides the direction, so [*:10] reads as "up to 10"; -*/+* stay available as the explicit spelling. A floor above the ceiling ([5:1]) is refused at registration, and so is a fractional bound on an integral type ([0:2.5] on an Integer), which could only ever mean [0:2]. Wrapping the whole thing in the arg's own <...>/[...] brackets is conventional but not required - the parser strips a leading/trailing bracket pair if present, and reads the value unchanged either way. |
| any number type |
"a|b|c" (e.g. "1|5|10") |
A fixed set of accepted numeric values. |
String, Argumento
|
"a|b|c" (e.g. "add|remove|list") |
A fixed, case-insensitive option set, also used for tab-complete. A context you wrote is always a choice, a single option included (context = "admin" accepts that word and nothing else). Only an EMPTY context falls back to the arg's own name, and a name of one word (<mode>) then means "any single token" - an Argumento with no context takes any token whatever its name says, since the raw token type is what it is for, and the name's options then only feed tab-complete. |
Enum (any) |
"A|B|C" |
A subset of the enum's constants (matched case-insensitively by their name()). An option naming no constant is refused at registration, with the constants that do exist - an unsatisfiable argument is never worth booting. Empty context means every constant (capped at 50 - ArgParserEnum throws past that, by design: that many choices belongs in a different UI). |
Boolean |
"yes|no" (exactly 2 options) |
A custom true/false pair instead of the built-in vocabulary, not on top of it: with a declared pair, [buy|sell] accepts buy and sell and nothing else - sim/yes/on stop being accepted, which is what the refusal message already claimed. The pair reads by position (first = true), so a pair the words already contradict ([off|on]) is refused at registration. A custom-context Boolean must have exactly 2 options - it fails at registration otherwise. |
// A fixed option set (String parser): only these three tokens accepted, and tab-completed.
@Arg(value = "<mode>", context = "add|remove|list") String mode
// Matching ignores case, but the value handed to the method is the DECLARED spelling:
// typing "add" delivers "Add", so a switch over the declared constants always hits.
@Arg(value = "<action>", context = "[Add|Remove]") String action
// A numeric range (Number parser): page >= 1.
@Arg(value = "[page]", context = "[1:*]") Integer pageThe same DSL applies to a @Arg.Flag's context(), applied to the flag's value parser - see
Flags.
These types resolve out of the box - just declare the parameter type (positional needs @Arg):
| Type | Notes |
|---|---|
String |
Free text, or a fixed option set via context. |
Integer, Long, Short, Byte, Float, Double
|
Numbers, with optional [min:max] range context. The parameter gets back its own type, and a value that type cannot hold is refused with the range it missed - never clamped to MAX_VALUE. |
Boolean |
true/false (and locale-aware variants). |
Enum (any) |
Any enum; defaults to all constants as options. |
UUID |
Parsed UUID. |
FPlayer |
Portable online player (both platforms). |
IPlayerData |
Resolves a player's data by name. |
NumberWrapper |
EverNifeCore's arbitrary-precision number. |
FCTimeFrame |
A duration like 10m, 2h, 3d. |
Argumento |
The raw single token, unparsed. |
PageVisualization |
A page number for a FancyText PageViewer. |
On the Minecraft platform these are registered additionally (lazily, see below): Player and
World, plus OreDictEntry (on modded servers) and FCWorldGuardRegion (when WorldGuard is present).
A parameter with no @Arg is filled from the invocation by an ArgParserContextual - it consumes no
token, so it changes neither the usage line nor which word lands on the next argument. The framework
knows these types out of the box:
| Type | Injected value |
|---|---|
FCommandSender |
The command sender (portable). |
FPlayer |
The sender as a player (command becomes player-only). |
String |
The command label the player typed. |
MultiArgumentos |
The tokens of THIS method's window, flags already extracted (see below). |
HelpLine |
This sub-command's help line, already rendered for the path typed, to sendTo(sender) on bad input. |
HelpContext |
The help of the node this dispatch reached (the nearest one that has children), not the root's, already bound to the path typed - sendTo(sender). Walk getNode().getParent() to go up. |
CommandPath |
The concrete path this dispatch walked - label, literals and captured tokens. |
PlayerData |
The sender's player data. |
PDSection |
A specific PlayerData section of the sender. |
On Minecraft you can additionally inject the native CommandSender, Player, and ItemStack (by
default the item in hand - see the equipment slots).
On Hytale, ItemStack is the Hytale one and reads the same way.
⚠️ HelpLineis one render of aHelpLineTemplate- the template is the shared, path-less object the framework builds at registration, andsendToonly exists on the render. InjectHelpLine.HelpContextandHelpContextTemplateare the same split for the same reason: what a method receives is the render, which already knows the line it was reached by, so it sends withhelpContext.sendTo(sender)(orsendTo(sender, page)) and takes no path.
MultiArgumentos is the escape hatch when you want to walk the tokens yourself. It is the window this
executable owns: index 0 is the first token after the command path, not after the label. For a leaf
three levels down, /mycmd user Steve set a b, getStringArg(0) is a. It is the window the framework
itself read the positionals from, so the flag tokens (and the bare --) are already gone from it - see
Flags:
@FinalCMD.SubCMD(subcmd = "set", usage = "<PluginName> <LocaleName>")
public void set(FCommandSender sender, MultiArgumentos argumentos, HelpLine helpLine) {
if (argumentos.emptyArgs(0, 1)) { helpLine.sendTo(sender); return; }
String pluginName = argumentos.getStringArg(0);
// ...
}The same holds for ArgInfo.getIndex() inside a parser: the index is local to the window, so a parser
never has to know how deep its command sits. Asking a non-positional ArgInfo for its index throws
instead of inventing a number - only a positional argument sits at a position, and getSource() is what
tells the families apart.
A parameter with no annotation at all is already contextual, and that stays the common case: an
FCommandSender sender needs nothing written on it. Write @Arg.Contextual only when you need one of
the three things a bare parameter cannot say:
public @interface Contextual {
String value(); // how the value is addressed afterwards - unique per method
String context() default ""; // parser-specific, e.g. the equipment slot below
Class<? extends ArgParserContextual> parser() default ArgParserContextual.class;
ResolutionPhase phase() default ResolutionPhase.PARSER_DEFAULT; // see below
}| You need | Write |
|---|---|
| to name the value, so another parser can ask for it by name | @Arg.Contextual("helmet") |
| a parser the manager would not pick for that type | @Arg.Contextual(value = "kit", parser = MyParser.class) |
| to move the parameter across the tokens | @Arg.Contextual(value = "late", phase = ResolutionPhase.AFTER_ARGUMENTS) |
The name is how the value is addressed once resolved, so it is unique among every parameter of the
method - @Arg, @Arg.Flag and @Arg.Contextual share one namespace, and declaring a name twice is
refused at registration. Names are compared exactly as written, so --amount and <amount> never
collide: nobody types one meaning the other.
The contextual ItemStack is the one built-in parser that reads context(). Empty means what it always
meant - whatever the player is holding - and naming a slot reads that piece of equipment instead:
@FinalCMD.SubCMD(subcmd = "repairhelmet")
public void repairHelmet(FPlayer player,
@Arg.Contextual(value = "helmet", context = "HELMET") ItemStack helmet) {
if (helmet == null) { /* bare-headed - a fact, not an error */ }
}| Platform | Accepted context()
|
|---|---|
| Minecraft |
HELMET, CHESTPLATE, LEGGINGS, BOOTS, OFF_HAND
|
| Hytale |
HEAD, CHEST, HANDS, LEGS
|
Matching is case-insensitive, and the hand is deliberately not on either list - it is what an empty
context() means. Three outcomes are worth telling apart:
-
an empty slot is not a refusal: not wearing a helmet is a fact the method may act on, so the
parameter simply arrives
null; - an empty hand still refuses: a command asking for what you are holding has nothing to work with, so the dispatch aborts with the standard "you need to be holding an item" message;
-
a
context()that names no slot is refused at registration with anArgMountExceptionlisting the ones that do exist - it is a mistake in the command, not in anything a player could type.
On Minecraft, OFF_HAND on a 1.8-or-older server resolves to null rather than exploding: the slot did
not exist before 1.9.
To make a parameter of your own type resolvable, extend ArgParser<T>. It needs an (ArgInfo)
constructor and one method per job - the framework decides which one runs, and when:
| Method | Answers | Called when |
|---|---|---|
parse(ParseCall) |
what the token converted to, or why it did not | there is a token, or a def() stands in for one |
absent(ParseCall) |
the value of an optional argument nobody typed | no token, optional, no def()
|
fromSender(ParseCall) |
the value read off the sender | no token, @Arg(fromSender = true)
|
Only parse is abstract, and all three return a ParseResult<T> - never a bare value, never null,
and never a thrown "no". The point of the split is that what a failure costs is the framework's
decision, and only the text of it is the parser's - a parser that had to ask argInfo.isRequired()
was rewriting the framework's decision tree in every implementation.
| Kind | Built with | What the framework does |
|---|---|---|
VALUE |
ParseResult.of(value) |
hands it to the method. Refuses null - say empty() instead |
EMPTY |
ParseResult.empty() |
the parameter gets null and the invocation goes on |
UNRECOGNIZED |
unrecognized(reason...) |
fatal only if the argument is required; on an optional one it silently becomes EMPTY and nobody is told |
DENIED |
denied(reason...) |
always fatal, optional or not - a value that converted and was refused by a rule |
INTERNAL_ERROR |
ParseResult.internalError(cause) |
stack to the owning plugin's log, generic line to whoever typed it |
MISSING |
(framework) or missing() on a contextual parser |
the command's own usage line is sent and the invocation stops. A required token nobody typed produces it without consulting any parser |
unrecognized and denied are also available as protected shortcuts on ArgParser itself, which is
what the example below uses. Each takes either the messages directly or a
Supplier<List<ILocaleMessageBase>> for a reason that is expensive to build - the supplier only runs if
somebody is actually going to read it.
public class ArgParserTemplateKit extends ArgParser<TemplateKit> {
@FCLocale(lang = LocaleType.EN_US, text = "&c There is no kit named &e${name}&c.")
@FCLocale(lang = LocaleType.PT_BR, text = "&c Não existe nenhum kit chamado &e${name}&c.")
public static LocaleMessage KIT_NOT_FOUND;
@FCLocale(lang = LocaleType.EN_US, text = "&c The kit &e${name}&c is not available in this world.")
@FCLocale(lang = LocaleType.PT_BR, text = "&c O kit &e${name}&c não está disponível neste mundo.")
public static LocaleMessage KIT_NOT_HERE;
public ArgParserTemplateKit(ArgInfo argInfo) {
super(argInfo);
}
@Override
public ParseResult<TemplateKit> parse(ParseCall call) {
TemplateKit kit = TemplateKit.getByName(call.getArgumento().toString());
if (kit == null){
return unrecognized(KIT_NOT_FOUND.addPlaceholder("name", call.getArgumento()));
}
if (!kit.isAvailableTo(call.getSender())){
//found it and refusing anyway: a rule, so it is fatal even on an [optional] argument
return denied(KIT_NOT_HERE.addPlaceholder("name", kit.getName()));
}
return ParseResult.of(kit);
}
@Override
public List<String> tabComplete(TabContext tabContext) {
String lastWord = tabContext.getLastWord().toLowerCase();
return TemplateKit.getAll().stream()
.map(TemplateKit::getName)
.filter(name -> name.toLowerCase().startsWith(lastWord))
.sorted(String.CASE_INSENSITIVE_ORDER)
.collect(Collectors.toList());
}
}📌 Never check
isRequired()to decide whether to complain. Say which of the two refusals it is -unrecognizedfor "that is not one of mine",deniedfor "it is mine and I refuse it" - and the framework applies the cost.unrecognized()with no argument falls back to a generic "invalid argument".
MESSAGE and MESSAGE.addPlaceholder(...) are interchangeable as a reason, because addPlaceholder
hands back a new message instead of mutating the shared static field - two senders at once never read
each other's text.
map, retype and asDenied carry an outcome across types without rebuilding its reason, which is how
a parser wraps another one:
@Override
public ParseResult<NumberWrapper> parse(ParseCall call) {
return argParserNumber.parse(call).map(NumberWrapper::of); // a failure retypes itself, untouched
}retype() moves a failure to another T (it refuses a result that has a value), and asDenied()
promotes an UNRECOGNIZED to DENIED for a token whose failure is fatal no matter how the argument was
declared.
Returning is the contract, but a helper three frames down can throw instead, carrying the same outcome:
throw new ArgParseException(denied(KIT_NOT_HERE.addPlaceholder("name", name)));The engine adopts it exactly as it would a returned one - the shortcut buys no different policy. The
exception has a single constructor (it always carries a failing ParseResult), no message of its own and
no stack trace: it is control flow, not a bug report. Anything else a parser throws becomes
INTERNAL_ERROR, so a broken parser stops one argument instead of the whole dispatch.
ParseCall is one object instead of three parameters, so giving parsers more context later is an
addition rather than a signature break in every third-party parser in existence. Both parser families
answer the same questions under the same names - the shared half is the IParseCall interface:
| Call | What it gives |
|---|---|
getSender() |
who ran the command |
getArgInfo() |
what the framework knows about the argument being resolved |
getPath() |
the concrete path this dispatch walked |
describeArgument() |
how this argument is named in a message or a log |
previouslyParsed(Class) |
the most recent value of that type this invocation resolved, from any family |
previouslyParsed(name, Class) |
the value of the parameter that declared exactly that name |
captured(nodePath, Class) |
what an ancestor node captured, keyed by node path because a path may capture the same type twice |
ParseCall adds getArgumento() (EMPTY when the parser is answering without a token) and
isFlagValue(). ContextualParseCall adds the three things only a command invocation has:
getArgumentos(), getHelpContext() and getHelpLine(). Those three are deliberately not on the
shared interface - the same engine also serves a config file, where none of them exists.
The same ArgParser class resolves both a positional @Arg and a @Arg.Flag's value - there is no
separate parser type for flags. ParseCall.isFlagValue() tells you which one you are in (also true
while resolving a flag's def()), if your parser needs to behave differently for the two - see
Flags.
Everything one invocation resolves lands in a single bag: a token, a flag, an ancestor's capture and a contextual parameter alike. There are two ways to read it back, and they answer different questions:
-
by type -
previouslyParsed(Ticket.class)- answers what was filed under exactly that class and, failing that, the most recently resolved value the class accepts. Base types and interfaces work (previouslyParsed(CharSequence.class)finds aString), and an exact match wins even when a subtype resolved later. It is a convenience, and it admits ties; -
by declared name -
previouslyParsed("<from>", String.class)- answers exactly, and answersnullwhen the name resolved to some other type entirely.
Prefer the name whenever it matters which one you get. That is the recipe for two parameters of the same type: they cannot share a name, so the name always tells them apart.
public void move(FCommandSender sender,
@Arg("<from>") String from,
@Arg("<to>") String to,
@Arg.Contextual(value = "report", parser = ReportingParser.class,
phase = ResolutionPhase.AFTER_ARGUMENTS) Report report) { }
// inside ReportingParser.parse(ContextualParseCall call):
call.previouslyParsed(String.class); // "the most recent String" - the <to> one
call.previouslyParsed("<from>", String.class); // exactly the <from> oneThe name a value is filed under is the one the annotation spells, brackets and dashes included:
<from>, --network. A parameter with no annotation declares no name and is only reachable by type;
an @Arg.NodeCaptured is addressed by the ancestor's node path through captured(...) instead.
For a value read off the invocation rather than off a token, extend ArgParserContextual<T>. Both
families share the root AbstractArgParser<T, C extends IParseCall>, so the shape is the one you
already know - an (ArgInfo) constructor, a parse returning a ParseResult<T>, and the same
unrecognized/denied shortcuts:
| Member | Purpose |
|---|---|
parse(ContextualParseCall) |
resolves the parameter, or says why it could not (abstract) |
requiresToBeAPlayer() |
(abstract) whether the parameter forces the command to be player-only - it gates help and tab visibility too, not just the dispatch |
defaultPhase() |
when this parser runs, for every parameter that does not override it. Defaults to BEFORE_ARGUMENTS
|
missing() |
(protected) abort and send the command's own usage line |
public class ArgParserContextualActiveKit extends ArgParserContextual<TemplateKit> {
public ArgParserContextualActiveKit(ArgInfo argInfo) {
super(argInfo);
}
@Override
public ParseResult<TemplateKit> parse(ContextualParseCall call) {
TemplateKit kit = KitService.activeKitOf(call.getSender());
//nothing to give, and the shape of the command is the honest answer
return kit != null ? ParseResult.of(kit) : missing();
}
@Override
public boolean requiresToBeAPlayer() {
return true;
}
}Nothing routes above a contextual parameter - there is no token to be optional about - so every failure
it reports is fatal exactly as it left the parser. missing() is the one to reach for when the honest
answer is the shape of the command itself: it sends the usage line and stops the invocation, the same
thing a required token nobody typed gets. Inside a @FinalCMD.Capture, the line sent is the node's.
requiresToBeAPlayer() stays abstract on purpose. It does not only gate the dispatch - it gates whether
the sub-command shows up in help and in tab-complete at all, so a false written without thinking leaks
a player-only branch to the console.
A contextual parameter resolves before the tokens by default. That is what lets the parser of a token read what the invocation produced, and it is the reason the default is the early one:
public enum ResolutionPhase {
PARSER_DEFAULT, // no opinion - leave the choice to the parser. Never an effective phase
BEFORE_ARGUMENTS, // before any token: flags, captures and positionals all see what this resolved
AFTER_ARGUMENTS // after every token: this parser sees the flags, the captures and the positionals
}One invoke() resolves a method's parameters in five steps, always in this order:
-
captures - what each ancestor
@FinalCMD.Capturereturned, handed to@Arg.NodeCaptured; -
contextual parameters whose phase is
BEFORE_ARGUMENTS(the default); -
flags -
@Arg.Flag, pulled out of the tail once for the whole path; -
positionals -
@Arg, in the window the path left behind; -
contextual parameters whose phase is
AFTER_ARGUMENTS.
Step 1 consults no parser of its own, so it leaves no trace in a log; it is first because the captures
were already resolved while the path was being walked, so everything below - a BEFORE_ARGUMENTS
contextual included - can read them through previouslyParsed.
The lever lives on the parser, and the parameter overrides it. Most contextual parameters carry no
annotation at all - a bare FCommandSender sender has nowhere to declare anything - so a parser that is
expensive, or that depends on a typed token, says so once in defaultPhase():
@Override
public ResolutionPhase defaultPhase() {
return ResolutionPhase.AFTER_ARGUMENTS; // this parser wants to see the tokens
}An individual parameter can still drag that parser to the other side with
@Arg.Contextual(value = "...", phase = ResolutionPhase.BEFORE_ARGUMENTS), and the annotation always
wins. A parser whose defaultPhase() answers PARSER_DEFAULT is refused at registration: that
value is the question, not an answer to it.
⚠️ ABEFORE_ARGUMENTSparser sees no token at all -previouslyParsedfor anything a token or a capture produced answersnull, and that is the contract, not a bug. If your parser needs a typed value, declareAFTER_ARGUMENTS.
tabComplete(TabContext) runs once per keystroke, and the traversal that leads to it counts tokens
without resolving any. So TabContext hands over facts, not values:
| Call | What it gives |
|---|---|
getLastWord() |
the word being completed |
getIndex() |
where it sits in the raw args
|
getLocalIndex() |
where it sits once the command path is sliced off - 0 is the first token after the path, flags included |
getPath() |
the segments already consumed, as typed |
getCaptureToken(nodePath) |
what an ancestor node captured, raw: never parsed, never validated |
@Override
public List<String> tabComplete(TabContext ctx) {
String userToken = ctx.getCaptureToken("user"); // exactly as typed, maybe not a real player
return homeCache.namesOf(userToken);
}Resolving that token is your business, with your own cache. The framework does not pay, on every keystroke, for a value the traversal never needed - and when it cannot answer a position, the answer is an empty list, never a list of online player names dressed up as a real suggestion.
Three ways, from most local to most global:
// 1. Per parameter - only this argument uses it.
public void kit(FCommandSender s, @Arg(value = "<kit>", parser = ArgParserTemplateKit.class) TemplateKit kit) { }
// 2. Per plugin - every TemplateKit parameter in THIS plugin's commands resolves through it.
ArgParserManager.addPluginParser(ecPluginData, TemplateKit.class, ArgParserTemplateKit.class);
// 3. Global - every ECPlugin can resolve this type.
ArgParserManager.addGlobalParser(TemplateKit.class, ArgParserTemplateKit.class);Register the parser before the command that uses it, so the framework already knows how to resolve the type at registration time:
public static void registerCommands(ECPluginData ecPluginData) {
ArgParserManager.addPluginParser(ecPluginData, TemplateKit.class, ArgParserTemplateKit.class);
FinalCMDManager.registerCommand(ecPluginData, TemplateCommand.class);
}Lookup order is plugin parsers first, then global, and inside each of those, the type itself
first, assignability afterwards: a parser registered for Truck answers for Truck even when one
for Vehicle was registered earlier, and Vehicle's still answers for every other Vehicle. That
matters because registration order is rarely yours to choose - every platform parser is registered
after the builtins that already cover its supertypes.
Registering the same exact type twice is a deliberate override: the last one wins and a line is
logged saying which parser replaced which. Contextual parsers use addPluginContextualParser /
addGlobalContextualParser and follow the identical rules.
The platform-specific parsers (Bukkit Player/World/ItemStack, Hytale types) are registered the
first time the command framework initializes, through IPlatform.registerArgParsers(). On Bukkit that
runs MinecraftArgParsers.initialize() exactly once (it's guarded to be idempotent), which is what
adds Player, World, and the conditional OreDictEntry/FCWorldGuardRegion parsers. You don't call
this yourself; it's part of the Platform Abstraction wiring.
-
Command Framework -
@FinalCMD, sub-commands, help, permissions, registration. -
Flags -
@Arg.Flag, the--name valuesyntax, and the manualMultiArgumentosflag API. -
Localization - the
@FCLocaleerror messages a parser sends. -
PlayerData and PDSections - the
PlayerData/PDSectiontypes you can inject. -
Platform Abstraction -
IPlatform.registerArgParsers()and theFPlayer/FCommandSendertypes.
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