Added new trim command, to trim off chunks well outside the border. Works similar to the fill command, but is much much faster.

Also made the fill command not unload any chunks which were already loaded before it was run.
This commit is contained in:
Brettflan 2011-08-15 10:01:03 -05:00
parent fdb8aca3af
commit 933f9cac16
6 changed files with 666 additions and 45 deletions

View File

@ -35,6 +35,7 @@ public class Config
public static DecimalFormat coord = new DecimalFormat("0.0");
private static int borderTask = -1;
public static WorldFillTask fillTask = null;
public static WorldTrimTask trimTask = null;
public static Set<String> movedPlayers = Collections.synchronizedSet(new HashSet<String>());
private static Runtime rt = Runtime.getRuntime();
private static ColouredConsoleSender console = null;
@ -237,6 +238,13 @@ public class Config
}
public static void StopTrimTask()
{
if (trimTask != null && trimTask.valid())
trimTask.cancel();
}
public static int AvailableMemory()
{
return (int)((rt.maxMemory() - rt.totalMemory() + rt.freeMemory()) / 1024L / 1024L);

View File

@ -0,0 +1,59 @@
package com.wimbli.WorldBorder;
// simple storage class for chunk x/z values
public class CoordXZ
{
public int x, z;
public CoordXZ(int x, int z)
{
this.x = x;
this.z = z;
}
// transform values between block, chunk, and region
// bit-shifting is used because it's mucho rapido
public static int blockToChunk(int blockVal)
{ // 1 chunk is 16x16 blocks
return blockVal >> 4; // ">>4" == "/16"
}
public static int blockToRegion(int blockVal)
{ // 1 region is 512x512 blocks
return blockVal >> 9; // ">>9" == "/512"
}
public static int chunkToRegion(int chunkVal)
{ // 1 region is 32x32 chunks
return chunkVal >> 5; // ">>5" == "/32"
}
public static int chunkToBlock(int chunkVal)
{
return chunkVal << 4; // "<<4" == "*16"
}
public static int regionToBlock(int regionVal)
{
return regionVal << 9; // "<<9" == "*512"
}
public static int regionToChunk(int regionVal)
{
return regionVal << 5; // "<<5" == "*32"
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
else if (obj == null || obj.getClass() != this.getClass())
return false;
CoordXZ test = (CoordXZ)obj;
return test.x == this.x && test.z == this.z;
}
@Override
public int hashCode()
{
return (this.x << 6) + this.z;
}
}

View File

@ -502,6 +502,60 @@ public class WBCommand implements CommandExecutor
cmdFill(sender, player, world, confirm, cancel, pause, pad, frequency);
}
// "trim" command from player or console, world specified
else if (split.length >= 2 && split[1].equalsIgnoreCase("trim"))
{
if (!Config.HasPermission(player, "trim")) return true;
boolean cancel = false, confirm = false, pause = false;
String pad = "", frequency = "";
if (split.length >= 3)
{
cancel = split[2].equalsIgnoreCase("cancel");
confirm = split[2].equalsIgnoreCase("confirm");
pause = split[2].equalsIgnoreCase("pause");
if (!cancel && !confirm && !pause)
frequency = split[2];
}
if (split.length >= 4)
pad = split[3];
String world = split[0];
cmdTrim(sender, player, world, confirm, cancel, pause, pad, frequency);
}
// "trim" command from player (or from console solely if using cancel or confirm), using current world
else if (split.length >= 1 && split[0].equalsIgnoreCase("trim"))
{
if (!Config.HasPermission(player, "trim")) return true;
boolean cancel = false, confirm = false, pause = false;
String pad = "", frequency = "";
if (split.length >= 2)
{
cancel = split[1].equalsIgnoreCase("cancel");
confirm = split[1].equalsIgnoreCase("confirm");
pause = split[1].equalsIgnoreCase("pause");
if (!cancel && !confirm && !pause)
frequency = split[1];
}
if (split.length >= 3)
pad = split[2];
String world = "";
if (player != null)
world = player.getWorld().getName();
if (!cancel && !confirm && !pause && world.isEmpty())
{
sender.sendMessage("You must specify a world! Example: " + cmdW+" trim " + clrOpt + "[freq] [pad]");
return true;
}
cmdTrim(sender, player, world, confirm, cancel, pause, pad, frequency);
}
// we couldn't decipher any known commands, so show help
else
{
@ -541,13 +595,15 @@ public class WBCommand implements CommandExecutor
if (page == 0 || page == 2)
{
sender.sendMessage(cmdW+" fill " + clrOpt + "[freq] [pad]" + clrDesc + " - generate world out to border.");
sender.sendMessage(cmdW+" trim " + clrOpt + "[freq] [pad]" + clrDesc + " - trim world outside of border.");
sender.sendMessage(cmd+" wshape " + ((player == null) ? clrReq + "<world>" : clrOpt + "[world]") + clrReq + " <round|square|default>" + clrDesc + " - shape override.");
sender.sendMessage(cmd+" getmsg" + clrDesc + " - display border message.");
sender.sendMessage(cmd+" setmsg " + clrReq + "<text>" + clrDesc + " - set border message.");
sender.sendMessage(cmd+" whoosh " + clrReq + "<on|off>" + clrDesc + " - turn knockback effect on or off.");
sender.sendMessage(cmd+" delay " + clrReq + "<amount>" + clrDesc + " - time between border checks.");
sender.sendMessage(cmd+" reload" + clrDesc + " - re-load data from config.yml.");
sender.sendMessage(cmd+" debug " + clrReq + "<on|off>" + clrDesc + " - turn console debug output on or off.");
if (player == null)
sender.sendMessage(cmd+" debug " + clrReq + "<on|off>" + clrDesc + " - turn console debug output on or off.");
if (page == 2)
sender.sendMessage(cmd + clrDesc + " - view first page of commands.");
}
@ -579,8 +635,8 @@ public class WBCommand implements CommandExecutor
private String fillWorld = "";
private int fillPadding = 16 * 11;
private int fillFrequency = 20;
private int fillPadding = CoordXZ.chunkToBlock(11);
private void fillDefaults()
{
@ -588,7 +644,7 @@ public class WBCommand implements CommandExecutor
fillFrequency = 20;
// with "view-distance=10" in server.properties and "Render Distance: Far" in client, hitting border during testing
// was loading 11 chunks beyond the border in a couple of directions (10 chunks in the other two directions); thus:
fillPadding = 16 * 11;
fillPadding = CoordXZ.chunkToBlock(11);
}
private boolean cmdFill(CommandSender sender, Player player, String world, boolean confirm, boolean cancel, boolean pause, String pad, String frequency)
@ -675,11 +731,115 @@ public class WBCommand implements CommandExecutor
}
String cmd = clrCmd + ((player == null) ? "wb" : "/wb");
sender.sendMessage(clrHead + "World generation task is ready for world \"" + fillWorld + "\", padding the map out to " + fillPadding + " blocks beyond the border (default " + (16 * 11) + "), and the task will try to process up to " + fillFrequency + " chunks per second (default 20).");
sender.sendMessage(clrHead + "World generation task is ready for world \"" + fillWorld + "\", padding the map out to " + fillPadding + " blocks beyond the border (default " + CoordXZ.chunkToBlock(11) + "), and the task will try to process up to " + fillFrequency + " chunks per second (default 20).");
sender.sendMessage(clrHead + "This process can take a very long time depending on the world's border size. Also, depending on the chunk processing rate, players will likely experience severe lag for the duration.");
sender.sendMessage(clrDesc + "You should now use " + cmd + " fill confirm" + clrDesc + " to start the process.");
sender.sendMessage(clrDesc + "You can cancel at any time with " + cmd + " fill cancel" + clrDesc + ", or pause/unpause with " + cmd + " fill pause" + clrDesc + ".");
}
return true;
}
private String trimWorld = "";
private int trimFrequency = 5000;
private int trimPadding = CoordXZ.chunkToBlock(12);
private void trimDefaults()
{
trimWorld = "";
trimFrequency = 5000;
trimPadding = CoordXZ.chunkToBlock(12);
}
private boolean cmdTrim(CommandSender sender, Player player, String world, boolean confirm, boolean cancel, boolean pause, String pad, String frequency)
{
if (cancel)
{
sender.sendMessage(clrHead + "Cancelling the world map trimming task.");
trimDefaults();
Config.StopTrimTask();
return true;
}
if (pause)
{
if (Config.trimTask == null || !Config.trimTask.valid())
{
sender.sendMessage(clrHead + "The world map trimming task is not currently running.");
return true;
}
Config.trimTask.pause();
sender.sendMessage(clrHead + "The world map trimming task is now " + (Config.trimTask.isPaused() ? "" : "un") + "paused.");
return true;
}
if (Config.trimTask != null && Config.trimTask.valid())
{
sender.sendMessage(clrHead + "The world map trimming task is already running.");
return true;
}
// set padding and/or delay if those were specified
try
{
if (!pad.isEmpty())
trimPadding = Math.abs(Integer.parseInt(pad));
if (!frequency.isEmpty())
trimFrequency = Math.abs(Integer.parseInt(frequency));
}
catch(NumberFormatException ex)
{
sender.sendMessage(clrErr + "The frequency and padding values must be integers.");
return false;
}
// set world if it was specified
if (!world.isEmpty())
trimWorld = world;
if (confirm)
{ // command confirmed, go ahead with it
if (trimWorld.isEmpty())
{
sender.sendMessage(clrErr + "You must first use this command successfully without confirming.");
return false;
}
if (player != null)
Config.Log("Trimming world beyond border at the command of player \"" + player.getName() + "\".");
int ticks = 1, repeats = 1;
if (trimFrequency > 20)
repeats = trimFrequency / 20;
else
ticks = 20 / trimFrequency;
Config.trimTask = new WorldTrimTask(plugin.getServer(), player, trimWorld, trimPadding, repeats);
if (Config.trimTask.valid())
{
int task = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, Config.trimTask, ticks, ticks);
Config.trimTask.setTaskID(task);
sender.sendMessage("WorldBorder map trimming task started.");
}
else
sender.sendMessage(clrErr + "The world map trimming task failed to start.");
trimDefaults();
}
else
{
if (trimWorld.isEmpty())
{
sender.sendMessage(clrErr + "You must first specify a valid world.");
return false;
}
String cmd = clrCmd + ((player == null) ? "wb" : "/wb");
sender.sendMessage(clrHead + "World trimming task is ready for world \"" + trimWorld + "\", trimming the map past " + trimPadding + " blocks beyond the border (default " + CoordXZ.chunkToBlock(12) + "), and the task will try to process up to " + trimFrequency + " chunks per second (default 5000).");
sender.sendMessage(clrHead + "This process can take a while depending on the world's overall size. Also, depending on the chunk processing rate, players may experience lag for the duration.");
sender.sendMessage(clrDesc + "You should now use " + cmd + " trim confirm" + clrDesc + " to start the process.");
sender.sendMessage(clrDesc + "You can cancel at any time with " + cmd + " trim cancel" + clrDesc + ", or pause/unpause with " + cmd + " trim pause" + clrDesc + ".");
}
return true;
}
}

