From 6fadafb4cf17611e246697d421c9c026441824cd Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 30 Sep 2025 14:07:31 -0700 Subject: [PATCH 1/6] WIP for #447 --- .../world/bentobox/aoneblock/AOneBlock.java | 66 ++++++++- .../aoneblock/listeners/BlockListener.java | 125 ++++++++++++++---- .../aoneblock/listeners/BlockProtect.java | 30 ++++- src/main/resources/locales/en-US.yml | 8 +- 4 files changed, 192 insertions(+), 37 deletions(-) diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlock.java b/src/main/java/world/bentobox/aoneblock/AOneBlock.java index 44acf90..ecde518 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlock.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlock.java @@ -38,6 +38,7 @@ import world.bentobox.bentobox.api.flags.Flag.Mode; import world.bentobox.bentobox.api.flags.Flag.Type; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.RanksManager; /** * Main OneBlock class - provides an island minigame in the sky @@ -46,29 +47,57 @@ */ public class AOneBlock extends GameModeAddon { + /** Suffix for the nether world */ private static final String NETHER = "_nether"; + /** Suffix for the end world */ private static final String THE_END = "_the_end"; + /** Whether ItemsAdder is present on the server */ private boolean hasItemsAdder = false; - // Settings + /** The addon settings */ private Settings settings; + /** The custom chunk generator for OneBlock worlds */ private ChunkGeneratorWorld chunkGenerator; + /** The configuration object for settings */ private final Config configObject = new Config<>(this, Settings.class); + /** The listener for block-related events */ private BlockListener blockListener; + /** The manager for OneBlock phases and blocks */ private OneBlocksManager oneBlockManager; + /** The placeholder manager for AOneBlock */ private AOneBlockPlaceholders phManager; + /** The listener for hologram-related events */ private HoloListener holoListener; - // Flag + /** + * Flag to enable or disable start safety for players. + */ public final Flag START_SAFETY = new Flag.Builder("START_SAFETY", Material.BAMBOO_BLOCK) .mode(Mode.BASIC) .type(Type.WORLD_SETTING) .listener(new StartSafetyListener(this)) .defaultSetting(false) .build(); + /** The listener for the boss bar */ private BossBarListener bossBar = new BossBarListener(this); - public final Flag ONEBLOCK_BOSSBAR = new Flag.Builder("ONEBLOCK_BOSSBAR", Material.DRAGON_HEAD).mode(Mode.BASIC) - .type(Type.SETTING).listener(bossBar).defaultSetting(true).build(); + /** + * Flag to enable or disable the OneBlock boss bar. + */ + public final Flag ONEBLOCK_BOSSBAR = new Flag.Builder("ONEBLOCK_BOSSBAR", Material.DRAGON_HEAD) + .mode(Mode.BASIC) + .type(Type.SETTING) + .listener(bossBar) + .defaultSetting(true) + .build(); + + /** + * Flag to set who can break the magic block. + */ + public final Flag MAGIC_BLOCK = new Flag.Builder("MAGIC_BLOCK", Material.GRASS_BLOCK) + .mode(Mode.BASIC) + .type(Type.PROTECTION) + .defaultRank(RanksManager.COOP_RANK) + .build(); @Override public void onLoad() { @@ -94,9 +123,15 @@ public void onLoad() { getPlugin().getFlagsManager().registerFlag(this, START_SAFETY); // Bossbar getPlugin().getFlagsManager().registerFlag(this, this.ONEBLOCK_BOSSBAR); + // Magic Block protection + getPlugin().getFlagsManager().registerFlag(this, this.MAGIC_BLOCK); } } + /** + * Loads the settings from the config file. + * @return true if settings were loaded successfully, false otherwise. + */ private boolean loadSettings() { // Load settings again to get worlds settings = configObject.loadConfigObject(); @@ -114,11 +149,14 @@ private boolean loadSettings() { @Override public void onEnable() { + // Initialize the OneBlock manager oneBlockManager = new OneBlocksManager(this); + // Load phase data if (loadData()) { // Failed to load - don't register anything return; } + // Initialize and register listeners blockListener = new BlockListener(this); registerListener(blockListener); registerListener(new NoBlockHandler(this)); @@ -138,12 +176,15 @@ public void onEnable() { registerListener(holoListener); } - // Load phase data + /** + * Load phase data from oneblock.yml. + * @return true if there was an error, false otherwise. + */ public boolean loadData() { try { oneBlockManager.loadPhases(); } catch (IOException e) { - // Disable + // Disable the addon if phase data cannot be loaded logError("AOneBlock settings could not load (oneblock.yml error)! Addon disabled."); logError(e.getMessage()); setState(State.DISABLED); @@ -169,6 +210,7 @@ public void onDisable() { public void onReload() { // save cache blockListener.saveCache(); + // Reload settings and phase data if (loadSettings()) { log("Reloaded AOneBlock settings"); loadData(); @@ -223,6 +265,7 @@ private World getWorld(String worldName2, Environment env, ChunkGeneratorWorld c worldName2 = env.equals(World.Environment.NETHER) ? worldName2 + NETHER : worldName2; worldName2 = env.equals(World.Environment.THE_END) ? worldName2 + THE_END : worldName2; WorldCreator wc = WorldCreator.name(worldName2).environment(env); + // Use custom generator if configured, otherwise default World w = settings.isUseOwnGenerator() ? wc.createWorld() : wc.generator(chunkGenerator2).createWorld(); // Set spawn rates if (w != null) { @@ -232,6 +275,10 @@ private World getWorld(String worldName2, Environment env, ChunkGeneratorWorld c } + /** + * Sets the spawn rates for a given world based on the addon's settings. + * @param w The world to set spawn rates for. + */ private void setSpawnRates(World w) { if (getSettings().getSpawnLimitMonsters() > 0) { w.setSpawnLimit(SpawnCategory.MONSTER, getSettings().getSpawnLimitMonsters()); @@ -298,6 +345,9 @@ public OneBlockIslands getOneBlocksIsland(@NonNull Island i) { return blockListener.getIsland(Objects.requireNonNull(i)); } + /** + * @return The OneBlock manager. + */ public OneBlocksManager getOneBlockManager() { return oneBlockManager; } @@ -341,6 +391,10 @@ public void setIslandWorld(World world) { } + /** + * Sets the addon's settings. Used only for testing. + * @param settings The settings to set. + */ public void setSettings(Settings settings) { this.settings = settings; } diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index d7ffe42..ca302ba 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java @@ -20,15 +20,14 @@ import org.bukkit.block.Biome; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.block.BrushableBlock; import org.bukkit.block.Chest; -import org.bukkit.block.SuspiciousSand; import org.bukkit.block.data.Brushable; import org.bukkit.block.data.type.Leaves; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -41,13 +40,10 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import org.bukkit.loot.LootTables; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import com.bgsoftware.wildstacker.api.loot.LootTable; - import world.bentobox.aoneblock.AOneBlock; import world.bentobox.aoneblock.dataobjects.OneBlockIslands; import world.bentobox.aoneblock.events.MagicBlockEntityEvent; @@ -60,14 +56,16 @@ import world.bentobox.bentobox.api.events.island.IslandCreatedEvent; import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.events.island.IslandResettedEvent; +import world.bentobox.bentobox.api.flags.FlagListener; import world.bentobox.bentobox.database.Database; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; /** + * This listener handles all the core logic for the OneBlock block, including breaking, phase changes, and entity spawning. * @author tastybento */ -public class BlockListener implements Listener { +public class BlockListener extends FlagListener implements Listener { /** * Main addon class. @@ -80,38 +78,36 @@ public class BlockListener implements Listener { private final OneBlocksManager oneBlocksManager; /** - * Oneblock data database + * Oneblock data database handler. */ private final Database handler; /** - * Oneblock cache. + * In-memory cache for OneBlock island data to reduce database lookups. */ private final Map cache; /** - * Phase checker class + * Helper class to check phase requirements. */ private final CheckPhase check; /** - * Sound player + * Helper class to play warning sounds for upcoming mobs. */ private final WarningSounder warningSounder; /** - * How many blocks ahead it should look. + * How many blocks ahead the queue should look when populating. */ public static final int MAX_LOOK_AHEAD = 5; /** - * How often data is saved. + * How often island data is saved to the database (in blocks broken). */ public static final int SAVE_EVERY = 50; - - private final Random random = new Random(); - - // Loot for suspicious blocks + + /** Loot table for suspicious blocks. Maps item to its probability. */ private static final Map LOOT; static { Map loot = new HashMap<>(); @@ -148,7 +144,8 @@ public class BlockListener implements Listener { } /** - * @param addon - OneBlock + * Constructs the BlockListener. + * @param addon - The AOneBlock addon instance. */ public BlockListener(@NonNull AOneBlock addon) { this.addon = addon; @@ -160,7 +157,7 @@ public BlockListener(@NonNull AOneBlock addon) { } /** - * Save the island cache + * Saves all island data from the cache to the database asynchronously. */ public void saveCache() { cache.values().forEach(handler::saveObjectAsync); @@ -170,6 +167,10 @@ public void saveCache() { // Section: Listeners // --------------------------------------------------------------------- + /** + * Sets up a new OneBlock island when a BentoBox island is created. + * @param e The IslandCreatedEvent. + */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandCreatedEvent e) { if (addon.inWorld(e.getIsland().getWorld())) { @@ -177,6 +178,10 @@ public void onNewIsland(IslandCreatedEvent e) { } } + /** + * Resets a OneBlock island when a BentoBox island is reset. + * @param e The IslandResettedEvent. + */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onNewIsland(IslandResettedEvent e) { if (addon.inWorld(e.getIsland().getWorld())) { @@ -184,6 +189,10 @@ public void onNewIsland(IslandResettedEvent e) { } } + /** + * Removes OneBlock data when a BentoBox island is deleted. + * @param e The IslandDeleteEvent. + */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onDeletedIsland(IslandDeleteEvent e) { if (addon.inWorld(e.getIsland().getWorld())) { @@ -207,6 +216,10 @@ public void onBlockFromTo(final BlockFromToEvent e) { e.setCancelled(addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).isPresent()); } + /** + * Handles the breaking of the magic block by a player. + * @param e The BlockBreakEvent. + */ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockBreak(final BlockBreakEvent e) { if (!addon.inWorld(e.getBlock().getWorld())) { @@ -286,6 +299,10 @@ public void onItemStackSpawn(EntitySpawnEvent event) { // Section: Processing methods // --------------------------------------------------------------------- + /** + * Sets up the initial state for a new OneBlock island. + * @param island The island to set up. + */ private void setUp(@NonNull Island island) { // Set the bedrock to the initial block Util.getChunkAtAsync(Objects.requireNonNull(island.getCenter())) @@ -298,20 +315,26 @@ private void setUp(@NonNull Island island) { } /** - * Main block processing method that handles the magic block mechanics. + * Main magic block processing method that handles the magic block mechanics. * This includes phase changes, block spawning, and event handling. * * @param e - event causing the processing - * @param i - island where it's happening + * @param island - island where it's happening * @param player - player who broke the block or who is involved - may be null * @param world - world where the block is being broken */ - private void process(@NonNull Cancellable e, @NonNull Island i, @Nullable Player player, @NonNull World world) { - Block block = Objects.requireNonNull(i.getCenter()).toVector().toLocation(world).getBlock(); - OneBlockIslands is = getIsland(i); + private void process(@NonNull Cancellable e, @NonNull Island island, @Nullable Player player, @NonNull World world) { + // Check if the player has authority to break the magic block + if (!checkIsland((@NonNull Event) e, player, island.getCenter(), addon.MAGIC_BLOCK)) { + // Not allowed + return; + } + + Block block = Objects.requireNonNull(island.getCenter()).toVector().toLocation(world).getBlock(); + OneBlockIslands is = getIsland(island); // Process phase changes and requirements - ProcessPhaseResult phaseResult = processPhase(e, i, is, player, world, block); + ProcessPhaseResult phaseResult = processPhase(e, island, is, player, world, block); if (e.isCancelled()) { return; } @@ -320,12 +343,18 @@ private void process(@NonNull Cancellable e, @NonNull Island i, @Nullable Player initializeQueue(is, phaseResult.phase, phaseResult.isCurrPhaseNew); // Process hologram and warning sounds - processHologramAndWarnings(i, is, phaseResult.phase, block); + processHologramAndWarnings(island, is, phaseResult.phase, block); // Process the next block - processNextBlock(e, i, player, block, is, phaseResult); + processNextBlock(e, island, player, block, is, phaseResult); } + /** + * A record to hold the result of phase processing. + * @param phase The current phase. + * @param isCurrPhaseNew Whether the current phase is new. + * @param blockNumber The block number within the current phase. + */ private record ProcessPhaseResult(OneBlockPhase phase, boolean isCurrPhaseNew, int blockNumber) {} /** @@ -353,12 +382,14 @@ private ProcessPhaseResult processPhase(Cancellable e, Island i, OneBlockIslands boolean isCurrPhaseNew = !is.getPhaseName().equalsIgnoreCase(currPhaseName); if (isCurrPhaseNew) { + // Check if the player meets the requirements for the new phase. if (check.phaseRequirementsFail(player, i, is, phase, world)) { e.setCancelled(true); return new ProcessPhaseResult(phase, true, 0); } handleNewPhase(player, i, is, phase, block, prevPhaseName); } else if (is.getBlockNumber() % SAVE_EVERY == 0) { + // Periodically save the island's progress. saveIsland(i); } @@ -481,7 +512,7 @@ private void handleEntitySpawn(Cancellable e, Island i, Player player, Block blo } /** - * Handles different types of block breaking events. + * Handles different types of block breaking events (player, bucket, entity). * * @param e - event being processed * @param i - island instance @@ -500,7 +531,7 @@ private void handleBlockBreak(Cancellable e, Island i, Player player, Block bloc } /** - * Handles bucket fill events including block spawning and event firing. + * Handles bucket fill events on the magic block. * * @param player - player filling bucket * @param i - island instance @@ -543,6 +574,11 @@ private OneBlockPhase handleGoto(OneBlockIslands is, int gotoBlock) { return oneBlocksManager.getPhase(gotoBlock); } + /** + * Sets the biome in a small radius around the given block. + * @param block The center block. + * @param biome The biome to set. + */ private void setBiome(@NonNull Block block, @Nullable Biome biome) { if (biome == null) { return; @@ -586,6 +622,11 @@ private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockO .callEvent(new MagicBlockEvent(island, player.getUniqueId(), tool, block, nextBlock.getMaterial())); } + /** + * Spawns the next block in the sequence, handling custom blocks and block data. + * @param nextBlock The object representing the block to spawn. + * @param block The block in the world to be replaced. + */ private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block) { if (nextBlock.isCustomBlock()) { nextBlock.getCustomBlock().execute(addon, block); @@ -607,7 +648,11 @@ private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block) } - @SuppressWarnings("deprecation") + /** + * Handles player interaction with suspicious blocks (sand/gravel) using a brush. + * This is currently for debugging purposes. + * @param e The PlayerInteractEvent. + */ @EventHandler public void onPlayerInteract(PlayerInteractEvent e) { if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; @@ -616,12 +661,18 @@ public void onPlayerInteract(PlayerInteractEvent e) { if (e.getClickedBlock().getType() != Material.SUSPICIOUS_GRAVEL && e.getClickedBlock().getType() != Material.SUSPICIOUS_SAND) return; if (e.getPlayer().getInventory().getItemInMainHand().getType() != Material.BRUSH) return; + // TODO FINISH THIS!!! BentoBox.getInstance().logDebug("Brushing " + e.getClickedBlock()); if (e.getClickedBlock() != null && e.getClickedBlock().getBlockData() instanceof Brushable bb) { BentoBox.getInstance().logDebug("item is brushable " + bb.getDusted()); } } + /** + * Gets a random loot item from the loot table based on probabilities. + * @param random The random number generator. + * @return A random Material from the loot table. + */ private static Material getRandomLoot(Random random) { double roll = random.nextDouble(); double cumulative = 0.0; @@ -638,6 +689,11 @@ private static Material getRandomLoot(Random random) { return materials.get(random.nextInt(materials.size())); } + /** + * Spawns an entity at the magic block location. + * @param nextBlock The object containing entity information. + * @param block The magic block. + */ private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block) { if (block.isEmpty()) block.setType(Material.STONE); @@ -650,6 +706,11 @@ private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); } + /** + * Fills a chest with items and adds particle effects based on rarity. + * @param nextBlock The object containing chest information. + * @param block The chest block. + */ private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) { Chest chest = (Chest) block.getState(); nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); @@ -692,6 +753,12 @@ public List getAllIslands() { return handler.loadObjects(); } + /** + * Loads an island's OneBlock data from the database into the cache. + * If it doesn't exist, a new object is created. + * @param uniqueId The unique ID of the island. + * @return The OneBlockIslands data object. + */ @NonNull private OneBlockIslands loadIsland(@NonNull String uniqueId) { if (handler.objectExists(uniqueId)) { diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockProtect.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockProtect.java index 2f9ac39..70babd9 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockProtect.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockProtect.java @@ -26,12 +26,20 @@ import world.bentobox.aoneblock.AOneBlock; import world.bentobox.bentobox.database.objects.Island; +/** + * This listener class provides protection for the OneBlock, + * preventing it from being destroyed or moved by various means. + */ public class BlockProtect implements Listener { + /** The color green for particles. */ public static final Color GREEN = Color.fromBGR(0, 100, 0); + /** The list of particles to be used for sparkles. */ private static final List PARTICLES = new ArrayList<>(List.of(Particle.DUST)); + /** An iterator for the particle list to cycle through them. */ private Iterator particleIterator = Collections.emptyIterator(); + /** The AOneBlock addon instance. */ private final AOneBlock addon; /** @@ -50,29 +58,39 @@ public void onBlockDamage(PlayerInteractEvent e) { Action action = e.getAction(); String clickType = addon.getSettings().getClickType(); + // Exit if click type is NONE, player is not in a OneBlock world, or no block was clicked. if (clickType.equalsIgnoreCase("NONE") || !addon.inWorld(e.getPlayer().getWorld()) || e.getClickedBlock() == null) { return; } + // Check if the action matches the configured click type. if ((action == Action.LEFT_CLICK_BLOCK && clickType.equalsIgnoreCase("LEFT")) || (action == Action.RIGHT_CLICK_BLOCK && clickType.equalsIgnoreCase("RIGHT"))) { Location l = e.getClickedBlock().getLocation(); + // If the clicked block is a OneBlock, show sparkles. addon.getIslands().getIslandAt(l).map(Island::getCenter).filter(center -> center.equals(l)) .ifPresent(this::showSparkles); } } + /** + * Spawns particles around a given location to create a sparkle effect. + * @param location The location to spawn particles at. + */ public void showSparkles(Location location) { + // Reset the particle iterator if it has been exhausted. if (!particleIterator.hasNext()) { Collections.shuffle(PARTICLES); particleIterator = PARTICLES.iterator(); } Particle p = particleIterator.next(); + // Iterate over a 2x1.5x2 box around the block to spawn particles. for (double x = -0.5; x <= 1.5; x += addon.getSettings().getParticleDensity()) { for (double y = 0.0; y <= 1.5; y += addon.getSettings().getParticleDensity()) { for (double z = -0.5; z < 1.5; z += addon.getSettings().getParticleDensity()) { + // Spawn a dust particle with the configured color and size. location.getWorld().spawnParticle(p, location.clone().add(new Vector(x, y, z)), 5, 0.1, 0, 0.1, 1, new Particle.DustOptions(addon.getSettings().getParticleColor(), addon.getSettings().getParticleSize().floatValue())); @@ -92,6 +110,7 @@ public void onBlockChange(final EntityChangeBlockEvent e) { return; } Location l = e.getBlock().getLocation(); + // If the block being changed is a OneBlock, cancel the event. addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).ifPresent(i -> e.setCancelled(true)); } @@ -104,6 +123,7 @@ public void onExplosion(final EntityExplodeEvent e) { if (!addon.inWorld(e.getLocation().getWorld())) { return; } + // Remove any OneBlock from the list of blocks to be exploded. e.blockList().removeIf(b -> addon.getIslands().getIslandAt(b.getLocation()).filter(i -> b.getLocation().equals(i.getCenter())).isPresent()); } @@ -123,6 +143,13 @@ public void onPistonExtend(BlockPistonExtendEvent e) { public void onPistonRetract(BlockPistonRetractEvent e) { checkPiston(e, e.getBlock(), e.getBlocks()); } + + /** + * Checks if a piston action (extend or retract) would move a OneBlock and cancels it if so. + * @param e The cancellable piston event. + * @param block The piston block. + * @param blocks The list of blocks that would be moved. + */ private void checkPiston(Cancellable e, Block block, List blocks) { if (!addon.inWorld(block.getWorld())) { return; @@ -144,7 +171,8 @@ public void onFallingBlockSpawn(EntitySpawnEvent e) { return; } Location l = e.getLocation(); - // Dropped blocks do not spawn on integer locations, so we have to check block values independently + // Dropped blocks do not spawn on integer locations, so we have to check block values independently. + // If the falling block is at the location of a OneBlock, cancel the spawn. addon.getIslands().getIslandAt(l).filter(i -> l.getBlockX() == i.getCenter().getBlockX() && l.getBlockY() == i.getCenter().getBlockY() && l.getBlockZ() == i.getCenter().getBlockZ() diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 3345596..24ee757 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -5,7 +5,13 @@ protection: flags: - START_SAFETY: + MAGIC_BLOCK: + name: Magic Block Protection + description: | + &b Rank that can break the magic + &b block if they can break blocks. + hint: &c Your rank cannot break the magic block! + START_SAFETY: name: Starting Safety description: | &b Prevents new players From 90e805c160f3d806a49be001e65ae04911010222 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 30 Sep 2025 14:49:27 -0700 Subject: [PATCH 2/6] Update build.yml to Java 21 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9296f26..6e88943 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,11 +14,11 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: 17 + java-version: 21 - name: Cache SonarCloud packages uses: actions/cache@v3 with: From f6382d9710f08f896113050bc685e6a4217cd9fb Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 9 Nov 2025 14:49:14 -0800 Subject: [PATCH 3/6] Version 1.21.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3d45431..ffd30e9 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ -LOCAL - 1.20.1 + 1.21.0 BentoBoxWorld_AOneBlock bentobox-world From f192e38091353b50bbcbad058d3cf31d2d08eb5c Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 9 Nov 2025 14:54:47 -0800 Subject: [PATCH 4/6] Fix YAML no tabs --- src/main/resources/locales/en-US.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 24ee757..40ed3a3 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -6,12 +6,12 @@ protection: flags: MAGIC_BLOCK: - name: Magic Block Protection - description: | - &b Rank that can break the magic - &b block if they can break blocks. - hint: &c Your rank cannot break the magic block! - START_SAFETY: + name: Magic Block Protection + description: | + &b Rank that can break the magic + &b block if they can break blocks. + hint: "&c Your rank cannot break the magic block!" + START_SAFETY: name: Starting Safety description: | &b Prevents new players From 7ccb37f8b53458edd8c8ea711cf258ecd3d9e228 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 9 Nov 2025 17:06:46 -0800 Subject: [PATCH 5/6] 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 ffd30e9..c019610 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 ecde518..3ea3ba7 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 0000000..4ac71d6 --- /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 5efbf3a..822f377 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 a2c88e1..0a6d23b 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 40ed3a3..65f7096 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 8014068..e2a5a9f 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 561075c..eebed38 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 5ad0baa..379f85b 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 cf6c13e..a064773 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 6/6] 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 65f7096..19a340e 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]"