From acb80597e9531805af3b737ea52f0c2e88e5e61d Mon Sep 17 00:00:00 2001 From: daniel-skopek Date: Sun, 20 Sep 2026 21:36:48 +0200 Subject: [PATCH] Fix soft-deleted islands still counting as a player's island Deleting an island (reset or /obadmin delete) cleared its owner but left it in the per-player UUID cache index. Until the next restart the island still counted towards the ex-owner's concurrent islands and was returned by getIslands(world, uuid). This blocked island transfers with the "already owns N islands" error, even though /obadmin delete said the player had no island. Drop the island from the UUID index when it is soft-deleted. It stays cached and in the grid so the region purge can still find and reap it. --- .../bentobox/managers/IslandsManager.java | 6 ++++++ .../bentobox/managers/island/IslandCache.java | 19 +++++++++++++++++ .../bentobox/managers/IslandsManagerTest.java | 20 ++++++++++++++++++ .../managers/island/IslandCacheTest.java | 21 +++++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index ef3ea8e16..fe0ab6253 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -297,6 +297,12 @@ public void deleteIsland(@NonNull Island island, boolean removeBlocks, @Nullable // Set the owner of the island to no one. island.setOwner(null); island.setFlag(Flags.LOCK, RanksManager.VISITOR_RANK); + // Drop the island from the per-player UUID index. It is unowned now, so it + // must not count towards anyone's concurrent islands or be returned by + // getIslands(world, uuid) until it is registered to a new owner again. This + // keeps the runtime index consistent with the one rebuilt on startup, which + // only indexes owned islands (IslandCache#addIsland). + islandCache.removeIslandFromUUIDIndex(island); if (removeBlocks) { // Remove players from island removePlayersFromIsland(island); diff --git a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java index 61e28b2d9..fcfacc636 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java @@ -194,6 +194,25 @@ public void deleteIslandFromCache(@NonNull String uniqueId) { } } + /** + * Removes every player's link to this island from the per-UUID lookup index + * without evicting the island from {@code islandsById} or the island grid. + *

+ * Used when an island is soft-deleted (see + * {@link world.bentobox.bentobox.managers.IslandsManager#deleteIsland}). The + * island becomes unowned, so it must no longer count towards any player's + * concurrent islands or show up in {@link #getIslands(World, UUID)}, yet it + * must stay known so its location remains reserved and the region-file purge + * can still find and reap it. This mirrors how the index is rebuilt on + * startup, where {@link #addIsland(Island)} only indexes owned islands. + * + * @param island the island to de-index + * @since 3.23.0 + */ + public void removeIslandFromUUIDIndex(@NonNull Island island) { + removeFromIslandsByUUID(island); + } + /** * Returns island referenced by player's UUID. Returns the island the player is * on now, or their last known island diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java index 66088f759..6ce2f61eb 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java @@ -471,6 +471,26 @@ void testDeleteIslandIslandBooleanRemoveBlocks() { verify(pim).callEvent(any(IslandDeleteEvent.class)); } + /** + * A soft-deleted island becomes unowned, so it must be dropped from the + * per-player UUID index. Otherwise a pending-deletion island keeps counting + * towards the ex-owner's concurrent islands (and shows up in + * {@code getIslands(world, uuid)}) until the server restarts. + */ + @Test + void testDeleteIslandRemovesIslandFromUUIDIndex() { + UUID localOwner = UUID.randomUUID(); + Island island = islandsManager.createIsland(location, localOwner); + assertNotNull(island); + assertEquals(1, islandsManager.getNumberOfConcurrentIslands(localOwner, world)); + + islandsManager.deleteIsland(island, true, localOwner); + + assertNull(island.getOwner()); + assertEquals(0, islandsManager.getNumberOfConcurrentIslands(localOwner, world)); + assertTrue(islandsManager.getIslands(world, localOwner).isEmpty()); + } + /** * Test method for * {@link world.bentobox.bentobox.managers.IslandsManager#undeleteIsland(world.bentobox.bentobox.database.objects.Island)}. diff --git a/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java b/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java index 7dbf777f0..704039092 100644 --- a/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/island/IslandCacheTest.java @@ -384,6 +384,27 @@ void testRemovePlayerIslandUUID() { ic.removePlayer(island, owner); } + /** + * Test for {@link IslandCache#removeIslandFromUUIDIndex(Island)}. The island + * must no longer be associated with any player, but must stay cached and + * known at its location so the region purge can still reap it. + */ + @Test + void testRemoveIslandFromUUIDIndex() { + ic.addIsland(island); + assertEquals(island, ic.getIsland(world, owner)); + + ic.removeIslandFromUUIDIndex(island); + + // Not associated with the player any more + assertTrue(ic.getIslands(world, owner).isEmpty()); + assertNull(ic.getIsland(world, owner)); + assertFalse(ic.hasIsland(world, owner)); + // Still cached and still occupying its grid slot + assertEquals(island, ic.getIslandById("uniqueId")); + assertEquals(island, ic.getIslandAt(island.getCenter())); + } + /** * Test method for {@link world.bentobox.bentobox.managers.island.IslandCache#size(org.bukkit.World)}. */