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
This commit is contained in:
tastybento 2019-11-15 19:31:58 -08:00 committed by GitHub
parent e383f79e3e
commit d4c9bd654d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 183 additions and 155 deletions

34
pom.xml
View File

@ -65,7 +65,7 @@
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>1.8.0</build.version>
<build.version>1.8.1</build.version>
</properties>
<!-- Profiles will allow to automatically change build version. -->
@ -147,6 +147,10 @@
<id>codemc-public</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
</repository>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
@ -182,6 +186,12 @@
<version>${bentobox.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.papermc</groupId>
<artifactId>paperlib</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
@ -278,6 +288,28 @@
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>
<relocations>
<relocation>
<pattern>io.papermc.lib</pattern>
<shadedPattern>world.bentobox.level.paperlib</shadedPattern> <!-- Replace this -->
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>

View File

@ -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<World> 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> chunkSnapshot = new HashSet<>();
if (checking) {
Iterator<Pair<Integer, Integer>> it = chunksToScan.iterator();
if (!it.hasNext()) {
// Nothing left
tidyUp();
return;
}
// Add chunk snapshots to the list
while (it.hasNext() && chunkSnapshot.size() < maxChunks) {
Pair<Integer, Integer> 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> 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<String> getReport() {
List<String> 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<Material> uwCount = HashMultiset.create();
private final Multiset<Material> ncCount = HashMultiset.create();
private final Multiset<Material> 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();
}
}

View File

@ -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

View File

@ -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<String> 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;
}
}

View File

@ -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)