Skip to content

Release 3.15#2945

Open
tastybento wants to merge 44 commits intomasterfrom
develop
Open

Release 3.15#2945
tastybento wants to merge 44 commits intomasterfrom
develop

Conversation

@tastybento
Copy link
Copy Markdown
Member

No description provided.

tastybento and others added 30 commits April 8, 2026 23:43
Scaffolding for the shift away from chunk-copy island deletion. No behavior
change yet — reset and admin delete still go through the old pipeline.

- Settings: add island.deletion.housekeeping.{enabled,interval-days,
  region-age-days} (defaults off/30/60). Deprecate keep-previous-island-on-reset
  and slow-deletion config entries (unbound from config; getters/setters kept
  as @deprecated(forRemoval=true) for binary compat until Phase 4).
- PurgeRegionsService: extract scan/filter/delete/player-cleanup logic out
  of AdminPurgeRegionsCommand so the command and the scheduler share one
  code path. Handles both pre-26.1 (DIM-1/DIM1 subfolders) and 26.1.1+
  (sibling world folders) dimension layouts.
- AdminPurgeRegionsCommand: reduced to ~180 LOC, delegates to the service
  and retains only the two-step confirmation UX + per-island display.
- HousekeepingManager: new manager wired in BentoBox.onEnable(). Hourly
  wall-clock check; runs the purge service across every gamemode overworld
  if enabled and interval has elapsed. Last-run timestamp persisted to
  <plugin-data-folder>/database/housekeeping.yml regardless of DB backend,
  so the schedule survives restarts. Progress logged to console.
- AdminPurgeRegionsCommandTest: stub plugin.getPurgeRegionsService() with
  a real service over the mocked plugin so the extraction is exercised
  exactly as the command runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Running /bbox purge regions confirm on Paper 26.1.1 tripped AsyncCatcher
because PurgeRegionsService.delete() was saving worlds from the async
worker thread, and World.save() is main-thread-only:

    IllegalStateException: Asynchronous world save!
      at PurgeRegionsService.delete(PurgeRegionsService.java:151)

The pre-refactor command ran the save on the main thread inside execute()
but I collapsed it into the service. Move the save back out of the
service so all callers are responsible for flushing on the main thread
before dispatching the async delete.

- PurgeRegionsService.delete(): no longer calls Bukkit.getWorlds().save().
  Javadoc updated to state the caller contract.
- AdminPurgeRegionsCommand.deleteEverything(): call Bukkit.getWorlds()
  .forEach(World::save) before scheduling the async delete. Runs on the
  main thread since execute() is invoked there.
- HousekeepingManager.executeCycle(): the existing runTask() save was
  fire-and-forget — the async cycle could start scanning/deleting before
  the save finished. Block via CompletableFuture.join() until the
  main-thread save completes.
- AdminPurgeRegionsCommandTest: add regression asserting the service
  never calls Bukkit.getWorlds() itself (would have caught this bug).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Paper rate-limits its built-in "plugin-induced save detected" warning,
so after the scan save fired once, the confirm-path save was silent and
looked like it wasn't running. Add explicit plugin.log lines on both
sides of every World.save() call in the purge code paths (scan, confirm,
housekeeping) so operators always see when the save is happening.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds /bbox admin purge age-regions <days> to rewrite per-chunk
timestamp tables in .mca files so regions become purgable without
waiting wall-clock time. The purge scanner reads timestamps from the
region header, not file mtime, so `touch` cannot fake ageing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
IslandsManager.deleteIsland() used to branch on keepPreviousIslandOnReset:
false -> evict from cache, enqueue IslandChunkDeletionManager, MultiLib
notify, delete DB row. true -> save with deletable=true and fire the
deletion event.

With the new region-file purge flow (Phase 1), physical cleanup no
longer happens inline at all - old islands are left in place with
deletable=true and reaped later by PurgeRegionsService /
HousekeepingManager. So the hard-path branch goes away entirely:
every call with removeBlocks=true now soft-deletes.

Consequences in this commit:
- AdminDeleteCommand also soft-deletes until Phase 3 splits it on
  GameModeAddon.isUsesNewChunkGeneration() (new-gen -> soft-delete,
  void gamemodes -> ChunkGenerator regen).
