Skip to content

Release 3.22.0 - #3043

Open
tastybento wants to merge 18 commits into
masterfrom
develop
Open

Release 3.22.0#3043
tastybento wants to merge 18 commits into
masterfrom
develop

Conversation

@tastybento

Copy link
Copy Markdown
Member

Release 3.22.0 — 17 commits since 3.21.0.

A commands-and-visibility release: BentoBox registers its commands with Paper's Brigadier API so completions appear while the player types, six new bStats charts answer which addon versions are actually deployed and where commands fail, and structureless worlds now protect themselves from the /locate-style searches that could freeze a server.

New features

PR Change
#3036 ⚙️ 🔺 Register commands with Paper's Brigadier API — closes #3023 (phases 1 and 2). New general.brigadier-commands config key, default on, legacy command map kept as fallback.
#3038 Addon/game mode version, addon API level, feature adoption and command failure bStats charts — closes #3022. Three dead chart IDs removed.
#3035 🔺 Auto-suppress structure searches in worlds whose generator places no structures. No config needed; per-structure force-enable remains the escape hatch.

Bug fixes

PR / commit Change
#3040 Rebuild the Brigadier tree once addons have enabled — fixes #3039. See known limitation below.
#3042 Dispatch our own commands that Brigadier refuses to parse — fixes BentoBoxWorld/Border#179.
#3041 Scope addon-created BlueMap marker sets to their own worlds; guard the api field and correct getBlueMapAPI() to @Nullable.
45c5402 Apply auto-load: false to worlds Multiverse already knows — relates to #3037.
a89bf9d Remove dead seed-world Multiverse registration.
c5d66ca SonarCloud reliability fixes; pin the Modrinth publish action.

Version

buildVersion bumped 3.21.1 → 3.22.0 in this PR. The general.brigadier-commands key on develop already documented itself as "Added since 3.22.0", and the release is feature-bearing rather than a patch. @since tags on the new BStats and CompositeCommand members were corrected to match.

Admin-facing notes

  • ⚙️ New general.brigadier-commands (default true) is written on first run. Set false to fall back to legacy registration if another plugin conflicts.
  • 🔺 Structure searches are now suppressed automatically in structureless worlds. Converted worlds containing pre-existing structures need a per-structure force-enable in the game mode config.
  • 🔡 No locale changes — custom locale files need no work.
  • Stale <world>/bentobox entries already in Multiverse's worlds.yml still need a manual /mv remove; a89bf9d only stops new ones being created.

Known limitation carried into this release

BrigadierCommandRegistrar#refreshTrees compares childCount(label) before and after the rebuild, but dispatcher.getRoot().getChild(label) returns Paper's wrapped ApiMirrorRootNode 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 because they use a plain RootCommandNode. Subcommands themselves execute correctly either way. Flagged in #3042 for follow-up.

Verification

./gradlew clean build green on the release commit: 3391 tests, 0 failures, 0 errors, 0 skipped.

Compatibility

✔️ Paper Minecraft 1.21.5 – 26.2
✔️ Java 25+

Full Changelog: 3.21.0...develop

tastybento and others added 18 commits July 23, 2026 08:05
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDVXb9Lg33agNqUHrs5mzR
A cartographer villager levelling up in a void world triggers a synchronous
findNearestMapStructure scan on the main thread. Because the world's custom
generator never places structures, the search can never succeed and grinds
through vanilla noise generation for every candidate position until the
watchdog kills the server — on auto-restarting hosts this repeats as soon as
the villager's chunk loads again, producing a reboot loop.

StructureListener now asks the world's Bukkit ChunkGenerator whether it
places structures at all (shouldGenerateStructures). If not, every
StructuresLocateEvent in that world is cancelled (or narrowed) with no
configuration needed — a search in a structureless world is always the
pathological scan-to-the-cap case. A per-world getStructureSettings() entry
of true still force-enables a structure, as an escape hatch for converted
worlds containing pre-existing structures. Game modes that do generate
structures (SkyGrid, Boxed with allow-structures, CaveBlock overworld)
report true and are unaffected, so no addon changes are required.

Also expands the world.disabled-structures config comments: how to list
valid keys in-game (/locate structure tab-completion) and a concrete
example disabling the structures that explorer/treasure maps search for
(monument, mansion, trial_chambers, buried_treasure).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDVXb9Lg33agNqUHrs5mzR
…ld-locate-suppression

Auto-suppress structure searches in structureless worlds
- Pin cloudnode-pro/modrinth-publish to commit SHA (security hotspot:
  unpinned third-party action)
- Add null guards on paths Sonar traces as nullable (S2259):
  scan.world() in PurgeRegionsService, raw in User.computeLegacy,
  world in both Multiverse hooks, getEditedFlag() in PVPListener
- Use possessive quantifiers in regexes flagged for super-linear
  backtracking (S8786) in PlaceholderGrouper and AddonClassLoader
- Pass explicit ZoneId to LocalDate.now() (S8688)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDVXb9Lg33agNqUHrs5mzR
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
…-registration

Register commands with Paper's Brigadier API (#3023 phases 1 and 2)
Seed worlds (`<world>/bentobox`) were dropped in 3.16.1 along with the
chunk-regen deletion path, but IslandWorldManager still looked one up
for every game mode world and registered it with the world management
hooks. Nothing creates those worlds any more, so the lookup always
returns null.

