From b534eb70d46b8e88cd316aa8b4e0ecc95a0cae97 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 26 Nov 2021 13:19:51 -0800 Subject: [PATCH] Version 2.8.1 (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Version 2.8.1 * Speeds up level calculation by doing more chunk scans async. If chests are scanned, then it will take longer because these have to be done sync. https://github.com/BentoBoxWorld/Level/issues/243 * add Vietnamese (#240) * Raw island level placeholder (#241) Co-authored-by: Huynh Tien Co-authored-by: Rubén <44579213+Rubenicos@users.noreply.github.com> --- pom.xml | 2 +- src/main/java/world/bentobox/level/Level.java | 3 + .../calculators/IslandLevelCalculator.java | 188 ++++++++++++++---- .../bentobox/level/calculators/Pipeliner.java | 48 ++--- src/main/resources/locales/vi.yml | 56 ++++++ 5 files changed, 221 insertions(+), 76 deletions(-) create mode 100644 src/main/resources/locales/vi.yml diff --git a/pom.xml b/pom.xml index b3f602a..b57a4d0 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ -LOCAL - 2.8.0 + 2.8.1 BentoBoxWorld_Level bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index 1416194..79c7a5e 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -197,6 +197,9 @@ public class Level extends Addon implements Listener { getPlugin().getPlaceholdersManager().registerPlaceholder(this, gm.getDescription().getName().toLowerCase() + "_island_level", user -> getManager().getIslandLevelString(gm.getOverWorld(), user.getUniqueId())); + getPlugin().getPlaceholdersManager().registerPlaceholder(this, + gm.getDescription().getName().toLowerCase() + "_island_level_raw", + user -> String.valueOf(getManager().getIslandLevel(gm.getOverWorld(), user.getUniqueId()))); getPlugin().getPlaceholdersManager().registerPlaceholder(this, gm.getDescription().getName().toLowerCase() + "_points_to_next_level", user -> getManager().getPointsToNextString(gm.getOverWorld(), user.getUniqueId())); diff --git a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java index 7052077..2a1ac3a 100644 --- a/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java +++ b/src/main/java/world/bentobox/level/calculators/IslandLevelCalculator.java @@ -1,14 +1,17 @@ package world.bentobox.level.calculators; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; @@ -16,6 +19,7 @@ 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; @@ -26,8 +30,7 @@ import org.bukkit.block.Container; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Slab; import org.bukkit.inventory.ItemStack; -import org.bukkit.util.Vector; -import org.eclipse.jdt.annotation.Nullable; +import org.bukkit.scheduler.BukkitTask; import com.bgsoftware.wildstacker.api.WildStackerAPI; import com.bgsoftware.wildstacker.api.objects.StackedBarrel; @@ -44,10 +47,19 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Util; import world.bentobox.level.Level; +import world.bentobox.level.calculators.Results.Result; public class IslandLevelCalculator { private static final String LINE_BREAK = "=================================="; public static final long MAX_AMOUNT = 10000; + private static final List CHESTS = Arrays.asList(Material.CHEST, Material.CHEST_MINECART, Material.TRAPPED_CHEST, + Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX, Material.BLUE_SHULKER_BOX, Material.BROWN_SHULKER_BOX, + Material.CYAN_SHULKER_BOX, Material.GRAY_SHULKER_BOX, Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX, + Material.LIGHT_GRAY_SHULKER_BOX, Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX, Material.ORANGE_SHULKER_BOX, + Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX, Material.RED_SHULKER_BOX, Material.RED_SHULKER_BOX, + Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX, Material.COMPOSTER, Material.BARREL, Material.DISPENSER, + Material.DROPPER, Material.SMOKER, Material.BLAST_FURNACE); + private static final int CHUNKS_TO_SCAN = 100; /** * Method to evaluate a mathematical equation @@ -156,6 +168,10 @@ public class IslandLevelCalculator { private final boolean zeroIsland; private final Map worlds = new EnumMap<>(Environment.class); private final int seaHeight; + private final List stackedBlocks = new ArrayList<>(); + private final Set chestBlocks = new HashSet<>(); + private BukkitTask finishTask; + /** * Constructor to get the level for an island @@ -339,17 +355,35 @@ public class IslandLevelCalculator { * @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, int x, int z) { + private CompletableFuture> getWorldChunk(Environment env, Queue> pairList) { if (worlds.containsKey(env)) { - CompletableFuture r2 = new CompletableFuture<>(); + CompletableFuture> r2 = new CompletableFuture<>(); + List chunkList = new ArrayList<>(); + World world = worlds.get(env); // Get the chunk, and then coincidentally check the RoseStacker - Util.getChunkAtAsync(worlds.get(env), x, z, true).thenAccept(chunk -> roseStackerCheck(r2, chunk)); + loadChunks(r2, world, pairList, chunkList); return r2; } - return CompletableFuture.completedFuture(null); + return CompletableFuture.completedFuture(Collections.emptyList()); } - private void roseStackerCheck(CompletableFuture r2, Chunk chunk) { + 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 @@ -360,7 +394,6 @@ public class IslandLevelCalculator { } }); } - r2.complete(chunk); } /** @@ -385,11 +418,10 @@ public class IslandLevelCalculator { /** * Count the blocks on the island - * @param result - the CompletableFuture that should be completed when this scan is done - * @param chunkSnapshot - the chunk to scan + * @param chunk chunk to scan */ - private void scanAsync(CompletableFuture result, ChunkSnapshot chunkSnapshot, Chunk chunk) { - List stackedBlocks = new ArrayList<>(); + 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) { @@ -413,29 +445,17 @@ public class IslandLevelCalculator { } // Hook for Wild Stackers (Blocks Only) - this has to use the real chunk if (addon.isStackersEnabled() && blockData.getMaterial() == Material.CAULDRON) { - stackedBlocks.add(new Vector(x,y,z)); + stackedBlocks.add(new Location(chunk.getWorld(), x + chunkSnapshot.getX() * 16,y,z + chunkSnapshot.getZ() * 16)); + } + // Scan chests + if (addon.getSettings().isIncludeChests() && CHESTS.contains(blockData.getMaterial())) { + chestBlocks.add(chunk); } // Add the value of the block's material checkBlock(blockData.getMaterial(), belowSeaLevel); } } } - // Complete the future - this must go back onto the primary thread to exit async otherwise subsequent actions will be async - Bukkit.getScheduler().runTask(addon.getPlugin(),() -> { - // Deal with any stacked blocks - stackedBlocks.forEach(v -> { - Block cauldronBlock = chunk.getBlock(v.getBlockX(), v.getBlockY(), v.getBlockZ()); - 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); - } - } - }); - result.complete(true); - }); } /** @@ -473,20 +493,21 @@ public class IslandLevelCalculator { /** * Scan the chunk chests and count the blocks - * @param chunk - the chunk to scan + * @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(@Nullable Chunk chunk) { + private CompletableFuture scanChunk(List chunks) { // If the chunk hasn't been generated, return - if (chunk == null) return CompletableFuture.completedFuture(false); - // Scan chests - if (addon.getSettings().isIncludeChests()) { - scanChests(chunk); + if (chunks == null || chunks.isEmpty()) { + return CompletableFuture.completedFuture(false); } // Count blocks in chunk CompletableFuture result = new CompletableFuture<>(); - ChunkSnapshot snapshot = chunk.getChunkSnapshot(); - Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> scanAsync(result, snapshot, chunk)); + + Bukkit.getScheduler().runTaskAsynchronously(BentoBox.getInstance(), () -> { + chunks.forEach(chunk -> scanAsync(chunk)); + Bukkit.getScheduler().runTask(addon.getPlugin(),() -> result.complete(true)); + }); return result; } @@ -501,16 +522,23 @@ public class IslandLevelCalculator { return CompletableFuture.completedFuture(false); } // Retrieve and remove from the queue - Pair p = chunksToCheck.poll(); + 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 - getWorldChunk(Environment.THE_END, p.x, p.z).thenAccept(endChunk -> - scanChunk(endChunk).thenAccept(b -> - getWorldChunk(Environment.NETHER, p.x, p.z).thenAccept(netherChunk -> - scanChunk(netherChunk).thenAccept(b2 -> - getWorldChunk(Environment.NORMAL, p.x, p.z).thenAccept(normalChunk -> - scanChunk(normalChunk).thenAccept(b3 -> + // 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())))) ) @@ -591,4 +619,76 @@ public class IslandLevelCalculator { boolean isNotZeroIsland() { return !zeroIsland; } + + 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) > addon.getSettings().getCalculationTimeout() * 60000) { + // Done + pipeliner.getInProcessQueue().remove(this); + getR().complete(new Results(Result.TIMEOUT)); + addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + "m for island: " + getIsland()); + if (!isNotZeroIsland()) { + addon.logError("Island level was being zeroed."); + } + 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(); + handleChests(); + long checkTime = System.currentTimeMillis(); + finishTask = Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> { + // Check every half second if all the chests and stacks have been cleared + if ((chestBlocks.isEmpty() && stackedBlocks.isEmpty()) || System.currentTimeMillis() - checkTime > MAX_AMOUNT) { + this.tidyUp(); + this.getR().complete(getResults()); + finishTask.cancel(); + } + }, 0, 10L); + + } + }); + } + + private void handleChests() { + Iterator it = chestBlocks.iterator(); + while(it.hasNext()) { + Chunk v = it.next(); + Util.getChunkAtAsync(v.getWorld(), v.getX(), v.getZ()).thenAccept(c -> { + scanChests(c); + it.remove(); + }); + } + } + + 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/level/calculators/Pipeliner.java b/src/main/java/world/bentobox/level/calculators/Pipeliner.java index 83af5e8..6daacfb 100644 --- a/src/main/java/world/bentobox/level/calculators/Pipeliner.java +++ b/src/main/java/world/bentobox/level/calculators/Pipeliner.java @@ -50,7 +50,7 @@ public class Pipeliner { if (!iD.getIsland().isDeleted() && !iD.getIsland().isUnowned()) { inProcessQueue.put(iD, System.currentTimeMillis()); // Start the scanning of a island with the first chunk - scanChunk(iD); + scanIsland(iD); } } }, 1L, 10L); @@ -71,42 +71,14 @@ public class Pipeliner { * Scans one chunk of an island and adds the results to a results object * @param iD */ - private void scanChunk(IslandLevelCalculator iD) { + private void scanIsland(IslandLevelCalculator 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; } - // Scan the next chunk - iD.scanNextChunk().thenAccept(r -> { - if (!Bukkit.isPrimaryThread()) { - addon.getPlugin().logError("scanChunk not on Primary Thread!"); - } - // Timeout check - if (System.currentTimeMillis() - inProcessQueue.get(iD) > addon.getSettings().getCalculationTimeout() * 60000) { - // Done - inProcessQueue.remove(iD); - iD.getR().complete(new Results(Result.TIMEOUT)); - addon.logError("Level calculation timed out after " + addon.getSettings().getCalculationTimeout() + "m for island: " + iD.getIsland()); - if (!iD.isNotZeroIsland()) { - addon.logError("Island level was being zeroed."); - } - return; - } - if (Boolean.TRUE.equals(r) || task.isCancelled()) { - // scanNextChunk returns true if there are more chunks to scan - scanChunk(iD); - } else { - // Done - inProcessQueue.remove(iD); - // Chunk finished - // This was the last chunk - iD.tidyUp(); - iD.getR().complete(iD.getResults()); - } - }); - + iD.scanIsland(this); } @@ -169,6 +141,20 @@ public class Pipeliner { this.toProcessQueue.clear(); } + /** + * @return the inProcessQueue + */ + protected Map getInProcessQueue() { + return inProcessQueue; + } + + /** + * @return the task + */ + protected BukkitTask getTask() { + return task; + } + diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml new file mode 100644 index 0000000..8ff6d15 --- /dev/null +++ b/src/main/resources/locales/vi.yml @@ -0,0 +1,56 @@ +# +# This is a YML file. Be careful when editing. Check your edits in a YAML checker like # +# the one at http://yaml-online-parser.appspot.com # +admin: + level: + parameters: + description: tính toán cấp độ đảo của người chơi + sethandicap: + parameters: + description: chỉnh cấp của các đảo bắt đầu + changed: '&a Cấp đảo bắt đầu đã chỉnh từ [number] thành [new_number].' + invalid-level: '&c Cấp không xác định. Hãy dùng số nguyên.' + levelstatus: + description: xem bao nhiêu đảo đang trong hàng chờ được quét + islands-in-queue: '&a Đảo đang chờ: [number]' + top: + description: xem bảng xếp hạng TOP 10 + unknown-world: '&c Thế giới không xác định!' + display: '&f[rank]. &a[name] &7- &b[level]' + remove: + description: xoá người khỏi TOP 10 + parameters: +island: + level: + parameters: '[người chơi]' + description: tính toán cấp đảo của bạn hoặc xem cấp đảo của [người chơi] + calculating: '&a Đang tính toán cấp đảo...' + estimated-wait: '&a Thời gian còn lại: [number] giây' + in-queue: '&a Bạn đang ở vị trí [number] trong hàng chờ' + island-level-is: '&a Cấp đảo là &b[level]' + required-points-to-next-level: '&a Cần [points] điểm để qua cấp tiếp theo' + deaths: '&c([number] lần chết)' + cooldown: '&c Bạn phải chờ &b[time] &c giây trước khi có thể làm điều đó' + in-progress: '&6 Quá trình tính toán cấp đảo đang thực hiện...' + time-out: '&c Tính toán cấp đảo quá lâu. Vui lòng thử lại sau.' + top: + description: xem TOP 10 + gui-title: '&a TOP 10' + gui-heading: '&6[name]: &B[rank]' + island-level: '&b Cấp [level]' + warp-to: '&A Đang dịch chuyển đến đảo của [name]' + level-details: + above-sea-level-blocks: Khối Trên Mực Nước Biển + spawners: Lồng Sinh Quái + underwater-blocks: Khối Dưới Nước + all-blocks: Toàn Bộ Khối + no-island: '&c Không có đảo!' + names-island: 'đảo của [name]' + syntax: '[name] x [number]' + hint: '&c Chạy lệnh cấp để xem báo cáo khối' + value: + description: xem giá trị của bất kì khối + success: '&7 Giá trị của khối này là: &e[value]' + success-underwater: '&7 Giá trị của khối này dưới mực nước biển: &e[value]' + empty-hand: '&c Không có khối nào trên tay bạn' + no-value: '&c Vật phẩm này vô giá trị.'