- Nether/End cascade is a no-op in the soft path (nothing touches
  chunks); PurgeRegionsService.scan already gates nether/end on
  isNetherIslands/isEndIslands so vanilla-owned dimensions are
  skipped when the regions are eventually reaped.
- keepPreviousIslandOnReset setter/getter remain as deprecated shims
  (no longer consulted at runtime); Phase 4 removes the field.
- The bentobox-deleteIsland MultiLib subscriber is now unreachable
  from this server's publishers but stays until Phase 4 deletes the
  deletion infrastructure wholesale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2 made reset leave orphaned islands in place with
deletable=true until the region purge reaps them. That meant admins
walking around a server had no way to tell an orphan from a normal
unowned island — /bbox admin info just showed "Unowned" and entering
the area was silent.

Two visible cues now:

- IslandInfo.showAdminInfo() prints a new "deletable: flagged for
  deletion and awaiting region purge" line when island.isDeletable()
  is true, right after the purge-protected line.

- LockAndBanListener notifies ops (once per entry, same pattern as
  the existing lock notification) when they step onto an island
  flagged deletable. Non-ops still see nothing; this is strictly an
  admin heads-up. The notification state is cleared when the op
  leaves the island, so walking back in re-triggers it.

New locale keys commands.admin.info.deletable and
protection.deletable-island-admin in en-US.yml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
/bbox admin delete used to always call deleteIsland(island, true, uuid),
which after Phase 2 soft-deletes unconditionally. That is the right
behavior for new-chunk-generation gamemodes like Boxed where chunks
are expensive and the region-file purge reaps them later on the
HousekeepingManager schedule. For void/simple-generator gamemodes it
is the wrong behavior — chunks are cheap, admins expect "delete" to
actually delete, and soft-deleted rows would linger forever because
the repainted region files always look fresh to the purge scan.

Branch on GameModeAddon.isUsesNewChunkGeneration():

  - true (new-gen): soft-delete via IslandsManager.deleteIsland(),
    same as /is reset. Physical cleanup happens later via
    PurgeRegionsService / HousekeepingManager.

  - false (void/simple): kick off DeleteIslandChunks (which
    routes to WorldRegenerator.regenerateSimple with correct
    nether/end cascade gating) to repaint the chunks via the
    addon's own ChunkGenerator, then hard-delete the island row
    immediately. DeleteIslandChunks snapshots the bounds in its
    constructor so the row can be removed before the async regen
    completes.

Adds IslandsManager.hardDeleteIsland(island): fires the pre-delete
event, kicks members, nulls owner, evicts from cache, deletes the DB
row. Does not touch world chunks — caller handles physical cleanup.

Phase 4 will remove DeleteIslandChunks, IslandDeletion, and the
CopyWorldRegenerator.regenerateCopy seed-world path; regenerateSimple
and the split here survive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a second purge mode that reaps region files for any island already
flagged as deletable, regardless of region-file age. Exposed as
/bbox admin purge deleted and run from HousekeepingManager on a
configurable hourly cadence (default 24h) alongside the existing
monthly age sweep. Closes the post-reset gap where orphan island
regions sat on disk for 60+ days waiting for the age threshold.

Fix: evict in-memory chunks via World.unloadChunk(cx, cz, false) on
the main thread before the async file delete, otherwise Paper's
autosave re-flushes the deleted region files with the stale chunks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Paper's internal chunk cache keeps serving stale block data even after
the .mca region files are deleted from disk. The chunks only clear on
server restart when Paper discards its cache. Deleting the island DB
row immediately left a window where players see old blocks but BentoBox
reports no island at that location.

The deleted sweep (days==0) now adds island IDs to a pendingDeletions
set instead of removing them from the DB inline. On plugin shutdown
(BentoBox.onDisable), flushPendingDeletions() processes the set. If
the server crashes before a clean shutdown, the islands stay
deletable=true and the next purge cycle retries safely.

The age-based sweep (days>0) keeps immediate DB removal with the
existing residual-region completeness check, since old regions won't
be in Paper's memory cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip the diagnostic logging added during development that printed
file size, removed status, and existsAfter for every .mca deletion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This setting was made obsolete by Phase 2 which changed /is reset to
always soft-delete. The only remaining references were in
AdminPurgeCommand for conditional logging — now simplified to always
use tier-based progress reporting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vice.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…/AdminPurgeDeletedCommand.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…DeleteCommand.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Rename `deleteableRegions` → `deletableRegions` in PurgeScanResult
  and all callers (fixes API surface typo)
