404 lines
12 KiB
Java
404 lines
12 KiB
Java
package com.wimbli.WorldBorder;
|
|
|
|
import java.io.*;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.Server;
|
|
import org.bukkit.World;
|
|
|
|
|
|
public class WorldTrimTask implements Runnable
|
|
{
|
|
// general task-related reference data
|
|
private transient Server server = null;
|
|
private transient World world = null;
|
|
private transient File regionFolder = null;
|
|
private transient File[] regionFiles = 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.setRadius(border.getRadius() + trimDistance);
|
|
|
|
regionFolder = new File(this.world.getWorldFolder(), "region");
|
|
if (!regionFolder.exists() || !regionFolder.isDirectory())
|
|
{
|
|
String mainRegionFolder = regionFolder.getPath();
|
|
regionFolder = new File(this.world.getWorldFolder(), "DIM-1"+File.separator+"region"); // nether worlds
|
|
if (!regionFolder.exists() || !regionFolder.isDirectory())
|
|
{
|
|
String subRegionFolder = regionFolder.getPath();
|
|
regionFolder = new File(this.world.getWorldFolder(), "DIM1"+File.separator+"region"); // "the end" worlds; not sure why "DIM1" vs "DIM-1", but that's how it is
|
|
if (!regionFolder.exists() || !regionFolder.isDirectory())
|
|
{
|
|
sendMessage("Could not validate folder for world's region files. Looked in: "+mainRegionFolder+" -and- "+subRegionFolder+" -and- "+regionFolder.getPath());
|
|
this.stop();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
regionFiles = regionFolder.listFiles(new RegionFileFilter());
|
|
|
|
if (regionFiles == null || regionFiles.length == 0)
|
|
{
|
|
sendMessage("Could not find any region files. Looked in: "+regionFolder.getPath());
|
|
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 = regionFiles.length * 3072;
|
|
|
|
// queue up the first file
|
|
if (!nextFile())
|
|
return;
|
|
|
|
this.readyToGo = true;
|
|
}
|
|
|
|
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;
|
|
|
|
counter = 0;
|
|
while (counter <= chunksPerRun)
|
|
{
|
|
// in case the task has been paused while we're repeating...
|
|
if (paused)
|
|
return;
|
|
|
|
// every 5 seconds or so, give basic progress report to let user know how it's going
|
|
if (Config.Now() > lastReport + 5000)
|
|
reportProgress();
|
|
|
|
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++;
|
|
if (!regionFiles[currentRegion].delete())
|
|
{
|
|
sendMessage("Error! Region file which is outside the border could not be deleted: "+regionFiles[currentRegion].getName());
|
|
wipeChunks();
|
|
}
|
|
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 >= regionFiles.length)
|
|
{ // hey, we're done
|
|
paused = true;
|
|
readyToGo = false;
|
|
finish();
|
|
return false;
|
|
}
|
|
|
|
counter += 16;
|
|
|
|
// get the X and Z coordinates of the current region from the filename
|
|
String[] coords = regionFiles[currentRegion].getName().split("\\.");
|
|
try
|
|
{
|
|
regionX = Integer.parseInt(coords[1]);
|
|
regionZ = Integer.parseInt(coords[2]);
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
sendMessage("Error! Region file found with abnormal name: "+regionFiles[currentRegion].getName());
|
|
return false;
|
|
}
|
|
|
|
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
|
|
// by the way, this method was created based on the divulged region file format: http://mojang.com/2011/02/16/minecraft-save-file-format-in-beta-1-3/
|
|
private void wipeChunks()
|
|
{
|
|
if (!regionFiles[currentRegion].canWrite())
|
|
{
|
|
regionFiles[currentRegion].setWritable(true);
|
|
if (!regionFiles[currentRegion].canWrite())
|
|
{
|
|
sendMessage("Error! region file is locked and can't be trimmed: "+regionFiles[currentRegion].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;
|
|
|
|
try
|
|
{
|
|
RandomAccessFile unChunk = new RandomAccessFile(regionFiles[currentRegion], "rwd");
|
|
for (CoordXZ wipe : trimChunks)
|
|
{ // 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);
|
|
}
|
|
unChunk.close();
|
|
reportTrimmedChunks += trimChunks.size();
|
|
}
|
|
catch (FileNotFoundException ex)
|
|
{
|
|
sendMessage("Error! Could not open region file to wipe individual chunks: "+regionFiles[currentRegion].getName());
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
sendMessage("Error! Could not modify region file to wipe individual chunks: "+regionFiles[currentRegion].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();
|
|
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 = ((double)(reportTotal) / (double)reportTarget) * 100;
|
|
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);
|
|
}
|
|
|
|
// filter for region files
|
|
private static class RegionFileFilter implements FileFilter
|
|
{
|
|
public boolean accept(File file)
|
|
{
|
|
return (
|
|
file.exists()
|
|
&& file.isFile()
|
|
&& file.getName().toLowerCase().endsWith(".mcr")
|
|
);
|
|
}
|
|
}
|
|
}
|