From 56d0e8ccd1df0d4bb67f8bbad8e73da9959f6285 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 18 Aug 2026 19:05:17 -0700 Subject: [PATCH 1/2] fix: shrink island protection range during Level scan to unlocked area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Level addon scans every chunk within island.getProtectionRange(). With the default of 240 blocks, that is ~961 chunks per dimension — most of them locked and empty in ChunkBlock. On slower servers this causes a 5-minute timeout. Listen for IslandPreLevelEvent and temporarily set the protection range to cover only the current unlocked ring (ring * 16 + 8). The Level calculator builds its chunk queue in the same tick, so the original range is restored on the next tick with no side effects. A player with only the center chunk now triggers 1 chunk scan instead of 961. Co-Authored-By: Claude Opus 4.6 (1M context) Claude-Session: https://claude.ai/code/session_01B94CWZDoiM9VevWtXb4RxF --- .../chunkblock/listeners/LevelListener.java | 22 ++++++++++ .../listeners/LevelListenerTest.java | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/main/java/world/bentobox/chunkblock/listeners/LevelListener.java b/src/main/java/world/bentobox/chunkblock/listeners/LevelListener.java index a76f81b..e491057 100644 --- a/src/main/java/world/bentobox/chunkblock/listeners/LevelListener.java +++ b/src/main/java/world/bentobox/chunkblock/listeners/LevelListener.java @@ -27,6 +27,7 @@ import world.bentobox.chunkblock.events.ChunkUnlockEvent; import world.bentobox.chunkblock.events.RingCompleteEvent; import world.bentobox.level.events.IslandLevelCalculatedEvent; +import world.bentobox.level.events.IslandPreLevelEvent; /** * Watches island level changes from the Level addon. Levels are chunk currency here: @@ -44,6 +45,27 @@ public LevelListener(ChunkBlock addon) { this.addon = addon; } + /** + * Shrinks the island's protection range to cover only the unlocked chunks so the + * Level addon scans the playable area instead of the entire 240-block default. + * The calculator reads the range in the same tick; the original is restored on + * the next tick. + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onIslandPreLevel(IslandPreLevelEvent e) { + Island island = e.getIsland(); + if (island == null || !addon.inWorld(island.getWorld())) { + return; + } + int ring = addon.getChunkManager().currentRing(island); + int needed = ring * 16 + ChunkManager.CHUNK_CENTER; + int stored = island.getProtectionRange(); + if (needed < stored) { + island.setProtectionRange(needed); + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> island.setProtectionRange(stored)); + } + } + /** * Fires after every island level calculation, before results are saved. Never * cancelled here — we only read the level. diff --git a/src/test/java/world/bentobox/chunkblock/listeners/LevelListenerTest.java b/src/test/java/world/bentobox/chunkblock/listeners/LevelListenerTest.java index 334ed61..aaab902 100644 --- a/src/test/java/world/bentobox/chunkblock/listeners/LevelListenerTest.java +++ b/src/test/java/world/bentobox/chunkblock/listeners/LevelListenerTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -28,6 +29,7 @@ import world.bentobox.chunkblock.events.ChunkRelockEvent; import world.bentobox.chunkblock.events.ChunkUnlockEvent; import world.bentobox.chunkblock.events.RingCompleteEvent; +import world.bentobox.level.events.IslandPreLevelEvent; /** * Tests the credit-announcement and LIFO re-lock flows in {@link LevelListener} and the @@ -216,6 +218,45 @@ void testIslandResetClearsRingRewards() { assertEquals(1, data.getUnlockedChunkCount()); } + @Test + void testPreLevelShrinksProtectionRangeToUnlockedArea() { + when(island.getProtectionRange()).thenReturn(240); + // Only center chunk is unlocked → ring 0 → needed = 8 + IslandPreLevelEvent pre = new IslandPreLevelEvent(uuid, island); + listener.onIslandPreLevel(pre); + verify(island).setProtectionRange(ChunkManager.CHUNK_CENTER); + // Restore scheduled for next tick + verify(sch).runTask(any(), any(Runnable.class)); + } + + @Test + void testPreLevelRangeMatchesCurrentRing() { + when(island.getProtectionRange()).thenReturn(240); + level = 8; + claimRingOne(); + // Ring 1 unlocked → needed = 1 * 16 + 8 = 24 + IslandPreLevelEvent pre = new IslandPreLevelEvent(uuid, island); + listener.onIslandPreLevel(pre); + verify(island).setProtectionRange(24); + } + + @Test + void testPreLevelSkipsWhenRangeAlreadySmallEnough() { + when(island.getProtectionRange()).thenReturn(8); + IslandPreLevelEvent pre = new IslandPreLevelEvent(uuid, island); + listener.onIslandPreLevel(pre); + verify(island, never()).setProtectionRange(anyInt()); + } + + @Test + void testPreLevelIgnoresOtherWorlds() { + when(addon.inWorld(world)).thenReturn(false); + when(island.getProtectionRange()).thenReturn(240); + IslandPreLevelEvent pre = new IslandPreLevelEvent(uuid, island); + listener.onIslandPreLevel(pre); + verify(island, never()).setProtectionRange(anyInt()); + } + /** Claims and celebrates all eight chunks of ring 1, closing it with the last one */ private void claimRingOne() { for (int[] offset : new int[][] { { 1, 0 }, { 0, 1 }, { -1, 0 }, { 0, -1 }, { 1, 1 }, { -1, 1 }, From ee8d22df1a7ba809adace6cc60063cdff80e6014 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 18 Aug 2026 19:08:18 -0700 Subject: [PATCH 2/2] fix: reduce default protection range from 240 to 168 240 supports up to ring 14 (841 chunks) but the default max-chunks is 441 (ring 10), so 168 is the minimum needed. The oversized default caused the Level addon to scan ~961 chunks instead of ~441. Co-Authored-By: Claude Opus 4.6 (1M context) Claude-Session: https://claude.ai/code/session_01B94CWZDoiM9VevWtXb4RxF --- src/main/java/world/bentobox/chunkblock/Settings.java | 3 ++- src/test/java/world/bentobox/chunkblock/SettingsTest.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/chunkblock/Settings.java b/src/main/java/world/bentobox/chunkblock/Settings.java index bc5a338..4defcee 100644 --- a/src/main/java/world/bentobox/chunkblock/Settings.java +++ b/src/main/java/world/bentobox/chunkblock/Settings.java @@ -314,8 +314,9 @@ public class Settings implements WorldSettings { @ConfigComment("Admins can change protection sizes for players individually using /chadmin range set ") @ConfigComment("or set this permission: chunkblock.island.range.") @ConfigComment("ChunkBlock: this must cover the largest unlockable ring of chunks (see chunkblock.max-chunks).") + @ConfigComment("With max-chunks 441 (21x21, ring 10) the minimum needed is 168.") @ConfigEntry(path = "world.protection-range") - private int islandProtectionRange = 240; + private int islandProtectionRange = 168; @ConfigComment("Start islands at these coordinates. This is where new islands will start in the") @ConfigComment("world. These must be a factor of your island distance, but the plugin will auto") diff --git a/src/test/java/world/bentobox/chunkblock/SettingsTest.java b/src/test/java/world/bentobox/chunkblock/SettingsTest.java index 30cf8f1..ede1583 100644 --- a/src/test/java/world/bentobox/chunkblock/SettingsTest.java +++ b/src/test/java/world/bentobox/chunkblock/SettingsTest.java @@ -80,7 +80,7 @@ void testGetIslandDistance() { */ @Test void testGetIslandProtectionRange() { - assertEquals(240, s.getIslandProtectionRange()); + assertEquals(168, s.getIslandProtectionRange()); } /**