- Distinguish "none-found" from "purge failed" in
  AdminPurgeRegionsCommand.deleteEverything() with dedicated locale key
- Replace CompletableFuture.runAsync() with Bukkit scheduler in
  AdminPurgeAgeRegionsCommand (ties task to plugin lifecycle)
- Clear notifiedPlayers and deletableNotified on PlayerQuitEvent in
  LockAndBanListener (prevents unbounded set growth)
- Add `commands.admin.purge.failed` locale key in en-US.yml

Agent-Logs-Url: https://github.com/BentoBoxWorld/BentoBox/sessions/99688668-0dd8-455e-9002-3f229fbefbdc

Co-authored-by: tastybento <4407265+tastybento@users.noreply.github.com>
The minimumRank validation was clamping SETTING flags that use -1
(disabled) as their defaultRank. Since -1 is a valid disabled state
for SETTING/WORLD_SETTING flags (Island.isAllowed checks >= 0),
restrict the validation to PROTECTION flags only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…bundle icon

The "icon" field in blueprint bundle JSON files now accepts three formats:
- Plain material name: "DIAMOND"
- Vanilla namespaced material: "minecraft:diamond"
- Custom item model key: "myserver:island_tropical"

Material.matchMaterial() is tried first, handling plain names and vanilla
namespaced keys. If the string contains a colon and is not a recognised
vanilla material, it is treated as a custom item model key applied to a
PAPER base item via ItemMeta.setItemModel().

getIcon() (returning Material) is preserved for binary compatibility.
getIconItemStack() is the new method that returns the full ItemStack with
model data applied. Panels now call getIconItemStack() to render icons.

Closes #2940

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an admin clicks an item from their inventory to set a blueprint bundle
icon, IconChanger now checks ItemMeta.hasItemModel() first. If the item
carries a custom model key (e.g. paper[item_model="myserver:island_tropical"]),
that NamespacedKey string is stored instead of the plain Material name,
preserving the full item model through to getIconItemStack().

Plain material items continue to work exactly as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Applies the same logic as BlueprintBundle to Blueprint.java:
- icon field changed from Material to String internally
- getIcon() preserved (returns Material, backward compatible)
- getIconItemStack() added — resolves plain names, minecraft: keys,
  and custom item-model keys (PAPER base + ItemMeta.setItemModel)
- setIcon(Material) and setIcon(String) both fluently return Blueprint

BlueprintManagementPanel now calls blueprint.getIconItemStack() so
custom-model icons are visible to admins in the management GUI.

IconChanger also applies item-model detection in the blueprint branch,
so admins can click a datapacked item to set a blueprint icon the same
way as a bundle icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation

SonarCloud flagged 8.9% code duplication and repeated "PAPER" literals.

- Add ItemParser.parseIconMaterial() and parseIconItemStack() as the
  single source of truth for icon string resolution
- Both BlueprintBundle and Blueprint now delegate to these methods
  instead of duplicating the logic
- Replace repeated "PAPER" string literals with a DEFAULT_ICON constant
  in each class
- Remove unused Sound import from IconChangerTest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…item-model-icon

feat: support item model keys and namespaced materials for blueprint bundle icon
tastybento and others added 14 commits April 14, 2026 04:41
…nms-directory

chore: remove accidentally committed `.paper-nms/` directory and add to `.gitignore`
…lags, cleanup

- Version 4.0.0 → 3.15.0; all @SInCE 3.14.0 → @SInCE 3.15.0
- hardDeleteIsland: fire Reason.DELETED post-event so hooks (Level, BlueMap)
  are notified consistently with the soft-delete path
- HousekeepingManager.saveAllWorlds: replace unbounded join() with a 2-minute
  timeout to avoid hanging the async cycle thread indefinitely
- Split single housekeepingEnabled flag into two independent flags:
  deleted-sweep.enabled (default true — safe, only reaps reset orphans) and
  age-sweep.enabled (default false — opt-in, more aggressive). Admins get
  automatic cleanup of reset islands with no config needed.
