From d4c9bd654dbb6fa77241da2db0733a61b66019f4 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 15 Nov 2019 19:31:58 -0800 Subject: [PATCH] Uses PaperLib to get chunks async (#95) * Uses PaperLib to get chunks async. Appears to work fine on regular Spigot too. * Removes config settings not required anymore. * Future is on main thread, so do calcs async * Implements multi-threaded level calculation. * Remove debug from version --- pom.xml | 34 ++- .../level/calculators/CalcIslandLevel.java | 220 +++++++++++------- .../level/calculators/PlayerLevel.java | 2 +- .../world/bentobox/level/config/Settings.java | 66 +----- src/main/resources/config.yml | 16 +- 5 files changed, 183 insertions(+), 155 deletions(-) diff --git a/pom.xml b/pom.xml index 3054232..93f075a 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ -LOCAL - 1.8.0 + 1.8.1 @@ -147,6 +147,10 @@ codemc-public https://repo.codemc.org/repository/maven-public/ + + papermc + https://papermc.io/repo/repository/maven-public/ + @@ -182,6 +186,12 @@ ${bentobox.version} provided + + io.papermc + paperlib + 1.0.2 + compile + @@ -278,6 +288,28 @@ maven-deploy-plugin 2.8.2 + + org.apache.maven.plugins + maven-shade-plugin + 3.1.1 + + ${project.build.directory}/dependency-reduced-pom.xml + + + io.papermc.lib + world.bentobox.level.paperlib + + + + + + package + + shade + + + + org.jacoco jacoco-maven-plugin diff --git a/src/main/java/world/bentobox/level/calculators/CalcIslandLevel.java b/src/main/java/world/bentobox/level/calculators/CalcIslandLevel.java index 9eb846a..4a19578 100644 --- a/src/main/java/world/bentobox/level/calculators/CalcIslandLevel.java +++ b/src/main/java/world/bentobox/level/calculators/CalcIslandLevel.java @@ -8,6 +8,8 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.bukkit.Bukkit; import org.bukkit.ChunkSnapshot; @@ -16,13 +18,13 @@ import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Slab; -import org.bukkit.scheduler.BukkitTask; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multisets; +import io.papermc.lib.PaperLib; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Util; @@ -31,11 +33,9 @@ import world.bentobox.level.Level; public class CalcIslandLevel { - private final int maxChunks; - private final long speed; private static final String LINE_BREAK = "=================================="; - private boolean checking; - private final BukkitTask task; + + public static final long MAX_AMOUNT = 10000; private final Level addon; @@ -49,6 +49,8 @@ public class CalcIslandLevel { private final World world; private final List worlds; + private int count; + /** * Calculate the island's level @@ -78,56 +80,22 @@ public class CalcIslandLevel { result = new Results(); // Set the initial island handicap - result.initialLevel = addon.getInitialIslandLevel(island); - - speed = addon.getSettings().getUpdateTickDelay(); - maxChunks = addon.getSettings().getChunksPerTick(); + result.setInitialLevel(addon.getInitialIslandLevel(island)); // Get chunks to scan chunksToScan = getChunksToScan(island); - - // Start checking - checking = true; - - // Start a recurring task until done or cancelled - task = addon.getServer().getScheduler().runTaskTimer(addon.getPlugin(), () -> { - Set chunkSnapshot = new HashSet<>(); - if (checking) { - Iterator> it = chunksToScan.iterator(); - if (!it.hasNext()) { - // Nothing left - tidyUp(); - return; - } - // Add chunk snapshots to the list - while (it.hasNext() && chunkSnapshot.size() < maxChunks) { - Pair pair = it.next(); - for (World worldToScan : worlds) { - if (!worldToScan.isChunkLoaded(pair.x, pair.z)) { - //worldToScan.loadChunk(pair.x, pair.z); - chunkSnapshot.add(worldToScan.getChunkAt(pair.x, pair.z).getChunkSnapshot()); - worldToScan.unloadChunk(pair.x, pair.z, false); - } else { - chunkSnapshot.add(worldToScan.getChunkAt(pair.x, pair.z).getChunkSnapshot()); - } + count = 0; + chunksToScan.forEach(c -> { + PaperLib.getChunkAtAsync(world, c.x, c.z).thenAccept(ch -> { + ChunkSnapshot snapShot = ch.getChunkSnapshot(); + Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { + this.scanChunk(snapShot); + count++; + if (count == chunksToScan.size()) { + this.tidyUp(); } - it.remove(); - } - // Move to next step - checking = false; - checkChunksAsync(chunkSnapshot); - } - }, 0L, speed); - } - - private void checkChunksAsync(final Set chunkSnapshot) { - // Run async task to scan chunks - addon.getServer().getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> { - for (ChunkSnapshot chunk: chunkSnapshot) { - scanChunk(chunk); - } - // Nothing happened, change state - checking = true; + }); + }); }); } @@ -168,10 +136,10 @@ public class CalcIslandLevel { private void checkBlock(BlockData bd, boolean belowSeaLevel) { int count = limitCount(bd.getMaterial()); if (belowSeaLevel) { - result.underWaterBlockCount += count; + result.underWaterBlockCount.addAndGet(count); result.uwCount.add(bd.getMaterial()); } else { - result.rawBlockCount += count; + result.rawBlockCount.addAndGet(count); result.mdCount.add(bd.getMaterial()); } } @@ -231,40 +199,40 @@ public class CalcIslandLevel { } private void tidyUp() { - // Cancel - task.cancel(); // Finalize calculations - result.rawBlockCount += (long)(result.underWaterBlockCount * addon.getSettings().getUnderWaterMultiplier()); + result.rawBlockCount.addAndGet((long)(result.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier())); // Set the death penalty if (this.addon.getSettings().isSumTeamDeaths()) { for (UUID uuid : this.island.getMemberSet()) { - this.result.deathHandicap += this.addon.getPlayers().getDeaths(this.world, uuid); + this.result.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(this.world, uuid)); } } else { // At this point, it may be that the island has become unowned. - this.result.deathHandicap = this.island.getOwner() == null ? 0 : - this.addon.getPlayers().getDeaths(this.world, this.island.getOwner()); + this.result.deathHandicap.set(this.island.getOwner() == null ? 0 : + this.addon.getPlayers().getDeaths(this.world, this.island.getOwner())); } - long blockAndDeathPoints = this.result.rawBlockCount; + long blockAndDeathPoints = this.result.rawBlockCount.get(); if (this.addon.getSettings().getDeathPenalty() > 0) { // Proper death penalty calculation. - blockAndDeathPoints -= this.result.deathHandicap * this.addon.getSettings().getDeathPenalty(); + blockAndDeathPoints -= this.result.deathHandicap.get() * this.addon.getSettings().getDeathPenalty(); } - - this.result.level = blockAndDeathPoints / this.addon.getSettings().getLevelCost() - this.island.getLevelHandicap() - result.initialLevel; - + this.result.level.set(calculateLevel(blockAndDeathPoints)); // Calculate how many points are required to get to the next level - this.result.pointsToNextLevel = this.addon.getSettings().getLevelCost() - - (blockAndDeathPoints % this.addon.getSettings().getLevelCost()); + long nextLevel = this.result.level.get(); + long blocks = blockAndDeathPoints; + while (nextLevel < this.result.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) { + nextLevel = calculateLevel(++blocks); + } + this.result.pointsToNextLevel.set(blocks - blockAndDeathPoints); // Report result.report = getReport(); @@ -275,16 +243,23 @@ public class CalcIslandLevel { } + private long calculateLevel(long blockAndDeathPoints) { + String calcString = addon.getSettings().getLevelCalc(); + String withValues = calcString.replace("blocks", String.valueOf(blockAndDeathPoints)).replace("level_cost", String.valueOf(this.addon.getSettings().getLevelCost())); + return (long)eval(withValues) - this.island.getLevelHandicap() - result.initialLevel.get(); + } + private List getReport() { List reportLines = new ArrayList<>(); // provide counts reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + " at " + Util.xyz(island.getCenter().toVector())); reportLines.add("Island owner UUID = " + island.getOwner()); - reportLines.add("Total block value count = " + String.format("%,d",result.rawBlockCount)); + reportLines.add("Total block value count = " + String.format("%,d",result.rawBlockCount.get())); + reportLines.add("Formula to calculate island level: " + addon.getSettings().getLevelCalc()); reportLines.add("Level cost = " + addon.getSettings().getLevelCost()); - reportLines.add("Deaths handicap = " + result.deathHandicap); - reportLines.add("Initial island level = " + (0L - result.initialLevel)); - reportLines.add("Level calculated = " + result.level); + reportLines.add("Deaths handicap = " + result.deathHandicap.get()); + reportLines.add("Initial island level = " + (0L - result.initialLevel.get())); + reportLines.add("Level calculated = " + result.level.get()); reportLines.add(LINE_BREAK); int total = 0; if (!result.uwCount.isEmpty()) { @@ -359,18 +334,19 @@ public class CalcIslandLevel { private final Multiset uwCount = HashMultiset.create(); private final Multiset ncCount = HashMultiset.create(); private final Multiset ofCount = HashMultiset.create(); - private long rawBlockCount = 0; - private long underWaterBlockCount = 0; - private long level = 0; - private int deathHandicap = 0; - private long pointsToNextLevel = 0; - private long initialLevel = 0; + // AtomicLong and AtomicInteger must be used because they are changed by multiple concurrent threads + private AtomicLong rawBlockCount = new AtomicLong(0); + private AtomicLong underWaterBlockCount = new AtomicLong(0); + private AtomicLong level = new AtomicLong(0); + private AtomicInteger deathHandicap = new AtomicInteger(0); + private AtomicLong pointsToNextLevel = new AtomicLong(0); + private AtomicLong initialLevel = new AtomicLong(0); /** * @return the deathHandicap */ public int getDeathHandicap() { - return deathHandicap; + return deathHandicap.get(); } /** @@ -384,27 +360,27 @@ public class CalcIslandLevel { * @param level - level */ public void setLevel(int level) { - this.level = level; + this.level.set(level); } /** * @return the level */ public long getLevel() { - return level; + return level.get(); } /** * @return the pointsToNextLevel */ public long getPointsToNextLevel() { - return pointsToNextLevel; + return pointsToNextLevel.get(); } public long getInitialLevel() { - return initialLevel; + return initialLevel.get(); } public void setInitialLevel(long initialLevel) { - this.initialLevel = initialLevel; + this.initialLevel.set(initialLevel); } /* (non-Javadoc) @@ -419,4 +395,84 @@ public class CalcIslandLevel { } } + + private static double eval(final String str) { + return new Object() { + int pos = -1, ch; + + void nextChar() { + ch = (++pos < str.length()) ? str.charAt(pos) : -1; + } + + boolean eat(int charToEat) { + while (ch == ' ') nextChar(); + if (ch == charToEat) { + nextChar(); + return true; + } + return false; + } + + double parse() { + nextChar(); + double x = parseExpression(); + if (pos < str.length()) throw new RuntimeException("Unexpected: " + (char)ch); + return x; + } + + // Grammar: + // expression = term | expression `+` term | expression `-` term + // term = factor | term `*` factor | term `/` factor + // factor = `+` factor | `-` factor | `(` expression `)` + // | number | functionName factor | factor `^` factor + + double parseExpression() { + double x = parseTerm(); + for (;;) { + if (eat('+')) x += parseTerm(); // addition + else if (eat('-')) x -= parseTerm(); // subtraction + else return x; + } + } + + double parseTerm() { + double x = parseFactor(); + for (;;) { + if (eat('*')) x *= parseFactor(); // multiplication + else if (eat('/')) x /= parseFactor(); // division + else return x; + } + } + + double parseFactor() { + if (eat('+')) return parseFactor(); // unary plus + if (eat('-')) return -parseFactor(); // unary minus + + double x; + int startPos = this.pos; + if (eat('(')) { // parentheses + x = parseExpression(); + eat(')'); + } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers + while ((ch >= '0' && ch <= '9') || ch == '.') nextChar(); + x = Double.parseDouble(str.substring(startPos, this.pos)); + } else if (ch >= 'a' && ch <= 'z') { // functions + while (ch >= 'a' && ch <= 'z') nextChar(); + String func = str.substring(startPos, this.pos); + x = parseFactor(); + if (func.equals("sqrt")) x = Math.sqrt(x); + else if (func.equals("sin")) x = Math.sin(Math.toRadians(x)); + else if (func.equals("cos")) x = Math.cos(Math.toRadians(x)); + else if (func.equals("tan")) x = Math.tan(Math.toRadians(x)); + else throw new RuntimeException("Unknown function: " + func); + } else { + throw new RuntimeException("Unexpected: " + (char)ch); + } + + if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation + + return x; + } + }.parse(); + } } diff --git a/src/main/java/world/bentobox/level/calculators/PlayerLevel.java b/src/main/java/world/bentobox/level/calculators/PlayerLevel.java index 68a71e4..90e3f3f 100644 --- a/src/main/java/world/bentobox/level/calculators/PlayerLevel.java +++ b/src/main/java/world/bentobox/level/calculators/PlayerLevel.java @@ -90,7 +90,7 @@ public class PlayerLevel { asker.sendMessage("island.level.deaths", "[number]", String.valueOf(results.getDeathHandicap())); } // Send player how many points are required to reach next island level - if (results.getPointsToNextLevel() >= 0) { + if (results.getPointsToNextLevel() >= 0 && results.getPointsToNextLevel() < CalcIslandLevel.MAX_AMOUNT) { asker.sendMessage("island.level.required-points-to-next-level", "[points]", String.valueOf(results.getPointsToNextLevel())); } // Tell other team members diff --git a/src/main/java/world/bentobox/level/config/Settings.java b/src/main/java/world/bentobox/level/config/Settings.java index a0e1e31..bdd290e 100644 --- a/src/main/java/world/bentobox/level/config/Settings.java +++ b/src/main/java/world/bentobox/level/config/Settings.java @@ -23,16 +23,6 @@ public class Settings { private long levelCost; private int levelWait; - /** - * Stores number of chunks that can be updated in single tick. - */ - private int chunksPerTick; - - /** - * Stores number of tick delay between each chunk loading. - */ - private long updateTickDelay; - private List gameModes; @@ -43,22 +33,6 @@ public class Settings { // GameModes gameModes = level.getConfig().getStringList("game-modes"); - // Level calculation chunk load speed - this.setUpdateTickDelay(level.getConfig().getLong("updatetickdelay", 1)); - - if (this.getUpdateTickDelay() <= 0) - { - this.setUpdateTickDelay(1); - } - - // Level calculation chunk count per update - this.setChunksPerTick(level.getConfig().getInt("chunkspertick", 200)); - - if (this.getChunksPerTick() <= 0) - { - this.setChunksPerTick(200); - } - setLevelWait(level.getConfig().getInt("levelwait", 60)); if (getLevelWait() < 0) { setLevelWait(0); @@ -246,45 +220,11 @@ public class Settings { return level.getConfig().getBoolean("shorthand"); } - /** - * This method returns the number of chunks that can be processed at single tick. - * @return the value of chunksPerTick. + * @return the formula to calculate island levels */ - public int getChunksPerTick() - { - return this.chunksPerTick; + public String getLevelCalc() { + return level.getConfig().getString("level-calc", "blocks / level_cost"); } - - /** - * This method sets the chunksPerTick value. - * @param chunksPerTick the chunksPerTick new value. - * - */ - public void setChunksPerTick(int chunksPerTick) - { - this.chunksPerTick = chunksPerTick; - } - - - /** - * This method returns the delay between each update call. - * @return the value of updateTickDelay. - */ - public long getUpdateTickDelay() - { - return this.updateTickDelay; - } - - - /** - * This method sets the updateTickDelay value. - * @param updateTickDelay the updateTickDelay new value. - * - */ - public void setUpdateTickDelay(long updateTickDelay) - { - this.updateTickDelay = updateTickDelay; - } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ad79e4a..ae1f500 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -37,17 +37,17 @@ underwater: 1.0 # Value of one island level. Default 100. Minimum value is 1. levelcost: 100 +# Island level calculation formula +# blocks - the sum total of all block values, less any death penalty +# level_cost - in a linear equation, the value of one level +# This formula can include +,=,*,/,sqrt,^,sin,cos,tan. Result will always be rounded to a long integer +# for example, an alternative non-linear option could be: 3 * sqrt(blocks / level_cost) +level-calc: blocks / level_cost + + # Cooldown between level requests in seconds levelwait: 60 -# Delay between each task that loads chunks for calculating island level. -# Increasing this will increase time to calculate island level. -updatetickdelay: 1 - -# Number of chunks that will be processed at the same tick. -# Decreasing this will increase time to calculate island level. -chunkspertick: 200 - # Death penalty # How many block values a player will lose per death. # Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)