diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index bbbc60f0..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,16 +0,0 @@ -image: maven:latest - -cache: - paths: - - .m2/repository/ - - target/ - -build: - stage: build - script: - - mvn compile - -test: - stage: test - script: - - mvn test \ No newline at end of file diff --git a/pom.xml b/pom.xml index 84322726..4916424b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.songoda skyblock - 2.3.19 + 2.3.21 jar UTF-8 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() { diff --git a/src/main/java/com/songoda/skyblock/blockscanner/CachedChunk.java b/src/main/java/com/songoda/skyblock/blockscanner/CachedChunk.java index 17864e92..b49d0091 100644 --- a/src/main/java/com/songoda/skyblock/blockscanner/CachedChunk.java +++ b/src/main/java/com/songoda/skyblock/blockscanner/CachedChunk.java @@ -49,6 +49,10 @@ public class CachedChunk { return PaperLib.getChunkAtAsync(world, this.x, this.z); } + public boolean isSnapshotAvailable() { + return latestSnapshot != null; + } + public ChunkSnapshot getSnapshot() { if (latestSnapshot == null) return takeSnapshot(); diff --git a/src/main/java/com/songoda/skyblock/command/commands/island/UpgradeCommand.java b/src/main/java/com/songoda/skyblock/command/commands/island/UpgradeCommand.java index a90df98c..aecd3043 100644 --- a/src/main/java/com/songoda/skyblock/command/commands/island/UpgradeCommand.java +++ b/src/main/java/com/songoda/skyblock/command/commands/island/UpgradeCommand.java @@ -29,7 +29,7 @@ public class UpgradeCommand extends SubCommand { configLoad.getString("Command.Island.Upgrade.Owner.Message")); soundManager.playSound(player, CompatibleSound.BLOCK_ANVIL_LAND.getSound(), 1.0F, 1.0F); } else { - if (!economy.isEnabled()) { + if (economy == null || !economy.isEnabled()) { messageManager.sendMessage(player, configLoad.getString("Command.Island.Upgrade.Disabled.Message")); soundManager.playSound(player, CompatibleSound.BLOCK_ANVIL_LAND.getSound(), 1.0F, 1.0F); return; diff --git a/src/main/java/com/songoda/skyblock/island/IslandStatus.java b/src/main/java/com/songoda/skyblock/island/IslandStatus.java index 682e9495..97bd7893 100644 --- a/src/main/java/com/songoda/skyblock/island/IslandStatus.java +++ b/src/main/java/com/songoda/skyblock/island/IslandStatus.java @@ -1,11 +1,16 @@ package com.songoda.skyblock.island; +import java.util.Arrays; + public enum IslandStatus { OPEN, CLOSED, WHITELISTED; public static IslandStatus getEnum(String value) { - return valueOf(value.toUpperCase()); + return Arrays.stream(values()) + .filter(status -> value.toUpperCase().equals(status.name())) + .findFirst() + .orElse(OPEN); } } diff --git a/src/main/java/com/songoda/skyblock/levelling/QueuedIslandScan.java b/src/main/java/com/songoda/skyblock/levelling/QueuedIslandScan.java index 77f4793d..312d4371 100644 --- a/src/main/java/com/songoda/skyblock/levelling/QueuedIslandScan.java +++ b/src/main/java/com/songoda/skyblock/levelling/QueuedIslandScan.java @@ -12,7 +12,6 @@ import org.bukkit.Bukkit; import org.bukkit.configuration.Configuration; import org.bukkit.entity.Player; -import java.io.File; import java.text.NumberFormat; import java.util.*; @@ -55,7 +54,7 @@ public class QueuedIslandScan { update(); if (toScan.isEmpty()) { - finalize(); + finalizeScan(); return false; } IslandWorld world = toScan.poll(); @@ -63,8 +62,7 @@ public class QueuedIslandScan { return true; } - public void finalize() { - + public void finalizeScan() { final Map materials = new HashMap<>(amounts.size()); for (Map.Entry entry : amounts.entrySet()) { diff --git a/src/main/java/com/songoda/skyblock/utils/structure/SchematicUtil.java b/src/main/java/com/songoda/skyblock/utils/structure/SchematicUtil.java index 697668a4..a74cb0b8 100644 --- a/src/main/java/com/songoda/skyblock/utils/structure/SchematicUtil.java +++ b/src/main/java/com/songoda/skyblock/utils/structure/SchematicUtil.java @@ -4,6 +4,7 @@ import com.songoda.skyblock.SkyBlock; import com.songoda.skyblock.utils.version.NMSUtil; import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.plugin.PluginManager; import java.io.File; import java.io.FileInputStream; @@ -14,7 +15,8 @@ import java.lang.reflect.Method; public class SchematicUtil { public static Float[] pasteSchematic(File schematicFile, org.bukkit.Location location) { - if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) + PluginManager pluginManager = Bukkit.getPluginManager(); + if (!pluginManager.isPluginEnabled("WorldEdit") && !pluginManager.isPluginEnabled("AsyncWorldEdit") && !pluginManager.isPluginEnabled("FastAsyncWorldEdit")) throw new IllegalStateException("Tried to generate an island using a schematic file without WorldEdit installed!"); Runnable pasteTask = () -> { diff --git a/src/main/java/com/songoda/skyblock/world/WorldManager.java b/src/main/java/com/songoda/skyblock/world/WorldManager.java index abb93f75..98451d38 100644 --- a/src/main/java/com/songoda/skyblock/world/WorldManager.java +++ b/src/main/java/com/songoda/skyblock/world/WorldManager.java @@ -47,9 +47,9 @@ public class WorldManager { String netherWorldGeneratorName = configLoad.getString("Island.World.End.CustomWorldGenerator"); String endWorldGeneratorName = configLoad.getString("Island.World.End.CustomWorldGenerator"); - normalWorldWorldGenerator = getWorldGenerator(normalWorldName, normalWorldGeneratorName); - netherWorldWorldGenerator = getWorldGenerator(netherWorldName, netherWorldGeneratorName); - endWorldWorldGenerator = getWorldGenerator(endWorldName, endWorldGeneratorName); + normalWorldWorldGenerator = getWorldGenerator(normalWorldName, normalWorldGeneratorName, IslandWorld.Normal); + netherWorldWorldGenerator = getWorldGenerator(netherWorldName, netherWorldGeneratorName, IslandWorld.Nether); + endWorldWorldGenerator = getWorldGenerator(endWorldName, endWorldGeneratorName, IslandWorld.End); normalWorld = Bukkit.getServer().getWorld(normalWorldName); netherWorld = Bukkit.getServer().getWorld(netherWorldName); @@ -138,9 +138,9 @@ public class WorldManager { return location; } - private ChunkGenerator getWorldGenerator(String mapName, String worldGeneratorName) { + private ChunkGenerator getWorldGenerator(String mapName, String worldGeneratorName, IslandWorld islandWorld) { if (worldGeneratorName == null || worldGeneratorName == "default" || worldGeneratorName.length() == 0) { - return new VoidGenerator(); + return new VoidGenerator(islandWorld); } ChunkGenerator customWorldGenerator = WorldCreator.getGeneratorForName(mapName, worldGeneratorName, null); @@ -149,7 +149,7 @@ public class WorldManager { return customWorldGenerator; } - return new VoidGenerator(); + return new VoidGenerator(islandWorld); } public ChunkGenerator getWorldGeneratorForMapName(String mapName) { @@ -159,6 +159,6 @@ public class WorldManager { if (endWorld != null && endWorld.getName().equals(mapName)) return endWorldWorldGenerator; - return new VoidGenerator(); + return new VoidGenerator(IslandWorld.Normal); } } diff --git a/src/main/java/com/songoda/skyblock/world/generator/VoidGenerator.java b/src/main/java/com/songoda/skyblock/world/generator/VoidGenerator.java index e35500ff..1a392224 100644 --- a/src/main/java/com/songoda/skyblock/world/generator/VoidGenerator.java +++ b/src/main/java/com/songoda/skyblock/world/generator/VoidGenerator.java @@ -21,6 +21,11 @@ import java.util.Random; public class VoidGenerator extends ChunkGenerator { + private final IslandWorld islandWorld; + public VoidGenerator(IslandWorld islandWorld) { + this.islandWorld = islandWorld; + } + @Override public @Nonnull ChunkData generateChunkData(@Nonnull World world, @Nonnull Random random, int chunkX, int chunkZ, @Nonnull BiomeGrid biomeGrid) { final ChunkData chunkData = createChunkData(world); @@ -28,12 +33,15 @@ public class VoidGenerator extends ChunkGenerator { final SkyBlock plugin = SkyBlock.getInstance(); final Configuration configLoad = plugin.getConfiguration(); final ConfigurationSection worldSection = configLoad.getConfigurationSection("Island.World"); - + Biome biome; switch (world.getEnvironment()) { case NORMAL: - biome = CompatibleBiome.valueOf(configLoad.getString("Island.Biome.Default.Type", "PLAINS").toUpperCase()).getBiome(); + biome = Arrays.stream(CompatibleBiome.values()) + .filter(compatibleBiome -> compatibleBiome.getBiome().name().equals(configLoad.getString("Island.Biome.Default.Type", "PLAINS").toUpperCase())) + .findFirst() + .orElse(CompatibleBiome.PLAINS).getBiome(); break; case NETHER: biome = CompatibleBiome.NETHER_WASTES.getBiome(); @@ -50,26 +58,19 @@ public class VoidGenerator extends ChunkGenerator { } else { setChunkBiome2D(biome, biomeGrid); } - - for (IslandWorld worldList : IslandWorld.values()) { - if (world.getEnvironment() == World.Environment.NETHER - || world.getEnvironment() == World.Environment.NORMAL - || world.getEnvironment() == World.Environment.THE_END) { - ConfigurationSection section = worldSection.getConfigurationSection(worldList.name()); + ConfigurationSection section = worldSection.getConfigurationSection(islandWorld.name()); - if (section.getBoolean("Liquid.Enable")) { - if (section.getBoolean("Liquid.Lava")) { - setBlock(chunkData, CompatibleMaterial.LAVA.getBlockMaterial(), section.getInt("Liquid.Height")); - } else { - setBlock(chunkData, CompatibleMaterial.WATER.getBlockMaterial(), section.getInt("Liquid.Height")); - } - } - - break; + if (section.getBoolean("Liquid.Enable")) { + if (section.getBoolean("Liquid.Lava")) { + setBlock(chunkData, CompatibleMaterial.LAVA.getBlockMaterial(), section.getInt("Liquid.Height")); + } else { + setBlock(chunkData, CompatibleMaterial.WATER.getBlockMaterial(), section.getInt("Liquid.Height")); } } + + return chunkData; } @@ -96,7 +97,7 @@ public class VoidGenerator extends ChunkGenerator { } } } - + // Do not use - Too laggy private void setChunkBiome3D(Biome biome, BiomeGrid grid, World world) { for(int x = 0; x < 16; x++){ @@ -107,7 +108,7 @@ public class VoidGenerator extends ChunkGenerator { } } } - + private void setChunkBiome2D(Biome biome, BiomeGrid grid) { for(int x = 0; x < 16; x++){ for(int z = 0; z < 16; z++){