From 5cdb3394677e3d0923da77031debadd09b2bcdb4 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 29 Jul 2026 08:24:51 -0700 Subject: [PATCH 1/2] Only offer 'Click to change' in the phases GUI to players who can use it The phases panel offered the SELECT action (and its 'Click to change' tooltip) based on island state and phase requirements alone, without checking whether the player actually holds the permission of the setcount command the click runs. A player with aoneblock.island.setcount negated saw the tooltip and then got a permission error when clicking. The panel now resolves the setcount subcommand and checks its permission before offering the action; if the command cannot be resolved it stays permissive and lets the command's own check decide. Same defect as BentoBoxWorld/ChunkBlock#11, whose panel code was copied from this one. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Y7xSdVPS5vRu6wqf6XshRq --- .../aoneblock/panels/PhasesPanel.java | 17 ++++ .../aoneblock/panels/PhasesPanelTest.java | 79 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java index f4ed70b..ba84b01 100644 --- a/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java +++ b/src/main/java/world/bentobox/aoneblock/panels/PhasesPanel.java @@ -539,6 +539,10 @@ private boolean canApplyPhase(OneBlockPhase phase) { return false; } + if (!this.hasSetCountPermission()) + { + return false; + } if (phase.getBlockNumberValue() >= this.oneBlockIsland.getLifetime()) { return false; @@ -546,6 +550,19 @@ private boolean canApplyPhase(OneBlockPhase phase) return !this.phaseRequirementsFail(phase, this.oneBlockIsland); } + /** + * Checks that the player holds the permission of the setcount command that a phase + * click runs, so the "click to change" action is only offered when it can succeed. + */ + private boolean hasSetCountPermission() + { + String label = this.addon.getSettings().getSetCountCommand().split(" ")[0]; + return this.addon.getPlayerCommand() + .flatMap(mainCommand -> mainCommand.getSubCommand(label)) + .map(subCommand -> this.user.hasPermission(subCommand.getPermission())) + .orElse(true); + } + /** * Builds the click handler for a phase button. */ diff --git a/src/test/java/world/bentobox/aoneblock/panels/PhasesPanelTest.java b/src/test/java/world/bentobox/aoneblock/panels/PhasesPanelTest.java index 7722fa7..850b1f5 100644 --- a/src/test/java/world/bentobox/aoneblock/panels/PhasesPanelTest.java +++ b/src/test/java/world/bentobox/aoneblock/panels/PhasesPanelTest.java @@ -42,6 +42,7 @@ import world.bentobox.bank.Bank; import world.bentobox.bank.BankManager; import world.bentobox.bank.data.Money; +import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.panels.TemplatedPanel; import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; import world.bentobox.bentobox.api.user.User; @@ -554,6 +555,84 @@ void testCanApplyPhaseSuccess() throws Exception { assertTrue(result); } + /** + * Test canApplyPhase returns false when the player lacks the setcount command + * permission — the "click to change" action must not be offered. + */ + @Test + void testCanApplyPhaseNoSetCountPermission() throws Exception { + setUpAddonMocks(); + Island testIsland = new Island(); + testIsland.setOwner(uuid); + OneBlockIslands oneBlockIsland = new OneBlockIslands(testIsland.getUniqueId()); + oneBlockIsland.setLifetime(100L); + oneBlockIsland.setLastPhaseChangeTime(System.currentTimeMillis()); + + User user = User.getInstance(mockPlayer); + when(im.getIsland(world, user)).thenReturn(testIsland); + when(blockListener.getIsland(testIsland)).thenReturn(oneBlockIsland); + when(oneBlockManager.getBlockProbs()).thenReturn(createBlockProbs()); + + // The setcount command exists and carries a permission the player does not have + CompositeCommand mainCommand = mock(CompositeCommand.class); + CompositeCommand setCountCommand = mock(CompositeCommand.class); + when(mainCommand.getSubCommand("setcount")).thenReturn(Optional.of(setCountCommand)); + when(setCountCommand.getPermission()).thenReturn("aoneblock.island.setcount"); + when(addon.getPlayerCommand()).thenReturn(Optional.of(mainCommand)); + when(mockPlayer.hasPermission("aoneblock.island.setcount")).thenReturn(false); + + Constructor constructor = PhasesPanel.class.getDeclaredConstructor(AOneBlock.class, World.class, User.class); + constructor.setAccessible(true); + panel = constructor.newInstance(addon, world, user); + + OneBlockPhase phase = createTestPhase("Plains"); + + Method method = PhasesPanel.class.getDeclaredMethod("canApplyPhase", OneBlockPhase.class); + method.setAccessible(true); + + boolean result = (boolean) method.invoke(panel, phase); + + assertFalse(result); + } + + /** + * Test canApplyPhase returns true when the player holds the setcount permission. + */ + @Test + void testCanApplyPhaseWithSetCountPermission() throws Exception { + setUpAddonMocks(); + Island testIsland = new Island(); + testIsland.setOwner(uuid); + OneBlockIslands oneBlockIsland = new OneBlockIslands(testIsland.getUniqueId()); + oneBlockIsland.setLifetime(100L); + oneBlockIsland.setLastPhaseChangeTime(System.currentTimeMillis()); + + User user = User.getInstance(mockPlayer); + when(im.getIsland(world, user)).thenReturn(testIsland); + when(blockListener.getIsland(testIsland)).thenReturn(oneBlockIsland); + when(oneBlockManager.getBlockProbs()).thenReturn(createBlockProbs()); + + CompositeCommand mainCommand = mock(CompositeCommand.class); + CompositeCommand setCountCommand = mock(CompositeCommand.class); + when(mainCommand.getSubCommand("setcount")).thenReturn(Optional.of(setCountCommand)); + when(setCountCommand.getPermission()).thenReturn("aoneblock.island.setcount"); + when(addon.getPlayerCommand()).thenReturn(Optional.of(mainCommand)); + when(mockPlayer.hasPermission("aoneblock.island.setcount")).thenReturn(true); + + Constructor constructor = PhasesPanel.class.getDeclaredConstructor(AOneBlock.class, World.class, User.class); + constructor.setAccessible(true); + panel = constructor.newInstance(addon, world, user); + + OneBlockPhase phase = createTestPhase("Plains"); + + Method method = PhasesPanel.class.getDeclaredMethod("canApplyPhase", OneBlockPhase.class); + method.setAccessible(true); + + boolean result = (boolean) method.invoke(panel, phase); + + assertTrue(result); + } + // ========================================================================= // Test phaseRequirementsFail // ========================================================================= From 2b79eb37818f8758f49c1aa86cfefe628e940f75 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 29 Jul 2026 08:30:32 -0700 Subject: [PATCH 2/2] Update build.version to 1.26.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3cd72d1..ef8b554 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ -LOCAL - 1.26.2 + 1.26.3 BentoBoxWorld_AOneBlock bentobox-world