-
Notifications
You must be signed in to change notification settings - Fork 10
Sub Commands
LeafRTP ships its own platform-agnostic command framework (commands-api). One command tree is authored once and dispatched on every platform: Bukkit / Paper / Folia via the Bukkit command dispatcher, Fabric (and planned Velocity) via the Brigadier bridge. This means your addon can add a /rtp sub-verb that works - with tab-completion - on all of them, with no per-platform command code.
This page is the author-facing primer. For the deeper bridge contract and recursion rules, see the commands-api author guide.
RTP.baseCommand.addSubCommand(new FooCmd(RTP.baseCommand));That is the whole registration. Parameter decoding and tab-completion are automatic.
The dispatcher accepts only two token shapes - there is no third "free positional" form:
| Token | Meaning | Routed via |
|---|---|---|
<bare-literal> |
a sub-command (e.g. apply, confirm) |
getCommandLookup() |
<name>=<value>[,<value>]* |
a typed parameter (e.g. id=42, region=R1,R2) |
getParameterLookup() |
A bare token that does not match a registered sub-command triggers msgInvalidCommand and the verb returns false.
!!! warning "Never take an argument as a bare positional"
/rtp foo low-performance does not work - low-performance is routed through sub-command lookup, fails to match, and never reaches your logic. Register a typed parameter instead and use /rtp foo id=low-performance. This is the #1 real-world regression the framework guards against.
You are adding /rtp foo bar id=<id> where bar is a leaf sub-verb and <id> enumerates against a live registry.
A CommandParameter's values() is the tab-completion enumerator; isRelevant is the execute-time validator.
public class FooIdParameter extends CommandParameter {
public FooIdParameter(String permission, String description,
BiFunction<UUID, String, Boolean> isRelevant) {
super(permission, description, isRelevant);
}
@Override
public Set<String> values() { // drives tab-complete
return FooRegistry.list().stream().map(Foo::id).collect(Collectors.toSet());
}
}!!! tip "Opaque inputs"
For a free-form input the user must type (a token, an arbitrary string), return Collections.emptySet() from values() and keep isRelevant permissive. Tab-complete then offers nothing but the parser still accepts whatever was typed.
public class FooBarCmd extends BaseRTPCmdImpl {
public FooBarCmd(@Nullable CommandsAPICommand parent) {
super(parent);
addParameter("id",
new FooIdParameter(FooCommand.PERMISSION, "registered foo id", (uuid, s) -> true));
}
@Override public String name() { return "bar"; }
@Override public String permission() { return FooCommand.PERMISSION; }
@Override public String description() { return "do the bar thing to a foo"; }
@Override
public boolean onCommand(UUID callerId,
Map<String, List<String>> parameterValues,
@Nullable CommandsAPICommand nextCommand) {
if (nextCommand != null) return true; // chained dispatch: let the child run
if (callerId == null) {
RTP.log(Level.WARNING, "/rtp foo bar rejected: no caller UUID");
return false;
}
List<String> idValues = parameterValues == null ? null : parameterValues.get("id");
String id = (idValues == null || idValues.isEmpty()) ? null : idValues.get(0);
if (id == null || id.isEmpty()) {
send(callerId, "&cUsage: &f/rtp foo bar id=<id>");
return false;
}
// ... do the work ...
return true;
}
}// in FooCommand's constructor:
addSubCommand(new FooBarCmd(this));msgInvalidCommand / msgBadParameter must be configurable (S-007) and propagate through the locale pipeline. Do not hardcode error strings.
These come from real LeafRTP regressions.
| Anti-pattern | Why it breaks | Do this instead |
|---|---|---|
Hand-rolling positional parsing by overriding the args[]-form onCommand
|
Bare tokens route through sub-command lookup; hyphenated ids hit msgInvalidCommand and break tab-completion |
Register a typed CommandParameter, use name=value
|
Calling nextCommand.onCommand(...) yourself |
The dispatcher already enqueued the child; you'd run it twice or on the wrong thread | if (nextCommand != null) return true; |
Returning false from the pre-pass when a child exists |
Short-circuits the chain before the child reads its own args | Use the nextCommand != null guard above |
| Hardcoded user-facing error strings | Violates S-007; ships untranslated and unconfigurable | Route through msgInvalidCommand(...)
|
Letting values() / isRelevant throw |
Brigadier silently swallows suggestion exceptions, deleting the suggestion list | Null-guard; return Collections.emptySet() if the registry is not yet bound |
CommandParameter.values() is the enumerator - override it to return a live set (world names, registered ids) and both Brigadier and Bukkit pick it up automatically. The Brigadier bridge attaches nested (subParams) and sibling parameters as child nodes so /rtp region=R world=W shape=S is reachable from any starting parameter.
!!! note "Suggestion vs validation"
isRelevant(UUID, String) is the always-enforced execute-time gate. isSuggestionRelevant(UUID, String) is the tab-completion filter and defaults permissive. Never rely on suggestion filtering as a security boundary.
-
commands-apiauthor guide - the full primer, bridge contract, and testing conventions. -
Commands - the built-in
/rtpverbs. - Addon development - the addon landing page.