Skip to content

Register commands with Paper's Brigadier API (#3023 phases 1 and 2) - #3036

Merged
tastybento merged 1 commit into
developfrom
feature/brigadier-command-registration
Jul 25, 2026
Merged

Register commands with Paper's Brigadier API (#3023 phases 1 and 2)#3036
tastybento merged 1 commit into
developfrom
feature/brigadier-command-registration

Conversation

@tastybento

Copy link
Copy Markdown
Member

Closes #3023 (phases 1 and 2).

Replaces the SimpleCommandMap reflection in CommandsManager with Brigadier registration, and exposes the CompositeCommand subcommand tree to clients so completions appear while the player types rather than only after enter or TAB.

Phases 1 and 2 are done together deliberately: the registration shim alone carries the entire risk of the change with none of the player-facing benefit.

The timing problem, and why the dispatcher is captured

Paper only permits Brigadier registration inside the LifecycleEvents.COMMANDS callback. Probing the real ordering on a 26.2 server gives:

onEnable returns  ->  COMMANDS fires (cause=INITIAL)  ->  first server tick: completeSetup()

completeSetup() is where enableAddons() runs, so game mode commands such as /island and /acid do not exist yet while the lifecycle window is open. Using the registrar after the window closes throws No lifecycle owner context is set, and calling getDispatcher() on it then throws cannot access the dispatcher in this context.

Three escape routes were tested and rejected:

  • Re-fire the eventCraftServer#syncCommands() does not re-run the COMMANDS lifecycle. Verified directly.
  • Move the addon enable earlier — that was tried and reverted in This changes the loading to not be on STARTUP #2214, so the deferred tick has to stay.
  • Use the registrar late — throws, as above.

What does work: the CommandDispatcher instance is the live one the server dispatches against. BrigadierCommandRegistrar captures it inside the window and registers nodes into it afterwards as addons come up. Those nodes survive syncCommands() and dispatch correctly for players and console.

Design consequences

  • No stale references. Brigadier has no API for removing a node, so nodes never capture a CompositeCommand — they resolve it by label on every use. A reload that swaps the command objects leaves nothing stale behind, and a label that goes away is hidden because its requires predicate stops matching.
  • Aliases and plugin:label are redirects, not duplicated trees. A game mode admin tree runs to dozens of nodes and every one of them is sent to every client.
  • Console keeps seeing player-only commands, so /ai from console still reports "This command is only available in-game" rather than Brigadier's generic "unknown command".
  • Literal tree is depth-capped at 4. Deeper subcommands still work — they fall through to the greedy argument — they are just not pre-advertised.

Config

Guarded by general.brigadier-commands (default true). Turning it off restores the legacy command map path, which is kept intact as the fallback and is also used automatically if the registrar cannot hook.

Testing

Verified on a Paper 26.2 test server with AcidIsland, AOneBlock, CaveBlock, Biomes, Challenges and Level:

  • top-level commands and aliases (/bbox version)
  • deep subcommands (/acid range set)
  • namespaced form (/acidisland:acid version)
  • unknown subcommand falling through to BentoBox's own error rather than Brigadier's
  • player-only commands from console giving the correct message
  • everything still working after /bentobox reload
  • in-game client behaviour confirmed by @tastybento

Full test suite passes. New unit tests cover the raw command line tokenizing and the suggestion offset arithmetic — the parts that have to agree exactly with what CompositeCommand expects.

Left for phase 3

Deliberately not half-done here:

  • No tooltips on subcommand names. A literal node's suggestion cannot carry one, and emitting a parallel tooltipped suggestion would show every subcommand twice, since Brigadier does not deduplicate across nodes.
  • No typed arguments, so no red-underline validation or quoting-free multi-word island names yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ATRqygFLZ5CA3zrWmutx7H

Replaces the SimpleCommandMap reflection with Brigadier registration, and
exposes the CompositeCommand subcommand tree to clients so completions
appear while the player types rather than only after enter or TAB.

Paper only allows Brigadier registration inside the COMMANDS lifecycle
callback, which fires after onEnable returns but before the first server
tick - and BentoBox enables its addons on that first tick, so game mode
commands do not exist yet while the window is open. Using the registrar
afterwards throws "No lifecycle owner context is set", and getDispatcher()
on it throws "cannot access the dispatcher in this context". Moving the
addon enable earlier is not an option; that was tried and reverted in
#2214. The dispatcher instance itself, however, stays the live one, so the
registrar captures it inside the window and registers nodes into it as
addons come up. Those nodes survive syncCommands() and dispatch correctly
for players and console.

Brigadier has no API for removing a node, so nodes never capture a
CompositeCommand; they resolve it by label on every use. A reload that
swaps the command objects therefore leaves nothing stale behind, and a
label that goes away is hidden because its requires predicate stops
matching.

Aliases and the plugin:label form redirect onto the main node instead of
duplicating a tree that can run to dozens of nodes per game mode.

The console keeps seeing player-only commands so it still gets "this
command is only available in-game" rather than Brigadier's generic
"unknown command".

Guarded by general.brigadier-commands (default true); turning it off
restores the command map path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATRqygFLZ5CA3zrWmutx7H
@sonarqubecloud

Copy link
Copy Markdown

@tastybento
tastybento merged commit e36b993 into develop Jul 25, 2026
3 checks passed
@tastybento
tastybento deleted the feature/brigadier-command-registration branch July 27, 2026 15:27
@tastybento tastybento mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design: migrate command registration to Paper's Brigadier API (phased)

1 participant