diff --git a/pom.xml b/pom.xml
index 3d45431..c019610 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,7 +64,7 @@
-LOCAL
- 1.20.1
+ 1.21.0
BentoBoxWorld_AOneBlock
bentobox-world
@@ -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 44acf90..3ea3ba7 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,65 @@
*/
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 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.
+ */
+ 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 +131,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 +157,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 +184,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 +218,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 +273,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 +283,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 +353,9 @@ public OneBlockIslands getOneBlocksIsland(@NonNull Island i) {
return blockListener.getIsland(Objects.requireNonNull(i));
}
+ /**
+ * @return The OneBlock manager.
+ */
public OneBlocksManager getOneBlockManager() {
return oneBlockManager;
}
@@ -341,6 +399,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/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/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
index 25fcd0a..d7f5381 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java
@@ -27,6 +27,7 @@
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;
@@ -56,14 +57,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.
@@ -76,36 +79,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;
-
- // Loot for suspicious blocks
+
+ /** Loot table for suspicious blocks. Maps item to its probability. */
private static final Map LOOT;
static {
Map loot = new HashMap<>();
@@ -142,7 +145,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;
@@ -154,7 +158,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);
@@ -164,6 +168,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())) {
@@ -171,6 +179,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())) {
@@ -178,6 +190,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())) {
@@ -201,6 +217,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())) {
@@ -303,6 +323,10 @@ public void onItemSpawn(ItemSpawnEvent 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()))
@@ -315,20 +339,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;
}
@@ -337,12 +367,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) {}
/**
@@ -370,12 +406,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);
}
@@ -498,7 +536,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
@@ -517,7 +555,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
@@ -560,6 +598,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;
@@ -603,6 +646,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);
@@ -624,6 +672,11 @@ private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block)
}
+ /**
+ * 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;
@@ -632,12 +685,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;
@@ -654,6 +713,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);
@@ -666,6 +730,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);
@@ -708,6 +777,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/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 3345596..19a340e 100755
--- a/src/main/resources/locales/en-US.yml
+++ b/src/main/resources/locales/en-US.yml
@@ -5,6 +5,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: Starting Safety
description: |
@@ -18,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"
@@ -29,6 +43,9 @@ aoneblock:
# SOLID, SEGMENTED_6, SEGMENTED_10, SEGMENTED_12, SEGMENTED_20
style: SOLID
not-active: "&c Boss Bar is not active for this island"
+ 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:
@@ -65,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());