From 7ccb37f8b53458edd8c8ea711cf258ecd3d9e228 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 9 Nov 2025 17:06:46 -0800 Subject: [PATCH 1/2] Add ActionBar support #450 Also updated to use PaperAPI only and not spigot. Needed to change how server mocks are done. --- pom.xml | 29 +++++---- .../world/bentobox/aoneblock/AOneBlock.java | 10 ++- .../island/IslandActionBarCommand.java | 37 +++++++++++ .../aoneblock/listeners/BossBarListener.java | 64 +++++++++++++++++-- .../aoneblock/oneblocks/OneBlocksManager.java | 6 +- src/main/resources/locales/en-US.yml | 17 ++++- .../bentobox/aoneblock/AOneBlockTest.java | 10 ++- .../aoneblock/listeners/BlockProtectTest.java | 6 ++ .../listeners/NoBlockHandlerTest.java | 9 +-- .../bentobox/aoneblock/mocks/ServerMocks.java | 5 +- 10 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 src/main/java/world/bentobox/aoneblock/commands/island/IslandActionBarCommand.java diff --git a/pom.xml b/pom.xml index ffd30e9b..c019610e 100644 --- a/pom.xml +++ b/pom.xml @@ -123,8 +123,12 @@ - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots + jitpack.io + https://jitpack.io + + + papermc + https://repo.papermc.io/repository/maven-public/ minecraft-repo @@ -153,18 +157,17 @@ - - org.spigotmc - spigot-api - ${spigot.version} - provided - + com.github.MockBukkit + MockBukkit + v1.21-SNAPSHOT + test + - org.spigotmc - plugin-annotations - 1.2.3-SNAPSHOT - compile + io.papermc.paper + paper-api + 1.21.10-R0.1-SNAPSHOT + provided @@ -223,7 +226,6 @@ ${items-adder.version} provided - @@ -292,7 +294,6 @@ 3.0.0-M5 - ${argLine} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlock.java b/src/main/java/world/bentobox/aoneblock/AOneBlock.java index ecde5180..3ea3ba7b 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlock.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlock.java @@ -89,7 +89,15 @@ public class AOneBlock extends GameModeAddon { .listener(bossBar) .defaultSetting(true) .build(); - + /** + * Flag to enable or disable the OneBlock action bar. + */ + public final Flag ONEBLOCK_ACTIONBAR = new Flag.Builder("ONEBLOCK_ACTIONBAR", Material.IRON_BARS) + .mode(Mode.BASIC) + .type(Type.SETTING) + .listener(bossBar) + .defaultSetting(true) + .build(); /** * Flag to set who can break the magic block. */ diff --git a/src/main/java/world/bentobox/aoneblock/commands/island/IslandActionBarCommand.java b/src/main/java/world/bentobox/aoneblock/commands/island/IslandActionBarCommand.java new file mode 100644 index 00000000..4ac71d6f --- /dev/null +++ b/src/main/java/world/bentobox/aoneblock/commands/island/IslandActionBarCommand.java @@ -0,0 +1,37 @@ +package world.bentobox.aoneblock.commands.island; + +import java.util.List; + +import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.user.User; + +public class IslandActionBarCommand extends CompositeCommand { + + private AOneBlock addon; + + public IslandActionBarCommand(CompositeCommand islandCommand, String label, String[] aliases) + { + super(islandCommand, label, aliases); + } + + @Override + public void setup() { + setDescription("aoneblock.commands.island.actionbar.description"); + setOnlyPlayer(true); + // Permission + setPermission("island.actionbar"); + addon = getAddon(); + } + + @Override + public boolean execute(User user, String label, List args) { + addon.getBossBar().toggleUser(user); + getIslands().getIslandAt(user.getLocation()).ifPresent(i -> { + if (!i.isAllowed(addon.ONEBLOCK_ACTIONBAR)) { + user.sendMessage("aoneblock.actionbar.not-active"); + } + }); + return true; + } +} diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java index 5efbf3a4..822f3771 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java @@ -17,6 +17,9 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.eclipse.jdt.annotation.NonNull; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import world.bentobox.aoneblock.AOneBlock; import world.bentobox.aoneblock.dataobjects.OneBlockIslands; import world.bentobox.aoneblock.events.MagicBlockEvent; @@ -30,6 +33,12 @@ public class BossBarListener implements Listener { private static final String AONEBLOCK_BOSSBAR = "aoneblock.bossbar"; + private static final String AONEBLOCK_ACTIONBAR = "aoneblock.actionbar"; + + private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder() + .character('&') + .hexColors() // Enables support for modern hex codes (e.g., &#FF0000) alongside legacy codes. + .build(); public BossBarListener(AOneBlock addon) { super(); @@ -45,12 +54,14 @@ public BossBarListener(AOneBlock addon) { public void onBreakBlockEvent(MagicBlockEvent e) { // Update boss bar tryToShowBossBar(e.getPlayerUUID(), e.getIsland()); + tryToShowActionBar(e.getPlayerUUID(), e.getIsland()); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onEnterIsland(IslandEnterEvent event) { if (addon.inWorld(event.getIsland().getWorld())) { tryToShowBossBar(event.getPlayerUUID(), event.getIsland()); + tryToShowActionBar(event.getPlayerUUID(), event.getIsland()); } } @@ -59,10 +70,56 @@ public void onFlagChange(FlagSettingChangeEvent e) { if (e.getEditedFlag() == addon.ONEBLOCK_BOSSBAR) { // Show to players on island. If it isn't allowed then this will clean up the boss bar too e.getIsland().getPlayersOnIsland().stream().map(Player::getUniqueId) - .forEach(uuid -> this.tryToShowBossBar(uuid, e.getIsland())); + .forEach(uuid -> { + tryToShowBossBar(uuid, e.getIsland()); + tryToShowActionBar(uuid, e.getIsland()); + }); } } + /** + * Converts a string containing Bukkit color codes ('&') into an Adventure Component. + * + * @param legacyString The string with Bukkit color and format codes. + * @return The resulting Adventure Component. + */ + public static Component bukkitToAdventure(String legacyString) { + if (legacyString == null) { + return Component.empty(); + } + return LEGACY_SERIALIZER.deserialize(legacyString); + } + + private void tryToShowActionBar(UUID uuid, Island island) { + User user = User.getInstance(uuid); + Player player = Bukkit.getPlayer(uuid); + + // Only show if enabled for island + if (!island.isAllowed(addon.ONEBLOCK_ACTIONBAR)) { + return; + } + // Default to showing boss bar unless it is explicitly turned off + if (!user.getMetaData(AONEBLOCK_ACTIONBAR).map(MetaDataValue::asBoolean).orElse(true)) { + // Remove any boss bar from user if they are in the world + removeBar(user, island); + // Do not show a boss bar + return; + } + // Get the progress + @NonNull + OneBlockIslands obi = addon.getOneBlocksIsland(island); + + // --- Create the Action Bar Component --- + int numBlocksToGo = addon.getOneBlockManager().getNextPhaseBlocks(obi); + int phaseBlocks = addon.getOneBlockManager().getPhaseBlocks(obi); + int done = phaseBlocks - numBlocksToGo; + String translation = user.getTranslationOrNothing("aoneblock.actionbar.status", "[togo]", + String.valueOf(numBlocksToGo), "[total]", String.valueOf(phaseBlocks), "[done]", String.valueOf(done), + "[phase-name]", obi.getPhaseName(), "[percent-done]", + Math.round(addon.getOneBlockManager().getPercentageDone(obi)) + "%"); + // Send + player.sendActionBar(bukkitToAdventure(translation)); + } /** * Try to show the bossbar to the player * @param uuid player's UUID @@ -128,7 +185,6 @@ private void tryToShowBossBar(UUID uuid, Island island) { } // Save the boss bar for later reference (e.g., when updating or removing) islandBossBars.put(island, bar); - } private void removeBar(User user, Island island) { @@ -159,7 +215,7 @@ public void onJoin(PlayerJoinEvent e) { return; } addon.getIslands().getIslandAt(e.getPlayer().getLocation()) - .ifPresent(is -> this.tryToShowBossBar(e.getPlayer().getUniqueId(), is)); + .ifPresent(is -> this.tryToShowBossBar(e.getPlayer().getUniqueId(), is)); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @@ -179,7 +235,7 @@ public void toggleUser(User user) { if (newState) { // If the player is on an island then show the bar addon.getIslands().getIslandAt(user.getLocation()).filter(is -> addon.inWorld(is.getWorld())) - .ifPresent(is -> this.tryToShowBossBar(user.getUniqueId(), is)); + .ifPresent(is -> this.tryToShowBossBar(user.getUniqueId(), is)); user.sendMessage("aoneblock.commands.island.bossbar.status_on"); } else { // Remove player from any boss bars. Adding happens automatically diff --git a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java index a2c88e1f..0a6d23b8 100644 --- a/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java +++ b/src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java @@ -19,7 +19,7 @@ import java.util.jar.JarFile; import java.util.stream.Collectors; -import org.apache.commons.lang.math.NumberUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; @@ -260,7 +260,7 @@ private Map parseFirstBlocksConfig(ConfigurationSection Map result = new HashMap<>(); for (String key : firstBlocksConfig.getKeys(false)) { - if (!NumberUtils.isNumber(key)) { + if (!NumberUtils.isCreatable(key)) { addon.logError("Fixed block key must be an integer. " + key); continue; } @@ -335,7 +335,7 @@ private void addHologramLines(OneBlockPhase obPhase, ConfigurationSection fb) { return; Map result = new HashMap<>(); for (String key : fb.getKeys(false)) { - if (!NumberUtils.isNumber(key)) { + if (!NumberUtils.isCreatable(key)) { addon.logError("Fixed block key must be an integer. " + key); continue; } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 40ed3a37..65f70964 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -24,6 +24,14 @@ protection: description: | &b Shows a status bar &b for each phase. + + ONEBLOCK_ACTIONBAR: + name: Action Bar + description: | + &b Shows a status + &b for each phase + &b in the Action Bar. + aoneblock: bossbar: title: "Blocks remaining" @@ -35,7 +43,10 @@ aoneblock: # SOLID, SEGMENTED_6, SEGMENTED_10, SEGMENTED_12, SEGMENTED_20 style: SOLID not-active: "&c Boss Bar is not active for this island" - commands: + actionbar: + status: "&a Phase: &b [phase-name] &d | &a Blocks: &b [done] &d / &b [total] &d | &a Progression: &b [percent-done]" + not-active: "&c Action Bar is not active for this island" + commands: admin: setcount: parameters: " [lifetime]" @@ -71,6 +82,10 @@ aoneblock: description: "toggles phase boss bar" status_on: "&b Bossbar turned &a on" status_off: "&b Bossbar turned &c off" + actionbar: + description: "toggles phase action bar" + status_on: "&b Action Bar turned &a on" + status_off: "&b Action Bar turned &c off" setcount: parameters: "" description: "set block count to previously completed value" diff --git a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java index 8014068c..e2a5a9fd 100644 --- a/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java +++ b/src/test/java/world/bentobox/aoneblock/AOneBlockTest.java @@ -7,7 +7,9 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_MOCKS; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockitoSession; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -40,6 +42,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockbukkit.mockbukkit.MockBukkit; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -110,7 +113,7 @@ public static void beforeClass() throws IllegalAccessException, InvocationTarget @After public void tearDown() throws IOException { - ServerMocks.unsetBukkitServer(); + MockBukkit.unmock(); User.clearUsers(); Mockito.framework().clearInlineMocks(); deleteAll(new File("database")); @@ -132,7 +135,8 @@ private void deleteAll(File file) throws IOException { */ @Before public void setUp() throws Exception { - Server server = ServerMocks.newServer(); + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + Server server = MockBukkit.mock(); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); @@ -174,7 +178,7 @@ public void setUp() throws Exception { .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); // Server - PowerMockito.mockStatic(Bukkit.class); + PowerMockito.mockStatic(Bukkit.class, RETURNS_MOCKS); when(Bukkit.getServer()).thenReturn(server); when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); diff --git a/src/test/java/world/bentobox/aoneblock/listeners/BlockProtectTest.java b/src/test/java/world/bentobox/aoneblock/listeners/BlockProtectTest.java index 561075cf..eebed384 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/BlockProtectTest.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/BlockProtectTest.java @@ -20,6 +20,7 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; +import org.bukkit.Server; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -39,11 +40,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockbukkit.mockbukkit.MockBukkit; import org.mockito.Mock; import org.powermock.modules.junit4.PowerMockRunner; import world.bentobox.aoneblock.AOneBlock; import world.bentobox.aoneblock.Settings; +import world.bentobox.aoneblock.mocks.ServerMocks; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.IslandsManager; @@ -75,6 +78,8 @@ public class BlockProtectTest { */ @Before public void setUp() throws Exception { + + Server server = MockBukkit.mock(); when(p.getWorld()).thenReturn(world); // In World @@ -105,6 +110,7 @@ public void setUp() throws Exception { */ @After public void tearDown() throws Exception { + MockBukkit.unmock(); } /** diff --git a/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java index 5ad0baa6..379f85b2 100644 --- a/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java +++ b/src/test/java/world/bentobox/aoneblock/listeners/NoBlockHandlerTest.java @@ -68,6 +68,7 @@ public void setUp() throws Exception { // Location when(location.getWorld()).thenReturn(world); + when(location.clone()).thenReturn(location); // Block when(location.getBlock()).thenReturn(block); @@ -106,7 +107,7 @@ public void testNoBlockHandler() { */ @Test public void testOnRespawnSolidBlock() { - PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH); + PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, false, RespawnReason.DEATH); nbh.onRespawn(event); verify(block, never()).setType(any(Material.class)); @@ -118,7 +119,7 @@ public void testOnRespawnSolidBlock() { @Test public void testOnRespawnAirBlock() { when(block.isEmpty()).thenReturn(true); - PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH); + PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, false, RespawnReason.DEATH); nbh.onRespawn(event); verify(block).setType(any(Material.class)); @@ -131,7 +132,7 @@ public void testOnRespawnAirBlock() { public void testOnRespawnAirBlockWrongWorld() { when(aob.inWorld(world)).thenReturn(false); when(block.isEmpty()).thenReturn(true); - PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH); + PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, true, RespawnReason.DEATH); nbh.onRespawn(event); verify(block, never()).setType(any(Material.class)); @@ -144,7 +145,7 @@ public void testOnRespawnAirBlockWrongWorld() { public void testOnRespawnAirBlockNoIsland() { when(im.getIsland(world, ID)).thenReturn(null); when(block.isEmpty()).thenReturn(true); - PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, RespawnReason.DEATH); + PlayerRespawnEvent event = new PlayerRespawnEvent(p, location, false, false, false, RespawnReason.DEATH); nbh.onRespawn(event); verify(block, never()).setType(any(Material.class)); diff --git a/src/test/java/world/bentobox/aoneblock/mocks/ServerMocks.java b/src/test/java/world/bentobox/aoneblock/mocks/ServerMocks.java index cf6c13e9..a0647731 100644 --- a/src/test/java/world/bentobox/aoneblock/mocks/ServerMocks.java +++ b/src/test/java/world/bentobox/aoneblock/mocks/ServerMocks.java @@ -1,5 +1,6 @@ package world.bentobox.aoneblock.mocks; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -24,6 +25,7 @@ public final class ServerMocks { + @SuppressWarnings({ "deprecation", "unchecked" }) public static @NonNull Server newServer() { Server mock = mock(Server.class); @@ -66,7 +68,8 @@ public final class ServerMocks { doReturn(key).when(keyed).getKey(); return keyed; }); - }).when(registry).get(notNull()); + // Cast the registry mock to explicitly define the generic type for the 'get' method resolution. + }).when((Registry) registry).get(any(NamespacedKey.class)); return registry; })).when(mock).getRegistry(notNull()); From f5e17561d38ffd25ea98910ae0197c9e37df4893 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 9 Nov 2025 17:09:22 -0800 Subject: [PATCH 2/2] Fix locale spacing --- src/main/resources/locales/en-US.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 65f70964..19a340e1 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -46,7 +46,7 @@ aoneblock: actionbar: status: "&a Phase: &b [phase-name] &d | &a Blocks: &b [done] &d / &b [total] &d | &a Progression: &b [percent-done]" not-active: "&c Action Bar is not active for this island" - commands: + commands: admin: setcount: parameters: " [lifetime]"