diff --git a/src/main/java/com/songoda/skyblock/blockscanner/BlockScanner.java b/src/main/java/com/songoda/skyblock/blockscanner/BlockScanner.java index 57956cfc..92c4fdf1 100644 --- a/src/main/java/com/songoda/skyblock/blockscanner/BlockScanner.java +++ b/src/main/java/com/songoda/skyblock/blockscanner/BlockScanner.java @@ -7,6 +7,7 @@ import com.songoda.skyblock.SkyBlock; import com.songoda.skyblock.island.Island; import com.songoda.skyblock.island.IslandEnvironment; import com.songoda.skyblock.world.WorldManager; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; @@ -15,26 +16,33 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.scheduler.BukkitRunnable; -import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public final class BlockScanner extends BukkitRunnable { private static final Method ID_FIELD; + private static final int MAX_CHUNKS_PER_ITERATION = 2; + private static final int MAX_EMPTY_ITERATIONS = 20; static { Method temp = null; try { temp = ChunkSnapshot.class.getMethod("getBlockTypeId", int.class, int.class, int.class); - } catch (NoSuchMethodException ignored) {} + } catch (NoSuchMethodException ignored) { + } ID_FIELD = temp; } @@ -57,9 +65,9 @@ public final class BlockScanner extends BukkitRunnable { private final int threadCount; private final Queue blocks; private final ScannerTasks tasks; - + private final Island island; - + private final boolean ignoreLiquids; private final boolean ignoreAir; @@ -84,35 +92,35 @@ public final class BlockScanner extends BukkitRunnable { for (Entry> entry : snapshots.entrySet()) { final List> parts = Lists.partition(entry.getValue(), 16); - + threadCount += parts.size(); World world = entry.getKey(); final String env; switch (world.getEnvironment()) { - case NETHER: - env = "Nether"; - break; - case THE_END: - env = "End"; - break; - default: - env = "Normal"; - break; + case NETHER: + env = "Nether"; + break; + case THE_END: + env = "End"; + break; + default: + env = "Normal"; + break; } final ConfigurationSection liquidSection = config.getConfigurationSection("Island.World." + env + ".Liquid"); int startY; - if(ignoreY){ + if (ignoreY) { startY = 255; } else { startY = !ignoreLiquidsY && liquidSection.getBoolean("Enable") && !config.getBoolean("Island.Levelling.ScanLiquid") ? liquidSection.getInt("Height") + 1 : 0; } for (List sub : parts) { - queueWork(world, startY, sub); + queueWork(world, startY, sub); } } @@ -121,63 +129,166 @@ public final class BlockScanner extends BukkitRunnable { private void queueWork(World world, int scanY, List subList) { WorldManager worldManager = SkyBlock.getInstance().getWorldManager(); - + + // The chunks that couldn't be taken snapshot async + List pendingChunks = new ArrayList<>(); + + // The chunks that are ready to be processed asynchronously + List readyChunks = new ArrayList<>(); + + // This lock will help to make the bukkit task wait after all the chunks that could be processed async are processed + Lock lock = new ReentrantLock(); + + // This is the actual object that we will use to wait + Condition emptyCondition = lock.newCondition(); + Bukkit.getServer().getScheduler().runTaskAsynchronously(SkyBlock.getInstance(), () -> { + // We need to hold the lock on the thread calling the await + lock.lock(); + LocationBounds bounds = null; - if(island != null) { + if (island != null) { Location islandLocation = island.getLocation(worldManager.getIslandWorld(world), IslandEnvironment.Island); - + Location minLocation = new Location(world, islandLocation.getBlockX() - island.getRadius(), 0, islandLocation.getBlockZ() - island.getRadius()); Location maxLocation = new Location(world, islandLocation.getBlockX() + island.getRadius(), world.getMaxHeight(), islandLocation.getBlockZ() + island.getRadius()); - + int minX = Math.min(maxLocation.getBlockX(), minLocation.getBlockX()); int minZ = Math.min(maxLocation.getBlockZ(), minLocation.getBlockZ()); - + int maxX = Math.max(maxLocation.getBlockX(), minLocation.getBlockX()); int maxZ = Math.max(maxLocation.getBlockZ(), minLocation.getBlockZ()); - + bounds = new LocationBounds(minX, minZ, maxX, maxZ); } - for (CachedChunk shot : subList) { - final int cX = shot.getX() << 4; - final int cZ = shot.getZ() << 4; - - int initX = 0; - int initZ = 0; - int lastX = 15; - int lastZ = 15; - - if(bounds != null) { - initX = Math.max(cX, bounds.getMinX())&0x000F; - initZ = Math.max(cZ, bounds.getMinZ())&0x000F; - - lastX = Math.min(cX | 15, bounds.getMaxX()-1)&0x000F; - lastZ = Math.min(cZ | 15, bounds.getMaxZ()-1)&0x000F; - } - - for (int x = initX; x <= lastX; x++) { - for (int z = initZ; z <= lastZ; z++) { - for (int y = scanY; y < world.getMaxHeight(); y++) { - final CompatibleMaterial type = CompatibleMaterial.getBlockMaterial( - ServerVersion.isServerVersionAtLeast(ServerVersion.V1_13) - ? shot.getSnapshot().getBlockType(x, y, z) : - MaterialIDHelper.getLegacyMaterial(getBlockTypeID(shot, x, y, z))); - - if(type == null){ - continue; - } else if(type.equals(CompatibleMaterial.AIR) && ignoreAir){ - continue; - } else if(type.equals(CompatibleMaterial.WATER) && ignoreLiquids){ - continue; - } - blocks.add(new BlockInfo(world, x + (cX), y, z + (cZ))); - } - } + for (CachedChunk shot : subList) { + if (!shot.isSnapshotAvailable() && !areAsyncChunksAvailable()) { + pendingChunks.add(shot); + + continue; } + + processCachedChunk(world, scanY, shot, bounds); } + + // Don't wait for the condition if the async chunks are available, since it would never be signalled + if (areAsyncChunksAvailable()) { + increment(); + + lock.unlock(); + return; + } + + try { + emptyCondition.await(); + } catch (InterruptedException e) { + // Pass the interruption + Thread.currentThread().interrupt(); + } + + // process the pending chunks + for (CachedChunk shot : readyChunks) { + processCachedChunk(world, scanY, shot, bounds); + } + + lock.unlock(); increment(); }); + + if (!areAsyncChunksAvailable()) { + startChunkSnapshotTask(pendingChunks, readyChunks, emptyCondition, lock); + } + } + + private boolean areAsyncChunksAvailable() { + return PaperLib.isVersion(9) && PaperLib.isPaper(); + } + + private void startChunkSnapshotTask(List pendingChunks, List readyChunks, Condition emptyCondition, Lock lock) { + new BukkitRunnable() { + // The number of iterations with the pendingChunks list empty + private int emptyIterations = 0; + + @Override + public void run() { + lock.lock(); + int updatedChunks = 0; + + Iterator chunkIterator = pendingChunks.iterator(); + + try { + while (chunkIterator.hasNext()) { + CachedChunk pendingChunk = chunkIterator.next(); + + if (updatedChunks >= MAX_CHUNKS_PER_ITERATION) { + break; + } + + // take the snapshot + pendingChunk.takeSnapshot(); + + chunkIterator.remove(); + readyChunks.add(pendingChunk); + + updatedChunks++; + } + + if (pendingChunks.isEmpty()) { + if (emptyIterations >= MAX_EMPTY_ITERATIONS) { + // Send the signal to unlock the async thread and continue with the processing + emptyCondition.signalAll(); + this.cancel(); + + return; + } + + emptyIterations++; + } + } finally { + lock.unlock(); + } + } + }.runTaskTimer(SkyBlock.getInstance(), 1, 1); + } + + private void processCachedChunk(World world, int scanY, CachedChunk shot, LocationBounds bounds) { + final int cX = shot.getX() << 4; + final int cZ = shot.getZ() << 4; + + int initX = 0; + int initZ = 0; + int lastX = 15; + int lastZ = 15; + + if (bounds != null) { + initX = Math.max(cX, bounds.getMinX()) & 0x000F; + initZ = Math.max(cZ, bounds.getMinZ()) & 0x000F; + + lastX = Math.min(cX | 15, bounds.getMaxX() - 1) & 0x000F; + lastZ = Math.min(cZ | 15, bounds.getMaxZ() - 1) & 0x000F; + } + + for (int x = initX; x <= lastX; x++) { + for (int z = initZ; z <= lastZ; z++) { + for (int y = scanY; y < world.getMaxHeight(); y++) { + final CompatibleMaterial type = CompatibleMaterial.getBlockMaterial( + ServerVersion.isServerVersionAtLeast(ServerVersion.V1_13) + ? shot.getSnapshot().getBlockType(x, y, z) : + MaterialIDHelper.getLegacyMaterial(getBlockTypeID(shot, x, y, z))); + + if (type == null) { + continue; + } else if (type.equals(CompatibleMaterial.AIR) && ignoreAir) { + continue; + } else if (type.equals(CompatibleMaterial.WATER) && ignoreLiquids) { + continue; + } + + blocks.add(new BlockInfo(world, x + (cX), y, z + (cZ))); + } + } + } } private synchronized int increment() {