Skip to content

Argument Parsing

Petrus Pradella edited this page Jul 23, 2026 · 10 revisions

Argument Parsing

What this page covers: how a command method's parameters become typed, validated arguments. Each parameter is either a positional argument the player typed (annotate it with @Arg) or a contextual value injected from the command context (the sender, the label, the raw args - no annotation needed). This page is the companion to Command Framework.


Two kinds of parameters

@FinalCMD.SubCMD(subcmd = "coins")
public void coins(FCommandSender sender, @Arg(name = "<amount>") Integer amount) { ... }
  • FCommandSender sender has no annotation -> it's a contextual parameter, injected by the framework (here, the command sender). Contextual parameters never consume a typed token.
  • @Arg(name = "<amount>") Integer amount is a positional argument: the first token the player typed is parsed into an Integer by the built-in number parser.

The rule the scanner uses: a parameter carrying @Arg is positional; a parameter with no argument annotation (or @ContextualArg) is contextual.


@Arg - positional arguments

public @interface Arg {
    String name();                                    // display name incl. brackets, e.g. "<amount>"
    String context() default "";                      // parser-specific constraint (see below)
    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)
}

Required vs nullable - the brackets in name

The brackets around the name decide whether the argument is required. This is parsed from the name string itself:

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.
<(name)> Required, or provided by context Advanced: may be filled positionally or from context.
[(name)] Optional, or provided by context Advanced: may be filled positionally or from context.
// Required: "/heal <target>" - target must be given.
public void heal(FCommandSender s, @Arg(name = "<target>") FPlayer target) { ... }

// Optional: "/heal [target]" - null target means "heal myself".
public void heal(FCommandSender s, @Arg(name = "[target]") FPlayer target) {
    FPlayer who = target != null ? target : (FPlayer) s;
    // ...
}

⚠️ Gotcha - an optional [name] argument is injected as null when omitted. Always null-check optional arguments before dereferencing them.

def() - a declarative default for an optional argument

def() is only legal on an [optional] argument - declaring it on a <required> or a provided-by-context form (<(x)>/[(x)]) 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(name = "[page]", context = "[1:*]", def = "1") Integer page

A value the player actually types always wins over def(). A def() that is not parseable for the argument's type behaves exactly like a bad optional value would: every built-in ArgParser only raises its error message when argInfo.isRequired() is true, and def() is exclusive to optional arguments, so an unparseable def() silently resolves to null instead of aborting - EXCEPT for a context bound/choice check, which always rejects regardless of required/optional (so a def() outside a declared numeric range still errors and aborts dispatch, same as a bad typed value would). def() also supports the %placeholder% mechanism ICustomFinalCMD.customize() resolves for the rest of ArgData (name, context, locales).

context - constraining an argument

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
Integer/Float/Double "min:max" (e.g. "1:10", "[1:*]") An inclusive numeric range. * on either side means unbounded in that direction. 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.
Integer/Float/Double "a|b|c" (e.g. "1|5|10") A fixed set of accepted numeric values.
String "a|b|c" (e.g. "add|remove|list") A fixed, case-insensitive option set, also used for tab-complete. Empty context falls back to the arg's own name (so a plain <mode> with no context just means "any single token").
Enum (any) "A|B|C" A subset of the enum's constants (matched case-insensitively by their name()). 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 default true/false tokens (first option = true, second = false). 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(name = "<mode>", context = "add|remove|list") String mode

// A numeric range (Number parser): page >= 1.
@Arg(name = "[page]", context = "[1:*]") Integer page

The same DSL applies to a @FlagArg's context(), applied to the flag's value parser - see Flags.


Built-in argument types

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, Float, Double Numbers, with optional [min:max] range context.
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.
PageVizualization 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).


Contextual parameters (auto-injected)

A parameter with no @Arg is filled from the command context by an ArgParserContextual. 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 whole raw argument list (see below).
HelpLine This sub-command's help line, to sendTo(sender) on bad input.
HelpContext The command's full help context.
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 (the item in hand).

MultiArgumentos is the escape hatch when you want to parse the raw tokens yourself:

@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); return; }
    String pluginName = argumentos.getStringArg(1);
    // ...
}

Writing a custom ArgParser

To make a parameter of your own type resolvable, extend ArgParser<T>. It needs an (ArgInfo) constructor, a parserArgument(...) that returns the value (or throws ArgParseException to abort), and an optional tabComplete(...):

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;

    public ArgParserTemplateKit(ArgInfo argInfo) {
        super(argInfo);
    }

    @Override
    public TemplateKit parserArgument(ArgParserCommandContext ctx, FCommandSender sender, Argumento argumento) throws ArgParseException {
        TemplateKit kit = TemplateKit.getByName(argumento.toString());
        if (kit == null && getArgInfo().isRequired()) {          // only fail for a REQUIRED arg
            KIT_NOT_FOUND.addPlaceholder("%name%", argumento).send(sender);
            throw new ArgParseException();
        }
        return kit;                                              // may be null for an optional arg
    }

    @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());
    }
}

📌 Note - honour getArgInfo().isRequired(): only throw ArgParseException when the argument is required. For an optional ([...]) argument, return null and let the method handle absence. This mirrors every built-in parser.

The same ArgParser class resolves both a positional @Arg and a @FlagArg's value - there is no separate parser type for flags. ArgParserCommandContext.isFlag() tells you which one you're in (also true while resolving a flag's def()), if your parser needs to behave differently for the two - see Flags.

For a contextual type, extend ArgParserContextual<T> instead: it takes an (ArgContextualInfo) constructor, a parserArgument(ctx, sender) returning the injected value, and requiresToBeAPlayer().


Registering a custom parser

Three ways, from most local to most global:

// 1. Per parameter - only this argument uses it.
public void kit(FCommandSender s, @Arg(name = "<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, matching by exact class or assignability (isAssignableFrom). Contextual parsers use addPluginContextualParser / addGlobalContextualParser.


Platform parsers are registered lazily

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.


See also

Clone this wiki locally