mirror of
https://github.com/Brettflan/WorldBorder.git
synced 2025-01-09 17:37:39 +01:00
Might need this... also, release 1.3.0
This commit is contained in:
parent
e2e1b60e2a
commit
794aed7b18
415
src/com/wimbli/WorldBorder/WorldFillTask.java
Normal file
415
src/com/wimbli/WorldBorder/WorldFillTask.java
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
package com.wimbli.WorldBorder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
|
||||||
|
public class WorldFillTask implements Runnable
|
||||||
|
{
|
||||||
|
// general task-related reference data
|
||||||
|
private transient Server server = null;
|
||||||
|
private transient World world = null;
|
||||||
|
private transient BorderData border = 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;
|
||||||
|
|
||||||
|
// these are only stored for saving task to config
|
||||||
|
private transient int fillDistance = 176;
|
||||||
|
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>();
|
||||||
|
|
||||||
|
// for reporting progress back to user occasionally
|
||||||
|
private transient long lastReport = 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)
|
||||||
|
{
|
||||||
|
this.server = theServer;
|
||||||
|
this.notifyPlayer = player;
|
||||||
|
this.fillDistance = fillDistance;
|
||||||
|
this.tickFrequency = tickFrequency;
|
||||||
|
this.chunksPerRun = chunksPerRun;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.border = (Config.Border(worldName) == null) ? null : Config.Border(worldName).copy();
|
||||||
|
if (this.border == null)
|
||||||
|
{
|
||||||
|
sendMessage("No border found for world \"" + worldName + "\"!");
|
||||||
|
this.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.border.setRadius(border.getRadius() + fillDistance);
|
||||||
|
this.x = (int) Math.floor(border.getX() / 16);
|
||||||
|
this.z = (int) Math.floor(border.getX() / 16);
|
||||||
|
|
||||||
|
int chunkWidth = (int) Math.ceil((double)((border.getRadius() + 16) * 2) / 16);
|
||||||
|
this.reportTarget = (chunkWidth * chunkWidth) + chunkWidth + 1;
|
||||||
|
|
||||||
|
this.readyToGo = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaskID(int ID)
|
||||||
|
{
|
||||||
|
this.taskID = ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
if (continueNotice)
|
||||||
|
{ // notify user that task has continued automatically
|
||||||
|
continueNotice = false;
|
||||||
|
String clrCmd = ChatColor.AQUA.toString();
|
||||||
|
String clrDesc = ChatColor.WHITE.toString();
|
||||||
|
sendMessage("World map generation task automatically continuing.");
|
||||||
|
sendMessage("Reminder: you can cancel at any time with " + clrCmd + "wb fill cancel" + clrDesc + ", or pause/unpause with " + clrCmd + "wb fill pause" + clrDesc + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pausedForMemory)
|
||||||
|
{ // if available memory gets too low, we automatically pause, so handle that
|
||||||
|
if (Config.AvailableMemory() < 500)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pausedForMemory = false;
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (int loop = 0; loop < chunksPerRun; loop++)
|
||||||
|
{
|
||||||
|
// in case the task has been paused while we're repeating...
|
||||||
|
if (paused)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// every 10 seconds or so, give basic progress report to let user know how it's going
|
||||||
|
if (Config.Now() > lastReport + 10000)
|
||||||
|
reportProgress();
|
||||||
|
|
||||||
|
// if we've made it at least partly outside the border, skip past any such chunks
|
||||||
|
while (!border.insideBorder((x << 4) + 8, (z << 4) + 8))
|
||||||
|
{
|
||||||
|
if (!moveToNext())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insideBorder = true;
|
||||||
|
|
||||||
|
// skip past any chunks which are currently loaded (they're definitely already generated)
|
||||||
|
while (world.isChunkLoaded(x, z))
|
||||||
|
{
|
||||||
|
insideBorder = true;
|
||||||
|
if (!moveToNext())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the target chunk and generate it if necessary (no way to check if chunk has been generated first, simply have to load it)
|
||||||
|
world.loadChunk(x, z, true);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Store the coordinates of these latest 2 chunks we just loaded, so we can unload them after a bit...
|
||||||
|
storedChunks.add(new coordXZ(x, z));
|
||||||
|
storedChunks.add(new coordXZ(popX, popZ));
|
||||||
|
|
||||||
|
// If enough stored chunks are buffered in, go ahead and unload the oldest to free up memory
|
||||||
|
if (storedChunks.size() > 6)
|
||||||
|
{
|
||||||
|
coordXZ coord = storedChunks.remove(0);
|
||||||
|
world.unloadChunk(coord.x, coord.z);
|
||||||
|
coord = storedChunks.remove(0);
|
||||||
|
world.unloadChunk(coord.x, coord.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // I ORIGINALLY DID IT THIS WAY INSTEAD: chunks were stored up to 1 full spiral worth to allow them to populate, but...
|
||||||
|
// even though it didn't need to re-load extra chunks like above (it just kept them loaded), it was somehow slower
|
||||||
|
// I also didn't realize there was a chunk generation memory leak in Bukkit, so I originally ditched this method based on memory usage
|
||||||
|
// left the code here, commented out, just for posterity
|
||||||
|
|
||||||
|
// keep track of chunks we loaded, so we can unload them after a bit...
|
||||||
|
// we have to keep them loaded for a while to have the server populate them with trees, snow, etc.
|
||||||
|
storedChunks.add(new coordXZ(x, z));
|
||||||
|
|
||||||
|
// then see if we're enough chunks ahead that we can unload the oldest one still stored
|
||||||
|
if (storedChunks.size() - 3 > (length << 2))
|
||||||
|
{
|
||||||
|
coordXZ coord = storedChunks.remove(0);
|
||||||
|
world.unloadChunk(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()
|
||||||
|
{
|
||||||
|
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 = !isZLeg;
|
||||||
|
if (isZLeg)
|
||||||
|
{ // every second leg (between X and Z legs, negative or positive), length increases
|
||||||
|
isNeg = !isNeg;
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 == false)
|
||||||
|
{ // 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()
|
||||||
|
{
|
||||||
|
reportProgress();
|
||||||
|
world.save();
|
||||||
|
sendMessage("task successfully completed!");
|
||||||
|
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);
|
||||||
|
world.unloadChunk(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;
|
||||||
|
sendMessage(reportNum + " more map chunks processed (" + (reportTotal + reportNum) + " total, " + ChatColor.GREEN.toString() + Config.coord.format(perc) + "%" + ChatColor.WHITE.toString() + ")");
|
||||||
|
reportTotal += reportNum;
|
||||||
|
reportNum = 0;
|
||||||
|
|
||||||
|
// try to keep memory usage in check and keep things speedy as much as possible...
|
||||||
|
world.save();
|
||||||
|
server.savePlayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a message to the server console/log and possibly to an in-game player
|
||||||
|
private void sendMessage(String text)
|
||||||
|
{
|
||||||
|
// Due to the apparent chunk generation memory leak, we need to track memory availability
|
||||||
|
int availMem = Config.AvailableMemory();
|
||||||
|
|
||||||
|
Config.Log("[Fill] " + text + ChatColor.GOLD.toString() + " (free mem: " + availMem + " MB)");
|
||||||
|
if (notifyPlayer != null)
|
||||||
|
notifyPlayer.sendMessage("[Fill] " + text);
|
||||||
|
|
||||||
|
if (availMem < 100)
|
||||||
|
{ // running low on memory, auto-pause
|
||||||
|
pausedForMemory = true;
|
||||||
|
Config.StoreFillTask();
|
||||||
|
text = "Available memory is very low, task is pausing. Will automatically continue if/when sufficient memory is freed up.\n Alternately, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple storage class for chunk x/z values
|
||||||
|
private static class coordXZ
|
||||||
|
{
|
||||||
|
public int x, z;
|
||||||
|
public coordXZ(int x, int z)
|
||||||
|
{
|
||||||
|
this.x = x;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user