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?"