mirror of
https://github.com/PryPurity/WorldBorder.git
synced 2024-11-15 10:25:14 +01:00
Use async Chunk generation, if possible, using PaperLib
This routes all world generation requests through PaperLib, which will generate Chunks asynchronously if the server allows it (Paper does, Spigot doesn't). This means changes to which chunks are still needed, and which can be unloaded, as well; the code keeps a list of Chunks that are needed for others, and will unload them only when the target chunk has been generated. Unloads by the server itself get prevented while the chunk is needed; else the server could decide on a tick that chunk has no players nearby and needs to be unloaded.
This commit is contained in:
parent
e6564300c7
commit
12bb4b1da9
57
pom.xml
57
pom.xml
@ -17,8 +17,8 @@
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
||||
<id>papermc</id>
|
||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>dynmap-repo</id>
|
||||
@ -29,10 +29,10 @@
|
||||
<dependencies>
|
||||
<!--Spigot-API-->
|
||||
<dependency>
|
||||
<groupId>com.destroystokyo.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.13.2-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
<groupId>com.destroystokyo.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.13.2-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!--Bukkit API-->
|
||||
<dependency>
|
||||
@ -46,6 +46,12 @@
|
||||
<artifactId>dynmap-api</artifactId>
|
||||
<version>2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.papermc</groupId>
|
||||
<artifactId>paperlib</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@ -61,6 +67,43 @@
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<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>com.wimbli.WorldBorder.paperlib</shadedPattern> <!-- Replace this -->
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>commons-lang:commons-lang</exclude>
|
||||
<exclude>com.googlecode.json-simple:json-simple</exclude>
|
||||
<exclude>junit:junit</exclude>
|
||||
<exclude>org.hamcrest:hamcrest-core</exclude>
|
||||
<exclude>com.google.guava:guava</exclude>
|
||||
<exclude>com.google.code.gson:gson</exclude>
|
||||
<exclude>org.yaml:snakeyaml</exclude>
|
||||
<exclude>org.bukkit:bukkit</exclude>
|
||||
<exclude>us.dynmap:dynmap-api</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
@ -7,6 +8,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.player.PlayerPortalEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
|
||||
|
||||
public class WBListener implements Listener
|
||||
@ -68,4 +70,24 @@ public class WBListener implements Listener
|
||||
Config.logWarn("Border-checking task was not running! Something on your server apparently killed it. It will now be restarted.");
|
||||
Config.StartBorderTimer();
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if there is a fill task running, and if yes, if it's for the
|
||||
* world that the unload event refers to and if the chunk should be
|
||||
* kept in memory because generation still needs it.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onChunkUnload(ChunkUnloadEvent e)
|
||||
{
|
||||
if (Config.fillTask!=null)
|
||||
{
|
||||
Chunk chunk=e.getChunk();
|
||||
if (e.getWorld() == Config.fillTask.getWorld()
|
||||
&& Config.fillTask.chunkOnUnloadPreventionList(chunk.getX(), chunk.getZ()))
|
||||
{
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package com.wimbli.WorldBorder;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
@ -13,8 +11,10 @@ import org.bukkit.World;
|
||||
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderFillFinishedEvent;
|
||||
import com.wimbli.WorldBorder.Events.WorldBorderFillStartEvent;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
|
||||
public class WorldFillTask implements Runnable
|
||||
@ -49,8 +49,6 @@ public class WorldFillTask implements Runnable
|
||||
private transient int length = -1;
|
||||
private transient int current = 0;
|
||||
private transient boolean insideBorder = true;
|
||||
private List<CoordXZ> storedChunks = new LinkedList<>();
|
||||
private Set<CoordXZ> originalChunks = new HashSet<>();
|
||||
private transient CoordXZ lastChunk = new CoordXZ(0, 0);
|
||||
|
||||
// for reporting progress back to user occasionally
|
||||
@ -60,8 +58,51 @@ public class WorldFillTask implements Runnable
|
||||
private transient int reportTotal = 0;
|
||||
private transient int reportNum = 0;
|
||||
|
||||
private transient boolean canUsePaperAPI = false;
|
||||
private Set<CompletableFuture<Chunk>> pendingChunks;
|
||||
// A map that holds to-be-loaded chunks, and their coordinates
|
||||
private transient Map<CompletableFuture<Chunk>, CoordXZ> pendingChunks;
|
||||
|
||||
// and a set of "Chunk a needed for Chunk b" dependencies, which
|
||||
// unfortunately can't be a Map as a chunk might be needed for
|
||||
// several others.
|
||||
private transient Set<UnloadDependency> preventUnload;
|
||||
|
||||
private class UnloadDependency
|
||||
{
|
||||
int neededX, neededZ;
|
||||
int forX, forZ;
|
||||
|
||||
UnloadDependency(int neededX, int neededZ, int forX, int forZ)
|
||||
{
|
||||
this.neededX=neededX;
|
||||
this.neededZ=neededZ;
|
||||
this.forX=forX;
|
||||
this.forZ=forZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other)
|
||||
{
|
||||
if (other == null || !(other instanceof UnloadDependency))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.neededX == ((UnloadDependency) other).neededX
|
||||
&& this.neededZ == ((UnloadDependency) other).neededZ
|
||||
&& this.forX == ((UnloadDependency) other).forX
|
||||
&& this.forZ == ((UnloadDependency) other).forZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int hash = 7;
|
||||
hash = 79 * hash + this.neededX;
|
||||
hash = 79 * hash + this.neededZ;
|
||||
hash = 79 * hash + this.forX;
|
||||
hash = 79 * hash + this.forZ;
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency, boolean forceLoad)
|
||||
{
|
||||
@ -99,10 +140,8 @@ public class WorldFillTask implements Runnable
|
||||
return;
|
||||
}
|
||||
|
||||
canUsePaperAPI = checkForPaperAPI();
|
||||
if (canUsePaperAPI) {
|
||||
pendingChunks = new HashSet<>();
|
||||
}
|
||||
pendingChunks = new HashMap<>();
|
||||
preventUnload = new HashSet<>();
|
||||
|
||||
this.border.setRadiusX(border.getRadiusX() + fillDistance);
|
||||
this.border.setRadiusZ(border.getRadiusZ() + fillDistance);
|
||||
@ -118,17 +157,10 @@ public class WorldFillTask implements Runnable
|
||||
//this.reportTarget = (this.border.getShape()) ? ((int) Math.ceil(chunkWidthX * chunkWidthZ / 4 * Math.PI + 2 * chunkWidthX)) : (chunkWidthX * chunkWidthZ);
|
||||
// Area of the ellipse just to be safe area of the rectangle
|
||||
|
||||
|
||||
// keep track of the chunks which are already loaded when the task starts, to not unload them
|
||||
Chunk[] originals = world.getLoadedChunks();
|
||||
for (Chunk original : originals)
|
||||
{
|
||||
originalChunks.add(new CoordXZ(original.getX(), original.getZ()));
|
||||
}
|
||||
|
||||
this.readyToGo = true;
|
||||
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderFillStartEvent(this));
|
||||
}
|
||||
|
||||
// for backwards compatibility
|
||||
public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency)
|
||||
{
|
||||
@ -140,16 +172,6 @@ public class WorldFillTask implements Runnable
|
||||
if (ID == -1) this.stop();
|
||||
this.taskID = ID;
|
||||
}
|
||||
|
||||
private boolean checkForPaperAPI() {
|
||||
try {
|
||||
Class.forName("com.destroystokyo.paper.PaperConfig");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
@ -177,39 +199,88 @@ public class WorldFillTask implements Runnable
|
||||
// this is set so it only does one iteration at a time, no matter how frequently the timer fires
|
||||
readyToGo = false;
|
||||
// and this is tracked to keep one iteration from dragging on too long and possibly choking the system if the user specified a really high frequency
|
||||
long loopStartTime = Config.Now();
|
||||
|
||||
// Process async results from last time. We don't make a difference
|
||||
// whether they were really async, or sync.
|
||||
|
||||
if (canUsePaperAPI) {
|
||||
Set<CompletableFuture<Chunk>> newPendingChunks = new HashSet<>();
|
||||
for (CompletableFuture<Chunk> cf: pendingChunks) {
|
||||
if (cf.isDone()) {
|
||||
try {
|
||||
Chunk chunk=cf.get();
|
||||
// System.out.println(chunk);
|
||||
if (chunk==null)
|
||||
continue;
|
||||
CoordXZ xz = new CoordXZ(chunk.getX(), chunk.getZ());
|
||||
worldData.chunkExistsNow(xz.x, xz.z);
|
||||
storedChunks.add(xz);
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
Config.log(ex.getMessage());
|
||||
}
|
||||
} else {
|
||||
newPendingChunks.add(cf);
|
||||
}
|
||||
// First, Check which chunk generations have been finished.
|
||||
// Mark those chunks as existing and unloadable, and remove
|
||||
// them from the pending set.
|
||||
int chunksProcessedLastTick = 0;
|
||||
Map<CompletableFuture<Chunk>, CoordXZ> newPendingChunks = new HashMap<>();
|
||||
Set<CoordXZ> chunksToUnload = new HashSet<>();
|
||||
for (CompletableFuture<Chunk> cf: pendingChunks.keySet())
|
||||
{
|
||||
if (cf.isDone())
|
||||
{
|
||||
++chunksProcessedLastTick;
|
||||
// If cf.get() returned the chunk reliably, pendingChunks could
|
||||
// be a set and we wouldn't have to map CFs to coords ...
|
||||
CoordXZ xz=pendingChunks.get(cf);
|
||||
worldData.chunkExistsNow(xz.x, xz.z);
|
||||
chunksToUnload.add(xz);
|
||||
}
|
||||
pendingChunks=newPendingChunks;
|
||||
if (pendingChunks.size() > chunksPerRun*2) {
|
||||
readyToGo = true;
|
||||
return;
|
||||
else
|
||||
{
|
||||
newPendingChunks.put(cf, pendingChunks.get(cf));
|
||||
}
|
||||
}
|
||||
pendingChunks = newPendingChunks;
|
||||
|
||||
// Next, check which chunks had been loaded because a to-be-generated
|
||||
// chunk needed them, and don't have to remain in memory any more.
|
||||
Set<UnloadDependency> newPreventUnload = new HashSet<>();
|
||||
for (UnloadDependency dependency: preventUnload)
|
||||
{
|
||||
if (worldData.doesChunkExist(dependency.forX, dependency.forZ))
|
||||
{
|
||||
chunksToUnload.add(new CoordXZ(dependency.neededX, dependency.neededZ));
|
||||
}
|
||||
else
|
||||
{
|
||||
newPreventUnload.add(dependency);
|
||||
}
|
||||
}
|
||||
preventUnload = newPreventUnload;
|
||||
|
||||
// Unload all chunks that aren't needed anymore. NB a chunk could have
|
||||
// been needed for two different others, or been generated and needed
|
||||
// for one other chunk, so it might be in the unload set wrongly.
|
||||
// The ChunkUnloadListener checks this anyway, but it doesn't hurt to
|
||||
// save a few µs by not even requesting the unload.
|
||||
|
||||
for (CoordXZ unload: chunksToUnload)
|
||||
{
|
||||
if (!chunkOnUnloadPreventionList(unload.x, unload.z))
|
||||
{
|
||||
world.unloadChunkRequest(unload.x, unload.z);
|
||||
}
|
||||
}
|
||||
|
||||
long loopStartTime = Config.Now();
|
||||
for (int loop = 0; loop < chunksPerRun; loop++)
|
||||
// Put some damper on chunksPerRun. We don't want the queue to be too
|
||||
// full; only fill it to a bit more than what we can
|
||||
// process per tick. This ensures the background task can run at
|
||||
// full speed and we recover from a temporary drop in generation rate,
|
||||
// but doesn't push user-induced chunk generations behind a very
|
||||
// long queue of fill-generations.
|
||||
|
||||
int chunksToProcess = chunksPerRun;
|
||||
if (chunksProcessedLastTick > 0 || pendingChunks.size() > 0)
|
||||
{
|
||||
// Note we generally queue 3 chunks, so real numbers are 1/3 of chunksProcessedLastTick and pendingchunks.size
|
||||
int chunksExpectedToGetProcessed = (chunksProcessedLastTick - pendingChunks.size()) / 3 + 3;
|
||||
if (chunksExpectedToGetProcessed < chunksToProcess)
|
||||
chunksToProcess = chunksExpectedToGetProcessed;
|
||||
}
|
||||
|
||||
for (int loop = 0; loop < chunksToProcess; loop++)
|
||||
{
|
||||
// in case the task has been paused while we're repeating...
|
||||
if (paused || pausedForMemory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long now = Config.Now();
|
||||
|
||||
@ -228,7 +299,9 @@ public class WorldFillTask implements Runnable
|
||||
while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8))
|
||||
{
|
||||
if (!moveToNext())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
insideBorder = true;
|
||||
|
||||
@ -241,7 +314,9 @@ public class WorldFillTask implements Runnable
|
||||
rLoop++;
|
||||
insideBorder = true;
|
||||
if (!moveToNext())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (rLoop > 255)
|
||||
{ // only skim through max 256 chunks (~8 region files) at a time here, to allow process to take a break if needed
|
||||
readyToGo = true;
|
||||
@ -250,51 +325,26 @@ public class WorldFillTask implements Runnable
|
||||
}
|
||||
}
|
||||
|
||||
// load the target chunk and generate it if necessary
|
||||
if (canUsePaperAPI) {
|
||||
pendingChunks.add(world.getChunkAtAsync(x, z, true));
|
||||
} else {
|
||||
world.loadChunk(x, z, true);
|
||||
worldData.chunkExistsNow(x, z);
|
||||
storedChunks.add(new CoordXZ(x, z));
|
||||
}
|
||||
pendingChunks.put(PaperLib.getChunkAtAsync(world, x, z, true), new CoordXZ(x, z));
|
||||
|
||||
// There need to be enough nearby chunks loaded to make the server populate a chunk with trees, snow, etc.
|
||||
// So, we keep the last few chunks loaded, and need to also temporarily load an extra inside chunk (neighbor closest to center of map)
|
||||
int popX = !isZLeg ? x : (x + (isNeg ? -1 : 1));
|
||||
int popZ = isZLeg ? z : (z + (!isNeg ? -1 : 1));
|
||||
|
||||
pendingChunks.put(PaperLib.getChunkAtAsync(world, popX, popZ, false), new CoordXZ(popX, popZ));
|
||||
preventUnload.add(new UnloadDependency(popX, popZ, x, z));
|
||||
|
||||
if (canUsePaperAPI) {
|
||||
pendingChunks.add(world.getChunkAtAsync(popX, popZ, false));
|
||||
} else {
|
||||
world.loadChunk(popX, popZ, false);
|
||||
storedChunks.add(new CoordXZ(popX, popZ));
|
||||
}
|
||||
|
||||
// make sure the previous chunk in our spiral is loaded as well (might have already existed and been skipped over)
|
||||
if (!storedChunks.contains(lastChunk) && !originalChunks.contains(lastChunk))
|
||||
{
|
||||
if (canUsePaperAPI) {
|
||||
pendingChunks.add(world.getChunkAtAsync(lastChunk.x, lastChunk.z, false));
|
||||
} else {
|
||||
world.loadChunk(lastChunk.x, lastChunk.z, false);
|
||||
storedChunks.add(new CoordXZ(lastChunk.x, lastChunk.z));
|
||||
}
|
||||
}
|
||||
|
||||
// If enough stored chunks are buffered in, go ahead and unload the oldest to free up memory
|
||||
while (storedChunks.size() > 8)
|
||||
{
|
||||
CoordXZ coord = storedChunks.remove(0);
|
||||
if (!originalChunks.contains(coord))
|
||||
world.unloadChunkRequest(coord.x, coord.z);
|
||||
}
|
||||
pendingChunks.put(PaperLib.getChunkAtAsync(world, lastChunk.x, lastChunk.z, false), new CoordXZ(lastChunk.x, lastChunk.z)); // <-- new CoordXZ as lastChunk isn't immutable
|
||||
preventUnload.add(new UnloadDependency(lastChunk.x, lastChunk.z, x, z));
|
||||
|
||||
// move on to next chunk
|
||||
if (!moveToNext())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ready for the next iteration to run
|
||||
readyToGo = true;
|
||||
}
|
||||
@ -401,13 +451,15 @@ public class WorldFillTask implements Runnable
|
||||
server.getScheduler().cancelTask(taskID);
|
||||
server = null;
|
||||
|
||||
// go ahead and unload any chunks we still have loaded
|
||||
while(!storedChunks.isEmpty())
|
||||
{
|
||||
CoordXZ coord = storedChunks.remove(0);
|
||||
if (!originalChunks.contains(coord))
|
||||
world.unloadChunkRequest(coord.x, coord.z);
|
||||
}
|
||||
// go ahead and unload any chunks we still have loaded
|
||||
// Set preventUnload to emptry first so the ChunkUnloadEvent Listener
|
||||
// doesn't get in our way
|
||||
Set<UnloadDependency> tempPreventUnload = preventUnload;
|
||||
preventUnload = null;
|
||||
for (UnloadDependency entry: tempPreventUnload)
|
||||
{
|
||||
world.unloadChunkRequest(entry.neededX, entry.neededZ);
|
||||
}
|
||||
}
|
||||
|
||||
// is this task still valid/workable?
|
||||
@ -442,6 +494,26 @@ public class WorldFillTask implements Runnable
|
||||
{
|
||||
return this.paused || this.pausedForMemory;
|
||||
}
|
||||
|
||||
public boolean chunkOnUnloadPreventionList(int x, int z)
|
||||
{
|
||||
if (preventUnload != null)
|
||||
{
|
||||
for (UnloadDependency entry: preventUnload)
|
||||
{
|
||||
if (entry.neededX == x && entry.neededZ == z)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public World getWorld()
|
||||
{
|
||||
return world;
|
||||
}
|
||||
|
||||
// let the user know how things are coming along
|
||||
private void reportProgress()
|
||||
|
Loading…
Reference in New Issue
Block a user