diff --git a/pom.xml b/pom.xml index e82c10d..47937f0 100644 --- a/pom.xml +++ b/pom.xml @@ -57,14 +57,14 @@ 2.0.9 - 1.16.1-R0.1-SNAPSHOT + 1.16.5-R0.1-SNAPSHOT 1.18.0-SNAPSHOT ${build.version}-SNAPSHOT -LOCAL - 1.18.1 + 1.19.0 BentoBoxWorld_Limits bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/limits/calculators/Pipeliner.java b/src/main/java/world/bentobox/limits/calculators/Pipeliner.java new file mode 100644 index 0000000..5d819cf --- /dev/null +++ b/src/main/java/world/bentobox/limits/calculators/Pipeliner.java @@ -0,0 +1,151 @@ +package world.bentobox.limits.calculators; + +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.limits.Limits; +import world.bentobox.limits.calculators.Results.Result; + +/** + * A pipeliner that will process one island at a time + * @author tastybento + * + */ +public class Pipeliner { + + private static final int START_DURATION = 10; // 10 seconds + private static final int CONCURRENT_COUNTS = 1; + private final Queue toProcessQueue; + private final Map inProcessQueue; + private final BukkitTask task; + private final Limits addon; + private long time; + private long count; + + /** + * Construct the pipeliner + */ + public Pipeliner(Limits addon) { + this.addon = addon; + toProcessQueue = new ConcurrentLinkedQueue<>(); + inProcessQueue = new HashMap<>(); + // Loop continuously - check every tick if there is an island to scan + task = Bukkit.getScheduler().runTaskTimer(BentoBox.getInstance(), () -> { + if (!BentoBox.getInstance().isEnabled()) { + cancel(); + return; + } + // Complete the current to Process queue first + if (!inProcessQueue.isEmpty() || toProcessQueue.isEmpty()) return; + for (int j = 0; j < CONCURRENT_COUNTS && !toProcessQueue.isEmpty(); j++) { + RecountCalculator iD = toProcessQueue.poll(); + // Ignore deleted or unonwed islands + if (!iD.getIsland().isDeleted() && !iD.getIsland().isUnowned()) { + inProcessQueue.put(iD, System.currentTimeMillis()); + // Start the scanning of a island with the first chunk + scanIsland(iD); + } + } + }, 1L, 10L); + } + + private void cancel() { + task.cancel(); + } + + /** + * @return number of islands currently in the queue or in process + */ + public int getIslandsInQueue() { + return inProcessQueue.size() + toProcessQueue.size(); + } + + /** + * Scans one chunk of an island and adds the results to a results object + * @param iD + */ + private void scanIsland(RecountCalculator iD) { + if (iD.getIsland().isDeleted() || iD.getIsland().isUnowned() || task.isCancelled()) { + // Island is deleted, so finish early with nothing + inProcessQueue.remove(iD); + iD.getR().complete(null); + return; + } + iD.scanIsland(this); + } + + + /** + * Adds an island to the scanning queue but only if the island is not already in the queue + * @param island - the island to scan + * @return CompletableFuture of the results. Results will be null if the island is already in the queue + */ + public CompletableFuture addIsland(Island island) { + // Check if queue already contains island + if (inProcessQueue.keySet().parallelStream().map(RecountCalculator::getIsland).anyMatch(island::equals) + || toProcessQueue.parallelStream().map(RecountCalculator::getIsland).anyMatch(island::equals)) { + return CompletableFuture.completedFuture(new Results(Result.IN_PROGRESS)); + } + return addToQueue(island); + } + + private CompletableFuture addToQueue(Island island) { + CompletableFuture r = new CompletableFuture<>(); + toProcessQueue.add(new RecountCalculator(addon, island, r)); + count++; + return r; + } + + /** + * Get the average time it takes to run a level check + * @return the average time in seconds + */ + public int getTime() { + return time == 0 || count == 0 ? START_DURATION : (int)((double)time/count/1000); + } + + /** + * Submit how long a level check took + * @param time the time to set + */ + public void setTime(long time) { + // Running average + this.time += time; + } + + /** + * Stop the current queue. + */ + public void stop() { + addon.log("Stopping Level queue"); + task.cancel(); + this.inProcessQueue.clear(); + this.toProcessQueue.clear(); + } + + /** + * @return the inProcessQueue + */ + protected Map getInProcessQueue() { + return inProcessQueue; + } + + /** + * @return the task + */ + protected BukkitTask getTask() { + return task; + } + + + + +} diff --git a/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java new file mode 100644 index 0000000..92f6293 --- /dev/null +++ b/src/main/java/world/bentobox/limits/calculators/RecountCalculator.java @@ -0,0 +1,359 @@ +package world.bentobox.limits.calculators; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Slab; +import org.bukkit.scheduler.BukkitTask; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.util.Pair; +import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.Limits; +import world.bentobox.limits.calculators.Results.Result; +import world.bentobox.limits.listeners.BlockLimitsListener; +import world.bentobox.limits.objects.IslandBlockCount; + +/** + * Counter for limits + * @author tastybento + * + */ +public class RecountCalculator { + public static final long MAX_AMOUNT = 10000; + private static final int CHUNKS_TO_SCAN = 100; + private static final int CALCULATION_TIMEOUT = 5; // Minutes + + + private final Limits addon; + private final Queue> chunksToCheck; + private final Island island; + private final CompletableFuture r; + + + private final Results results; + private final Map worlds = new EnumMap<>(Environment.class); + private final List stackedBlocks = new ArrayList<>(); + private BukkitTask finishTask; + private final BlockLimitsListener bll; + private final World world; + private IslandBlockCount ibc; + + + /** + * Constructor to get the level for an island + * @param addon - addon + * @param island - the island to scan + * @param r - completable result that will be completed when the calculation is complete + */ + public RecountCalculator(Limits addon, Island island, CompletableFuture r) { + this.addon = addon; + this.bll = addon.getBlockLimitListener(); + this.island = island; + this.ibc = bll.getIsland(Objects.requireNonNull(island).getUniqueId()); + this.r = r; + results = new Results(); + chunksToCheck = getChunksToScan(island); + // Set up the worlds + this.world = Objects.requireNonNull(Util.getWorld(island.getWorld())); + worlds.put(Environment.NORMAL, world); + boolean isNether = addon.getPlugin().getIWM().isNetherGenerate(world) && addon.getPlugin().getIWM().isNetherIslands(world); + boolean isEnd = addon.getPlugin().getIWM().isEndGenerate(world) && addon.getPlugin().getIWM().isEndIslands(world); + + // Nether + if (isNether) { + World nether = addon.getPlugin().getIWM().getNetherWorld(island.getWorld()); + if (nether != null) { + worlds.put(Environment.NETHER, nether); + } + } + // End + if (isEnd) { + World end = addon.getPlugin().getIWM().getEndWorld(island.getWorld()); + if (end != null) { + worlds.put(Environment.THE_END, end); + } + } + } + + private void checkBlock(BlockData b) { + Material md = bll.fixMaterial(b); + // md is limited + if (bll.getMaterialLimits(world, island.getUniqueId()).containsKey(md)) { + results.mdCount.add(md); + } + } + /** + * 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; + } + + + /** + * @return the island + */ + public Island getIsland() { + return island; + } + + /** + * Get the completable result for this calculation + * @return the r + */ + public CompletableFuture getR() { + return r; + } + + /** + * @return the results + */ + public Results getResults() { + return results; + } + + /** + * Get a chunk async + * @param env - the environment + * @param x - chunk x coordinate + * @param z - chunk z coordinate + * @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()); + } + + 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 + }); + } + /* + 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); + } + }); + } + } + */ + /** + * Count the blocks on the island + * @param chunk chunk to scan + */ + private void scanAsync(Chunk chunk) { + ChunkSnapshot chunkSnapshot = chunk.getChunkSnapshot(); + 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 (chunkSnapshot.getX() * 16 + x < island.getMinProtectedX() || 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 (chunkSnapshot.getZ() * 16 + z < island.getMinProtectedZ() || 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 = chunk.getWorld().getMinHeight(); y < chunk.getWorld().getMaxHeight(); y++) { + BlockData blockData = chunkSnapshot.getBlockData(x, y, z); + // 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); + } + } + // Hook for Wild Stackers (Blocks Only) - this has to use the real chunk + /* + if (addon.isStackersEnabled() && blockData.getMaterial() == Material.CAULDRON) { + stackedBlocks.add(new Location(chunk.getWorld(), x + chunkSnapshot.getX() * 16,y,z + chunkSnapshot.getZ() * 16)); + } + */ + // Add the value of the block's material + checkBlock(blockData); + } + } + } + } + + /** + * Scan the chunk chests and count the blocks + * @param chunks - the chunk 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 + */ + 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<>(); + + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { + chunks.forEach(chunk -> scanAsync(chunk)); + Bukkit.getScheduler().runTask(addon.getPlugin(),() -> result.complete(true)); + }); + return result; + } + + /** + * 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 + */ + 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())))) + ) + ) + ); + + return result; + } + + /** + * Finalizes the calculations and makes the report + */ + public void tidyUp() { + // Finalize calculations + if (ibc == null) { + ibc = new IslandBlockCount(island.getUniqueId(), addon.getPlugin().getIWM().getAddon(world).map(a -> a.getDescription().getName()).orElse("default")); + } + ibc.getBlockCounts().clear(); + results.getMdCount().forEach(ibc::add); + bll.setIsland(island.getUniqueId(), ibc); + //Bukkit.getScheduler().runTask(addon.getPlugin(), () -> sender.sendMessage("admin.limits.calc.finished")); + + // All done. + } + + public void scanIsland(Pipeliner pipeliner) { + // Scan the next chunk + scanNextChunk().thenAccept(r -> { + if (!Bukkit.isPrimaryThread()) { + addon.getPlugin().logError("scanChunk not on Primary Thread!"); + } + // Timeout check + if (System.currentTimeMillis() - pipeliner.getInProcessQueue().get(this) > CALCULATION_TIMEOUT * 60000) { + // Done + pipeliner.getInProcessQueue().remove(this); + getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + CALCULATION_TIMEOUT + "m for island: " + getIsland()); + return; + } + if (Boolean.TRUE.equals(r) && !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(); + long checkTime = System.currentTimeMillis(); + finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + // Check every half second if all the chests and stacks have been cleared + if ((stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { + this.tidyUp(); + this.getR().complete(getResults()); + finishTask.cancel(); + } + }, 0, 10L); + + } + }); + } + + 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 cauldronBlock = v.getBlock(); + boolean belowSeaLevel = seaHeight > 0 && v.getBlockY() <= seaHeight; + if (WildStackerAPI.getWildStacker().getSystemManager().isStackedBarrel(cauldronBlock)) { + StackedBarrel barrel = WildStackerAPI.getStackedBarrel(cauldronBlock); + int barrelAmt = WildStackerAPI.getBarrelAmount(cauldronBlock); + for (int _x = 0; _x < barrelAmt; _x++) { + checkBlock(barrel.getType(), belowSeaLevel); + } + } + it.remove(); + }); + } + */ + } +} diff --git a/src/main/java/world/bentobox/limits/calculators/Results.java b/src/main/java/world/bentobox/limits/calculators/Results.java new file mode 100644 index 0000000..b2c01e0 --- /dev/null +++ b/src/main/java/world/bentobox/limits/calculators/Results.java @@ -0,0 +1,57 @@ +package world.bentobox.limits.calculators; + +import org.bukkit.Material; +import org.bukkit.entity.EntityType; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; + +public class Results { + public enum Result { + /** + * A level calc is already in progress + */ + IN_PROGRESS, + /** + * Results will be available + */ + AVAILABLE, + /** + * Result if calculation timed out + */ + TIMEOUT + } + final Multiset mdCount = HashMultiset.create(); + final Multiset entityCount = HashMultiset.create(); + + final Result state; + + public Results(Result state) { + this.state = state; + } + + public Results() { + this.state = Result.AVAILABLE; + } + /** + * @return the mdCount + */ + public Multiset getMdCount() { + return mdCount; + } + + /** + * @return the state + */ + public Result getState() { + return state; + } + + /** + * @return the entityCount + */ + public Multiset getEntityCount() { + return entityCount; + } + +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/limits/calculators/package-info.java b/src/main/java/world/bentobox/limits/calculators/package-info.java new file mode 100644 index 0000000..33f9efa --- /dev/null +++ b/src/main/java/world/bentobox/limits/calculators/package-info.java @@ -0,0 +1 @@ +package world.bentobox.limits.calculators; \ No newline at end of file diff --git a/src/main/java/world/bentobox/limits/commands/CalcCommand.java b/src/main/java/world/bentobox/limits/commands/CalcCommand.java index 83d22a2..c4cb0ee 100644 --- a/src/main/java/world/bentobox/limits/commands/CalcCommand.java +++ b/src/main/java/world/bentobox/limits/commands/CalcCommand.java @@ -7,16 +7,19 @@ import java.util.UUID; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; import world.bentobox.limits.Limits; +import world.bentobox.limits.calculators.Pipeliner; /** * - * @author YellowZaki + * @author YellowZaki, tastybento */ public class CalcCommand extends CompositeCommand { private final Limits addon; + private Island island; /** * Admin command @@ -49,10 +52,26 @@ public class CalcCommand extends CompositeCommand { if (playerUUID == null) { user.sendMessage("general.errors.unknown-player", args.get(0)); return true; + } + island = addon.getIslands().getIsland(getWorld(), playerUUID); + if (island == null) { + user.sendMessage("general.errors.player-has-no-island"); + return false; } else { //Calculate - calcLimits(playerUUID, user); + user.sendMessage("island.limits.recount.now-recounting"); + new Pipeliner(addon).addIsland(island).thenAccept(results -> { + if (results == null) { + user.sendMessage("island.limits.recount.in-progress"); + } else { + switch (results.getState()) { + case TIMEOUT -> user.sendMessage("admin.limits.calc.timeout"); + default -> user.sendMessage("admin.limits.calc.finished"); + } + } + }); } + return true; } else { showHelp(this, user); @@ -60,13 +79,7 @@ public class CalcCommand extends CompositeCommand { } } - private void calcLimits(UUID targetPlayer, User sender) { - if (addon.getIslands().getIsland(getWorld(), targetPlayer) != null) { - new LimitsCalc(getWorld(), getPlugin(), targetPlayer, addon, sender); - } else { - sender.sendMessage("general.errors.player-has-no-island"); - } - } + @Override public Optional> tabComplete(User user, String alias, List args) { diff --git a/src/main/java/world/bentobox/limits/commands/LimitsCalc.java b/src/main/java/world/bentobox/limits/commands/LimitsCalc.java deleted file mode 100644 index 3928c13..0000000 --- a/src/main/java/world/bentobox/limits/commands/LimitsCalc.java +++ /dev/null @@ -1,153 +0,0 @@ -package world.bentobox.limits.commands; - -import java.util.EnumMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import org.bukkit.Bukkit; -import org.bukkit.ChunkSnapshot; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; - -import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.user.User; -import world.bentobox.bentobox.database.objects.Island; -import world.bentobox.bentobox.util.Pair; -import world.bentobox.bentobox.util.Util; -import world.bentobox.limits.Limits; -import world.bentobox.limits.listeners.BlockLimitsListener; -import world.bentobox.limits.objects.IslandBlockCount; - -/** - * - * @author YellowZaki, tastybento - */ -public class LimitsCalc { - - private final Limits addon; - private final World world; - private final Island island; - private final BlockLimitsListener bll; - private IslandBlockCount ibc; - private final Map blockCount; - private final User sender; - private int count; - private final int chunksToScanCount; - private final BentoBox plugin; - - - /** - * Perform a count of all limited blocks or entities on an island - * @param world - game world to scan - * @param instance - BentoBox - * @param targetPlayer - target player's island - * @param addon - addon instance - * @param sender - requester of the count - */ - LimitsCalc(World world, BentoBox instance, UUID targetPlayer, Limits addon, User sender) { - this.plugin = instance; - this.addon = addon; - this.island = instance.getIslands().getIsland(world, targetPlayer); - this.bll = addon.getBlockLimitListener(); - this.ibc = bll.getIsland(Objects.requireNonNull(island).getUniqueId()); - blockCount = new EnumMap<>(Material.class); - this.sender = sender; - this.world = world; - - // Get chunks to scan - Set> chunksToScan = getChunksToScan(island); - count = 0; - - boolean isNether = plugin.getIWM().isNetherGenerate(world) && plugin.getIWM().isNetherIslands(world); - boolean isEnd = plugin.getIWM().isEndGenerate(world) && plugin.getIWM().isEndIslands(world); - // Calculate how many chunks need to be scanned - chunksToScanCount = chunksToScan.size() + (isNether ? chunksToScan.size():0) + (isEnd ? chunksToScan.size():0); - chunksToScan.forEach(c -> { - asyncScan(world, c); - if (isNether) asyncScan(plugin.getIWM().getNetherWorld(world), c); - if (isEnd) asyncScan(plugin.getIWM().getEndWorld(world), c); - }); - - } - - - - private void asyncScan(World world2, Pair c) { - Util.getChunkAtAsync(world2, c.x, c.z).thenAccept(ch -> { - ChunkSnapshot snapShot = ch.getChunkSnapshot(); - Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { - this.scanChunk(snapShot); - count++; - if (count == chunksToScanCount) { - this.tidyUp(); - } - }); - }); - } - - - - private void scanChunk(ChunkSnapshot chunk) { - 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 (chunk.getX() * 16 + x < island.getMinProtectedX() || chunk.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 (chunk.getZ() * 16 + z < island.getMinProtectedZ() || chunk.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionRange() * 2) { - continue; - } - for (int y = 0; y < Objects.requireNonNull(island.getCenter().getWorld()).getMaxHeight(); y++) { - BlockData blockData = chunk.getBlockData(x, y, z); - // Air is free - if (!blockData.getMaterial().equals(Material.AIR)) { - checkBlock(blockData); - } - } - } - } - } - - private void checkBlock(BlockData b) { - Material md = bll.fixMaterial(b); - // md is limited - if (bll.getMaterialLimits(world, island.getUniqueId()).containsKey(md)) { - if (!blockCount.containsKey(md)) { - blockCount.put(md, new AtomicInteger(1)); - } else { - blockCount.get(md).getAndIncrement(); - } - } - } - - private Set> getChunksToScan(Island island) { - Set> chunkSnapshot = new HashSet<>(); - 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) { - Pair pair = new Pair<>(world.getBlockAt(x, 0, z).getChunk().getX(), world.getBlockAt(x, 0, z).getChunk().getZ()); - chunkSnapshot.add(pair); - } - } - return chunkSnapshot; - } - - private void tidyUp() { - if (ibc == null) { - ibc = new IslandBlockCount(island.getUniqueId(), plugin.getIWM().getAddon(world).map(a -> a.getDescription().getName()).orElse("default")); - } - ibc.setBlockCounts(blockCount.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> entry.getValue().get()))); - bll.setIsland(island.getUniqueId(), ibc); - Bukkit.getScheduler().runTask(addon.getPlugin(), () -> sender.sendMessage("admin.limits.calc.finished")); - } - -} diff --git a/src/main/java/world/bentobox/limits/commands/RecountCommand.java b/src/main/java/world/bentobox/limits/commands/RecountCommand.java index 6f92873..5f31c8b 100644 --- a/src/main/java/world/bentobox/limits/commands/RecountCommand.java +++ b/src/main/java/world/bentobox/limits/commands/RecountCommand.java @@ -2,9 +2,13 @@ package world.bentobox.limits.commands; import java.util.List; +import org.eclipse.jdt.annotation.Nullable; + import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.limits.Limits; +import world.bentobox.limits.calculators.Pipeliner; /** * @@ -13,6 +17,7 @@ import world.bentobox.limits.Limits; public class RecountCommand extends CompositeCommand { private final Limits addon; + private @Nullable Island island; /** * Player command to do a recount. Has a cooldown @@ -44,7 +49,8 @@ public class RecountCommand extends CompositeCommand { showHelp(this, user); return false; } - if (addon.getIslands().getIsland(getWorld(), user) == null) { + island = addon.getIslands().getIsland(getWorld(), user); + if (island == null) { user.sendMessage("general.errors.no-island"); return false; } @@ -54,7 +60,17 @@ public class RecountCommand extends CompositeCommand { public boolean execute(User user, String label, List args) { // Set cooldown setCooldown(user.getUniqueId(), addon.getConfig().getInt("cooldown", 120)); - new LimitsCalc(getWorld(), getPlugin(), user.getUniqueId(), addon, user); + user.sendMessage("island.limits.recount.now-recounting"); + new Pipeliner(addon).addIsland(island).thenAccept(results -> { + if (results == null) { + user.sendMessage("island.limits.recount.in-progress"); + } else { + switch (results.getState()) { + case TIMEOUT -> user.sendMessage("admin.limits.calc.timeout"); + default -> user.sendMessage("admin.limits.calc.finished"); + } + } + }); return true; } diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 55a213c..31463c8 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -277,10 +277,11 @@ public class BlockLimitsListener implements Listener { } } - - - - // Return equivalents. b can be Block/Material + /** + * Return equivalents. Maps things like wall materials to their non-wall equivalents + * @param b block data + * @return material that matches the block data + */ public Material fixMaterial(BlockData b) { Material mat = b.getMaterial(); diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 211b49a..e1b9488 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -19,7 +19,7 @@ admin: calc: parameters: "" description: "recalculate the island limits for player" - finished: "&aIsland recalc finished successfully!" + finished: "&a Island recalc finished successfully!" island: limits: @@ -30,4 +30,7 @@ island: no-limits: "&cNo limits set in this world" recount: description: "recounts limits for your island" + now-recounting: "&b Now recounting. This could take a while, please wait..." + in-progress: "&c Island recound is in progress. Please wait..." + time-out: "&c Time out when recounting. Is the island really big?"