diff --git a/pom.xml b/pom.xml index b07ba71..5a390b7 100644 --- a/pom.xml +++ b/pom.xml @@ -50,12 +50,13 @@ UTF-8 UTF-8 - 17 + 21 2.0.9 1.21.3-R0.1-SNAPSHOT 2.7.1-SNAPSHOT + 4.0.10 2.6.2 1.3.0 @@ -63,7 +64,7 @@ -LOCAL - 1.19.0 + 1.20.0 BentoBoxWorld_AOneBlock bentobox-world @@ -145,6 +146,10 @@ jitpack-repo https://jitpack.io + + matteodev + https://maven.devs.beer/ + @@ -213,9 +218,9 @@ provided - com.github.LoneDev6 - API-ItemsAdder - 3.5.0b + dev.lone + api-itemsadder + ${items-adder.version} provided diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java index b81d3a6..d7ffe42 100644 --- a/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java +++ b/src/main/java/world/bentobox/aoneblock/listeners/BlockListener.java @@ -1,5 +1,6 @@ package world.bentobox.aoneblock.listeners; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,6 +22,8 @@ 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; @@ -29,18 +32,22 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.entity.EntityInteractEvent; import org.bukkit.event.entity.EntitySpawnEvent; import org.bukkit.event.player.PlayerBucketFillEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import org.bukkit.loot.LootTable; 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; @@ -49,6 +56,7 @@ import world.bentobox.aoneblock.oneblocks.OneBlockObject; import world.bentobox.aoneblock.oneblocks.OneBlockPhase; import world.bentobox.aoneblock.oneblocks.OneBlocksManager; +import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.events.island.IslandCreatedEvent; import world.bentobox.bentobox.api.events.island.IslandDeleteEvent; import world.bentobox.bentobox.api.events.island.IslandResettedEvent; @@ -103,568 +111,618 @@ public class BlockListener implements Listener { private final Random random = new Random(); - /** - * @param addon - OneBlock - */ - public BlockListener(@NonNull AOneBlock addon) { - this.addon = addon; - handler = new Database<>(addon, OneBlockIslands.class); - cache = new HashMap<>(); - oneBlocksManager = addon.getOneBlockManager(); - check = new CheckPhase(addon, this); - warningSounder = new WarningSounder(addon); - } - - /** - * Save the island cache - */ - public void saveCache() { - cache.values().forEach(handler::saveObjectAsync); - } - - // --------------------------------------------------------------------- - // Section: Listeners - // --------------------------------------------------------------------- - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onNewIsland(IslandCreatedEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - setUp(e.getIsland()); - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onNewIsland(IslandResettedEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - setUp(e.getIsland()); - } - } - - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onDeletedIsland(IslandDeleteEvent e) { - if (addon.inWorld(e.getIsland().getWorld())) { - cache.remove(e.getIsland().getUniqueId()); - handler.deleteID(e.getIsland().getUniqueId()); - } - } - - /** - * Prevents liquids flowing into magic block - * - * @param e BlockFromToEvent - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlockFromTo(final BlockFromToEvent e) { - if (!addon.inWorld(e.getBlock().getWorld())) { - return; - } - Location l = e.getToBlock().getLocation(); - // Cannot flow to center block - e.setCancelled(addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).isPresent()); - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onBlockBreak(final BlockBreakEvent e) { - if (!addon.inWorld(e.getBlock().getWorld())) { - return; - } - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) - .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); - } - - /** - * Handles JetsMinions. These are special armor stands. Requires Minions 6.9.3 - * or later - * - * @param e - event - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlockBreakByMinion(final EntityInteractEvent e) { - if (!addon.inWorld(e.getBlock().getWorld()) || !e.getEntityType().equals(EntityType.ARMOR_STAND)) { - return; - } - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) - .ifPresent(i -> process(e, i, null, e.getBlock().getWorld())); - } - - /** - * Check for water grabbing - * - * @param e - event (note that you cannot register PlayerBucketEvent) - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onBlockBreak(final PlayerBucketFillEvent e) { - if (addon.inWorld(e.getBlock().getWorld())) { - Location l = e.getBlock().getLocation(); - addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) - .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); - } + // Loot for suspicious blocks + private static final Map LOOT; + static { + Map loot = new HashMap<>(); + loot.put(Material.BLUE_DYE, 0.044); + loot.put(Material.BRICK, 0.044); + loot.put( Material.BROWN_CANDLE, 0.044); + loot.put(Material.EMERALD, 0.044); + loot.put(Material.GREEN_CANDLE, 0.044); + loot.put(Material.LIGHT_BLUE_DYE, 0.044); + loot.put(Material.ORANGE_DYE, 0.044); + loot.put(Material.PURPLE_CANDLE, 0.044); + loot.put(Material.RED_CANDLE, 0.044); + loot.put(Material.WHEAT, 0.044); + loot.put(Material.WOODEN_HOE, 0.044); + loot.put(Material.YELLOW_DYE, 0.044); + loot.put(Material.BEETROOT_SEEDS, 0.022); + loot.put(Material.BLUE_STAINED_GLASS_PANE, 0.022); + loot.put(Material.COAL, 0.022); + loot.put(Material.DEAD_BUSH, 0.022); + loot.put(Material.FLOWER_POT, 0.022); + loot.put(Material.LEAD, 0.022); + loot.put(Material.LIGHT_BLUE_STAINED_GLASS_PANE, 0.022); + loot.put(Material.MAGENTA_STAINED_GLASS_PANE, 0.022); + loot.put(Material.OAK_HANGING_SIGN, 0.022); + loot.put(Material.PINK_STAINED_GLASS_PANE, 0.022); + loot.put(Material.PURPLE_STAINED_GLASS_PANE, 0.022); + loot.put(Material.RED_STAINED_GLASS_PANE, 0.022); + loot.put(Material.SPRUCE_HANGING_SIGN, 0.022); + loot.put(Material.STRING, 0.022); + loot.put(Material.WHEAT_SEEDS, 0.022); + loot.put(Material.YELLOW_STAINED_GLASS_PANE, 0.022); + loot.put(Material.GOLD_NUGGET, 0.022); + LOOT = loot; } /** - * Drop items at the top of the block. - * - * @param event EntitySpawnEvent object. - */ - @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) - public void onItemStackSpawn(EntitySpawnEvent event) { - if (!this.addon.getSettings().isDropOnTop()) { - // Do nothing as item spawning is not interested in this case. - return; - } - - if (!EntityType.ITEM.equals(event.getEntityType())) { - // We are interested only in dropped item entities. - return; - } - - if (!this.addon.inWorld(event.getLocation().getWorld())) { - // Not correct world - return; - } - - Entity entity = event.getEntity(); - Location location = event.getLocation(); - - Optional optionalIsland = this.addon.getIslands().getIslandAt(location) - .filter(island -> location.getBlock().getLocation().equals(island.getCenter())); - - if (optionalIsland.isPresent()) { - // Teleport entity to the top of magic block. - entity.teleport(optionalIsland.get().getCenter().add(0.5, 1, 0.5)); - entity.setVelocity(new Vector(0, 0, 0)); - } - } - - // --------------------------------------------------------------------- - // Section: Processing methods - // --------------------------------------------------------------------- - - private void setUp(@NonNull Island island) { - // Set the bedrock to the initial block - Util.getChunkAtAsync(Objects.requireNonNull(island.getCenter())) - .thenRun(() -> island.getCenter().getBlock().setType(Material.GRASS_BLOCK)); - // Create a database entry - OneBlockIslands is = new OneBlockIslands(island.getUniqueId()); - cache.put(island.getUniqueId(), is); - handler.saveObjectAsync(is); - addon.getHoloListener().setUp(island, is, true); - } - - /** - * Main 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 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); - - // Process phase changes and requirements - ProcessPhaseResult phaseResult = processPhase(e, i, is, player, world, block); - if (e.isCancelled()) { - return; - } - - // Initialize queue if needed - initializeQueue(is, phaseResult.phase, phaseResult.isCurrPhaseNew); - - // Process hologram and warning sounds - processHologramAndWarnings(i, is, phaseResult.phase, block); - - // Process the next block - processNextBlock(e, i, player, block, is, phaseResult); - } - - private record ProcessPhaseResult(OneBlockPhase phase, boolean isCurrPhaseNew, int blockNumber) {} - - /** - * Processes phase changes and requirements for the magic block. - * Returns a record containing phase info, whether it's a new phase, and block number. - * - * @param e - event being processed - * @param i - island instance - * @param is - oneblock island data - * @param player - player involved - * @param world - world where processing occurs - * @param block - block being processed - * @return ProcessPhaseResult containing phase details - */ - private ProcessPhaseResult processPhase(Cancellable e, Island i, OneBlockIslands is, Player player, World world, Block block) { - OneBlockPhase phase = oneBlocksManager.getPhase(is.getBlockNumber()); - String prevPhaseName = is.getPhaseName(); - - if (Objects.requireNonNull(phase).getGotoBlock() != null) { - phase = handleGoto(is, phase.getGotoBlock()); - } - - String currPhaseName = phase.getPhaseName() == null ? "" : phase.getPhaseName(); - handlePhaseChange(is, phase, currPhaseName); - - boolean isCurrPhaseNew = !is.getPhaseName().equalsIgnoreCase(currPhaseName); - if (isCurrPhaseNew) { - 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) { - saveIsland(i); - } - - int materialBlocksInQueue = (int) is.getQueue().stream() - .filter(obo -> obo.isMaterial() || obo.isCustomBlock()) - .count(); - int blockNumber = is.getBlockNumber() - (phase.getBlockNumberValue() - 1) + materialBlocksInQueue; - - return new ProcessPhaseResult(phase, isCurrPhaseNew, blockNumber); - } - - /** - * Handles the initialization of a new phase including biome setting and event firing. - * - * @param player - player triggering the phase change - * @param i - island instance - * @param is - oneblock island data - * @param phase - new phase being entered - * @param block - block being processed - * @param prevPhaseName - name of the previous phase - */ - private void handleNewPhase(Player player, Island i, OneBlockIslands is, OneBlockPhase phase, Block block, String prevPhaseName) { - check.setNewPhase(player, i, is, phase); - is.clearQueue(); - setBiome(block, phase.getPhaseBiome()); - Bukkit.getPluginManager().callEvent(new MagicBlockPhaseEvent(i, - player == null ? null : player.getUniqueId(), - block, phase.getPhaseName(), prevPhaseName, is.getBlockNumber())); - } - - /** - * Handles phase transition mechanics including setting timestamps for phase changes. - * - * @param is - oneblock island data - * @param phase - current phase - * @param currPhaseName - name of current phase - */ - private void handlePhaseChange(OneBlockIslands is, OneBlockPhase phase, String currPhaseName) { - OneBlockPhase nextPhase = oneBlocksManager.getPhase(is.getBlockNumber() + 1); - if (Objects.requireNonNull(nextPhase).getGotoBlock() != null) { - nextPhase = oneBlocksManager.getPhase(nextPhase.getGotoBlock()); - } - String nextPhaseName = nextPhase == null || nextPhase.getPhaseName() == null ? "" : nextPhase.getPhaseName(); - if (!currPhaseName.equalsIgnoreCase(nextPhaseName)) { - is.setLastPhaseChangeTime(System.currentTimeMillis()); - } - } - - /** - * Initializes the block queue for a phase with upcoming blocks. - * - * @param is - oneblock island data - * @param phase - current phase - * @param isCurrPhaseNew - whether this is a new phase - */ - private void initializeQueue(OneBlockIslands is, OneBlockPhase phase, boolean isCurrPhaseNew) { - if (is.getQueue().isEmpty() || isCurrPhaseNew) { - for (int j = 0; j < MAX_LOOK_AHEAD; j++) { - is.add(phase.getNextBlock(addon, j)); - } - } - } - - /** - * Updates holograms and plays warning sounds if configured. - * - * @param i - island instance - * @param is - oneblock island data - * @param phase - current phase - * @param block - block being processed - */ - private void processHologramAndWarnings(Island i, OneBlockIslands is, OneBlockPhase phase, Block block) { - addon.getHoloListener().process(i, is, phase); - if (addon.getSettings().getMobWarning() > 0) { - warningSounder.play(is, block); - } - } - - /** - * Processes the next block in the sequence, handling entities and block changes. - * - * @param e - event being processed - * @param i - island instance - * @param player - player involved - * @param block - block being processed - * @param is - oneblock island data - * @param phaseResult - result from phase processing - */ - private void processNextBlock(Cancellable e, Island i, Player player, Block block, OneBlockIslands is, ProcessPhaseResult phaseResult) { - OneBlockObject nextBlock = (phaseResult.isCurrPhaseNew && phaseResult.phase.getFirstBlock() != null) - ? phaseResult.phase.getFirstBlock() - : is.pollAndAdd(phaseResult.phase.getNextBlock(addon, phaseResult.blockNumber)); - - if (nextBlock.isEntity()) { - handleEntitySpawn(e, i, player, block, nextBlock); - return; - } - - is.incrementBlockNumber(); - handleBlockBreak(e, i, player, block, nextBlock); - } - - /** - * Handles entity spawning for entity-type blocks. - * - * @param e - event being processed - * @param i - island instance - * @param player - player involved - * @param block - block where entity will spawn - * @param nextBlock - next block object containing entity info - */ - private void handleEntitySpawn(Cancellable e, Island i, Player player, Block block, OneBlockObject nextBlock) { - if (!(e instanceof EntitySpawnEvent)) { - e.setCancelled(true); - } - spawnEntity(nextBlock, block); - Bukkit.getPluginManager().callEvent(new MagicBlockEntityEvent(i, - player == null ? null : player.getUniqueId(), - block, nextBlock.getEntityType())); - } - - /** - * Handles different types of block breaking events. - * - * @param e - event being processed - * @param i - island instance - * @param player - player involved - * @param block - block being broken - * @param nextBlock - next block to spawn - */ - private void handleBlockBreak(Cancellable e, Island i, Player player, Block block, OneBlockObject nextBlock) { - if (e instanceof BlockBreakEvent) { - breakBlock(player, block, nextBlock, i); - } else if (e instanceof PlayerBucketFillEvent) { - handleBucketFill(player, i, block, nextBlock); - } else if (e instanceof EntitySpawnEvent || e instanceof EntityInteractEvent) { - handleEntityBreak(i, block, nextBlock, e instanceof EntityInteractEvent); - } - } - - /** - * Handles bucket fill events including block spawning and event firing. - * - * @param player - player filling bucket - * @param i - island instance - * @param block - block being processed - * @param nextBlock - next block to spawn - */ - private void handleBucketFill(Player player, Island i, Block block, OneBlockObject nextBlock) { - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); - ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); - Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, player.getUniqueId(), tool, block, nextBlock.getMaterial())); - } - - /** - * Handles entity-related block breaking including minion interactions. - * - * @param i - island instance - * @param block - block being broken - * @param nextBlock - next block to spawn - * @param isMinion - whether the breaker is a minion - */ - private void handleEntityBreak(Island i, Block block, OneBlockObject nextBlock, boolean isMinion) { - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); - if (isMinion) { - Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, null, null, block, nextBlock.getMaterial())); - } - } - - /** - * Handles goto block mechanics, updating block numbers and lifetime. - * - * @param is - oneblock island data - * @param gotoBlock - target block number - * @return OneBlockPhase for the target block - */ - private OneBlockPhase handleGoto(OneBlockIslands is, int gotoBlock) { - // Store lifetime - is.setLifetime(is.getLifetime() + gotoBlock); - // Set current block - is.setBlockNumber(gotoBlock); - return oneBlocksManager.getPhase(gotoBlock); - } - - private void setBiome(@NonNull Block block, @Nullable Biome biome) { - if (biome == null) { - return; - } - for (int x = -4; x <= 4; x++) { - for (int z = -4; z <= 4; z++) { - for (int y = -4; y <= 4; y++) { - block.getWorld().setBiome(block.getX() + x, block.getY() + y, block.getZ() + z, biome); - } - } - } - } - - /** - * This method is called when block is removed, and next must be spawned. It - * also teleports player above the magic block, to avoid falling in void. - * - * @param player Player who breaks the block. - * @param block Block that was broken. - * @param nextBlock Next Block that will be summoned. - * @param island Island where player is located. - */ - private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockObject nextBlock, - @NonNull Island island) { - ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); - - // Break normally and lift the player up so they don't fall - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> this.spawnBlock(nextBlock, block)); - - if (player.getLocation().getBlock().equals(block)) { - double delta = 1 - (player.getLocation().getY() - block.getY()); - player.teleport(player.getLocation().add(new Vector(0, delta, 0))); - player.setVelocity(new Vector(0, 0, 0)); - } else if (player.getLocation().getBlock().equals(block.getRelative(BlockFace.UP))) { - player.teleport(player.getLocation()); - player.setVelocity(new Vector(0, 0, 0)); - } - - // Fire event - Bukkit.getPluginManager() - .callEvent(new MagicBlockEvent(island, player.getUniqueId(), tool, block, nextBlock.getMaterial())); - } - - private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - if (nextBlock.isCustomBlock()) { - nextBlock.getCustomBlock().execute(addon, block); - } else { - @NonNull - Material type = nextBlock.getMaterial(); - // Place new block with no physics - block.setType(type, false); - // Fill the chest - if (type.equals(Material.CHEST) && nextBlock.getChest() != null) { - fillChest(nextBlock, block); - return; - } else if (Tag.LEAVES.isTagged(type)) { - Leaves leaves = (Leaves) block.getState().getBlockData(); - leaves.setPersistent(true); - block.setBlockData(leaves); - } else if (block.getState() instanceof BrushableBlock bb) { - LootTable lt = switch (bb.getBlock().getBiome().getKey().getKey()) { - case "DESERT" -> LootTables.DESERT_PYRAMID_ARCHAEOLOGY.getLootTable(); - case "FROZEN_OCEAN" -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); - case "OCEAN" -> LootTables.OCEAN_RUIN_COLD_ARCHAEOLOGY.getLootTable(); - case "WARM_OCEAN" -> LootTables.OCEAN_RUIN_WARM_ARCHAEOLOGY.getLootTable(); - default -> { - if (random.nextDouble() < 0.8) { - yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_COMMON.getLootTable(); - } else { - // 20% rare - yield LootTables.TRAIL_RUINS_ARCHAEOLOGY_RARE.getLootTable(); - } - } - }; - bb.setLootTable(lt); - bb.update(); - } - } - - } - - private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - if (block.isEmpty()) - block.setType(Material.STONE); - Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D)); - Entity entity = block.getWorld().spawnEntity(spawnLoc, nextBlock.getEntityType()); - // Make space for entity - this will blot out blocks - if (addon.getSettings().isClearBlocks()) { - new MakeSpace(addon).makeSpace(entity, spawnLoc); - } - block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); - } - - private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) { - Chest chest = (Chest) block.getState(); - nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); - Color color = Color.fromBGR(0, 255, 255); // yellow - switch (nextBlock.getRarity()) { - case EPIC: - color = Color.fromBGR(255, 0, 255); // magenta - break; - case RARE: - color = Color.fromBGR(255, 255, 255); // cyan - break; - case UNCOMMON: - // Yellow - break; - default: - // No sparkles for regular chests - return; - } - block.getWorld().spawnParticle(Particle.DUST, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, - 0, 0.5, 1, new Particle.DustOptions(color, 1)); - } - - /** - * Get the one block island data - * - * @param i - island - * @return one block island - */ - @NonNull - public OneBlockIslands getIsland(@NonNull Island i) { - return cache.containsKey(i.getUniqueId()) ? cache.get(i.getUniqueId()) : loadIsland(i.getUniqueId()); - } - - /** - * Get all the OneBlockIslands from the Database - * - * @return list of oneblock islands - */ - public List getAllIslands() { - return handler.loadObjects(); - } - - @NonNull - private OneBlockIslands loadIsland(@NonNull String uniqueId) { - if (handler.objectExists(uniqueId)) { - OneBlockIslands island = handler.loadObject(uniqueId); - if (island != null) { - // Add to cache - cache.put(island.getUniqueId(), island); - return island; - } - } - return cache.computeIfAbsent(uniqueId, OneBlockIslands::new); - } - - /** - * @return the oneBlocksManager - */ - public OneBlocksManager getOneBlocksManager() { - return oneBlocksManager; - } - - /** - * Saves the island progress to the database async - * - * @param island - island - * @return CompletableFuture - true if saved or not in cache, false if save - * failed + * @param addon - OneBlock */ - public CompletableFuture saveIsland(@NonNull Island island) { - if (cache.containsKey(island.getUniqueId())) { - return handler.saveObjectAsync(cache.get(island.getUniqueId())); - } - return CompletableFuture.completedFuture(true); - } + public BlockListener(@NonNull AOneBlock addon) { + this.addon = addon; + handler = new Database<>(addon, OneBlockIslands.class); + cache = new HashMap<>(); + oneBlocksManager = addon.getOneBlockManager(); + check = new CheckPhase(addon, this); + warningSounder = new WarningSounder(addon); + } + + /** + * Save the island cache + */ + public void saveCache() { + cache.values().forEach(handler::saveObjectAsync); + } + + // --------------------------------------------------------------------- + // Section: Listeners + // --------------------------------------------------------------------- + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onNewIsland(IslandCreatedEvent e) { + if (addon.inWorld(e.getIsland().getWorld())) { + setUp(e.getIsland()); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onNewIsland(IslandResettedEvent e) { + if (addon.inWorld(e.getIsland().getWorld())) { + setUp(e.getIsland()); + } + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onDeletedIsland(IslandDeleteEvent e) { + if (addon.inWorld(e.getIsland().getWorld())) { + cache.remove(e.getIsland().getUniqueId()); + handler.deleteID(e.getIsland().getUniqueId()); + } + } + + /** + * Prevents liquids flowing into magic block + * + * @param e BlockFromToEvent + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlockFromTo(final BlockFromToEvent e) { + if (!addon.inWorld(e.getBlock().getWorld())) { + return; + } + Location l = e.getToBlock().getLocation(); + // Cannot flow to center block + e.setCancelled(addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())).isPresent()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBreak(final BlockBreakEvent e) { + if (!addon.inWorld(e.getBlock().getWorld())) { + return; + } + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); + } + + /** + * Handles JetsMinions. These are special armor stands. Requires Minions 6.9.3 + * or later + * + * @param e - event + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlockBreakByMinion(final EntityInteractEvent e) { + if (!addon.inWorld(e.getBlock().getWorld()) || !e.getEntityType().equals(EntityType.ARMOR_STAND)) { + return; + } + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, null, e.getBlock().getWorld())); + } + + /** + * Check for water grabbing + * + * @param e - event (note that you cannot register PlayerBucketEvent) + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onBlockBreak(final PlayerBucketFillEvent e) { + if (addon.inWorld(e.getBlock().getWorld())) { + Location l = e.getBlock().getLocation(); + addon.getIslands().getIslandAt(l).filter(i -> l.equals(i.getCenter())) + .ifPresent(i -> process(e, i, e.getPlayer(), e.getPlayer().getWorld())); + } + } + + /** + * Drop items at the top of the block. + * + * @param event EntitySpawnEvent object. + */ + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onItemStackSpawn(EntitySpawnEvent event) { + if (!this.addon.getSettings().isDropOnTop()) { + // Do nothing as item spawning is not interested in this case. + return; + } + + if (!EntityType.ITEM.equals(event.getEntityType())) { + // We are interested only in dropped item entities. + return; + } + + if (!this.addon.inWorld(event.getLocation().getWorld())) { + // Not correct world + return; + } + + Entity entity = event.getEntity(); + Location location = event.getLocation(); + + Optional optionalIsland = this.addon.getIslands().getIslandAt(location) + .filter(island -> location.getBlock().getLocation().equals(island.getCenter())); + + if (optionalIsland.isPresent()) { + // Teleport entity to the top of magic block. + entity.teleport(optionalIsland.get().getCenter().add(0.5, 1, 0.5)); + entity.setVelocity(new Vector(0, 0, 0)); + } + } + + // --------------------------------------------------------------------- + // Section: Processing methods + // --------------------------------------------------------------------- + + private void setUp(@NonNull Island island) { + // Set the bedrock to the initial block + Util.getChunkAtAsync(Objects.requireNonNull(island.getCenter())) + .thenRun(() -> island.getCenter().getBlock().setType(Material.GRASS_BLOCK)); + // Create a database entry + OneBlockIslands is = new OneBlockIslands(island.getUniqueId()); + cache.put(island.getUniqueId(), is); + handler.saveObjectAsync(is); + addon.getHoloListener().setUp(island, is, true); + } + + /** + * Main 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 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); + + // Process phase changes and requirements + ProcessPhaseResult phaseResult = processPhase(e, i, is, player, world, block); + if (e.isCancelled()) { + return; + } + + // Initialize queue if needed + initializeQueue(is, phaseResult.phase, phaseResult.isCurrPhaseNew); + + // Process hologram and warning sounds + processHologramAndWarnings(i, is, phaseResult.phase, block); + + // Process the next block + processNextBlock(e, i, player, block, is, phaseResult); + } + + private record ProcessPhaseResult(OneBlockPhase phase, boolean isCurrPhaseNew, int blockNumber) {} + + /** + * Processes phase changes and requirements for the magic block. + * Returns a record containing phase info, whether it's a new phase, and block number. + * + * @param e - event being processed + * @param i - island instance + * @param is - oneblock island data + * @param player - player involved + * @param world - world where processing occurs + * @param block - block being processed + * @return ProcessPhaseResult containing phase details + */ + private ProcessPhaseResult processPhase(Cancellable e, Island i, OneBlockIslands is, Player player, World world, Block block) { + OneBlockPhase phase = oneBlocksManager.getPhase(is.getBlockNumber()); + String prevPhaseName = is.getPhaseName(); + + if (Objects.requireNonNull(phase).getGotoBlock() != null) { + phase = handleGoto(is, phase.getGotoBlock()); + } + + String currPhaseName = phase.getPhaseName() == null ? "" : phase.getPhaseName(); + handlePhaseChange(is, phase, currPhaseName); + + boolean isCurrPhaseNew = !is.getPhaseName().equalsIgnoreCase(currPhaseName); + if (isCurrPhaseNew) { + 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) { + saveIsland(i); + } + + int materialBlocksInQueue = (int) is.getQueue().stream() + .filter(obo -> obo.isMaterial() || obo.isCustomBlock()) + .count(); + int blockNumber = is.getBlockNumber() - (phase.getBlockNumberValue() - 1) + materialBlocksInQueue; + + return new ProcessPhaseResult(phase, isCurrPhaseNew, blockNumber); + } + + /** + * Handles the initialization of a new phase including biome setting and event firing. + * + * @param player - player triggering the phase change + * @param i - island instance + * @param is - oneblock island data + * @param phase - new phase being entered + * @param block - block being processed + * @param prevPhaseName - name of the previous phase + */ + private void handleNewPhase(Player player, Island i, OneBlockIslands is, OneBlockPhase phase, Block block, String prevPhaseName) { + check.setNewPhase(player, i, is, phase); + is.clearQueue(); + setBiome(block, phase.getPhaseBiome()); + Bukkit.getPluginManager().callEvent(new MagicBlockPhaseEvent(i, + player == null ? null : player.getUniqueId(), + block, phase.getPhaseName(), prevPhaseName, is.getBlockNumber())); + } + + /** + * Handles phase transition mechanics including setting timestamps for phase changes. + * + * @param is - oneblock island data + * @param phase - current phase + * @param currPhaseName - name of current phase + */ + private void handlePhaseChange(OneBlockIslands is, OneBlockPhase phase, String currPhaseName) { + OneBlockPhase nextPhase = oneBlocksManager.getPhase(is.getBlockNumber() + 1); + if (Objects.requireNonNull(nextPhase).getGotoBlock() != null) { + nextPhase = oneBlocksManager.getPhase(nextPhase.getGotoBlock()); + } + String nextPhaseName = nextPhase == null || nextPhase.getPhaseName() == null ? "" : nextPhase.getPhaseName(); + if (!currPhaseName.equalsIgnoreCase(nextPhaseName)) { + is.setLastPhaseChangeTime(System.currentTimeMillis()); + } + } + + /** + * Initializes the block queue for a phase with upcoming blocks. + * + * @param is - oneblock island data + * @param phase - current phase + * @param isCurrPhaseNew - whether this is a new phase + */ + private void initializeQueue(OneBlockIslands is, OneBlockPhase phase, boolean isCurrPhaseNew) { + if (is.getQueue().isEmpty() || isCurrPhaseNew) { + for (int j = 0; j < MAX_LOOK_AHEAD; j++) { + is.add(phase.getNextBlock(addon, j)); + } + } + } + + /** + * Updates holograms and plays warning sounds if configured. + * + * @param i - island instance + * @param is - oneblock island data + * @param phase - current phase + * @param block - block being processed + */ + private void processHologramAndWarnings(Island i, OneBlockIslands is, OneBlockPhase phase, Block block) { + addon.getHoloListener().process(i, is, phase); + if (addon.getSettings().getMobWarning() > 0) { + warningSounder.play(is, block); + } + } + + /** + * Processes the next block in the sequence, handling entities and block changes. + * + * @param e - event being processed + * @param i - island instance + * @param player - player involved + * @param block - block being processed + * @param is - oneblock island data + * @param phaseResult - result from phase processing + */ + private void processNextBlock(Cancellable e, Island i, Player player, Block block, OneBlockIslands is, ProcessPhaseResult phaseResult) { + OneBlockObject nextBlock = (phaseResult.isCurrPhaseNew && phaseResult.phase.getFirstBlock() != null) + ? phaseResult.phase.getFirstBlock() + : is.pollAndAdd(phaseResult.phase.getNextBlock(addon, phaseResult.blockNumber)); + + if (nextBlock.isEntity()) { + handleEntitySpawn(e, i, player, block, nextBlock); + return; + } + + is.incrementBlockNumber(); + handleBlockBreak(e, i, player, block, nextBlock); + } + + /** + * Handles entity spawning for entity-type blocks. + * + * @param e - event being processed + * @param i - island instance + * @param player - player involved + * @param block - block where entity will spawn + * @param nextBlock - next block object containing entity info + */ + private void handleEntitySpawn(Cancellable e, Island i, Player player, Block block, OneBlockObject nextBlock) { + if (!(e instanceof EntitySpawnEvent)) { + e.setCancelled(true); + } + spawnEntity(nextBlock, block); + Bukkit.getPluginManager().callEvent(new MagicBlockEntityEvent(i, + player == null ? null : player.getUniqueId(), + block, nextBlock.getEntityType())); + } + + /** + * Handles different types of block breaking events. + * + * @param e - event being processed + * @param i - island instance + * @param player - player involved + * @param block - block being broken + * @param nextBlock - next block to spawn + */ + private void handleBlockBreak(Cancellable e, Island i, Player player, Block block, OneBlockObject nextBlock) { + if (e instanceof BlockBreakEvent) { + breakBlock(player, block, nextBlock, i); + } else if (e instanceof PlayerBucketFillEvent) { + handleBucketFill(player, i, block, nextBlock); + } else if (e instanceof EntitySpawnEvent || e instanceof EntityInteractEvent) { + handleEntityBreak(i, block, nextBlock, e instanceof EntityInteractEvent); + } + } + + /** + * Handles bucket fill events including block spawning and event firing. + * + * @param player - player filling bucket + * @param i - island instance + * @param block - block being processed + * @param nextBlock - next block to spawn + */ + private void handleBucketFill(Player player, Island i, Block block, OneBlockObject nextBlock) { + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); + ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); + Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, player.getUniqueId(), tool, block, nextBlock.getMaterial())); + } + + /** + * Handles entity-related block breaking including minion interactions. + * + * @param i - island instance + * @param block - block being broken + * @param nextBlock - next block to spawn + * @param isMinion - whether the breaker is a minion + */ + private void handleEntityBreak(Island i, Block block, OneBlockObject nextBlock, boolean isMinion) { + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> spawnBlock(nextBlock, block)); + if (isMinion) { + Bukkit.getPluginManager().callEvent(new MagicBlockEvent(i, null, null, block, nextBlock.getMaterial())); + } + } + + /** + * Handles goto block mechanics, updating block numbers and lifetime. + * + * @param is - oneblock island data + * @param gotoBlock - target block number + * @return OneBlockPhase for the target block + */ + private OneBlockPhase handleGoto(OneBlockIslands is, int gotoBlock) { + // Store lifetime + is.setLifetime(is.getLifetime() + gotoBlock); + // Set current block + is.setBlockNumber(gotoBlock); + return oneBlocksManager.getPhase(gotoBlock); + } + + private void setBiome(@NonNull Block block, @Nullable Biome biome) { + if (biome == null) { + return; + } + for (int x = -4; x <= 4; x++) { + for (int z = -4; z <= 4; z++) { + for (int y = -4; y <= 4; y++) { + block.getWorld().setBiome(block.getX() + x, block.getY() + y, block.getZ() + z, biome); + } + } + } + } + + /** + * This method is called when block is removed, and next must be spawned. It + * also teleports player above the magic block, to avoid falling in void. + * + * @param player Player who breaks the block. + * @param block Block that was broken. + * @param nextBlock Next Block that will be summoned. + * @param island Island where player is located. + */ + private void breakBlock(@Nullable Player player, Block block, @NonNull OneBlockObject nextBlock, + @NonNull Island island) { + ItemStack tool = Objects.requireNonNull(player).getInventory().getItemInMainHand(); + + // Break normally and lift the player up so they don't fall + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> this.spawnBlock(nextBlock, block)); + + if (player.getLocation().getBlock().equals(block)) { + double delta = 1 - (player.getLocation().getY() - block.getY()); + player.teleport(player.getLocation().add(new Vector(0, delta, 0))); + player.setVelocity(new Vector(0, 0, 0)); + } else if (player.getLocation().getBlock().equals(block.getRelative(BlockFace.UP))) { + player.teleport(player.getLocation()); + player.setVelocity(new Vector(0, 0, 0)); + } + + // Fire event + Bukkit.getPluginManager() + .callEvent(new MagicBlockEvent(island, player.getUniqueId(), tool, block, nextBlock.getMaterial())); + } + + private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block) { + if (nextBlock.isCustomBlock()) { + nextBlock.getCustomBlock().execute(addon, block); + } else { + @NonNull + Material type = nextBlock.getMaterial(); + // Place new block with no physics + block.setType(type, false); + // Fill the chest + if (type.equals(Material.CHEST) && nextBlock.getChest() != null) { + fillChest(nextBlock, block); + return; + } else if (Tag.LEAVES.isTagged(type)) { + Leaves leaves = (Leaves) block.getState().getBlockData(); + leaves.setPersistent(true); + block.setBlockData(leaves); + } + } + + } + + @SuppressWarnings("deprecation") + @EventHandler + public void onPlayerInteract(PlayerInteractEvent e) { + if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; + if (e.getHand() != EquipmentSlot.HAND) return; + if (e.getClickedBlock() == null) return; + if (e.getClickedBlock().getType() != Material.SUSPICIOUS_GRAVEL && + e.getClickedBlock().getType() != Material.SUSPICIOUS_SAND) return; + if (e.getPlayer().getInventory().getItemInMainHand().getType() != Material.BRUSH) return; + BentoBox.getInstance().logDebug("Brushing " + e.getClickedBlock()); + if (e.getClickedBlock() != null && e.getClickedBlock().getBlockData() instanceof Brushable bb) { + BentoBox.getInstance().logDebug("item is brushable " + bb.getDusted()); + } + } + + private static Material getRandomLoot(Random random) { + double roll = random.nextDouble(); + double cumulative = 0.0; + + for (Map.Entry entry : LOOT.entrySet()) { + cumulative += entry.getValue(); + if (roll <= cumulative) { + return entry.getKey(); + } + } + + // Fallback in case of rounding errors + List materials = new ArrayList<>(LOOT.keySet()); + return materials.get(random.nextInt(materials.size())); + } + + private void spawnEntity(@NonNull OneBlockObject nextBlock, @NonNull Block block) { + if (block.isEmpty()) + block.setType(Material.STONE); + Location spawnLoc = block.getLocation().add(new Vector(0.5D, 1D, 0.5D)); + Entity entity = block.getWorld().spawnEntity(spawnLoc, nextBlock.getEntityType()); + // Make space for entity - this will blot out blocks + if (addon.getSettings().isClearBlocks()) { + new MakeSpace(addon).makeSpace(entity, spawnLoc); + } + block.getWorld().playSound(block.getLocation(), Sound.ENTITY_ENDERMAN_TELEPORT, 1F, 2F); + } + + private void fillChest(@NonNull OneBlockObject nextBlock, @NonNull Block block) { + Chest chest = (Chest) block.getState(); + nextBlock.getChest().forEach(chest.getBlockInventory()::setItem); + Color color = Color.fromBGR(0, 255, 255); // yellow + switch (nextBlock.getRarity()) { + case EPIC: + color = Color.fromBGR(255, 0, 255); // magenta + break; + case RARE: + color = Color.fromBGR(255, 255, 255); // cyan + break; + case UNCOMMON: + // Yellow + break; + default: + // No sparkles for regular chests + return; + } + block.getWorld().spawnParticle(Particle.DUST, block.getLocation().add(new Vector(0.5, 1.0, 0.5)), 50, 0.5, + 0, 0.5, 1, new Particle.DustOptions(color, 1)); + } + + /** + * Get the one block island data + * + * @param i - island + * @return one block island + */ + @NonNull + public OneBlockIslands getIsland(@NonNull Island i) { + return cache.containsKey(i.getUniqueId()) ? cache.get(i.getUniqueId()) : loadIsland(i.getUniqueId()); + } + + /** + * Get all the OneBlockIslands from the Database + * + * @return list of oneblock islands + */ + public List getAllIslands() { + return handler.loadObjects(); + } + + @NonNull + private OneBlockIslands loadIsland(@NonNull String uniqueId) { + if (handler.objectExists(uniqueId)) { + OneBlockIslands island = handler.loadObject(uniqueId); + if (island != null) { + // Add to cache + cache.put(island.getUniqueId(), island); + return island; + } + } + return cache.computeIfAbsent(uniqueId, OneBlockIslands::new); + } + + /** + * @return the oneBlocksManager + */ + public OneBlocksManager getOneBlocksManager() { + return oneBlocksManager; + } + + /** + * Saves the island progress to the database async + * + * @param island - island + * @return CompletableFuture - true if saved or not in cache, false if save + * failed + */ + public CompletableFuture saveIsland(@NonNull Island island) { + if (cache.containsKey(island.getUniqueId())) { + return handler.saveObjectAsync(cache.get(island.getUniqueId())); + } + return CompletableFuture.completedFuture(true); + } }