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. --> <!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number> <build.number>-LOCAL</build.number>
<!-- This allows to change between versions. --> <!-- This allows to change between versions. -->
<build.version>1.8.0</build.version> <build.version>1.8.1</build.version>
</properties> </properties>
<!-- Profiles will allow to automatically change build version. --> <!-- Profiles will allow to automatically change build version. -->
@ -147,6 +147,10 @@
<id>codemc-public</id> <id>codemc-public</id>
<url>https://repo.codemc.org/repository/maven-public/</url> <url>https://repo.codemc.org/repository/maven-public/</url>
</repository> </repository>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
</repositories> </repositories>
<dependencies> <dependencies>
@ -182,6 +186,12 @@
<version>${bentobox.version}</version> <version>${bentobox.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>io.papermc</groupId>
<artifactId>paperlib</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -278,6 +288,28 @@
<artifactId>maven-deploy-plugin</artifactId> <artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version> <version>2.8.2</version>
</plugin> </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> <plugin>
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <artifactId>jacoco-maven-plugin</artifactId>

View File

@ -8,6 +8,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChunkSnapshot; import org.bukkit.ChunkSnapshot;
@ -16,13 +18,13 @@ import org.bukkit.Tag;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Slab; import org.bukkit.block.data.type.Slab;
import org.bukkit.scheduler.BukkitTask;
import com.google.common.collect.HashMultiset; import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset; import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Multisets; import com.google.common.collect.Multisets;
import io.papermc.lib.PaperLib;
import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Pair; import world.bentobox.bentobox.util.Pair;
import world.bentobox.bentobox.util.Util; import world.bentobox.bentobox.util.Util;
@ -31,11 +33,9 @@ import world.bentobox.level.Level;
public class CalcIslandLevel { public class CalcIslandLevel {
private final int maxChunks;
private final long speed;
private static final String LINE_BREAK = "=================================="; private static final String LINE_BREAK = "==================================";
private boolean checking;
private final BukkitTask task; public static final long MAX_AMOUNT = 10000;
private final Level addon; private final Level addon;
@ -49,6 +49,8 @@ public class CalcIslandLevel {
private final World world; private final World world;
private final List<World> worlds; private final List<World> worlds;
private int count;
/** /**
* Calculate the island's level * Calculate the island's level
@ -78,56 +80,22 @@ public class CalcIslandLevel {
result = new Results(); result = new Results();
// Set the initial island handicap // Set the initial island handicap
result.initialLevel = addon.getInitialIslandLevel(island); result.setInitialLevel(addon.getInitialIslandLevel(island));
speed = addon.getSettings().getUpdateTickDelay();
maxChunks = addon.getSettings().getChunksPerTick();
// Get chunks to scan // Get chunks to scan
chunksToScan = getChunksToScan(island); chunksToScan = getChunksToScan(island);
count = 0;
// Start checking chunksToScan.forEach(c -> {
checking = true; PaperLib.getChunkAtAsync(world, c.x, c.z).thenAccept(ch -> {
ChunkSnapshot snapShot = ch.getChunkSnapshot();
// Start a recurring task until done or cancelled Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> {
task = addon.getServer().getScheduler().runTaskTimer(addon.getPlugin(), () -> { this.scanChunk(snapShot);
Set<ChunkSnapshot> chunkSnapshot = new HashSet<>(); count++;
if (checking) { if (count == chunksToScan.size()) {
Iterator<Pair<Integer, Integer>> it = chunksToScan.iterator(); this.tidyUp();
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());
}
}
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) { private void checkBlock(BlockData bd, boolean belowSeaLevel) {
int count = limitCount(bd.getMaterial()); int count = limitCount(bd.getMaterial());
if (belowSeaLevel) { if (belowSeaLevel) {
result.underWaterBlockCount += count; result.underWaterBlockCount.addAndGet(count);
result.uwCount.add(bd.getMaterial()); result.uwCount.add(bd.getMaterial());
} else { } else {
result.rawBlockCount += count; result.rawBlockCount.addAndGet(count);
result.mdCount.add(bd.getMaterial()); result.mdCount.add(bd.getMaterial());
} }
} }
@ -231,40 +199,40 @@ public class CalcIslandLevel {
} }
private void tidyUp() { private void tidyUp() {
// Cancel
task.cancel();
// Finalize calculations // Finalize calculations
result.rawBlockCount += (long)(result.underWaterBlockCount * addon.getSettings().getUnderWaterMultiplier()); result.rawBlockCount.addAndGet((long)(result.underWaterBlockCount.get() * addon.getSettings().getUnderWaterMultiplier()));
// Set the death penalty // Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) if (this.addon.getSettings().isSumTeamDeaths())
{ {
for (UUID uuid : this.island.getMemberSet()) 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 else
{ {
// At this point, it may be that the island has become unowned. // At this point, it may be that the island has become unowned.
this.result.deathHandicap = this.island.getOwner() == null ? 0 : this.result.deathHandicap.set(this.island.getOwner() == null ? 0 :
this.addon.getPlayers().getDeaths(this.world, this.island.getOwner()); 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) if (this.addon.getSettings().getDeathPenalty() > 0)
{ {
// Proper death penalty calculation. // Proper death penalty calculation.
blockAndDeathPoints -= this.result.deathHandicap * this.addon.getSettings().getDeathPenalty(); blockAndDeathPoints -= this.result.deathHandicap.get() * this.addon.getSettings().getDeathPenalty();
} }
this.result.level.set(calculateLevel(blockAndDeathPoints));
this.result.level = blockAndDeathPoints / this.addon.getSettings().getLevelCost() - this.island.getLevelHandicap() - result.initialLevel;
// Calculate how many points are required to get to the next level // Calculate how many points are required to get to the next level
this.result.pointsToNextLevel = this.addon.getSettings().getLevelCost() - long nextLevel = this.result.level.get();
(blockAndDeathPoints % this.addon.getSettings().getLevelCost()); long blocks = blockAndDeathPoints;
while (nextLevel < this.result.level.get() + 1 && blocks - blockAndDeathPoints < MAX_AMOUNT) {
nextLevel = calculateLevel(++blocks);
}
this.result.pointsToNextLevel.set(blocks - blockAndDeathPoints);
// Report // Report
result.report = getReport(); 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() { private List<String> getReport() {
List<String> reportLines = new ArrayList<>(); List<String> reportLines = new ArrayList<>();
// provide counts // provide counts
reportLines.add("Level Log for island in " + addon.getPlugin().getIWM().getFriendlyName(island.getWorld()) + " at " + Util.xyz(island.getCenter().toVector())); 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("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("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Deaths handicap = " + result.deathHandicap); reportLines.add("Deaths handicap = " + result.deathHandicap.get());
reportLines.add("Initial island level = " + (0L - result.initialLevel)); reportLines.add("Initial island level = " + (0L - result.initialLevel.get()));
reportLines.add("Level calculated = " + result.level); reportLines.add("Level calculated = " + result.level.get());
reportLines.add(LINE_BREAK); reportLines.add(LINE_BREAK);
int total = 0; int total = 0;
if (!result.uwCount.isEmpty()) { if (!result.uwCount.isEmpty()) {
@ -359,18 +334,19 @@ public class CalcIslandLevel {
private final Multiset<Material> uwCount = HashMultiset.create(); private final Multiset<Material> uwCount = HashMultiset.create();
private final Multiset<Material> ncCount = HashMultiset.create(); private final Multiset<Material> ncCount = HashMultiset.create();
private final Multiset<Material> ofCount = HashMultiset.create(); private final Multiset<Material> ofCount = HashMultiset.create();
private long rawBlockCount = 0; // AtomicLong and AtomicInteger must be used because they are changed by multiple concurrent threads
private long underWaterBlockCount = 0; private AtomicLong rawBlockCount = new AtomicLong(0);
private long level = 0; private AtomicLong underWaterBlockCount = new AtomicLong(0);
private int deathHandicap = 0; private AtomicLong level = new AtomicLong(0);
private long pointsToNextLevel = 0; private AtomicInteger deathHandicap = new AtomicInteger(0);
private long initialLevel = 0; private AtomicLong pointsToNextLevel = new AtomicLong(0);
private AtomicLong initialLevel = new AtomicLong(0);
/** /**
* @return the deathHandicap * @return the deathHandicap
*/ */
public int getDeathHandicap() { public int getDeathHandicap() {
return deathHandicap; return deathHandicap.get();
} }
/** /**
@ -384,27 +360,27 @@ public class CalcIslandLevel {
* @param level - level * @param level - level
*/ */
public void setLevel(int level) { public void setLevel(int level) {
this.level = level; this.level.set(level);
} }
/** /**
* @return the level * @return the level
*/ */
public long getLevel() { public long getLevel() {
return level; return level.get();
} }
/** /**
* @return the pointsToNextLevel * @return the pointsToNextLevel
*/ */
public long getPointsToNextLevel() { public long getPointsToNextLevel() {
return pointsToNextLevel; return pointsToNextLevel.get();
} }
public long getInitialLevel() { public long getInitialLevel() {
return initialLevel; return initialLevel.get();
} }
public void setInitialLevel(long initialLevel) { public void setInitialLevel(long initialLevel) {
this.initialLevel = initialLevel; this.initialLevel.set(initialLevel);
} }
/* (non-Javadoc) /* (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())); asker.sendMessage("island.level.deaths", "[number]", String.valueOf(results.getDeathHandicap()));
} }
// Send player how many points are required to reach next island level // 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())); asker.sendMessage("island.level.required-points-to-next-level", "[points]", String.valueOf(results.getPointsToNextLevel()));
} }
// Tell other team members // Tell other team members

View File

@ -23,16 +23,6 @@ public class Settings {
private long levelCost; private long levelCost;
private int levelWait; 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; private List<String> gameModes;
@ -43,22 +33,6 @@ public class Settings {
// GameModes // GameModes
gameModes = level.getConfig().getStringList("game-modes"); 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)); setLevelWait(level.getConfig().getInt("levelwait", 60));
if (getLevelWait() < 0) { if (getLevelWait() < 0) {
setLevelWait(0); setLevelWait(0);
@ -246,45 +220,11 @@ public class Settings {
return level.getConfig().getBoolean("shorthand"); return level.getConfig().getBoolean("shorthand");
} }
/** /**
* This method returns the number of chunks that can be processed at single tick. * @return the formula to calculate island levels
* @return the value of chunksPerTick.
*/ */
public int getChunksPerTick() public String getLevelCalc() {
{ return level.getConfig().getString("level-calc", "blocks / level_cost");
return this.chunksPerTick;
} }
/**
* 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. # Value of one island level. Default 100. Minimum value is 1.
levelcost: 100 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 # Cooldown between level requests in seconds
levelwait: 60 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 # Death penalty
# How many block values a player will lose per death. # 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) # Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)