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