Commands and keybinds
How a feature adds a /vantage subcommand, takes arguments, replies, opens a screen, and is counted and documented; and how it takes a key as a Keybind option and reads it in the world, while held, or inside a menu.
On this page
A feature reaches the player two ways besides the menu and the HUD: a chat command under /vantage, and a key.
This page is for someone adding either. The command half covers registering a subcommand, its arguments, replying,
opening a screen without it closing at once, the Stats count and how the command reaches /vt help and this
website. The key half explains why feature keys are options rather than Minecraft key mappings, and the three ways a
feature reads one. The code is in core/command/, core/config/types/Keybind.java and core/util/Input.java. As of
2.0.1.
A subcommand
Every Vantage command lives under one root, /vantage, with /vt redirecting to the same tree. A feature adds a
branch with Commands.registerSubcommand, passing a Brigadier builder made with Fabric's ClientCommands:
Commands.registerSubcommand(ClientCommands.literal("items").executes(ctx -> {
Commands.openScreenLater(() -> ItemBrowserScreen.open(null, ""));
return 1;
}));That one line, from the Item Browser, makes /vantage items and
/vt items both open the browser. The subcommand appears under both names, completes in chat like any other
command, and needs nothing else registered.
There are two good places to call it:
| Where | When to use it | Example in the mod |
|---|---|---|
the feature's onInit() | the command belongs to one feature | ItemBrowserFeature, which registers recipe and items |
the area's XFeatures.register() | several features share a command family | QolCommands, called from QolFeatures, which registers wp, warp, navigate, alias and more |
Both run before the command tree is first built, so the command exists from the first world join. The call is safe at any time, though: registered after the player has joined a server, the node is attached to the live tree at once.
Arguments
Arguments are ordinary Brigadier. Chain .then(ClientCommands.argument(name, type)) and read the value back in
executes. The /vt calc command takes the rest of the line as one argument:
Commands.registerSubcommand(ClientCommands.literal("calc")
.then(ClientCommands.argument("expression", StringArgumentType.greedyString())
.executes(ctx -> {
String expression = StringArgumentType.getString(ctx, "expression");
// evaluate, then answer
return 1;
})));| Argument type | Takes |
|---|---|
StringArgumentType.word() | one word, no spaces; feature ids and category names use it |
StringArgumentType.string() | one word, or a quoted string with spaces; config profile names use it |
StringArgumentType.greedyString() | everything to the end of the line; it must be the last argument |
.suggests(…) on an argument adds tab completion. The built-in commands use it for feature ids, config profile
names and categories.
The command outlives the feature
A registered command stays in the tree whether or not its feature is switched on, active here, or even registered
at all: a feature can fail to construct while its area's commands are still there. Check before acting. Inside the
feature, isActive() answers it. From an area's command class, Commands.requireFeature finds the feature or tells
the player why the command did nothing:
CommandAliasesFeature aliases = Commands.requireFeature(ctx, CommandAliasesFeature.class, "The alias feature is not loaded.");
if (aliases == null) return 0;Replying
Commands.reply(source, text) and Commands.replyError(source, text) send one line to the player's chat, prefixed
with an aqua [Vantage], in white or red. Use them for the answer to a command, so every command reads the same.
For anything richer, such as a clickable value, build a Component and send it through the notifier, as /vt calc
does to make its result click-to-copy. A Component you build yourself carries its own click and hover events;
the rule about keeping them is at the end of this page.
Opening a screen from a command
A screen opened directly from a command handler closes again before it is drawn. Minecraft's chat screen runs the command and then closes itself, taking whatever screen you just opened with it. Defer the opening by a tick:
Commands.openScreenLater(() -> ItemBrowserScreen.open(null, ""));openScreenLater is Deferred.nextTick, which runs the task on the client thread at the end of the next client
tick; Deferred.after(ticks, task) waits longer. Both are safe to call from any thread. The built-in /vantage and
/vantage config <feature> work the same way: they post CommandEvents.OpenMenu one tick later, and the menu
listens for it.
Counted for Vantage Stats
The command tree wraps every first-level subcommand so that running it counts one use under the key
vantage.commands.<literal>, so /vt items counts as items and /vt profile switch x as profile. Bare
/vantage, which opens the menu, is not counted.
The count goes through Stats.count, which records nothing unless the player has given Vantage Stats consent with
its "Vantage" category switched on, the category whose sentence is "Which Vantage features you use, and your Vantage
Vet score."
With it off, a command costs one volatile read. A new subcommand needs no catalogue entry: the Stats key catalogue in
tools/stats_catalogue.py already has the wildcard vantage.commands.<NAME>. The literal has to fit the key
grammar to be counted: it starts with a letter, a digit or _, may also use a few punctuation characters, and runs
to at most 64 characters.
In /vt help, and on this website
/vt help prints the built-in commands with a line of explanation each, then one line for every feature
subcommand: /vt <literal> - added by a feature. It shows the literal only, so choose one that says what it does.
The command tables on this website are generated by tools/sitedocs.py, which reads every registerSubcommand
call as a Brigadier chain. A subcommand's description is taken, most specific first, from an in-game help row for
its path, from the Javadoc directly above the registerSubcommand call, or from the description of the feature its
handlers drive. To control what the website says, put a one-line Javadoc right above the call:
/** Opens the item browser. */
Commands.registerSubcommand(ClientCommands.literal("items").executes(ctx -> { … }));Then run python tools/sitedocs.py and commit docs/site-catalog.json. The player's list of every command is
Commands.
A key: an option, not a mapping
Vantage registers exactly 2 Minecraft key mappings, in core/ui/UiBootstrap.java, under a "Vantage" category
in Minecraft's Controls screen:
| Action | Default key |
|---|---|
| Open Vantage menu | Right Shift |
| Open HUD editor | Not bound |
Every other key in the mod is a Keybind option on its feature, set on the feature's page in the Vantage menu:
@Option(name = "Open browser", description = "Opens the item browser. Works in the world and inside any menu.",
section = S_KEYS, order = 1)
public Keybind openKey = Keybind.NONE;Being an option is what makes a feature key behave like the rest of the feature's settings: it is saved per config
profile, travels in an export, can be reset, sits next to the settings it belongs with, and is found by search. A
Keybind holds a key or a mouse button, and a set of modifiers that must be held with it.
| Value | Means |
|---|---|
Keybind.NONE | unbound; most feature keys ship like this |
Keybind.key(GLFW.GLFW_KEY_Z) | one key, no modifiers |
new Keybind(key, mouse, modifiers) | a key or mouse button, with a mask of GLFW_MOD_* bits |
Ship a key unbound unless it is the point of the feature. Only a handful of feature keys in the mod start bound; Keybinds lists them, and it is the player's page on all of this.
Reading a key
How a feature reads its key depends on when the key should work.
| When | How | Example in the mod |
|---|---|---|
| pressed in the world, no screen open | @Subscribe to Events.KeyPress, test with Input.matches(bind, event) | the Item Browser's open key |
| held down, anywhere | poll bind.held() | chat focus mode, container previews |
| pressed inside a container menu | ContainerHooks.key(priority, handler) and bind.matches(…) | the Item Browser's lookup key |
A press in the world
@Subscribe
public void onKeyPress(Events.KeyPress event) {
if (!Input.matches(openKey, event)) return;
event.cancel();
openBrowser(null);
}Events.KeyPress is posted for a key going down, not repeats or releases, while a world is loaded and no screen is
open. Cancelling it stops Minecraft's own key mappings from seeing the press, so the player's key does one thing.
Because the handler is a @Subscribe method, it only runs while the feature is active. Input.matches is for
keyboard binds; an unbound or null bind never matches, so there is nothing to guard.
Held down
bind.held() is true while the key or mouse button is physically down and every modifier it declares is held. It
asks GLFW directly, so it works with a screen open, and it must be called from the render thread; off it, it returns
false. It suits "hold to reveal" features and mouse-button binds, which the key-press event does not cover.
Inside a menu
The key-press event is not posted while a screen is open. For a key that should work in a Hypixel menu, register a
handler with ContainerHooks.key, as the Item Browser does once from onInit():
ContainerHooks.key(ContainerHooks.PRIORITY_NORMAL, (container, key, modifiers) -> handleKey(container, key));The handler returns true to consume the key. It lives for the whole session, so it checks isActive() itself.
PRIORITY_INPUT_CAPTURE runs first, for search boxes, then PRIORITY_PANEL, then PRIORITY_NORMAL for ordinary
shortcuts.
How a combination matches
Every modifier the bind declares must be held, and extra modifiers are ignored. So a bind of plain H also fires on
Ctrl+H, and a bind of Ctrl+H does not fire on H alone. bind.displayName() gives the human name, such as the
"Right Shift" the menu shows.
A global key, if you need one
A key that should appear in Minecraft's Controls screen, like the menu key, is a KeyMapping. UiBootstrap shows
the whole pattern: register it with Fabric's KeyMappingHelper in the shared category, give it a name in
assets/vantage/lang/en_us.json, and drain its clicks every tick.
menuKey = KeyMappingHelper.registerKeyMapping(new KeyMapping("key.vantage.menu",
InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, CATEGORY));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (menuKey.consumeClick()) openMenu();
});The language file is the one place Vantage uses translation keys, and only for these names. tools/sitedocs.py
lists every new KeyMapping( it finds as a row in the keybinds table above, named from that file. Keep it for keys
that are not owned by one feature; a feature's key is an option.
Chat lines that keep their clicks
Hypixel's chat is full of lines that work by being clicked: [ACCEPT], [WARP], the stash prompts. A feature that
rewrites a chat line, through setReplacement on Events.ChatMessage, must not turn one of those into dead text.
The framework enforces it. Every chat rewrite in the mod passes through CoreHooks, which hands the original and
the rewrite to TextUtils.keepActions. If the original carried a click action and the rewrite carries none, the
original's click, and its hover text when the rewrite has none of its own, are put back on the whole rewritten line.
A rewrite that brings its own click is left alone. Two rules follow for your code:
- When you rebuild a line from its parts, keep each part's style, click and hover included, rather than flattening it to text and parsing it back.
- When you build a new line that should be clickable, give it its own
ClickEvent; the safety net only restores what the server sent.
ChatText.keepActions in features/chat is the same check under another name, for a chat feature that builds its
replacement from text rather than from the original's styled runs.