Skip to content

Withdraw an addon's commands when the addon never enables - #3060

Merged
tastybento merged 1 commit into
developfrom
fix/3059-worldless-gamemode-commands
Aug 8, 2026
Merged

Withdraw an addon's commands when the addon never enables#3060
tastybento merged 1 commit into
developfrom
fix/3059-worldless-gamemode-commands

Conversation

@tastybento

Copy link
Copy Markdown
Member

Fixes #3059.

Reported on Discord: a player ran /ch on a server with ChunkBlock installed but not the Level addon it depends on, and every subcommand answered "An unexpected error occurred while running the command" with an NPE in the console.

The bug

A game mode addon builds its commands in onLoad(), and CompositeCommand's constructor registers them with the Bukkit command map the moment they are constructed. The world is only attached much later, in AddonsManager#enableAddon:

gameMode.getPlayerCommand().ifPresent(c -> c.setWorld(gameMode.getOverWorld()));

sortAddons() spots the missing dependency after onLoad() has run. It set the state to MISSING_DEPENDENCY, unregistered the addon's flags and dropped it from the addon list — so enableAddon never ran — but it left the commands registered. /ch stayed live with getWorld() == null, and IslandWorldManager.getWorldSettings(null) threw on the first lookup.

The same hole existed for an addon abandoned because it is incompatible, or because it threw during onLoad/onEnable: whatever it registered on the way up stayed behind.

The fix

AddonsManager#withdrawAddon takes back everything an addon registered on its way up — listeners, flags and commands — and is now called from all three abandonment paths (missing dependency, incompatibility, unhandled exception during load or enable). Previously only flags were unregistered, and only on the missing-dependency path.

CommandsManager#unregisterCommands(Addon) is the new method backing that: it pulls the addon's top level commands out of the internal map and the Bukkit command map. Brigadier keeps its nodes, since it has no removal API, but a node whose label no longer resolves to a command is refused by its requires predicate and does nothing if run anyway, so it disappears from clients too.

CompositeCommand#call refuses outright when a command belongs to a GameModeAddon and has no world, sending general.errors.general and logging which addon is not enabled. Defence in depth for any path the above misses. BentoBox's own world-less commands such as /bentobox are not affected.

IslandWorldManager#getWorldSettings now builds its "non-game world" message lazily. It used to evaluate world.getName() eagerly, so a null world threw inside the very string meant to explain the problem — which is why the report showed a bare NPE with no useful message.

API

CommandsManager#unregisterCommands(Addon) is new and additive, so binary compatibility with existing addons is unaffected.

Tests

  • GameModeCommandWithoutWorldTest — new: a world-less game mode command is refused rather than throwing, tells the player, applies to sub-commands, and does not catch non-game-mode commands.
  • AddonsManagerTest — a missing dependency unregisters the addon's commands; a satisfied one does not.
  • CommandsManagerTest — per-addon unregister removes that addon's commands from the command map, leaves other addons alone, and is safe for an addon with no commands.

Full suite passes.

Version

Bumped buildVersion to 3.22.2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJPRnxgoMiVypeN21uRFW4

A game mode builds its commands in onLoad(), and CompositeCommand's
constructor registers them with the command map the moment they are
constructed. The world, though, is only attached later, in
AddonsManager#enableAddon.

sortAddons() spots a missing dependency after onLoad() has run. It set the
state to MISSING_DEPENDENCY, unregistered the addon's flags and dropped it
from the addon list - so enableAddon never ran - but left the commands
registered. The result was a live /ch whose getWorld() was null, and the
first world lookup threw a NullPointerException for every player who ran
it. The same hole existed for an addon abandoned as incompatible or after
an exception on the way up.

AddonsManager now withdraws listeners, flags and commands together from all
three abandonment paths, backed by a new CommandsManager#unregisterCommands
that takes an addon's top level commands back out of the command map.
Brigadier keeps its nodes - it has no removal API - but a node whose label
no longer resolves is refused by its requires predicate, so it disappears
from clients too.

As defence in depth, CompositeCommand#call refuses outright when a game
mode command has no world, and IslandWorldManager#getWorldSettings builds
its message lazily: it used to throw while constructing the very string
meant to explain the problem, which is why the report showed a bare NPE.

Fixes #3059

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

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@tastybento
tastybento merged commit e3fe62d into develop Aug 8, 2026
3 checks passed
@tastybento
tastybento deleted the fix/3059-worldless-gamemode-commands branch August 8, 2026 16:43
@tastybento tastybento mentioned this pull request Aug 10, 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.

Game mode commands survive an addon that never enables, then NPE with a null world

1 participant