View File

@ -1,9 +1,12 @@
package com.wimbli.WorldBorder;
import java.util.HashSet;
import java.util.List;
import java.util.LinkedList;
import java.util.Set;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.entity.Player;
import org.bukkit.Server;
import org.bukkit.World;
@ -39,7 +42,8 @@ public class WorldFillTask implements Runnable
private transient int length = -1;
private transient int current = 0;
private transient boolean insideBorder = true;
private List<coordXZ> storedChunks = new LinkedList<coordXZ>();
private List<CoordXZ> storedChunks = new LinkedList<CoordXZ>();
private Set<CoordXZ> originalChunks = new HashSet<CoordXZ>();
// for reporting progress back to user occasionally
private transient long lastReport = Config.Now();
@ -76,12 +80,19 @@ public class WorldFillTask implements Runnable
}
this.border.setRadius(border.getRadius() + fillDistance);
this.x = (int) Math.floor(border.getX() / 16);
this.z = (int) Math.floor(border.getX() / 16);
this.x = CoordXZ.blockToChunk((int)border.getX());
this.z = CoordXZ.blockToChunk((int)border.getZ());
int chunkWidth = (int) Math.ceil((double)((border.getRadius() + 16) * 2) / 16);
this.reportTarget = (chunkWidth * chunkWidth) + chunkWidth + 1;
// 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;
}
@ -128,7 +139,7 @@ public class WorldFillTask implements Runnable
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))
while (!border.insideBorder(CoordXZ.chunkToBlock(x) + 8, CoordXZ.chunkToBlock(z) + 8))
{
if (!moveToNext())
return;
@ -153,34 +164,20 @@ public class WorldFillTask implements Runnable
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));
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);
CoordXZ coord = storedChunks.remove(0);
if (!originalChunks.contains(coord))
world.unloadChunk(coord.x, coord.z);
coord = storedChunks.remove(0);
world.unloadChunk(coord.x, coord.z);
if (!originalChunks.contains(coord))
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;
@ -217,10 +214,10 @@ public class WorldFillTask implements Runnable
else
{ // one leg/side of the spiral down...
current = 0;
isZLeg = !isZLeg;
isZLeg ^= true;
if (isZLeg)
{ // every second leg (between X and Z legs, negative or positive), length increases
isNeg = !isNeg;
isNeg ^= true;
length++;
}
}
@ -286,8 +283,9 @@ public class WorldFillTask implements Runnable
// go ahead and unload any chunks we still have loaded
while(!storedChunks.isEmpty())
{
coordXZ coord = storedChunks.remove(0);
world.unloadChunk(coord.x, coord.z);
CoordXZ coord = storedChunks.remove(0);
if (!originalChunks.contains(coord))
world.unloadChunk(coord.x, coord.z);
}
}
@ -360,17 +358,6 @@ public class WorldFillTask implements Runnable
}
}
// 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)
{

View File

@ -0,0 +1,398 @@
package com.wimbli.WorldBorder;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.ChatColor;
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("./"+worldName+"/region");
if (!regionFolder.exists() || !regionFolder.isDirectory())
{
String mainRegionFolder = regionFolder.getPath();
regionFolder = new File("./"+worldName+"/DIM-1/region");
if (!regionFolder.exists() || !regionFolder.isDirectory())
{
sendMessage("Could not validate folder for world's region files. Looked in: "+mainRegionFolder+" -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 10 seconds or so, give basic progress report to let user know how it's going
if (Config.Now() > lastReport + 10000)
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)
{
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 (" + ChatColor.GREEN.toString() + Config.coord.format(perc) + "% done" + ChatColor.WHITE.toString() + ")");
}
// 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")
);
}
}
}

View File

@ -1,8 +1,12 @@
name: WorldBorder
author: Brettflan
description: Efficient, feature-rich plugin for limiting the size of your worlds.
version: 1.3.1
version: 1.4.0
main: com.wimbli.WorldBorder.WorldBorder
softdepend:
- Essentials
- GroupManager
- Permissions
commands:
wborder:
description: Primary command for WorldBorder.
@ -23,6 +27,7 @@ commands:
/<command> delay <amount> - time between border checks.
/<command> wshape [world] <round|square|default> - override shape.
/<command> [world] fill [freq] [pad] - generate world out to border.
/<command> [world] trim [freq] [pad] - trim world outside of border.
/<command> debug <on/off> - turn debug mode on or off.
permissions:
worldborder.*:
@ -41,6 +46,7 @@ permissions:
worldborder.delay: true
worldborder.wshape: true
worldborder.fill: true
worldborder.trim: true
worldborder.help: true
worldborder.whoosh: true
worldborder.set:
@ -82,6 +88,9 @@ permissions:
worldborder.fill:
description: Can fill in (generate) any missing map chunks out to the border
default: op
worldborder.trim:
description: Can trim (remove) any excess map chunks outside of the border
default: op
worldborder.help:
description: Can view the command reference help pages
default: op