Skip to content

Release 3.22.1 - #3062

Merged
tastybento merged 11 commits into
masterfrom
develop
Aug 10, 2026
Merged

Release 3.22.1#3062
tastybento merged 11 commits into
masterfrom
develop

Conversation

@tastybento

Copy link
Copy Markdown
Member

Release PR for BentoBox 3.22.1 — a bug-fix patch on top of 3.22.0.

Four fixes, no new features, no config keys and no locale changes.

Plus a local-build plugin.yml version fix and README/CLAUDE.md documentation updates.

Draft release notes accompany this PR. Full Changelog: 3.22.0...develop

tastybento and others added 11 commits August 1, 2026 07:45
Fixes #3052

/island team trust <Name> and /island info <Name> could fail for a player who
plainly exists. Nothing in the command path lower cases a typed argument, and
getUUID already compared with equalsIgnoreCase, so the capitalization itself was
not the fault - but the record it compared against could be the wrong one.

setPlayerName dropped the previous Names record from the cache and never deleted
it from the database. After a restart every leftover was loaded back, and getUUID
took findFirst() over an unordered list, so a name could resolve to a UUID that
had not held it for months. On a case sensitive file system a leftover
oli713664.json and the current Oli713664.json both matched equalsIgnoreCase and
whichever the directory listing yielded first won. That produces both reported
symptoms: no such player on trust, and "does not have an island" on info, which
is what you get when the lookup returns a stale UUID rather than null.

So key the cache on the lower cased name. Minecraft names are unique ignoring
case, so there is exactly one entry per name and the match no longer depends on
how it was spelled when stored. setPlayerName now deletes any record still
pointing at this player under another spelling, and removePlayer drops the name
lookup as well, so existing databases heal as players log in.

getUUID also consults the online player first, since a player standing on the
server is authoritative and beats anything stored, and falls back to the server's
own user cache for players BentoBox has never recorded. Both are in-memory: the
Names table exists so that resolving a name never blocks the caller, and
getOfflinePlayerIfCached is the variant that returns null on a miss instead of
asking Mojang. The cache itself moves to a ConcurrentHashMap - it was a plain
ArrayList mutated from the join listener while commands read it - and the lookup
drops from a linear scan of every name ever seen to a single hash lookup.

CommonTestSetup mocks Bukkit with RETURNS_DEEP_STUBS, which handed back a mock
player for every name ever looked up. It now reports nobody online and nothing
cached, which is what MockBukkit's empty server should say; tests that want an
online player stub it themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvPaJ5jKchGph4FvaJjRu8
Resolve player names without depending on stored capitalization
Document the actual build: use the bundled Gradle wrapper rather than a
system Gradle, note the JDK 25 requirement, give the real jar path and
name, warn about the one-off Paperweight dev-bundle download, and add a
table of the other useful tasks plus the toolchain-discovery flag CI uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJPRnxgoMiVypeN21uRFW4
The suffix order was wrong: local builds produce 3.22.1-SNAPSHOT-LOCAL,
not 3.22.1-LOCAL-SNAPSHOT, and CI does not append -b{BUILD_NUMBER} to the
Gradle artifact version. Separate the artifact version from the plugin.yml
version, which is the only one that carries the build number, and record
the doubled -LOCAL marker local builds produce so it is not mistaken for a
bug to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJPRnxgoMiVypeN21uRFW4
plugin.yml renders ${project.version}${build.number}. For a local build
both carried the marker -- project.version ended in -LOCAL and
build.number was -LOCAL -- so the plugin reported its version as
3.22.1-SNAPSHOT-LOCAL-LOCAL.

Keep the marker in the revision, so the jar filename still distinguishes
a local build from a CI snapshot, and leave build.number empty. Local
plugin.yml now reads 3.22.1-SNAPSHOT-LOCAL. The CI and origin/master
paths are untouched: verified the version resolves to 3.22.1-SNAPSHOT
with BUILD_NUMBER set and 3.22.1 on origin/master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJPRnxgoMiVypeN21uRFW4
Geyser expands one Bedrock tap into several Java click packets - a take and
a place-back - and delivers them in the same server tick. The panel click
cooldown treated the second packet as spam, so a Bedrock player's very first
click on the island settings GUI produced general.errors.slow-down and, since
the check moved ahead of the click handler, was dropped entirely.

Clicks that arrive in the same tick as the window opened are now treated as
one physical gesture: a player cannot click twice inside a single tick. The
first click of the gesture that can actually do work is let through, the rest
are dropped silently rather than notifying. Which packet arrived first no
longer matters, so a no-op click on a filler icon or the player's own
inventory no longer consumes the window the real click needs.

Clicks in a later tick but still inside the cooldown are unchanged: rejected,
with the notice sent at most once per window.

Fixes #3049

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

Stop the panel click cooldown eating Bedrock players' clicks
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
…de-commands

Withdraw an addon's commands when the addon never enables
When a game mode uses a shared (non-island) nether with portal linking off,
PlayerTeleportListener.handleFromStandardNetherOrEnd resolved the return
destination from the island's spawn point only:

    getIsland(overWorld, player).map(island -> island.getSpawnPoint(NORMAL))

Optional.map collapses to empty when the mapper returns null, so an island
with no spawn point was indistinguishable from having no island at all, and
the code fell through to the world-spawn fallback.

An island only gets a spawn point when its blueprint contains a sign whose
first line is {spawn here} (DefaultPasteUtil.handleSpawnSign). AOneBlock's
default blueprint is a single bedrock block, so its islands never have one
and every player returning from the nether landed at the world spawn - in
practice the island nearest 0,0, which belongs to somebody else.

Fall back to the island's home location, which is @nonnull and defaults to
the protection centre, so the player always arrives on their own island.
The end-exit-portal path already does this via getSafeRespawnLocation.

Fixes BentoBoxWorld/AOneBlock#549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ijeqd5GdQNHbJQh6MwQyP
Send players returning from a standard nether to their own island
@sonarqubecloud

Copy link
Copy Markdown

@tastybento
tastybento merged commit 1d016b7 into master Aug 10, 2026
5 checks passed
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