Skip to content

Run our own commands that Brigadier will not parse - #3042

Merged
tastybento merged 1 commit into
developfrom
fix/border-179-brigadier-permission-parse
Jul 27, 2026
Merged

Run our own commands that Brigadier will not parse#3042
tastybento merged 1 commit into
developfrom
fix/border-179-brigadier-permission-parse

Conversation

@tastybento

Copy link
Copy Markdown
Member

Fixes BentoBoxWorld/Border#179/oneblock border color green produced an unhandled CommandException out of a BentoBox scheduled task, and never changed the border colour.

Root cause

BrigadierCommandRegistrar puts a permission check in each sub-command literal's Brigadier requires predicate, so clients only see what they can use. Hiding a node that way is not passive:

  • CommandNode#getRelevantNodes returns only the literal matching the typed token, so the greedy catch-all argument beside it is never considered;
  • CommandDispatcher#parseNodes then silently skips that literal when canUse(source) is false.

So a player without [gamemode].border.color gets a parse that walks oneblockborder and dead-ends at color: Brigadier's INCORRECT_ARGUMENT at the cursor where color starts (exactly the ...ck border <--[HERE] in the report). The command line never reaches CompositeCommand, so instead of "you do not have permission" the player gets a red Brigadier error, and nothing runs.

Paper reports any CommandSyntaxException from Commands#finishParsing as an UnknownCommandEvent, not just genuinely unknown commands. That produced the rest of the report:

  1. CommandMatcher re-attaches the tokens it could not match verbatim, so it suggested the very line that had just failed;
  2. accepting that suggestion re-dispatched it through Player#performCommand inside a scheduled task with nothing catching what Bukkit throws — the unhandled CommandException in the report.

Changes

  • DidYouMeanListener — a command line of ours that Brigadier refused to parse is dispatched through the command tree, which checks its own permissions and reports for itself; Brigadier's error is then dropped. Nothing has run at that point (Paper fails in its parse step, before execution), so there is no double execution.
  • CommandsManager#resolveCommand — resolves a top level command from a typed first token: label, alias, or either in addon:label form.
  • SuggestionsManager — never suggest a line identical to what was typed.
  • DidYouMeanListener#acceptPending — log dispatch failures instead of letting them escape the scheduled task.

Testing

New UnparseableCommandTest reproduces the report end to end (permission-hidden sub-command → command reports for itself, nothing suggested back, nothing left pending, dispatch failures swallowed). Verified each test fails without the corresponding fix. Full suite green: 3391 tests, 0 failures.

Not addressed here

BrigadierCommandRegistrar#refreshTrees compares childCount(label) before and after the rebuild, but dispatcher.getRoot().getChild(label) returns Paper's wrapped (ApiMirrorRootNode) node while the merge lands on the NMS copy — so the count never changes and refreshClients() is never called. #3039's symptom is likely still present on a real server; the unit tests pass only because they use a plain RootCommandNode. Worth a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E6rYr6Z66LQz9XTbnbUupy

Brigadier hides nodes the sender may not use, and hiding is not passive:
CommandNode#getRelevantNodes returns only the literal that matches the typed
token, so the catch-all argument beside it is never considered, and
CommandDispatcher#parseNodes then skips that literal when canUse is false. A
player without the permission for a sub-command therefore got Brigadier's
"Incorrect argument for command" instead of the command's own no-permission
message, and the command never ran.

Paper reports any parse failure as an unknown command, so pick our own command
lines up there and dispatch them through the command tree, which checks its own
permissions and reports for itself. Nothing has run at that point - Paper fails
in its parse step, before execution - so there is no double execution.

Two knock-on faults from the same report:

- the matcher re-attaches tokens it could not match verbatim, so it suggested
  the very line that had just failed. Drop matches identical to the input.
- accepting a suggestion ran it through Player#performCommand in a scheduled
  task with nothing catching what Bukkit throws, which is how the report's
  unhandled CommandException got out. Log it instead.

Fixes BentoBoxWorld/Border#179

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

Copy link
Copy Markdown

@tastybento
tastybento merged commit b3334c4 into develop Jul 27, 2026
3 checks passed
@tastybento
tastybento deleted the fix/border-179-brigadier-permission-parse branch July 27, 2026 15:51
@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.

Unhandled CommandSyntaxException from DidYouMeanListener when using /oneblock border color green

1 participant