diff --git a/pom.xml b/pom.xml
index 5718d06..4bc2ea1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,10 @@
dynmap-repo
https://repo.mikeprimm.com/
+
+ papermc
+ https://papermc.io/repo/repository/maven-public/
+
@@ -32,19 +36,28 @@
org.spigotmc
spigot-api
1.13.1-R0.1-SNAPSHOT
+ provided
org.bukkit
bukkit
1.13.1-R0.1-SNAPSHOT
+ provided
us.dynmap
dynmap-api
2.5
+ provided
+
+ io.papermc
+ paperlib
+ 1.0.2
+ compile
+
@@ -60,6 +73,28 @@
1.8
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.1.1
+
+ ${project.build.directory}/dependency-reduced-pom.xml
+
+
+ io.papermc.lib
+ com.wimbli.WorldBorder.paperlib
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java
index ad29191..e232fe4 100644
--- a/src/main/java/com/wimbli/WorldBorder/WBListener.java
+++ b/src/main/java/com/wimbli/WorldBorder/WBListener.java
@@ -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);
+ }
+ }
+ }
+
}
diff --git a/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java b/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java
index 8ba7ced..502f9b4 100644
--- a/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java
+++ b/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java
@@ -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,6 +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;
public class WorldFillTask implements Runnable
@@ -47,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 storedChunks = new LinkedList<>();
- private Set originalChunks = new HashSet<>();
private transient CoordXZ lastChunk = new CoordXZ(0, 0);
// for reporting progress back to user occasionally
@@ -57,7 +57,52 @@ public class WorldFillTask implements Runnable
private transient int reportTarget = 0;
private transient int reportTotal = 0;
private transient int reportNum = 0;
+
+ // A map that holds to-be-loaded chunks, and their coordinates
+ private transient Map, 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 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)
{
@@ -94,6 +139,9 @@ public class WorldFillTask implements Runnable
this.stop();
return;
}
+
+ pendingChunks = new HashMap<>();
+ preventUnload = new HashSet<>();
this.border.setRadiusX(border.getRadiusX() + fillDistance);
this.border.setRadiusZ(border.getRadiusZ() + fillDistance);
@@ -109,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)
{
@@ -132,7 +173,6 @@ public class WorldFillTask implements Runnable
this.taskID = ID;
}
-
@Override
public void run()
{
@@ -161,11 +201,86 @@ public class WorldFillTask implements Runnable
// 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();
- for (int loop = 0; loop < chunksPerRun; loop++)
+ // Process async results from last time. We don't make a difference
+ // whether they were really async, or sync.
+
+ // 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, CoordXZ> newPendingChunks = new HashMap<>();
+ Set chunksToUnload = new HashSet<>();
+ for (CompletableFuture 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);
+ }
+ 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 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);
+ }
+ }
+
+ // 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();
@@ -184,7 +299,9 @@ public class WorldFillTask implements Runnable
while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8))
{
if (!moveToNext())
+ {
return;
+ }
}
insideBorder = true;
@@ -197,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;
@@ -206,40 +325,26 @@ public class WorldFillTask implements Runnable
}
}
- // load the target chunk and generate it if necessary
- world.loadChunk(x, z, true);
- worldData.chunkExistsNow(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));
- world.loadChunk(popX, popZ, false);
+ pendingChunks.put(PaperLib.getChunkAtAsync(world, popX, popZ, false), new CoordXZ(popX, popZ));
+ preventUnload.add(new UnloadDependency(popX, popZ, x, z));
+
// 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))
- {
- world.loadChunk(lastChunk.x, lastChunk.z, false);
- storedChunks.add(new CoordXZ(lastChunk.x, lastChunk.z));
- }
-
- // Store the coordinates of these latest 2 chunks we just loaded, so we can unload them after a bit...
- storedChunks.add(new CoordXZ(popX, popZ));
- storedChunks.add(new CoordXZ(x, 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;
}
@@ -347,11 +452,13 @@ public class WorldFillTask implements Runnable
server = null;
// go ahead and unload any chunks we still have loaded
- while(!storedChunks.isEmpty())
+ // Set preventUnload to emptry first so the ChunkUnloadEvent Listener
+ // doesn't get in our way
+ Set tempPreventUnload = preventUnload;
+ preventUnload = null;
+ for (UnloadDependency entry: tempPreventUnload)
{
- CoordXZ coord = storedChunks.remove(0);
- if (!originalChunks.contains(coord))
- world.unloadChunkRequest(coord.x, coord.z);
+ world.unloadChunkRequest(entry.neededX, entry.neededZ);
}
}
@@ -387,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()