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)