Withdraw an addon's commands when the addon never enables - #3060
Merged
Conversation
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
|
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #3059.
Reported on Discord: a player ran
/chon 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(), andCompositeCommand's constructor registers them with the Bukkit command map the moment they are constructed. The world is only attached much later, inAddonsManager#enableAddon:sortAddons()spots the missing dependency afteronLoad()has run. It set the state toMISSING_DEPENDENCY, unregistered the addon's flags and dropped it from the addon list — soenableAddonnever ran — but it left the commands registered./chstayed live withgetWorld() == null, andIslandWorldManager.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#withdrawAddontakes 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 itsrequirespredicate and does nothing if run anyway, so it disappears from clients too.CompositeCommand#callrefuses outright when a command belongs to aGameModeAddonand has no world, sendinggeneral.errors.generaland logging which addon is not enabled. Defence in depth for any path the above misses. BentoBox's own world-less commands such as/bentoboxare not affected.IslandWorldManager#getWorldSettingsnow builds its "non-game world" message lazily. It used to evaluateworld.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
buildVersionto 3.22.2.🤖 Generated with Claude Code
https://claude.ai/code/session_01JJPRnxgoMiVypeN21uRFW4