diff --git a/pom.xml b/pom.xml index a9a033e..e55ff5a 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,8 @@ - papermc - https://papermc.io/repo/repository/maven-public/ + papermc + https://papermc.io/repo/repository/maven-public/ dynmap-repo @@ -29,10 +29,10 @@ - com.destroystokyo.paper - paper-api - 1.13.2-R0.1-SNAPSHOT - provided + com.destroystokyo.paper + paper-api + 1.13.2-R0.1-SNAPSHOT + provided @@ -46,6 +46,12 @@ dynmap-api 2.5 + + io.papermc + paperlib + 1.0.2 + compile + @@ -61,6 +67,43 @@ 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 + + + + + commons-lang:commons-lang + com.googlecode.json-simple:json-simple + junit:junit + org.hamcrest:hamcrest-core + com.google.guava:guava + com.google.code.gson:gson + org.yaml:snakeyaml + org.bukkit:bukkit + us.dynmap:dynmap-api + + + + + + + diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java index ad29191..6797a18 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 49eab92..328db9c 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,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 storedChunks = new LinkedList<>(); - private Set 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> pendingChunks; + // 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) { @@ -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> newPendingChunks = new HashSet<>(); - for (CompletableFuture 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, 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); } - 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 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 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()