WorldBorder/src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java

433 lines
12 KiB
Java

package com.wimbli.WorldBorder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.Server;
import org.bukkit.World;
import com.wimbli.WorldBorder.Events.WorldBorderTrimFinishedEvent;
import com.wimbli.WorldBorder.Events.WorldBorderTrimStartEvent;
public class WorldTrimTask implements Runnable
{
// general task-related reference data
private transient Server server = null;
private transient World world = null;
private transient WorldFileData worldData = null;
private transient BorderData border = null;
private transient boolean readyToGo = false;
private transient boolean paused = false;
private transient int taskID = -1;
private transient Player notifyPlayer = null;
private transient int chunksPerRun = 1;
// values for what chunk in the current region we're at
private transient int currentRegion = -1; // region(file) we're at in regionFiles
private transient int regionX = 0; // X location value of the current region
private transient int regionZ = 0; // X location value of the current region
private transient int currentChunk = 0; // chunk we've reached in the current region (regionChunks)
private transient List<CoordXZ> regionChunks = new ArrayList<CoordXZ>(1024);
private transient List<CoordXZ> trimChunks = new ArrayList<CoordXZ>(1024);
private transient int counter = 0;
// 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 reportTrimmedRegions = 0;
private transient int reportTrimmedChunks = 0;
public WorldTrimTask(Server theServer, Player player, String worldName, int trimDistance, int chunksPerRun)
{
this.server = theServer;
this.notifyPlayer = player;
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.setRadiusX(border.getRadiusX() + trimDistance);
this.border.setRadiusZ(border.getRadiusZ() + trimDistance);
worldData = WorldFileData.create(world, notifyPlayer);
if (worldData == null)
{
this.stop();
return;
}
// each region file covers up to 1024 chunks; with all operations we might need to do, let's figure 3X that
this.reportTarget = worldData.regionFileCount() * 3072;
// queue up the first file
if (!nextFile())
return;
this.readyToGo = true;
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimStartEvent(this));
}
public void setTaskID(int ID)
{
this.taskID = ID;
}
public void run()
{
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();
counter = 0;
while (counter <= chunksPerRun)
{
// in case the task has been paused while we're repeating...
if (paused)
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; shouldn't normally be possible with Trim, but just in case
if (now > loopStartTime + 45)
{
readyToGo = true;
return;
}
if (regionChunks.isEmpty())
addCornerChunks();
else if (currentChunk == 4)
{ // determine if region is completely _inside_ border based on corner chunks
if (trimChunks.isEmpty())
{ // it is, so skip it and move on to next file
counter += 4;
nextFile();
continue;
}
addEdgeChunks();
addInnerChunks();
}
else if (currentChunk == 124 && trimChunks.size() == 124)
{ // region is completely _outside_ border based on edge chunks, so delete file and move on to next
counter += 16;
trimChunks = regionChunks;
unloadChunks();
reportTrimmedRegions++;
File regionFile = worldData.regionFile(currentRegion);
if (!regionFile.delete())
{
sendMessage("Error! Region file which is outside the border could not be deleted: "+regionFile.getName());
wipeChunks();
}
else
{
// if DynMap is installed, re-render the trimmed region ... disabled since it's not currently working, oh well
// DynMapFeatures.renderRegion(world.getName(), new CoordXZ(regionX, regionZ));
}
nextFile();
continue;
}
else if (currentChunk == 1024)
{ // last chunk of the region has been checked, time to wipe out whichever chunks are outside the border
counter += 32;
unloadChunks();
wipeChunks();
nextFile();
continue;
}
// check whether chunk is inside the border or not, add it to the "trim" list if not
CoordXZ chunk = regionChunks.get(currentChunk);
if (!isChunkInsideBorder(chunk))
trimChunks.add(chunk);
currentChunk++;
counter++;
}
reportTotal += counter;
// ready for the next iteration to run
readyToGo = true;
}
// Advance to the next region file. Returns true if successful, false if the next file isn't accessible for any reason
private boolean nextFile()
{
reportTotal = currentRegion * 3072;
currentRegion++;
regionX = regionZ = currentChunk = 0;
regionChunks = new ArrayList<CoordXZ>(1024);
trimChunks = new ArrayList<CoordXZ>(1024);
// have we already handled all region files?
if (currentRegion >= worldData.regionFileCount())
{ // hey, we're done
paused = true;
readyToGo = false;
finish();
return false;
}
counter += 16;
// get the X and Z coordinates of the current region
CoordXZ coord = worldData.regionFileCoordinates(currentRegion);
if (coord == null)
return false;
regionX = coord.x;
regionZ = coord.z;
return true;
}
// add just the 4 corner chunks of the region; can determine if entire region is _inside_ the border
private void addCornerChunks()
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ)));
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ)));
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX), CoordXZ.regionToChunk(regionZ) + 31));
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX) + 31, CoordXZ.regionToChunk(regionZ) + 31));
}
// add all chunks along the 4 edges of the region (minus the corners); can determine if entire region is _outside_ the border
private void addEdgeChunks()
{
int chunkX = 0, chunkZ;
for (chunkZ = 1; chunkZ < 31; chunkZ++)
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ));
}
chunkX = 31;
for (chunkZ = 1; chunkZ < 31; chunkZ++)
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ));
}
chunkZ = 0;
for (chunkX = 1; chunkX < 31; chunkX++)
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ));
}
chunkZ = 31;
for (chunkX = 1; chunkX < 31; chunkX++)
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ));
}
counter += 4;
}
// add the remaining interior chunks (after corners and edges)
private void addInnerChunks()
{
for (int chunkX = 1; chunkX < 31; chunkX++)
{
for (int chunkZ = 1; chunkZ < 31; chunkZ++)
{
regionChunks.add(new CoordXZ(CoordXZ.regionToChunk(regionX)+chunkX, CoordXZ.regionToChunk(regionZ)+chunkZ));
}
}
counter += 32;
}
// make sure chunks set to be trimmed are not currently loaded by the server
private void unloadChunks()
{
for (CoordXZ unload : trimChunks)
{
if (world.isChunkLoaded(unload.x, unload.z))
world.unloadChunk(unload.x, unload.z, false, false);
}
counter += trimChunks.size();
}
// edit region file to wipe all chunk pointers for chunks outside the border
private void wipeChunks()
{
File regionFile = worldData.regionFile(currentRegion);
if (!regionFile.canWrite())
{
if (!regionFile.setWritable(true))
throw new RuntimeException();
if (!regionFile.canWrite())
{
sendMessage("Error! region file is locked and can't be trimmed: "+regionFile.getName());
return;
}
}
// since our stored chunk positions are based on world, we need to offset those to positions in the region file
int offsetX = CoordXZ.regionToChunk(regionX);
int offsetZ = CoordXZ.regionToChunk(regionZ);
long wipePos = 0;
int chunkCount = 0;
try
{
RandomAccessFile unChunk = new RandomAccessFile(regionFile, "rwd");
for (CoordXZ wipe : trimChunks)
{
// if the chunk pointer is empty (chunk doesn't technically exist), no need to wipe the already empty pointer
if (!worldData.doesChunkExist(wipe.x, wipe.z))
continue;
// wipe this extraneous chunk's pointer... note that this method isn't perfect since the actual chunk data is left orphaned,
// but Minecraft will overwrite the orphaned data sector if/when another chunk is created in the region, so it's not so bad
wipePos = 4 * ((wipe.x - offsetX) + ((wipe.z - offsetZ) * 32));
unChunk.seek(wipePos);
unChunk.writeInt(0);
chunkCount++;
}
unChunk.close();
// if DynMap is installed, re-render the trimmed chunks ... disabled since it's not currently working, oh well
// DynMapFeatures.renderChunks(world.getName(), trimChunks);
reportTrimmedChunks += chunkCount;
}
catch (FileNotFoundException ex)
{
sendMessage("Error! Could not open region file to wipe individual chunks: "+regionFile.getName());
}
catch (IOException ex)
{
sendMessage("Error! Could not modify region file to wipe individual chunks: "+regionFile.getName());
}
counter += trimChunks.size();
}
private boolean isChunkInsideBorder(CoordXZ chunk)
{
return border.insideBorder(CoordXZ.chunkToBlock(chunk.x) + 8, CoordXZ.chunkToBlock(chunk.z) + 8);
}
// for successful completion
public void finish()
{
reportTotal = reportTarget;
reportProgress();
Bukkit.getServer().getPluginManager().callEvent(new WorldBorderTrimFinishedEvent(world, reportTotal));
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;
}
// is this task still valid/workable?
public boolean valid()
{
return this.server != null;
}
// handle pausing/unpausing the task
public void pause()
{
pause(!this.paused);
}
public void pause(boolean pause)
{
this.paused = pause;
if (pause)
reportProgress();
}
public boolean isPaused()
{
return this.paused;
}
// let the user know how things are coming along
private void reportProgress()
{
lastReport = Config.Now();
double perc = getPercentageCompleted();
sendMessage(reportTrimmedRegions + " entire region(s) and " + reportTrimmedChunks + " individual chunk(s) trimmed so far (" + Config.coord.format(perc) + "% done" + ")");
}
// send a message to the server console/log and possibly to an in-game player
private void sendMessage(String text)
{
Config.log("[Trim] " + text);
if (notifyPlayer != null)
notifyPlayer.sendMessage("[Trim] " + text);
}
/**
* Get the percentage completed for the trim task.
*
* @return Percentage
*/
public double getPercentageCompleted() {
return ((double) (reportTotal) / (double) reportTarget) * 100;
}
/**
* Amount of chunks completed for the trim task.
*
* @return Number of chunks processed.
*/
public int getChunksCompleted() {
return reportTotal;
}
/**
* Total amount of chunks that need to be trimmed for the trim task.
*
* @return Number of chunks that need to be processed.
*/
public int getChunksTotal() {
return reportTarget;
}
}