package com.leontg77.uhc.worlds.pregen; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.entity.Player; import com.leontg77.uhc.utils.LocationUtils; public class WorldFillTask implements Runnable { // general task-related reference data private transient Server server = null; private transient World world = null; private transient WorldFileData worldData = null; private transient boolean readyToGo = false; private transient boolean paused = false; private transient boolean pausedForMemory = false; private transient int taskID = -1; private transient Player notifyPlayer = null; private transient int chunksPerRun = 1; private transient boolean continueNotice = false; private transient boolean forceLoad = false; // these are only stored for saving task to config private transient int fillDistance = 208; private transient int tickFrequency = 1; private transient int refX = 0, lastLegX = 0; private transient int refZ = 0, lastLegZ = 0; private transient int refLength = -1; private transient int refTotal = 0, lastLegTotal = 0; // values for the spiral pattern check which fills out the map to the border private transient int x = 0; private transient int z = 0; private transient boolean isZLeg = false; private transient boolean isNeg = false; private transient int length = -1; private transient int current = 0; private transient boolean insideBorder = true; private List<CoordXZ> storedChunks = new LinkedList<CoordXZ>(); private Set<CoordXZ> originalChunks = new HashSet<CoordXZ>(); private transient CoordXZ lastChunk = new CoordXZ(0, 0); // for reporting progress back to user occasionally private transient long lastReport = Config.Now(); private transient long lastAutosave = Config.Now(); private transient int reportTarget = 0; private transient int reportTotal = 0; private transient int reportNum = 0; public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency, boolean forceLoad) { this.server = theServer; this.notifyPlayer = player; this.fillDistance = fillDistance; this.tickFrequency = tickFrequency; this.chunksPerRun = chunksPerRun; this.forceLoad = forceLoad; this.world = server.getWorld(worldName); if (this.world == null) { if (worldName.isEmpty()) sendMessage("You must specify a world!"); else sendMessage("World \"" + worldName + "\" not found!"); this.stop(); return; } // load up a new WorldFileData for the world in question, used to scan // region files for which chunks are already fully generated and such worldData = WorldFileData.create(world, notifyPlayer); if (worldData == null) { this.stop(); return; } this.x = CoordXZ.blockToChunk(world.getWorldBorder().getCenter() .getBlockX()); this.z = CoordXZ.blockToChunk(world.getWorldBorder().getCenter() .getBlockZ()); int chunkWidthX = (int) Math.ceil((double) (((world.getWorldBorder() .getSize() / 2) + 16) * 2) / 16); int chunkWidthZ = (int) Math.ceil((double) (((world.getWorldBorder() .getSize() / 2) + 16) * 2) / 16); int biggerWidth = (chunkWidthX > chunkWidthZ) ? chunkWidthX : chunkWidthZ; // We need to calculate the reportTarget with the // bigger width, since the spiral will only stop // if it has a size of biggerWidth x biggerWidth this.reportTarget = (biggerWidth * biggerWidth) + biggerWidth + 1; // This would be another way to calculate reportTarget, it assumes that // we don't need time to check if the chunk is outside and then skip it // (it calculates the area of the rectangle/ellipse) // 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; } // for backwards compatibility public WorldFillTask(Server theServer, Player player, String worldName, int fillDistance, int chunksPerRun, int tickFrequency) { this(theServer, player, worldName, fillDistance, chunksPerRun, tickFrequency, false); } public void setTaskID(int ID) { if (ID == -1) this.stop(); this.taskID = ID; } @Override public void run() { if (continueNotice) { // notify user that task has continued // automatically continueNotice = false; sendMessage("World map generation task automatically continuing."); sendMessage("Reminder: you can cancel at any time with \"wb fill cancel\", or pause/unpause with \"wb fill pause\"."); } if (pausedForMemory) { // if available memory gets too low, we // automatically pause, so handle that if (Config.AvailableMemoryTooLow()) return; pausedForMemory = false; readyToGo = true; sendMessage("Available memory is sufficient, automatically continuing."); } if (server == null || !readyToGo || paused) return; // 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(); for (int loop = 0; loop < chunksPerRun; loop++) { // in case the task has been paused while we're repeating... if (paused || pausedForMemory) return; long now = Config.Now(); // every 5 seconds or so, give basic progress report to let user // know how it's going if (now > lastReport + 5000) reportProgress(); // if this iteration has been running for 45ms (almost 1 tick) or // more, stop to take a breather if (now > loopStartTime + 45) { readyToGo = true; return; } // if we've made it at least partly outside the border, skip past // any such chunks while (LocationUtils.isOutsideOfBorder(new Location(world, CoordXZ .chunkToBlock(x) + 8, 100, CoordXZ.chunkToBlock(z) + 8))) { if (!moveToNext()) { return; } } insideBorder = true; if (!forceLoad) { // skip past any chunks which are confirmed as fully generated // using our super-special isChunkFullyGenerated routine while (worldData.isChunkFullyGenerated(x, z)) { insideBorder = true; if (!moveToNext()) return; } } // load the target chunk and generate it if necessary world.loadChunk(x, z, true); worldData.chunkExistsNow(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); // 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); } // move on to next chunk if (!moveToNext()) return; } // ready for the next iteration to run readyToGo = true; } // step through chunks in spiral pattern from center; returns false if we're // done, otherwise returns true public boolean moveToNext() { if (paused || pausedForMemory) return false; reportNum++; // keep track of progress in case we need to save to config for // restoring progress after server restart if (!isNeg && current == 0 && length > 3) { if (!isZLeg) { lastLegX = x; lastLegZ = z; lastLegTotal = reportTotal + reportNum; } else { refX = lastLegX; refZ = lastLegZ; refTotal = lastLegTotal; refLength = length - 1; } } // make sure of the direction we're moving (X or Z? negative or // positive?) if (current < length) current++; else { // one leg/side of the spiral down... current = 0; isZLeg ^= true; if (isZLeg) { // every second leg (between X and Z legs, negative or // positive), length increases isNeg ^= true; length++; } } // keep track of the last chunk we were at lastChunk.x = x; lastChunk.z = z; // move one chunk further in the appropriate direction if (isZLeg) z += (isNeg) ? -1 : 1; else x += (isNeg) ? -1 : 1; // if we've been around one full loop (4 legs)... if (isZLeg && isNeg && current == 0) { // see if we've been outside the // border for the whole loop if (!insideBorder) { // and finish if so finish(); return false; } // otherwise, reset the "inside border" flag else insideBorder = false; } return true; /* * reference diagram used, should move in this pattern: 8 * [>][>][>][>][>] etc. [^][6][>][>][>][>][>][6] * [^][^][4][>][>][>][4][v] [^][^][^][2][>][2][v][v] * [^][^][^][^][0][v][v][v] [^][^][^][1][1][v][v][v] * [^][^][3][<][<][3][v][v] [^][5][<][<][<][<][5][v] * [7][<][<][<][<][<][<][7] */ } // for successful completion public void finish() { this.paused = true; reportProgress(); world.save(); sendMessage("task successfully completed for world \"" + refWorld() + "\"!"); this.stop(); } // for cancelling prematurely public void cancel() { this.stop(); } // we're done, whether finished or cancelled private void stop() { if (server == null) return; readyToGo = false; if (taskID != -1) 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); } } // is this task still valid/workable? public boolean valid() { return this.server != null; } // handle pausing/unpausing the task public void pause() { if (this.pausedForMemory) pause(false); else pause(!this.paused); } public void pause(boolean pause) { if (this.pausedForMemory && !pause) this.pausedForMemory = false; else this.paused = pause; if (this.paused) { Config.StoreFillTask(); reportProgress(); } else Config.UnStoreFillTask(); } public boolean isPaused() { return this.paused || this.pausedForMemory; } // let the user know how things are coming along private void reportProgress() { lastReport = Config.Now(); double perc = ((double) (reportTotal + reportNum) / (double) reportTarget) * 100; if (perc > 100) perc = 100; sendMessage(reportNum + " more chunks processed (" + (reportTotal + reportNum) + " total, ~" + Config.coord.format(perc) + "%" + ")"); reportTotal += reportNum; reportNum = 0; // go ahead and save world to disk every 30 seconds or so by default, // just in case; can take a couple of seconds or more, so we don't want // to run it too often if (Config.FillAutosaveFrequency() > 0 && lastAutosave + (Config.FillAutosaveFrequency() * 1000) < lastReport) { lastAutosave = lastReport; sendMessage("Saving the world to disk, just to be on the safe side."); world.save(); } } // send a message to the server console/log and possibly to an in-game // player private void sendMessage(String text) { // Due to chunk generation eating up memory and Java being too slow // about GC, we need to track memory availability int availMem = Config.AvailableMemory(); Config.log("[Fill] " + text + " (free mem: " + availMem + " MB)"); if (notifyPlayer != null) notifyPlayer.sendMessage("[Fill] " + text); if (availMem < 200) { // running low on memory, auto-pause pausedForMemory = true; Config.StoreFillTask(); text = "Available memory is very low, task is pausing. A cleanup will be attempted now, and the task will automatically continue if/when sufficient memory is freed up.\n Alternatively, if you restart the server, this task will automatically continue once the server is back up."; Config.log("[Fill] " + text); if (notifyPlayer != null) notifyPlayer.sendMessage("[Fill] " + text); // prod Java with a request to go ahead and do GC to clean unloaded // chunks from memory; this seems to work wonders almost immediately // yes, explicit calls to System.gc() are normally bad, but in this // case it otherwise can take a long long long time for Java to // recover memory System.gc(); } } // stuff for saving / restoring progress public void continueProgress(int x, int z, int length, int totalDone) { this.x = x; this.z = z; this.length = length; this.reportTotal = totalDone; this.continueNotice = true; } public int refX() { return refX; } public int refZ() { return refZ; } public int refLength() { return refLength; } public int refTotal() { return refTotal; } public int refFillDistance() { return fillDistance; } public int refTickFrequency() { return tickFrequency; } public int refChunksPerRun() { return chunksPerRun; } public String refWorld() { return world.getName(); } public boolean refForceLoad() { return forceLoad; } }