mirror of
https://github.com/BentoBoxWorld/Limits.git
synced 2024-11-22 02:25:43 +01:00
Improved recount code. Should be accurate.
This uses the latest approach done by Level addon. This does not yet include entity recounting! https://github.com/BentoBoxWorld/Limits/issues/123
This commit is contained in:
parent
4cff598aee
commit
343d7bbbde
4
pom.xml
4
pom.xml
@ -57,14 +57,14 @@
|
||||
<!-- Non-minecraft related dependencies -->
|
||||
<powermock.version>2.0.9</powermock.version>
|
||||
<!-- More visible way how to change dependency versions -->
|
||||
<spigot.version>1.16.1-R0.1-SNAPSHOT</spigot.version>
|
||||
<spigot.version>1.16.5-R0.1-SNAPSHOT</spigot.version>
|
||||
<bentobox.version>1.18.0-SNAPSHOT</bentobox.version>
|
||||
<!-- Revision variable removes warning about dynamic version -->
|
||||
<revision>${build.version}-SNAPSHOT</revision>
|
||||
<!-- Do not change unless you want different name for local builds. -->
|
||||
<build.number>-LOCAL</build.number>
|
||||
<!-- This allows to change between versions. -->
|
||||
<build.version>1.18.1</build.version>
|
||||
<build.version>1.19.0</build.version>
|
||||
<sonar.projectKey>BentoBoxWorld_Limits</sonar.projectKey>
|
||||
<sonar.organization>bentobox-world</sonar.organization>
|
||||
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
|
||||
|
151
src/main/java/world/bentobox/limits/calculators/Pipeliner.java
Normal file
151
src/main/java/world/bentobox/limits/calculators/Pipeliner.java
Normal file
@ -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<RecountCalculator> toProcessQueue;
|
||||
private final Map<RecountCalculator, Long> 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<Results> 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<Results> addToQueue(Island island) {
|
||||
CompletableFuture<Results> 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<RecountCalculator, Long> getInProcessQueue() {
|
||||
return inProcessQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the task
|
||||
*/
|
||||
protected BukkitTask getTask() {
|
||||
return task;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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<Pair<Integer, Integer>> chunksToCheck;
|
||||
private final Island island;
|
||||
private final CompletableFuture<Results> r;
|
||||
|
||||
|
||||
private final Results results;
|
||||
private final Map<Environment, World> worlds = new EnumMap<>(Environment.class);
|
||||
private final List<Location> 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<Results> 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<Pair<Integer, Integer>> getChunksToScan(Island island) {
|
||||
Queue<Pair<Integer, Integer>> 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<Results> 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<List<Chunk>> getWorldChunk(Environment env, Queue<Pair<Integer, Integer>> pairList) {
|
||||
if (worlds.containsKey(env)) {
|
||||
CompletableFuture<List<Chunk>> r2 = new CompletableFuture<>();
|
||||
List<Chunk> 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<List<Chunk>> r2, World world, Queue<Pair<Integer, Integer>> pairList,
|
||||
List<Chunk> chunkList) {
|
||||
if (pairList.isEmpty()) {
|
||||
r2.complete(chunkList);
|
||||
return;
|
||||
}
|
||||
Pair<Integer, Integer> 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<Boolean> scanChunk(List<Chunk> chunks) {
|
||||
// If the chunk hasn't been generated, return
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
// Count blocks in chunk
|
||||
CompletableFuture<Boolean> 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<Boolean> 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<Pair<Integer, Integer>> pairList = new ConcurrentLinkedQueue<>();
|
||||
int i = 0;
|
||||
while (!chunksToCheck.isEmpty() && i++ < CHUNKS_TO_SCAN) {
|
||||
pairList.add(chunksToCheck.poll());
|
||||
}
|
||||
Queue<Pair<Integer, Integer>> endPairList = new ConcurrentLinkedQueue<>(pairList);
|
||||
Queue<Pair<Integer, Integer>> netherPairList = new ConcurrentLinkedQueue<>(pairList);
|
||||
// Set up the result
|
||||
CompletableFuture<Boolean> 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<Location> 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();
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
51
src/main/java/world/bentobox/limits/calculators/Results.java
Normal file
51
src/main/java/world/bentobox/limits/calculators/Results.java
Normal file
@ -0,0 +1,51 @@
|
||||
package world.bentobox.limits.calculators;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
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<Material> mdCount = HashMultiset.create();
|
||||
// AtomicLong and AtomicInteger must be used because they are changed by multiple concurrent threads
|
||||
AtomicLong rawBlockCount = new AtomicLong(0);
|
||||
|
||||
final Result state;
|
||||
|
||||
public Results(Result state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Results() {
|
||||
this.state = Result.AVAILABLE;
|
||||
}
|
||||
/**
|
||||
* @return the mdCount
|
||||
*/
|
||||
public Multiset<Material> getMdCount() {
|
||||
return mdCount;
|
||||
}
|
||||
/**
|
||||
* @return the state
|
||||
*/
|
||||
public Result getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
package world.bentobox.limits.calculators;
|
@ -7,8 +7,10 @@ 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;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -17,6 +19,7 @@ import world.bentobox.limits.Limits;
|
||||
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.sendRawMessage("Now recounting. This could take a while, please wait...");
|
||||
new Pipeliner(addon).addIsland(island).thenAccept(results -> {
|
||||
if (results == null) {
|
||||
user.sendRawMessage("Already counting...");
|
||||
} else {
|
||||
switch (results.getState()) {
|
||||
case TIMEOUT -> user.sendRawMessage("Time out when recounting. Is the island really big?");
|
||||
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<List<String>> tabComplete(User user, String alias, List<String> args) {
|
||||
|
@ -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<Material, AtomicInteger> 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<Pair<Integer, Integer>> 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<Integer, Integer> 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<Pair<Integer, Integer>> getChunksToScan(Island island) {
|
||||
Set<Pair<Integer, Integer>> 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<Integer, Integer> 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"));
|
||||
}
|
||||
|
||||
}
|
@ -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<String> args) {
|
||||
// Set cooldown
|
||||
setCooldown(user.getUniqueId(), addon.getConfig().getInt("cooldown", 120));
|
||||
new LimitsCalc(getWorld(), getPlugin(), user.getUniqueId(), addon, user);
|
||||
user.sendRawMessage("Now recounting. This could take a while, please wait...");
|
||||
new Pipeliner(addon).addIsland(island).thenAccept(results -> {
|
||||
if (results == null) {
|
||||
user.sendRawMessage("Already counting...");
|
||||
} else {
|
||||
switch (results.getState()) {
|
||||
case TIMEOUT -> user.sendRawMessage("Time out when recounting. Is the island really big?");
|
||||
default -> user.sendMessage("admin.limits.calc.finished");
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user