Worth noting for #3037: this registration is why stale `acid/bentobox`
entries linger in Multiverse's worlds.yml — BentoBox deleted the seed
world folder on shutdown but never unregistered it, so MV autoload
reports WORLD_FOLDER_INVALID. Removing the call stops any future
version re-adding such an entry; existing ones still need `/mv remove`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATRqygFLZ5CA3zrWmutx7H
Six new bStats charts, plus a cleanup of three that were sending data
into the void.

Addon and game mode versions (drilldown pies) and addon API versions
(advanced pie): we knew *that* a server ran Level, but not *which*
Level, so there was no way to see how long the tail is after a release
or judge when an API break is safe.

Non-default settings (bar chart): only deviations from default are
reported, so every bar is a signal. A default-off setting appears under
its own name when enabled, a default-on setting as no-<name> when
disabled. Comparing a bar to the server count gives the adoption or
rejection rate of a feature.

Command failures and top failing commands (#3022): CompositeCommand.call
is the single funnel where every failure mode is distinguishable, so it
now records the kind of failure and the command it happened on. Only the
failure kind and a stable command key are sent - never what the player
typed - because bStats data is public.

Labels cannot be used as that key: the top level label comes from the
game mode config and getLabel() is overwritten with whatever alias was
typed. The permission is hard coded in setup() and carries the addon
prefix, so bskyblock.island.team.invite means the same thing on every
server. Commands with no permission fall back to their class name.
Distinct keys are capped at 200 between submissions so an addon with
generated commands cannot grow the map without limit.

Removed the addons, gameModeAddons and hooks advanced pies. Those chart
ids do not exist on bstats.org, so the data was discarded on arrival.
The addonsBar, gameModeAddonsBar and hooksBar charts carry identical
data and do have charts.

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

A top level command's literal children are baked in when its node is built,
and that happens while Paper's COMMANDS lifecycle window is open - right after
onEnable returns. Addons add their sub-commands to game mode commands from
enableAddons(), which BentoBox defers to the first tick, so every addon
sub-command missed the tree.

They still ran, because the greedy argument catches anything the literals do
not match and CompositeCommand.execute walks the live map, and they still
showed in /<gamemode> help, which iterates that same live map. Only the client
was never told they exist, so they were absent from tab completion and from
the suggestions shown while typing.

Brigadier merges same-named literals rather than replacing them
(CommandNode#addChild copies the incoming command across and recursively
merges its children), so re-registering a freshly built tree grafts the new
children onto the live node. refreshTrees() does that for every registered top
level command and is called at the end of enableAddons() and again after
allLoaded(), which runs later still from the blueprint polling task. Aliases
are not re-registered: they redirect to the original node, which is the one
merged into. A rebuild that adds nothing does not push the tree to clients.

literalSiblings() also had to change. It removed every live sub-command from
the greedy argument's suggestions on the grounds that Brigadier already
suggests them - true only for sub-commands that have a literal node, so a late
sub-command was dropped from the dynamic fallback as well and had no route to
the client at all. It now consults the dispatcher, so any future divergence
between the tree and the live command map is covered by the fallback rather
than silenced by it.

Tested with AcidIsland plus an addon that adds a sub-command in onEnable.
Adds BrigadierTreeRefreshTest, which runs a real dispatcher and pins both the
broken state and the fix, plus two AddonsManagerTest cases for the wiring.

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

Rebuild the Brigadier tree once addons have enabled
Add addon version, feature adoption and command failure metrics
createMarkerSet() attached the new set to every map on the server via
api.getMaps(), and populateAll() re-attached it to every map again after a
BlueMap reload. An addon registering, say, a warps marker set for one game
mode put that set on the sidebar of every other game mode's map, and of any
non-BentoBox world's map too. removeMarkerSet() had the same fan-out.

MapHook#createMarkerSet carries no world, so the set's worlds are now learned
lazily from the markers themselves: addPointMarker/addAreaMarker/addPolygonMarker
record the world they were given and attach the set to just that world's maps.
populateAll() replays the recorded worlds after a BlueMap reload, and
removeMarkerSet() detaches from only those worlds.

A set with no markers yet is no longer attached anywhere, which is also the
more useful behaviour - an empty set was previously listed on every map.

Island marker sets are unaffected; registerGameMode() already scoped them to
the game mode's own worlds.

Also guards the api field before use. createMarkerSet() and removeMarkerSet()
dereferenced it unconditionally, so an addon calling either before BlueMap had
loaded, or while it was reloading, hit an NPE. getBlueMapAPI() is annotated
@nullable for the same reason - it was marked @nonnull but returns null in both
those windows, which suppressed the null checks callers needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pHWC6XapJj5gRtsFr74Tf
…et-world-scope

Scope addon-created BlueMap marker sets to their own worlds
registerWorld() set auto-load false inside a peek() on the importWorld()
result, so it only ever ran on a first-time import. If Multiverse already
had the world in worlds.yml the import fails with WORLD_EXIST_LOADED or
WORLD_EXIST_UNLOADED, the peek is skipped, and the world keeps auto-load
true - which is what #3037's worlds.yml shows for every BentoBox world.

Look the world up after the import instead and clear the flag there, so
existing entries get fixed too, saving worlds.yml when the flag changed.
BentoBox creates and loads its own worlds in onEnable; Multiverse should
not be autoloading them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kee1qur11mLWdE4ygvh4pu
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
…permission-parse

Run our own commands that Brigadier will not parse
This release adds Brigadier command registration, expanded bStats
telemetry and automatic structure-search suppression, so it takes a
minor version rather than a patch. The new general.brigadier-commands
config key already documented itself as "Added since 3.22.0"; the
@SInCE tags on the new BStats and CompositeCommand members are
corrected to match.

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

Copy link
Copy Markdown

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.

1 participant