- AdminPurgeDeletedCommand: remove spurious implements Listener / registerListener
  (class has no @eventhandler methods)
- LockAndBanListener.notifyIfDeletable: lambda → Island::isDeletable method ref
- Settings: restore keepPreviousIslandOnReset as @deprecated(forRemoval=true)
  no-ops returning false to preserve binary compatibility for external addons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… javadoc

The DELETED event already fires immediately in deleteIsland() when the island
is marked deletable — addons (OneBlock, Level, etc.) receive it at /is reset
time, not when the housekeeping eventually reclaims the region files.

- Fix DELETED javadoc: clarify it fires at logical-delete time (soft-delete or
  hard-delete), not "after all island chunks deleted" as the old wording said
- Add Reason.PURGED: fires when the region files AND database row are both
  physically gone. Fires from:
  - PurgeRegionsService.flushPendingDeletions() (deleted-sweep, at shutdown)
  - PurgeRegionsService.delete() age-sweep branch when the DB row is removed
- For age-swept islands that were never soft-deleted (isDeletable == false),
  also fire DELETED before the DB row is removed so addons can clean up data
  for islands that were pruned by age rather than by an explicit reset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gions-reset

Purge region files for soft-deleted islands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y /purge unowned

The old /purge command only soft-flagged islands as deletable and left the
.mca region files in place. Disk wasn't freed until the next housekeeping
sweep (or a manual /purge regions). This merges the region-file deletion
directly into /purge <days> so admins get the full reap in one step, and
removes now-redundant subcommands.

Changes:
- /purge <days> now delegates to PurgeRegionsService: scans for region files
  older than N days whose islands aren't protected/spawn/unowned, lists each
  island location to the log, and reaps the .mca files on confirm.
- Dropped /purge regions (merged in), /purge status, /purge stop (regions
  approach is synchronous from the admin's POV — no state to supervise).
- /purge unowned: simplified to flag orphan islands deletable via
  IslandsManager.deleteIsland(island, true, null); logs each orphan's
  location; tells the admin to run /purge deleted to reap.
- /purge deleted unchanged — still reaps anything already flagged.
- Locale cleanup: removed keys for the dropped subcommands; updated
  description to reflect disk-freeing behavior; added unowned.flagged.
- Tests: AdminPurgeCommandTest rewritten end-to-end against a real
  PurgeRegionsService with a tempDir region layout; AdminPurgeRegionsCommandTest
  removed; AdminPurgeUnownedCommandTest gains coverage for already-deletable
  islands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop always-true `this.user.equals(user)` guards in AdminPurgeCommand
  and AdminPurgeDeletedCommand — `this.user = user` was assigned on the
  prior line, so the check could never fail.
- Parameterize the three days-validation tests (notanumber / 0 / -3) as
  a single @ParameterizedTest.
- Extract `wireIsland(id, deletable, purgeProtected, spawn)`,
  `wireEmptyGrid()`, and `createRegionFile()` helpers in
  AdminPurgeCommandTest to collapse ~15-line setup blocks duplicated
  across the 8 scenario tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ommands

AdminPurgeCommand and AdminPurgeDeletedCommand each carried their own
inPurge/toBeConfirmed/lastScan state machine and near-identical
canExecute / scan-and-prompt / deleteEverything bodies (~75 lines apiece,
diverging only in scan source, log prefix, confirm message and an optional
chunk-evict step). Pull the shared scaffolding into a package-private
AbstractPurgeCommand, with subclasses supplying:

- logPrefix() — log line prefix
- successMessageKey() — locale key sent on a successful delete
- sendConfirmPrompt() — main-thread prompt(s) before user types confirm
- beforeDelete(scan) — optional pre-delete hook (used by /purge deleted to
  evict in-memory chunks before the async file delete)
- logScanContents(islands, scan) — optional scan-time logging

Behaviour is preserved: same locale keys, same log prefixes, same async
threading. The "after a non-empty scan" tail on the failed-delete log line
in /purge deleted is dropped — it was always true (we'd already returned
on empty) and made the message harder to share.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Merge /purge regions into /purge; drop status/stop; simplify /purge unowned
@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.

2 participants