diff --git a/pom.xml b/pom.xml index 46b6078..3d0fae9 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 2.0.9 - 1.19.4-R0.1-SNAPSHOT + 1.20.4-R0.1-SNAPSHOT 2.0.0-SNAPSHOT 1.12.0 @@ -410,6 +410,8 @@ **/*Names* + + org/bukkit/Material* diff --git a/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java b/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java new file mode 100644 index 0000000..c822410 --- /dev/null +++ b/src/main/java/world/bentobox/level/calculators/EquationEvaluator.java @@ -0,0 +1,121 @@ +package world.bentobox.level.calculators; + +import java.text.ParseException; + +/** + * @author tastybento + */ +public class EquationEvaluator { + + private static class Parser { + private final String input; + private int pos = -1; + private int currentChar; + + public Parser(String input) { + this.input = input; + moveToNextChar(); + } + + private void moveToNextChar() { + currentChar = (++pos < input.length()) ? input.charAt(pos) : -1; + } + + private boolean tryToEat(int charToEat) { + while (currentChar == ' ') + moveToNextChar(); + if (currentChar == charToEat) { + moveToNextChar(); + return true; + } + return false; + } + + public double evaluate() throws ParseException { + double result = parseExpression(); + if (pos < input.length()) { + throw new ParseException("Unexpected character: " + (char) currentChar, pos); + } + return result; + } + + private double parseExpression() throws ParseException { + double result = parseTerm(); + while (true) { + if (tryToEat('+')) + result += parseTerm(); + else if (tryToEat('-')) + result -= parseTerm(); + else + return result; + } + } + + private double parseFactor() throws ParseException { + if (tryToEat('+')) + return parseFactor(); // unary plus + if (tryToEat('-')) + return -parseFactor(); // unary minus + + double x; + int startPos = this.pos; + if (tryToEat('(')) { // parentheses + x = parseExpression(); + tryToEat(')'); + } else if ((currentChar >= '0' && currentChar <= '9') || currentChar == '.') { // numbers + while ((currentChar >= '0' && currentChar <= '9') || currentChar == '.') + moveToNextChar(); + x = Double.parseDouble(input.substring(startPos, this.pos)); + } else if (currentChar >= 'a' && currentChar <= 'z') { // functions + while (currentChar >= 'a' && currentChar <= 'z') + moveToNextChar(); + String func = input.substring(startPos, this.pos); + x = parseFactor(); + switch (func) { + case "sqrt": + x = Math.sqrt(x); + break; + case "sin": + x = Math.sin(Math.toRadians(x)); + break; + case "cos": + x = Math.cos(Math.toRadians(x)); + break; + case "tan": + x = Math.tan(Math.toRadians(x)); + break; + case "log": + x = Math.log(x); + break; + default: + throw new ParseException("Unknown function: " + func, startPos); + } + } else { + throw new ParseException("Unexpected: " + (char) currentChar, startPos); + } + + if (tryToEat('^')) + x = Math.pow(x, parseFactor()); // exponentiation + + return x; + } + + private double parseTerm() throws ParseException { + double x = parseFactor(); + for (;;) { + if (tryToEat('*')) + x *= parseFactor(); // multiplication + else if (tryToEat('/')) + x /= parseFactor(); // division + else + return x; + } + } + + } + + public static double eval(final String equation) throws ParseException { + return new Parser(equation).evaluate(); + } + +} diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 9c2a6cf..7fc1cca 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -1,6 +1,6 @@ package world.bentobox.level.calculators; -import java.io.IOException; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -16,9 +16,6 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; -import com.songoda.ultimatestacker.UltimateStacker; -import com.songoda.ultimatestacker.core.compatibility.CompatibleMaterial; -import com.songoda.ultimatestacker.stackable.block.BlockStack; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; @@ -27,7 +24,11 @@ import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.World.Environment; -import org.bukkit.block.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Container; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.ShulkerBox; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Slab; import org.bukkit.inventory.ItemStack; @@ -39,6 +40,9 @@ import com.bgsoftware.wildstacker.api.objects.StackedBarrel; import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multisets; +import com.songoda.ultimatestacker.UltimateStacker; +import com.songoda.ultimatestacker.core.compatibility.CompatibleMaterial; +import com.songoda.ultimatestacker.stackable.block.BlockStack; import dev.rosewood.rosestacker.api.RoseStackerAPI; import us.lynuxcraft.deadsilenceiv.advancedchests.AdvancedChestsAPI; @@ -54,121 +58,21 @@ import world.bentobox.level.calculators.Results.Result; public class IslandLevelCalculator { private static final String LINE_BREAK = "=================================="; public static final long MAX_AMOUNT = 10000000; - private static final List CHESTS = Arrays.asList(Material.CHEST, Material.CHEST_MINECART, Material.TRAPPED_CHEST, - Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX, Material.BLUE_SHULKER_BOX, Material.BROWN_SHULKER_BOX, - Material.CYAN_SHULKER_BOX, Material.GRAY_SHULKER_BOX, Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX, - Material.LIGHT_GRAY_SHULKER_BOX, Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX, Material.ORANGE_SHULKER_BOX, - Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX, Material.RED_SHULKER_BOX, Material.RED_SHULKER_BOX, - Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX, Material.COMPOSTER, Material.BARREL, Material.DISPENSER, - Material.DROPPER, Material.SMOKER, Material.BLAST_FURNACE); + private static final List CHESTS = Arrays.asList(Material.CHEST, Material.CHEST_MINECART, + Material.TRAPPED_CHEST, Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX, Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, Material.CYAN_SHULKER_BOX, Material.GRAY_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX, Material.LIGHT_GRAY_SHULKER_BOX, + Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX, Material.ORANGE_SHULKER_BOX, + Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX, Material.RED_SHULKER_BOX, Material.RED_SHULKER_BOX, + Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX, Material.COMPOSTER, Material.BARREL, + Material.DISPENSER, Material.DROPPER, Material.SMOKER, Material.BLAST_FURNACE); private static final int CHUNKS_TO_SCAN = 100; - - /** - * Method to evaluate a mathematical equation - * @param str - equation to evaluate - * @return value of equation - */ - private static double eval(final String str) throws IOException { - return new Object() { - int pos = -1; - int ch; - - boolean eat(int charToEat) { - while (ch == ' ') nextChar(); - if (ch == charToEat) { - nextChar(); - return true; - } - return false; - } - - void nextChar() { - ch = (++pos < str.length()) ? str.charAt(pos) : -1; - } - - double parse() throws IOException { - nextChar(); - double x = parseExpression(); - if (pos < str.length()) throw new IOException("Unexpected: " + (char)ch); - return x; - } - - // Grammar: - // expression = term | expression `+` term | expression `-` term - // term = factor | term `*` factor | term `/` factor - // factor = `+` factor | `-` factor | `(` expression `)` - // | number | functionName factor | factor `^` factor - - double parseExpression() throws IOException { - double x = parseTerm(); - for (;;) { - if (eat('+')) x += parseTerm(); // addition - else if (eat('-')) x -= parseTerm(); // subtraction - else return x; - } - } - - double parseFactor() throws IOException { - if (eat('+')) return parseFactor(); // unary plus - if (eat('-')) return -parseFactor(); // unary minus - - double x; - int startPos = this.pos; - if (eat('(')) { // parentheses - x = parseExpression(); - eat(')'); - } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers - while ((ch >= '0' && ch <= '9') || ch == '.') nextChar(); - x = Double.parseDouble(str.substring(startPos, this.pos)); - } else if (ch >= 'a' && ch <= 'z') { // functions - while (ch >= 'a' && ch <= 'z') nextChar(); - String func = str.substring(startPos, this.pos); - x = parseFactor(); - switch (func) { - case "sqrt": - x = Math.sqrt(x); - break; - case "sin": - x = Math.sin(Math.toRadians(x)); - break; - case "cos": - x = Math.cos(Math.toRadians(x)); - break; - case "tan": - x = Math.tan(Math.toRadians(x)); - break; - case "log": - x = Math.log(x); - break; - default: - throw new IOException("Unknown function: " + func); - } - } else { - throw new IOException("Unexpected: " + (char)ch); - } - - if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation - - return x; - } - - double parseTerm() throws IOException { - double x = parseFactor(); - for (;;) { - if (eat('*')) x *= parseFactor(); // multiplication - else if (eat('/')) x /= parseFactor(); // division - else return x; - } - } - }.parse(); - } private final Level addon; private final Queue> chunksToCheck; private final Island island; private final Map limitCount; private final CompletableFuture r; - private final Results results; private long duration; private final boolean zeroIsland; @@ -178,569 +82,600 @@ public class IslandLevelCalculator { private final Set chestBlocks = new HashSet<>(); private BukkitTask finishTask; - /** * Constructor to get the level for an island - * @param addon - Level addon - * @param island - the island to scan - * @param r - completable result that will be completed when the calculation is complete + * + * @param addon - Level addon + * @param island - the island to scan + * @param r - completable result that will be completed when the + * calculation is complete * @param zeroIsland - true if the calculation is due to an island zeroing */ public IslandLevelCalculator(Level addon, Island island, CompletableFuture r, boolean zeroIsland) { - this.addon = addon; - this.island = island; - this.r = r; - this.zeroIsland = zeroIsland; - results = new Results(); - duration = System.currentTimeMillis(); - chunksToCheck = getChunksToScan(island); - this.limitCount = new EnumMap<>(addon.getBlockConfig().getBlockLimits()); - // Get the initial island level - results.initialLevel.set(addon.getInitialIslandLevel(island)); - // Set up the worlds - worlds.put(Environment.NORMAL, Util.getWorld(island.getWorld())); - // Nether - if (addon.getSettings().isNether()) { - World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); - if (nether != null) { - worlds.put(Environment.NETHER, nether); - } - } - // End - if (addon.getSettings().isEnd()) { - World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); - if (end != null) { - worlds.put(Environment.THE_END, end); - } - } - // Sea Height - seaHeight = addon.getPlugin().getIWM().getSeaHeight(island.getWorld()); + this.addon = addon; + this.island = island; + this.r = r; + this.zeroIsland = zeroIsland; + results = new Results(); + duration = System.currentTimeMillis(); + chunksToCheck = getChunksToScan(island); + this.limitCount = new EnumMap<>(addon.getBlockConfig().getBlockLimits()); + // Get the initial island level + results.initialLevel.set(addon.getInitialIslandLevel(island)); + // Set up the worlds + worlds.put(Environment.NORMAL, Util.getWorld(island.getWorld())); + // Nether + if (addon.getSettings().isNether()) { + World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); + if (nether != null) { + worlds.put(Environment.NETHER, nether); + } + } + // End + if (addon.getSettings().isEnd()) { + World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); + if (end != null) { + worlds.put(Environment.THE_END, end); + } + } + // Sea Height + seaHeight = addon.getPlugin().getIWM().getSeaHeight(island.getWorld()); } /** * Calculate the level based on the raw points + * * @param blockAndDeathPoints - raw points counted on island * @return level of island */ private long calculateLevel(long blockAndDeathPoints) { - String calcString = addon.getSettings().getLevelCalc(); - String withValues = calcString.replace("blocks", String.valueOf(blockAndDeathPoints)).replace("level_cost", String.valueOf(this.addon.getSettings().getLevelCost())); - long evalWithValues; - try { - evalWithValues = (long)eval(withValues); - return evalWithValues - (addon.getSettings().isZeroNewIslandLevels() ? results.initialLevel.get() : 0); + String calcString = addon.getSettings().getLevelCalc(); + String withValues = calcString.replace("blocks", String.valueOf(blockAndDeathPoints)).replace("level_cost", + String.valueOf(this.addon.getSettings().getLevelCost())); + long evalWithValues; + try { + evalWithValues = (long) EquationEvaluator.eval(withValues); + return evalWithValues - (addon.getSettings().isZeroNewIslandLevels() ? results.initialLevel.get() : 0); - } catch (IOException e) { - addon.getPlugin().logStacktrace(e); - return 0L; - } + } catch (ParseException e) { + addon.getPlugin().logStacktrace(e); + return 0L; + } } /** - * Adds value to the results based on the material and whether the block is below sea level or not - * @param mat - material of the block + * Adds value to the results based on the material and whether the block is + * below sea level or not + * + * @param mat - material of the block * @param belowSeaLevel - true if below sea level */ private void checkBlock(Material mat, boolean belowSeaLevel) { - int count = limitCount(mat); - if (belowSeaLevel) { - results.underWaterBlockCount.addAndGet(count); - results.uwCount.add(mat); - } else { - results.rawBlockCount.addAndGet(count); - results.mdCount.add(mat); - } + int count = limitCount(mat); + if (belowSeaLevel) { + results.underWaterBlockCount.addAndGet(count); + results.uwCount.add(mat); + } else { + results.rawBlockCount.addAndGet(count); + results.mdCount.add(mat); + } } /** * Get a set of all the chunks in island + * * @param island - island * @return - set of pairs of x,z coordinates to check */ private Queue> getChunksToScan(Island island) { - Queue> chunkQueue = new ConcurrentLinkedQueue<>(); - for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + 16); x += 16) { - for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + 16); z += 16) { - chunkQueue.add(new Pair<>(x >> 4, z >> 4)); - } - } - return chunkQueue; + Queue> chunkQueue = new ConcurrentLinkedQueue<>(); + for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionRange() * 2 + + 16); x += 16) { + for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionRange() * 2 + + 16); z += 16) { + chunkQueue.add(new Pair<>(x >> 4, z >> 4)); + } + } + return chunkQueue; } - /** * @return the island */ public Island getIsland() { - return island; + return island; } /** * Get the completable result for this calculation + * * @return the r */ public CompletableFuture getR() { - return r; + return r; } /** * Get the full analysis report + * * @return a list of lines */ private List getReport() { - List reportLines = new ArrayList<>(); - // provide counts - reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + " at " + Util.xyz(island.getCenter().toVector())); - reportLines.add("Island owner UUID = " + island.getOwner()); - reportLines.add("Total block value count = " + String.format("%,d",results.rawBlockCount.get())); - reportLines.add("Formula to calculate island level: " + addon.getSettings().getLevelCalc()); - reportLines.add("Level cost = " + addon.getSettings().getLevelCost()); - reportLines.add("Deaths handicap = " + results.deathHandicap.get()); - if (addon.getSettings().isZeroNewIslandLevels()) { - reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island))); - } - reportLines.add("Previous level = " + addon.getManager().getIslandLevel(island.getWorld(), island.getOwner())); - reportLines.add("New level = " + results.getLevel()); - reportLines.add(LINE_BREAK); - int total = 0; - if (!results.uwCount.isEmpty()) { - reportLines.add("Underwater block count (Multiplier = x" + addon.getSettings().getUnderWaterMultiplier() + ") value"); - reportLines.add("Total number of underwater blocks = " + String.format("%,d",results.uwCount.size())); - reportLines.addAll(sortedReport(total, results.uwCount)); - } - reportLines.add("Regular block count"); - reportLines.add("Total number of blocks = " + String.format("%,d",results.mdCount.size())); - reportLines.addAll(sortedReport(total, results.mdCount)); + List reportLines = new ArrayList<>(); + // provide counts + reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + + " at " + Util.xyz(island.getCenter().toVector())); + reportLines.add("Island owner UUID = " + island.getOwner()); + reportLines.add("Total block value count = " + String.format("%,d", results.rawBlockCount.get())); + reportLines.add("Formula to calculate island level: " + addon.getSettings().getLevelCalc()); + reportLines.add("Level cost = " + addon.getSettings().getLevelCost()); + reportLines.add("Deaths handicap = " + results.deathHandicap.get()); + if (addon.getSettings().isZeroNewIslandLevels()) { + reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island))); + } + reportLines.add("Previous level = " + addon.getManager().getIslandLevel(island.getWorld(), island.getOwner())); + reportLines.add("New level = " + results.getLevel()); + reportLines.add(LINE_BREAK); + int total = 0; + if (!results.uwCount.isEmpty()) { + reportLines.add("Underwater block count (Multiplier = x" + addon.getSettings().getUnderWaterMultiplier() + + ") value"); + reportLines.add("Total number of underwater blocks = " + String.format("%,d", results.uwCount.size())); + reportLines.addAll(sortedReport(total, results.uwCount)); + } + reportLines.add("Regular block count"); + reportLines.add("Total number of blocks = " + String.format("%,d", results.mdCount.size())); + reportLines.addAll(sortedReport(total, results.mdCount)); - reportLines.add("Blocks not counted because they exceeded limits: " + String.format("%,d",results.ofCount.size())); - Iterable> entriesSortedByCount = results.ofCount.entrySet(); - Iterator> it = entriesSortedByCount.iterator(); - while (it.hasNext()) { - Entry type = it.next(); - Integer limit = addon.getBlockConfig().getBlockLimits().get(type.getElement()); - String explain = ")"; - if (limit == null) { - Material generic = type.getElement(); - limit = addon.getBlockConfig().getBlockLimits().get(generic); - explain = " - All types)"; - } - reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks (max " + limit + explain); - } - reportLines.add(LINE_BREAK); - reportLines.add("Blocks on island that are not in config.yml"); - reportLines.add("Total number = " + String.format("%,d",results.ncCount.size())); - entriesSortedByCount = results.ncCount.entrySet(); - it = entriesSortedByCount.iterator(); - while (it.hasNext()) { - Entry type = it.next(); - reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks"); - } - reportLines.add(LINE_BREAK); + reportLines.add( + "Blocks not counted because they exceeded limits: " + String.format("%,d", results.ofCount.size())); + Iterable> entriesSortedByCount = results.ofCount.entrySet(); + Iterator> it = entriesSortedByCount.iterator(); + while (it.hasNext()) { + Entry type = it.next(); + Integer limit = addon.getBlockConfig().getBlockLimits().get(type.getElement()); + String explain = ")"; + if (limit == null) { + Material generic = type.getElement(); + limit = addon.getBlockConfig().getBlockLimits().get(generic); + explain = " - All types)"; + } + reportLines.add(type.getElement().toString() + ": " + String.format("%,d", type.getCount()) + + " blocks (max " + limit + explain); + } + reportLines.add(LINE_BREAK); + reportLines.add("Blocks on island that are not in config.yml"); + reportLines.add("Total number = " + String.format("%,d", results.ncCount.size())); + entriesSortedByCount = results.ncCount.entrySet(); + it = entriesSortedByCount.iterator(); + while (it.hasNext()) { + Entry type = it.next(); + reportLines.add(type.getElement().toString() + ": " + String.format("%,d", type.getCount()) + " blocks"); + } + reportLines.add(LINE_BREAK); - return reportLines; + return reportLines; } /** * @return the results */ public Results getResults() { - return results; + return results; } + /** - * Get value of a material - * World blocks trump regular block values + * Get value of a material World blocks trump regular block values + * * @param md - Material to check * @return value of a material */ private int getValue(Material md) { - Integer value = addon.getBlockConfig().getValue(island.getWorld(), md); - if (value == null) { - // Not in config - results.ncCount.add(md); - return 0; - } - return value; + Integer value = addon.getBlockConfig().getValue(island.getWorld(), md); + if (value == null) { + // Not in config + results.ncCount.add(md); + return 0; + } + return value; } /** * Get a chunk async - * @param env - the environment + * + * @param env - the environment * @param pairList - chunk coordinate - * @return a future chunk or future null if there is no chunk to load, e.g., there is no island nether + * @return a future chunk or future null if there is no chunk to load, e.g., + * there is no island nether */ private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { - if (worlds.containsKey(env)) { - CompletableFuture> r2 = new CompletableFuture<>(); - List chunkList = new ArrayList<>(); - World world = worlds.get(env); - // Get the chunk, and then coincidentally check the RoseStacker - loadChunks(r2, world, pairList, chunkList); - return r2; - } - return CompletableFuture.completedFuture(Collections.emptyList()); + if (worlds.containsKey(env)) { + CompletableFuture> r2 = new CompletableFuture<>(); + List chunkList = new ArrayList<>(); + World world = worlds.get(env); + // Get the chunk, and then coincidentally check the RoseStacker + loadChunks(r2, world, pairList, chunkList); + return r2; + } + return CompletableFuture.completedFuture(Collections.emptyList()); } private void loadChunks(CompletableFuture> r2, World world, Queue> pairList, - List chunkList) { - if (pairList.isEmpty()) { - r2.complete(chunkList); - return; - } - Pair p = pairList.poll(); - Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { - if (chunk != null) { - chunkList.add(chunk); - roseStackerCheck(chunk); - } - loadChunks(r2, world, pairList, chunkList); // Iteration - }); + List chunkList) { + if (pairList.isEmpty()) { + r2.complete(chunkList); + return; + } + Pair p = pairList.poll(); + Util.getChunkAtAsync(world, p.x, p.z, world.getEnvironment().equals(Environment.NETHER)).thenAccept(chunk -> { + if (chunk != null) { + chunkList.add(chunk); + roseStackerCheck(chunk); + } + loadChunks(r2, world, pairList, chunkList); // Iteration + }); } private void roseStackerCheck(Chunk chunk) { - if (addon.isRoseStackersEnabled()) { - RoseStackerAPI.getInstance().getStackedBlocks(Collections.singletonList(chunk)).forEach(e -> { - // Blocks below sea level can be scored differently - boolean belowSeaLevel = seaHeight > 0 && e.getLocation().getY() <= seaHeight; - // Check block once because the base block will be counted in the chunk snapshot - for (int _x = 0; _x < e.getStackSize() - 1; _x++) { - checkBlock(e.getBlock().getType(), belowSeaLevel); - } - }); - } + if (addon.isRoseStackersEnabled()) { + RoseStackerAPI.getInstance().getStackedBlocks(Collections.singletonList(chunk)).forEach(e -> { + // Blocks below sea level can be scored differently + boolean belowSeaLevel = seaHeight > 0 && e.getLocation().getY() <= seaHeight; + // Check block once because the base block will be counted in the chunk snapshot + for (int _x = 0; _x < e.getStackSize() - 1; _x++) { + checkBlock(e.getBlock().getType(), belowSeaLevel); + } + }); + } } /** - * Checks if a block has been limited or not and whether a block has any value or not + * Checks if a block has been limited or not and whether a block has any value + * or not + * * @param md Material * @return value of the block if can be counted */ private int limitCount(Material md) { - if (limitCount.containsKey(md)) { - int count = limitCount.get(md); - if (count > 0) { - limitCount.put(md, --count); - return getValue(md); - } else { - results.ofCount.add(md); - return 0; - } - } - return getValue(md); + if (limitCount.containsKey(md)) { + int count = limitCount.get(md); + if (count > 0) { + limitCount.put(md, --count); + return getValue(md); + } else { + results.ofCount.add(md); + return 0; + } + } + return getValue(md); } - /** * Scan all containers in a chunk and count their blocks + * * @param chunk - the chunk to scan */ private void scanChests(Chunk chunk) { - // Count blocks in chests - for (BlockState bs : chunk.getTileEntities()) { - if (bs instanceof Container container) { - if (addon.isAdvChestEnabled()) { - AdvancedChest aChest = AdvancedChestsAPI.getChestManager().getAdvancedChest(bs.getLocation()); - if (aChest != null && aChest.getChestType().getName().equals("NORMAL")) { - aChest.getPages().stream().map(ChestPage::getItems).forEach(c -> { - for (Object i : c) { - countItemStack((ItemStack)i); - } - }); - continue; - } - } - // Regular chest - container.getSnapshotInventory().forEach(this::countItemStack); - } - } + // Count blocks in chests + for (BlockState bs : chunk.getTileEntities()) { + if (bs instanceof Container container) { + if (addon.isAdvChestEnabled()) { + AdvancedChest aChest = AdvancedChestsAPI.getChestManager().getAdvancedChest(bs.getLocation()); + if (aChest != null && aChest.getChestType().getName().equals("NORMAL")) { + aChest.getPages().stream().map(ChestPage::getItems).forEach(c -> { + for (Object i : c) { + countItemStack((ItemStack) i); + } + }); + continue; + } + } + // Regular chest + container.getSnapshotInventory().forEach(this::countItemStack); + } + } } private void countItemStack(ItemStack i) { - if (i == null || !i.getType().isBlock()) return; + if (i == null || !i.getType().isBlock()) + return; - for (int c = 0; c < i.getAmount(); c++) { - if (addon.getSettings().isIncludeShulkersInChest() - && i.getItemMeta() instanceof BlockStateMeta blockStateMeta - && blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) { - shulkerBox.getSnapshotInventory().forEach(this::countItemStack); - } + for (int c = 0; c < i.getAmount(); c++) { + if (addon.getSettings().isIncludeShulkersInChest() + && i.getItemMeta() instanceof BlockStateMeta blockStateMeta + && blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) { + shulkerBox.getSnapshotInventory().forEach(this::countItemStack); + } - checkBlock(i.getType(), false); - } + checkBlock(i.getType(), false); + } } /** - * Scan the chunk chests and count the blocks. Note that the chunks are a list of all the island chunks - * in a particular world, so the memory usage is high, but I think most servers can handle it. + * Scan the chunk chests and count the blocks. Note that the chunks are a list + * of all the island chunks in a particular world, so the memory usage is high, + * but I think most servers can handle it. + * * @param chunks - a list of chunks to scan - * @return future that completes when the scan is done and supplies a boolean that will be true if the scan was successful, false if not + * @return future that completes when the scan is done and supplies a boolean + * that will be true if the scan was successful, false if not */ private CompletableFuture scanChunk(List chunks) { - // If the chunk hasn't been generated, return - if (chunks == null || chunks.isEmpty()) { - return CompletableFuture.completedFuture(false); - } - // Count blocks in chunk - CompletableFuture result = new CompletableFuture<>(); - /* - * At this point, we need to grab a snapshot of each chunk and then scan it async. - * At the end, we make the CompletableFuture true to show it is done. - * I'm not sure how much lag this will cause, but as all the chunks are loaded, maybe not that much. - */ - List preLoad = chunks.stream().map(c -> new ChunkPair(c.getWorld(), c, c.getChunkSnapshot())).toList(); - Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { - preLoad.forEach(this::scanAsync); - // Once they are all done, return to the main thread. - Bukkit.getScheduler().runTask(addon.getPlugin(),() -> result.complete(true)); - }); - return result; + // If the chunk hasn't been generated, return + if (chunks == null || chunks.isEmpty()) { + return CompletableFuture.completedFuture(false); + } + // Count blocks in chunk + CompletableFuture result = new CompletableFuture<>(); + /* + * At this point, we need to grab a snapshot of each chunk and then scan it + * async. At the end, we make the CompletableFuture true to show it is done. I'm + * not sure how much lag this will cause, but as all the chunks are loaded, + * maybe not that much. + */ + List preLoad = chunks.stream().map(c -> new ChunkPair(c.getWorld(), c, c.getChunkSnapshot())) + .toList(); + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { + preLoad.forEach(this::scanAsync); + // Once they are all done, return to the main thread. + Bukkit.getScheduler().runTask(addon.getPlugin(), () -> result.complete(true)); + }); + return result; } - record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) {} + record ChunkPair(World world, Chunk chunk, ChunkSnapshot chunkSnapshot) { + } /** * Count the blocks on the island + * * @param cp chunk to scan */ private void scanAsync(ChunkPair cp) { - for (int x = 0; x< 16; x++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (cp.chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || cp.chunkSnapshot.getX() * 16 + x >= island.getMinProtectedX() + island.getProtectionRange() * 2) { - continue; - } - for (int z = 0; z < 16; z++) { - // Check if the block coordinate is inside the protection zone and if not, don't count it - if (cp.chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || cp.chunkSnapshot.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { - continue; - } - // Only count to the highest block in the world for some optimization - for (int y = cp.world.getMinHeight(); y < cp.world.getMaxHeight(); y++) { - BlockData blockData = cp.chunkSnapshot.getBlockData(x, y, z); - boolean belowSeaLevel = seaHeight > 0 && y <= seaHeight; - // Slabs can be doubled, so check them twice - if (Tag.SLABS.isTagged(blockData.getMaterial())) { - Slab slab = (Slab)blockData; - if (slab.getType().equals(Slab.Type.DOUBLE)) { - checkBlock(blockData.getMaterial(), belowSeaLevel); - } - } - // Hook for Wild Stackers (Blocks and Spawners Only) - this has to use the real chunk - if (addon.isStackersEnabled() && (blockData.getMaterial().equals(Material.CAULDRON) || blockData.getMaterial().equals(Material.SPAWNER))) { - stackedBlocks.add(new Location(cp.world, (double)x + cp.chunkSnapshot.getX() * 16, y, (double)z + cp.chunkSnapshot.getZ() * 16)); - } + for (int x = 0; x < 16; x++) { + // Check if the block coordinate is inside the protection zone and if not, don't + // count it + if (cp.chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || cp.chunkSnapshot.getX() * 16 + + x >= island.getMinProtectedX() + island.getProtectionRange() * 2) { + continue; + } + for (int z = 0; z < 16; z++) { + // Check if the block coordinate is inside the protection zone and if not, don't + // count it + if (cp.chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || cp.chunkSnapshot.getZ() * 16 + + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { + continue; + } + // Only count to the highest block in the world for some optimization + for (int y = cp.world.getMinHeight(); y < cp.world.getMaxHeight(); y++) { + BlockData blockData = cp.chunkSnapshot.getBlockData(x, y, z); + boolean belowSeaLevel = seaHeight > 0 && y <= seaHeight; + // Slabs can be doubled, so check them twice + if (Tag.SLABS.isTagged(blockData.getMaterial())) { + Slab slab = (Slab) blockData; + if (slab.getType().equals(Slab.Type.DOUBLE)) { + checkBlock(blockData.getMaterial(), belowSeaLevel); + } + } + // Hook for Wild Stackers (Blocks and Spawners Only) - this has to use the real + // chunk + if (addon.isStackersEnabled() && (blockData.getMaterial().equals(Material.CAULDRON) + || blockData.getMaterial().equals(Material.SPAWNER))) { + stackedBlocks.add(new Location(cp.world, (double) x + cp.chunkSnapshot.getX() * 16, y, + (double) z + cp.chunkSnapshot.getZ() * 16)); + } - Block block = cp.chunk.getBlock(x, y, z); + Block block = cp.chunk.getBlock(x, y, z); - if (addon.isUltimateStackerEnabled()) { - if (!blockData.getMaterial().equals(Material.AIR)) { - BlockStack stack = UltimateStacker.getInstance().getBlockStackManager().getBlock(block, CompatibleMaterial.getMaterial(block)); - if (stack != null) { - int value = limitCount(blockData.getMaterial()); - if (belowSeaLevel) { - results.underWaterBlockCount.addAndGet((long) stack.getAmount() * value); - results.uwCount.add(blockData.getMaterial()); - } else { - results.rawBlockCount.addAndGet((long) stack.getAmount() * value); - results.mdCount.add(blockData.getMaterial()); - } - } - } - } + if (addon.isUltimateStackerEnabled()) { + if (!blockData.getMaterial().equals(Material.AIR)) { + BlockStack stack = UltimateStacker.getInstance().getBlockStackManager().getBlock(block, + CompatibleMaterial.getMaterial(block)); + if (stack != null) { + int value = limitCount(blockData.getMaterial()); + if (belowSeaLevel) { + results.underWaterBlockCount.addAndGet((long) stack.getAmount() * value); + results.uwCount.add(blockData.getMaterial()); + } else { + results.rawBlockCount.addAndGet((long) stack.getAmount() * value); + results.mdCount.add(blockData.getMaterial()); + } + } + } + } - // Scan chests - if (addon.getSettings().isIncludeChests() && CHESTS.contains(blockData.getMaterial())) { - chestBlocks.add(cp.chunk); - } - // Add the value of the block's material - checkBlock(blockData.getMaterial(), belowSeaLevel); - } - } - } + // Scan chests + if (addon.getSettings().isIncludeChests() && CHESTS.contains(blockData.getMaterial())) { + chestBlocks.add(cp.chunk); + } + // Add the value of the block's material + checkBlock(blockData.getMaterial(), belowSeaLevel); + } + } + } } /** * Scan the next chunk on the island - * @return completable boolean future that will be true if more chunks are left to be scanned, and false if not + * + * @return completable boolean future that will be true if more chunks are left + * to be scanned, and false if not */ public CompletableFuture scanNextChunk() { - if (chunksToCheck.isEmpty()) { - addon.logError("Unexpected: no chunks to scan!"); - // This should not be needed, but just in case - return CompletableFuture.completedFuture(false); - } - // Retrieve and remove from the queue - Queue> pairList = new ConcurrentLinkedQueue<>(); - int i = 0; - while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { - pairList.add(chunksToCheck.poll()); - } - Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); - Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); - // Set up the result - CompletableFuture result = new CompletableFuture<>(); - // Get chunks and scan - // Get chunks and scan - getWorldChunk(Environment.THE_END, endPairList).thenAccept(endChunks -> - scanChunk(endChunks).thenAccept(b -> - getWorldChunk(Environment.NETHER, netherPairList).thenAccept(netherChunks -> - scanChunk(netherChunks).thenAccept(b2 -> - getWorldChunk(Environment.NORMAL, pairList).thenAccept(normalChunks -> - scanChunk(normalChunks).thenAccept(b3 -> - // Complete the result now that all chunks have been scanned - result.complete(!chunksToCheck.isEmpty())))) - ) - ) - ); + if (chunksToCheck.isEmpty()) { + addon.logError("Unexpected: no chunks to scan!"); + // This should not be needed, but just in case + return CompletableFuture.completedFuture(false); + } + // Retrieve and remove from the queue + Queue> pairList = new ConcurrentLinkedQueue<>(); + int i = 0; + while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) { + pairList.add(chunksToCheck.poll()); + } + Queue> endPairList = new ConcurrentLinkedQueue<>(pairList); + Queue> netherPairList = new ConcurrentLinkedQueue<>(pairList); + // Set up the result + CompletableFuture result = new CompletableFuture<>(); + // Get chunks and scan + // Get chunks and scan + getWorldChunk(Environment.THE_END, endPairList).thenAccept( + endChunks -> scanChunk(endChunks).thenAccept(b -> getWorldChunk(Environment.NETHER, netherPairList) + .thenAccept(netherChunks -> scanChunk(netherChunks) + .thenAccept(b2 -> getWorldChunk(Environment.NORMAL, pairList) + .thenAccept(normalChunks -> scanChunk(normalChunks).thenAccept(b3 -> + // Complete the result now that all chunks have been scanned + result.complete(!chunksToCheck.isEmpty()))))))); - return result; + return result; } private Collection sortedReport(int total, Multiset materialCount) { - Collection result = new ArrayList<>(); - Iterable> entriesSortedByCount = Multisets.copyHighestCountFirst(materialCount).entrySet(); - for (Entry en : entriesSortedByCount) { - Material type = en.getElement(); + Collection result = new ArrayList<>(); + Iterable> entriesSortedByCount = Multisets.copyHighestCountFirst(materialCount) + .entrySet(); + for (Entry en : entriesSortedByCount) { + Material type = en.getElement(); - int value = getValue(type); + int value = getValue(type); - result.add(type.toString() + ":" - + String.format("%,d", en.getCount()) + " blocks x " + value + " = " + (value * en.getCount())); - total += (value * en.getCount()); + result.add(type.toString() + ":" + String.format("%,d", en.getCount()) + " blocks x " + value + " = " + + (value * en.getCount())); + total += (value * en.getCount()); - } - result.add("Subtotal = " + total); - result.add(LINE_BREAK); - return result; + } + result.add("Subtotal = " + total); + result.add(LINE_BREAK); + return result; } - /** * Finalizes the calculations and makes the report */ public void tidyUp() { - // Finalize calculations - results.rawBlockCount.addAndGet((long)(results.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier())); + // Finalize calculations + results.rawBlockCount + .addAndGet((long) (results.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier())); - // Set the death penalty - if (this.addon.getSettings().isSumTeamDeaths()) - { - for (UUID uuid : this.island.getMemberSet()) - { - this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid)); - } - } - else - { - // At this point, it may be that the island has become unowned. - this.results.deathHandicap.set(this.island.getOwner() == null ? 0 : - this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner())); - } + // Set the death penalty + if (this.addon.getSettings().isSumTeamDeaths()) { + for (UUID uuid : this.island.getMemberSet()) { + this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid)); + } + } else { + // At this point, it may be that the island has become unowned. + this.results.deathHandicap.set(this.island.getOwner() == null ? 0 + : this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner())); + } - long blockAndDeathPoints = this.results.rawBlockCount.get(); - this.results.totalPoints.set(blockAndDeathPoints); + long blockAndDeathPoints = this.results.rawBlockCount.get(); + this.results.totalPoints.set(blockAndDeathPoints); - if (this.addon.getSettings().getDeathPenalty() > 0) - { - // Proper death penalty calculation. - blockAndDeathPoints -= this.results.deathHandicap.get() * this.addon.getSettings().getDeathPenalty(); - } - this.results.level.set(calculateLevel(blockAndDeathPoints)); + if (this.addon.getSettings().getDeathPenalty() > 0) { + // Proper death penalty calculation. + blockAndDeathPoints -= this.results.deathHandicap.get() * this.addon.getSettings().getDeathPenalty(); + } + this.results.level.set(calculateLevel(blockAndDeathPoints)); - // Calculate how many points are required to get to the next level - long nextLevel = this.results.level.get(); - long blocks = blockAndDeathPoints; - while (nextLevel < this.results.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { - nextLevel = calculateLevel(++blocks); - } - this.results.pointsToNextLevel.set(blocks - blockAndDeathPoints); + // Calculate how many points are required to get to the next level + long nextLevel = this.results.level.get(); + long blocks = blockAndDeathPoints; + while (nextLevel < this.results.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { + nextLevel = calculateLevel(++blocks); + } + this.results.pointsToNextLevel.set(blocks - blockAndDeathPoints); - // Report - results.report = getReport(); - // Set the duration - addon.getPipeliner().setTime(System.currentTimeMillis() - duration); - // All done. + // Report + results.report = getReport(); + // Set the duration + addon.getPipeliner().setTime(System.currentTimeMillis() - duration); + // All done. } /** * @return the zeroIsland */ boolean isNotZeroIsland() { - return !zeroIsland; + return !zeroIsland; } public void scanIsland(Pipeliner pipeliner) { - // Scan the next chunk - scanNextChunk().thenAccept(result -> { - if (!Bukkit.isPrimaryThread()) { - addon.getPlugin().logError("scanChunk not on Primary Thread!"); - } - // Timeout check - if (System.currentTimeMillis() - pipeliner.getInProcessQueue().get(this) > addon.getSettings().getCalculationTimeout() * 60000) { - // Done - pipeliner.getInProcessQueue().remove(this); - getR().complete(new Results(Result.TIMEOUT)); - addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + "m for island: " + getIsland()); - if (!isNotZeroIsland()) { - addon.logError("Island level was being zeroed."); - } - return; - } - if (Boolean.TRUE.equals(result) && !pipeliner.getTask().isCancelled()) { - // scanNextChunk returns true if there are more chunks to scan - scanIsland(pipeliner); - } else { - // Done - pipeliner.getInProcessQueue().remove(this); - // Chunk finished - // This was the last chunk - handleStackedBlocks(); - handleChests(); - long checkTime = System.currentTimeMillis(); - finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { - // Check every half second if all the chests and stacks have been cleared - if ((chestBlocks.isEmpty() && stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { - this.tidyUp(); - this.getR().complete(getResults()); - finishTask.cancel(); - } - }, 0, 10L); + // Scan the next chunk + scanNextChunk().thenAccept(result -> { + if (!Bukkit.isPrimaryThread()) { + addon.getPlugin().logError("scanChunk not on Primary Thread!"); + } + // Timeout check + if (System.currentTimeMillis() + - pipeliner.getInProcessQueue().get(this) > addon.getSettings().getCalculationTimeout() * 60000) { + // Done + pipeliner.getInProcessQueue().remove(this); + getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + + "m for island: " + getIsland()); + if (!isNotZeroIsland()) { + addon.logError("Island level was being zeroed."); + } + return; + } + if (Boolean.TRUE.equals(result) && !pipeliner.getTask().isCancelled()) { + // scanNextChunk returns true if there are more chunks to scan + scanIsland(pipeliner); + } else { + // Done + pipeliner.getInProcessQueue().remove(this); + // Chunk finished + // This was the last chunk + handleStackedBlocks(); + handleChests(); + long checkTime = System.currentTimeMillis(); + finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + // Check every half second if all the chests and stacks have been cleared + if ((chestBlocks.isEmpty() && stackedBlocks.isEmpty()) + || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { + this.tidyUp(); + this.getR().complete(getResults()); + finishTask.cancel(); + } + }, 0, 10L); - } - }); + } + }); } private void handleChests() { - Iterator it = chestBlocks.iterator(); - while(it.hasNext()) { - Chunk v = it.next(); - Util.getChunkAtAsync(v.getWorld(), v.getX(), v.getZ()).thenAccept(c -> { - scanChests(c); - it.remove(); - }); - } + Iterator it = chestBlocks.iterator(); + while (it.hasNext()) { + Chunk v = it.next(); + Util.getChunkAtAsync(v.getWorld(), v.getX(), v.getZ()).thenAccept(c -> { + scanChests(c); + it.remove(); + }); + } } private void handleStackedBlocks() { - // Deal with any stacked blocks - Iterator it = stackedBlocks.iterator(); - while (it.hasNext()) { - Location v = it.next(); - Util.getChunkAtAsync(v).thenAccept(c -> { - Block stackedBlock = v.getBlock(); - boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; - if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(stackedBlock)) { - StackedBarrel barrel = WildStackerAPI.getStackedBarrel(stackedBlock); - int barrelAmt = WildStackerAPI.getBarrelAmount(stackedBlock); - for (int _x = 0; _x < barrelAmt; _x++) { - checkBlock(barrel.getType(), belowSeaLevel); - } - } else if (WildStackerAPI.getWildStacker().getSystemManager().isStackedSpawner(stackedBlock)) { - int spawnerAmt = WildStackerAPI.getSpawnersAmount((CreatureSpawner) stackedBlock.getState()); - for (int _x = 0; _x < spawnerAmt; _x++) { - checkBlock(stackedBlock.getType(), belowSeaLevel); - } - } - it.remove(); - }); - } + // Deal with any stacked blocks + Iterator it = stackedBlocks.iterator(); + while (it.hasNext()) { + Location v = it.next(); + Util.getChunkAtAsync(v).thenAccept(c -> { + Block stackedBlock = v.getBlock(); + boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; + if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(stackedBlock)) { + StackedBarrel barrel = WildStackerAPI.getStackedBarrel(stackedBlock); + int barrelAmt = WildStackerAPI.getBarrelAmount(stackedBlock); + for (int _x = 0; _x < barrelAmt; _x++) { + checkBlock(barrel.getType(), belowSeaLevel); + } + } else if (WildStackerAPI.getWildStacker().getSystemManager().isStackedSpawner(stackedBlock)) { + int spawnerAmt = WildStackerAPI.getSpawnersAmount((CreatureSpawner) stackedBlock.getState()); + for (int _x = 0; _x < spawnerAmt; _x++) { + checkBlock(stackedBlock.getType(), belowSeaLevel); + } + } + it.remove(); + }); + } } }