2011-08-15 17:01:03 +02:00
package com.wimbli.WorldBorder ;
2015-09-04 20:20:27 +02:00
import java.io.File ;
import java.io.FileNotFoundException ;
import java.io.IOException ;
import java.io.RandomAccessFile ;
import java.util.ArrayList ;
import java.util.List ;
2011-08-15 17:01:03 +02:00
2015-09-05 11:13:10 +02:00
import org.bukkit.Bukkit ;
import org.bukkit.entity.Player ;
import org.bukkit.Server ;
import org.bukkit.World ;
import com.wimbli.WorldBorder.Events.WorldBorderTrimFinishedEvent ;
2016-01-12 20:47:10 +01:00
import com.wimbli.WorldBorder.Events.WorldBorderTrimStartEvent ;
2015-09-05 11:13:10 +02:00
2011-08-15 17:01:03 +02:00
public class WorldTrimTask implements Runnable
{
// general task-related reference data
private transient Server server = null ;
private transient World world = null ;
2012-02-11 03:20:47 +01:00
private transient WorldFileData worldData = null ;
2011-08-15 17:01:03 +02:00
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 ;
}
2013-02-14 10:35:35 +01:00
this . border . setRadiusX ( border . getRadiusX ( ) + trimDistance ) ;
this . border . setRadiusZ ( border . getRadiusZ ( ) + trimDistance ) ;
2011-08-15 17:01:03 +02:00
2012-02-11 03:20:47 +01:00
worldData = WorldFileData . create ( world , notifyPlayer ) ;
if ( worldData = = null )
2011-08-15 17:01:03 +02:00
{
this . stop ( ) ;
return ;
}
// each region file covers up to 1024 chunks; with all operations we might need to do, let's figure 3X that
2012-02-11 03:20:47 +01:00
this . reportTarget = worldData . regionFileCount ( ) * 3072 ;
2011-08-15 17:01:03 +02:00
// queue up the first file
if ( ! nextFile ( ) )
return ;
this . readyToGo = true ;
2016-01-12 20:47:10 +01:00
Bukkit . getServer ( ) . getPluginManager ( ) . callEvent ( new WorldBorderTrimStartEvent ( this ) ) ;
2011-08-15 17:01:03 +02:00
}
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 ;
2013-07-12 09:17:47 +02:00
// 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 ( ) ;
2011-08-15 17:01:03 +02:00
counter = 0 ;
while ( counter < = chunksPerRun )
{
// in case the task has been paused while we're repeating...
if ( paused )
return ;
2013-07-12 09:17:47 +02:00
long now = Config . Now ( ) ;
2011-08-16 02:04:02 +02:00
// every 5 seconds or so, give basic progress report to let user know how it's going
2013-07-12 09:17:47 +02:00
if ( now > lastReport + 5000 )
2011-08-15 17:01:03 +02:00
reportProgress ( ) ;
2013-07-12 09:25:36 +02:00
// 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
2013-07-12 09:17:47 +02:00
if ( now > loopStartTime + 45 )
{
readyToGo = true ;
return ;
}
2011-08-15 17:01:03 +02:00
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 + + ;
2012-02-11 03:20:47 +01:00
File regionFile = worldData . regionFile ( currentRegion ) ;
if ( ! regionFile . delete ( ) )
2011-08-15 17:01:03 +02:00
{
2012-02-11 03:20:47 +01:00
sendMessage ( " Error! Region file which is outside the border could not be deleted: " + regionFile . getName ( ) ) ;
2011-08-15 17:01:03 +02:00
wipeChunks ( ) ;
}
2012-03-16 06:26:13 +01:00
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));
}
2011-08-15 17:01:03 +02:00
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?
2012-02-11 03:20:47 +01:00
if ( currentRegion > = worldData . regionFileCount ( ) )
2011-08-15 17:01:03 +02:00
{ // hey, we're done
paused = true ;
readyToGo = false ;
finish ( ) ;
return false ;
}
counter + = 16 ;
2012-02-11 03:20:47 +01:00
// get the X and Z coordinates of the current region
CoordXZ coord = worldData . regionFileCoordinates ( currentRegion ) ;
if ( coord = = null )
2011-08-15 17:01:03 +02:00
return false ;
2012-02-11 03:20:47 +01:00
regionX = coord . x ;
regionZ = coord . z ;
2011-08-15 17:01:03 +02:00
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 )
{
2011-10-18 18:44:12 +02:00
if ( world . isChunkLoaded ( unload . x , unload . z ) )
world . unloadChunk ( unload . x , unload . z , false , false ) ;
2011-08-15 17:01:03 +02:00
}
counter + = trimChunks . size ( ) ;
}
// edit region file to wipe all chunk pointers for chunks outside the border
private void wipeChunks ( )
{
2012-02-11 03:20:47 +01:00
File regionFile = worldData . regionFile ( currentRegion ) ;
if ( ! regionFile . canWrite ( ) )
2011-08-15 17:01:03 +02:00
{
2014-02-17 13:37:18 +01:00
if ( ! regionFile . setWritable ( true ) )
throw new RuntimeException ( ) ;
2012-02-11 03:20:47 +01:00
if ( ! regionFile . canWrite ( ) )
2011-08-15 17:01:03 +02:00
{
2012-02-11 03:20:47 +01:00
sendMessage ( " Error! region file is locked and can't be trimmed: " + regionFile . getName ( ) ) ;
2011-08-15 17:01:03 +02:00
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 ;
2013-07-12 08:56:27 +02:00
int chunkCount = 0 ;
2011-08-15 17:01:03 +02:00
try
{
2012-02-11 03:20:47 +01:00
RandomAccessFile unChunk = new RandomAccessFile ( regionFile , " rwd " ) ;
2011-08-15 17:01:03 +02:00
for ( CoordXZ wipe : trimChunks )
2013-07-12 08:56:27 +02:00
{
// 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,
2011-08-15 17:01:03 +02:00
// 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 ) ;
2013-07-12 08:56:27 +02:00
chunkCount + + ;
2011-08-15 17:01:03 +02:00
}
unChunk . close ( ) ;
2012-03-16 06:26:13 +01:00
// if DynMap is installed, re-render the trimmed chunks ... disabled since it's not currently working, oh well
// DynMapFeatures.renderChunks(world.getName(), trimChunks);
2013-07-12 08:56:27 +02:00
reportTrimmedChunks + = chunkCount ;
2011-08-15 17:01:03 +02:00
}
catch ( FileNotFoundException ex )
{
2012-02-11 03:20:47 +01:00
sendMessage ( " Error! Could not open region file to wipe individual chunks: " + regionFile . getName ( ) ) ;
2011-08-15 17:01:03 +02:00
}
catch ( IOException ex )
{
2012-02-11 03:20:47 +01:00
sendMessage ( " Error! Could not modify region file to wipe individual chunks: " + regionFile . getName ( ) ) ;
2011-08-15 17:01:03 +02:00
}
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 ( ) ;
2015-09-04 20:20:27 +02:00
Bukkit . getServer ( ) . getPluginManager ( ) . callEvent ( new WorldBorderTrimFinishedEvent ( world , reportTotal ) ) ;
2011-08-15 17:01:03 +02:00
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 ( ) ;
2016-01-15 12:38:17 +01:00
double perc = getPercentageCompleted ( ) ;
2011-09-22 15:10:49 +02:00
sendMessage ( reportTrimmedRegions + " entire region(s) and " + reportTrimmedChunks + " individual chunk(s) trimmed so far ( " + Config . coord . format ( perc ) + " % done " + " ) " ) ;
2011-08-15 17:01:03 +02:00
}
// send a message to the server console/log and possibly to an in-game player
private void sendMessage ( String text )
{
2014-02-17 13:37:18 +01:00
Config . log ( " [Trim] " + text ) ;
2011-08-15 17:01:03 +02:00
if ( notifyPlayer ! = null )
notifyPlayer . sendMessage ( " [Trim] " + text ) ;
}
2016-01-15 12:38:17 +01:00
/ * *
* 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 ;
}
2011-08-15 17:01:03 +02:00
}