<commit_before>/*
    GriefPrevention Server Plugin for Minecraft
    Copyright (C) 2012 <NAME>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
package me.ryanhamshire.GriefPrevention;

import java.util.ArrayList;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.entity.Player;

//non-main-thread task which processes world data to repair the unnatural
//after processing is complete, creates a main thread task to make the necessary changes to the world
class RestoreNatureProcessingTask implements Runnable 
{
	//world information captured from the main thread
	//will be updated and sent back to main thread to be applied to the world
	private BlockSnapshot[][][] snapshots;
	
	//other information collected from the main thread.
	//not to be updated, only to be passed back to main thread to provide some context about the operation
	private int miny;
	private Environment environment;
	private Location lesserBoundaryCorner;
	private Location greaterBoundaryCorner;
	private Player player;			//absolutely must not be accessed.  not thread safe.
	private Biome biome;
	private boolean creativeMode;
	private int seaLevel;
	private boolean aggressiveMode;
	
	//two lists of materials
	private ArrayList<Integer> notAllowedToHang;    //natural blocks which don't naturally hang in their air
	private ArrayList<Integer> playerBlocks;		//a "complete" list of player-placed blocks.  MUST BE MAINTAINED as patches introduce more
	
	public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode, Player player)
	{
		this.snapshots = snapshots;
		this.miny = miny;
		if(this.miny < 0) this.miny = 0;
		this.environment = environment;
		this.lesserBoundaryCorner = lesserBoundaryCorner;
		this.greaterBoundaryCorner = greaterBoundaryCorner;
		this.biome = biome;
		this.seaLevel = seaLevel;
		this.aggressiveMode = aggressiveMode;
		this.player = player;
		this.creativeMode = creativeMode;
		
		this.notAllowedToHang = new ArrayList<Integer>();
		this.notAllowedToHang.add(Material.DIRT.getId());
		this.notAllowedToHang.add(Material.LONG_GRASS.getId());
		this.notAllowedToHang.add(Material.SNOW.getId());
		this.notAllowedToHang.add(Material.LOG.getId());
		
		if(this.aggressiveMode)
		{
			this.notAllowedToHang.add(Material.GRASS.getId());			
			this.notAllowedToHang.add(Material.STONE.getId());
		}
		
		this.playerBlocks = new ArrayList<Integer>();
		this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome));
		
		//in aggressive or creative world mode, also treat these blocks as user placed, to be removed
		//this is helpful in the few cases where griefers intentionally use natural blocks to grief,
		//like a single-block tower of iron ore or a giant penis constructed with melons
		if(this.aggressiveMode || this.creativeMode)
		{
			this.playerBlocks.add(Material.IRON_ORE.getId());
			this.playerBlocks.add(Material.GOLD_ORE.getId());
			this.playerBlocks.add(Material.DIAMOND_ORE.getId());
			this.playerBlocks.add(Material.MELON_BLOCK.getId());
			this.playerBlocks.add(Material.MELON_STEM.getId());
			this.playerBlocks.add(Material.BEDROCK.getId());
			this.playerBlocks.add(Material.COAL_ORE.getId());
			this.playerBlocks.add(Material.PUMPKIN.getId());
			this.playerBlocks.add(Material.PUMPKIN_STEM.getId());
			this.playerBlocks.add(Material.MELON.getId());
		}
		
		if(this.aggressiveMode)
		{
			this.playerBlocks.add(Material.LEAVES.getId());
			this.playerBlocks.add(Material.LOG.getId());
			this.playerBlocks.add(Material.VINE.getId());
		}
	}
	
	@Override
	public void run()
	{
		//order is important!
		
		//remove sandstone which appears to be unnatural
		this.removeSandstone();
		
		//remove any blocks which are definitely player placed
		this.removePlayerBlocks();
		
		//reduce large outcroppings of stone, sandstone
		this.reduceStone();
		
		//reduce logs, except in jungle biomes
		this.reduceLogs();
		
		//remove natural blocks which are unnaturally hanging in the air
		this.removeHanging();
		
		//remove natural blocks which are unnaturally stacked high
		this.removeWallsAndTowers();
		
		//fill unnatural thin trenches and single-block potholes
		this.fillHolesAndTrenches();
		
		//fill water depressions and fix unnatural surface ripples
		this.fixWater();
		
		//remove water/lava above sea level
		this.removeDumpedFluids();
		
		//cover over any gaping holes in creative mode worlds
		if(this.creativeMode && this.environment == Environment.NORMAL)
		{
			this.fillBigHoles();
		}
		
		//cover surface stone and gravel with sand or grass, as the biome requires
		this.coverSurfaceStone();
		
		//remove any player-placed leaves
		this.removePlayerLeaves();
		
		//schedule main thread task to apply the result to the world
		RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player);
		GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task);
	}
	
	private void removePlayerLeaves()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = this.seaLevel - 1; y < snapshots[0].length; y++)
				{
					//note: see minecraft wiki data values for leaves
					BlockSnapshot block = snapshots[x][y][z];
					if(block.typeId == Material.LEAVES.getId() && (block.data & 0x4) != 0)
					{
						block.typeId = Material.AIR.getId();
					}
				}
			}
		}		
	}

	private void fillBigHoles()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				//replace air, lava, or running water at sea level with stone
				if(this.snapshots[x][this.seaLevel - 2][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 2][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 2][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 2][z].data != 0))
				{
					this.snapshots[x][this.seaLevel - 2][z].typeId = Material.STONE.getId();
				}
				
				//do the same for one layer beneath that (because a future restoration step may convert surface stone to sand, which falls down)
				if(this.snapshots[x][this.seaLevel - 3][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 3][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 3][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 3][z].data != 0))
				{
					this.snapshots[x][this.seaLevel - 3][z].typeId = Material.STONE.getId();
				}
			}
		}
	}
	
	//converts sandstone adjacent to sand to sand, and any other sandstone to air
	private void removeSandstone()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = snapshots[0].length - 2; y > miny; y--)
				{
					if(snapshots[x][y][z].typeId != Material.SANDSTONE.getId()) continue;
					
					BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
					BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
					BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
					BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z];
					
					//skip blocks which may cause a cave-in
					if(aboveBlock.typeId == Material.SAND.getId() && underBlock.typeId == Material.AIR.getId()) continue;
					
					//count adjacent non-air/non-leaf blocks
					if(	leftBlock.typeId == Material.SAND.getId() || 
						rightBlock.typeId == Material.SAND.getId() ||
						upBlock.typeId == Material.SAND.getId() ||
						downBlock.typeId == Material.SAND.getId() ||
						aboveBlock.typeId == Material.SAND.getId() ||
						underBlock.typeId == Material.SAND.getId())
					{
						snapshots[x][y][z].typeId = Material.SAND.getId();
					}
					else
					{
						snapshots[x][y][z].typeId = Material.AIR.getId();
					}					
				}				
			}
		}
	}
	
	private void reduceStone()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				int thisy = this.highestY(x, z, true);
				
				while(thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].typeId == Material.STONE.getId() || this.snapshots[x][thisy][z].typeId == Material.SANDSTONE.getId()))
				{
					BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z];
					BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1];
					
					//count adjacent non-air/non-leaf blocks
					byte adjacentBlockCount = 0;
					if(leftBlock.typeId != Material.AIR.getId() && leftBlock.typeId != Material.LEAVES.getId() && leftBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(rightBlock.typeId != Material.AIR.getId() && rightBlock.typeId != Material.LEAVES.getId() && rightBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(downBlock.typeId != Material.AIR.getId() && downBlock.typeId != Material.LEAVES.getId() && downBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(upBlock.typeId != Material.AIR.getId() && upBlock.typeId != Material.LEAVES.getId() && upBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					
					if(adjacentBlockCount < 3)
					{
						this.snapshots[x][thisy][z].typeId = Material.AIR.getId();
					}

					thisy--;
				}				
			}
		}
	}
	
	private void reduceLogs()
	{
		boolean jungleBiome = this.biome == Biome.JUNGLE || this.biome == Biome.JUNGLE_HILLS;
		
		//scan all blocks above sea level
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = this.seaLevel - 1; y < snapshots[0].length; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					
					//skip non-logs
					if(block.typeId != Material.LOG.getId()) continue;
					
					//if in jungle biome, skip jungle logs
					if(jungleBiome && block.data == 3) continue;
				
					//examine adjacent blocks for logs
					BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
					BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
					
					//if any, remove the log
					if(leftBlock.typeId == Material.LOG.getId() || rightBlock.typeId == Material.LOG.getId() || upBlock.typeId == Material.LOG.getId() || downBlock.typeId == Material.LOG.getId())
					{
						this.snapshots[x][y][z].typeId = Material.AIR.getId();
					}
				}				
			}
		}
	}
	
	private void removePlayerBlocks()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		//remove all player blocks
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					if(this.playerBlocks.contains(block.typeId))
					{
						block.typeId = Material.AIR.getId();
					}
				}
			}
		}				
	}
	
	private void removeHanging()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					BlockSnapshot underBlock = snapshots[x][y - 1][z];
					
					if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.STATIONARY_WATER.getId() || underBlock.typeId == Material.STATIONARY_LAVA.getId() || underBlock.typeId == Material.LEAVES.getId())
					{
						if(this.notAllowedToHang.contains(block.typeId))
						{
							block.typeId = Material.AIR.getId();
						}
					}
				}
			}
		}
	}
	
	private void removeWallsAndTowers()
	{
		int [] excludedBlocksArray = new int []
		{
			Material.CACTUS.getId(),
			Material.LONG_GRASS.getId(),
			Material.RED_MUSHROOM.getId(),
			Material.BROWN_MUSHROOM.getId(),
			Material.DEAD_BUSH.getId(),
			Material.SAPLING.getId(),
			Material.YELLOW_FLOWER.getId(),
			Material.RED_ROSE.getId(),
			Material.SUGAR_CANE_BLOCK.getId(),
			Material.VINE.getId(),
			Material.PUMPKIN.getId(),
			Material.WATER_LILY.getId(),
			Material.LEAVES.getId()
		};
		
		ArrayList<Integer> excludedBlocks = new ArrayList<Integer>();
		for(int i = 0; i < excludedBlocksArray.length; i++) excludedBlocks.add(excludedBlocksArray[i]);
		
		boolean changed;
		do
		{
			changed = false;
			for(int x = 1; x < snapshots.length - 1; x++)
			{
				for(int z = 1; z < snapshots[0][0].length - 1; z++)
				{
					int thisy = this.highestY(x, z, false);
					if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue;
						
					int righty = this.highestY(x + 1, z, false);
					int lefty = this.highestY(x - 1, z, false);
					while(lefty < thisy && righty < thisy)
					{
						this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
						changed = true;
					}
					
					int upy = this.highestY(x, z + 1, false);
					int downy = this.highestY(x, z - 1, false);
					while(upy < thisy && downy < thisy)
					{
						this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
						changed = true;
					}
				}
			}
		}while(changed);
	}
	
	private void coverSurfaceStone()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				int y = this.highestY(x, z, true);
				BlockSnapshot block = snapshots[x][y][z];
				
				if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.SOIL.getId() || block.typeId == Material.DIRT.getId() || block.typeId == Material.SANDSTONE.getId())
				{
					if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH)
					{
						this.snapshots[x][y][z].typeId = Material.SAND.getId();
					}
					else
					{
						this.snapshots[x][y][z].typeId = Material.GRASS.getId();
					}
				}
			}
		}
	}
	
	private void fillHolesAndTrenches()
	{
		ArrayList<Integer> fillableBlocks = new ArrayList<Integer>();
		fillableBlocks.add(Material.AIR.getId());
		fillableBlocks.add(Material.STATIONARY_WATER.getId());
		fillableBlocks.add(Material.STATIONARY_LAVA.getId());
		fillableBlocks.add(Material.LONG_GRASS.getId());
		
		ArrayList<Integer> notSuitableForFillBlocks = new ArrayList<Integer>();
		notSuitableForFillBlocks.add(Material.LONG_GRASS.getId());
		notSuitableForFillBlocks.add(Material.CACTUS.getId());
		notSuitableForFillBlocks.add(Material.STATIONARY_WATER.getId());
		notSuitableForFillBlocks.add(Material.STATIONARY_LAVA.getId());
		
		boolean changed;
		do
		{
			changed = false;
			for(int x = 1; x < snapshots.length - 1; x++)
			{
				for(int z = 1; z < snapshots[0][0].length - 1; z++)
				{
					for(int y = 0; y < snapshots[0].length - 1; y++)
					{
						BlockSnapshot block = this.snapshots[x][y][z];
						if(!fillableBlocks.contains(block.typeId)) continue;
							
						BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
						BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
						
						if(!fillableBlocks.contains(leftBlock.typeId) && !fillableBlocks.contains(rightBlock.typeId))
						{
							if(!notSuitableForFillBlocks.contains(rightBlock.typeId))
							{
								block.typeId = rightBlock.typeId;
								changed = true;
							}
						}
					
						BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
						BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
						
						if(!fillableBlocks.contains(upBlock.typeId) && !fillableBlocks.contains(downBlock.typeId))
						{	
							if(!notSuitableForFillBlocks.contains(downBlock.typeId))
							{
								block.typeId = downBlock.typeId;
								changed = true;
							}
						}
					}
				}
			}
		}while(changed);
	}	
	
	private void fixWater()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		boolean changed;
		
		//remove hanging water or lava
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = this.snapshots[x][y][z];
					BlockSnapshot underBlock = this.snapshots[x][y][z];
					if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId())
					{
						if(underBlock.typeId == Material.AIR.getId() || (underBlock.data != 0))
						{
							block.typeId = Material.AIR.getId();
						}
					}
				}
			}
		}
		
		//fill water depressions
		do
		{
			changed = false;		
			for(int y = this.seaLevel - 10; y <= this.seaLevel; y++)			
			{
				for(int x = 1; x < snapshots.length - 1; x++)				
				{
					for(int z = 1; z < snapshots[0][0].length - 1; z++)
					{
						BlockSnapshot block = snapshots[x][y][z];
						
						//only consider air blocks and flowing water blocks for upgrade to water source blocks
						if(block.typeId == Material.AIR.getId() || (block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0))
						{
							BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
							BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
							BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
							BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
							BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
							
							//block underneath MUST be source water
							if(underBlock.typeId != Material.STATIONARY_WATER.getId() || underBlock.data != 0) continue;
							
							//count adjacent source water blocks
							byte adjacentSourceWaterCount = 0;
							if(leftBlock.typeId == Material.STATIONARY_WATER.getId() && leftBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(rightBlock.typeId == Material.STATIONARY_WATER.getId() && rightBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(upBlock.typeId == Material.STATIONARY_WATER.getId() && upBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(downBlock.typeId == Material.STATIONARY_WATER.getId() && downBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							
							//at least two adjacent blocks must be source water
							if(adjacentSourceWaterCount >= 2)
							{
								block.typeId = Material.STATIONARY_WATER.getId();
								block.data = 0;
								changed = true;
							}
						}
					}
				}
			}
		}while(changed);
	}
	
	private void removeDumpedFluids()
	{
		//remove any surface water or lava above sea level, presumed to be placed by players
		//sometimes, this is naturally generated.  but replacing it is very easy with a bucket, so overall this is a good plan
		if(this.environment == Environment.NETHER) return;<commit_msg>Fixed /rn not working well with sea level very low.<commit_after>/*
    GriefPrevention Server Plugin for Minecraft
    Copyright (C) 2012 <NAME>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
package me.ryanhamshire.GriefPrevention;

import java.util.ArrayList;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.entity.Player;

//non-main-thread task which processes world data to repair the unnatural
//after processing is complete, creates a main thread task to make the necessary changes to the world
class RestoreNatureProcessingTask implements Runnable 
{
	//world information captured from the main thread
	//will be updated and sent back to main thread to be applied to the world
	private BlockSnapshot[][][] snapshots;
	
	//other information collected from the main thread.
	//not to be updated, only to be passed back to main thread to provide some context about the operation
	private int miny;
	private Environment environment;
	private Location lesserBoundaryCorner;
	private Location greaterBoundaryCorner;
	private Player player;			//absolutely must not be accessed.  not thread safe.
	private Biome biome;
	private boolean creativeMode;
	private int seaLevel;
	private boolean aggressiveMode;
	
	//two lists of materials
	private ArrayList<Integer> notAllowedToHang;    //natural blocks which don't naturally hang in their air
	private ArrayList<Integer> playerBlocks;		//a "complete" list of player-placed blocks.  MUST BE MAINTAINED as patches introduce more
	
	public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode, Player player)
	{
		this.snapshots = snapshots;
		this.miny = miny;
		if(this.miny < 0) this.miny = 0;
		this.environment = environment;
		this.lesserBoundaryCorner = lesserBoundaryCorner;
		this.greaterBoundaryCorner = greaterBoundaryCorner;
		this.biome = biome;
		this.seaLevel = seaLevel;
		this.aggressiveMode = aggressiveMode;
		this.player = player;
		this.creativeMode = creativeMode;
		
		this.notAllowedToHang = new ArrayList<Integer>();
		this.notAllowedToHang.add(Material.DIRT.getId());
		this.notAllowedToHang.add(Material.LONG_GRASS.getId());
		this.notAllowedToHang.add(Material.SNOW.getId());
		this.notAllowedToHang.add(Material.LOG.getId());
		
		if(this.aggressiveMode)
		{
			this.notAllowedToHang.add(Material.GRASS.getId());			
			this.notAllowedToHang.add(Material.STONE.getId());
		}
		
		this.playerBlocks = new ArrayList<Integer>();
		this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome));
		
		//in aggressive or creative world mode, also treat these blocks as user placed, to be removed
		//this is helpful in the few cases where griefers intentionally use natural blocks to grief,
		//like a single-block tower of iron ore or a giant penis constructed with melons
		if(this.aggressiveMode || this.creativeMode)
		{
			this.playerBlocks.add(Material.IRON_ORE.getId());
			this.playerBlocks.add(Material.GOLD_ORE.getId());
			this.playerBlocks.add(Material.DIAMOND_ORE.getId());
			this.playerBlocks.add(Material.MELON_BLOCK.getId());
			this.playerBlocks.add(Material.MELON_STEM.getId());
			this.playerBlocks.add(Material.BEDROCK.getId());
			this.playerBlocks.add(Material.COAL_ORE.getId());
			this.playerBlocks.add(Material.PUMPKIN.getId());
			this.playerBlocks.add(Material.PUMPKIN_STEM.getId());
			this.playerBlocks.add(Material.MELON.getId());
		}
		
		if(this.aggressiveMode)
		{
			this.playerBlocks.add(Material.LEAVES.getId());
			this.playerBlocks.add(Material.LOG.getId());
			this.playerBlocks.add(Material.VINE.getId());
		}
	}
	
	@Override
	public void run()
	{
		//order is important!
		
		//remove sandstone which appears to be unnatural
		this.removeSandstone();
		
		//remove any blocks which are definitely player placed
		this.removePlayerBlocks();
		
		//reduce large outcroppings of stone, sandstone
		this.reduceStone();
		
		//reduce logs, except in jungle biomes
		this.reduceLogs();
		
		//remove natural blocks which are unnaturally hanging in the air
		this.removeHanging();
		
		//remove natural blocks which are unnaturally stacked high
		this.removeWallsAndTowers();
		
		//fill unnatural thin trenches and single-block potholes
		this.fillHolesAndTrenches();
		
		//fill water depressions and fix unnatural surface ripples
		this.fixWater();
		
		//remove water/lava above sea level
		this.removeDumpedFluids();
		
		//cover over any gaping holes in creative mode worlds
		if(this.creativeMode && this.environment == Environment.NORMAL)
		{
			this.fillBigHoles();
		}
		
		//cover surface stone and gravel with sand or grass, as the biome requires
		this.coverSurfaceStone();
		
		//remove any player-placed leaves
		this.removePlayerLeaves();
		
		//schedule main thread task to apply the result to the world
		RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player);
		GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task);
	}
	
	private void removePlayerLeaves()
	{
		if(this.seaLevel < 1) return;
	    
	    for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = this.seaLevel - 1; y < snapshots[0].length; y++)
				{
					//note: see minecraft wiki data values for leaves
					BlockSnapshot block = snapshots[x][y][z];
					if(block.typeId == Material.LEAVES.getId() && (block.data & 0x4) != 0)
					{
						block.typeId = Material.AIR.getId();
					}
				}
			}
		}		
	}

	private void fillBigHoles()
	{
		if(this.seaLevel < 3) return;
	    
	    for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				//replace air, lava, or running water at sea level with stone
				if(this.snapshots[x][this.seaLevel - 2][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 2][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 2][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 2][z].data != 0))
				{
					this.snapshots[x][this.seaLevel - 2][z].typeId = Material.STONE.getId();
				}
				
				//do the same for one layer beneath that (because a future restoration step may convert surface stone to sand, which falls down)
				if(this.snapshots[x][this.seaLevel - 3][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 3][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 3][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 3][z].data != 0))
				{
					this.snapshots[x][this.seaLevel - 3][z].typeId = Material.STONE.getId();
				}
			}
		}
	}
	
	//converts sandstone adjacent to sand to sand, and any other sandstone to air
	private void removeSandstone()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = snapshots[0].length - 2; y > miny; y--)
				{
					if(snapshots[x][y][z].typeId != Material.SANDSTONE.getId()) continue;
					
					BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
					BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
					BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
					BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z];
					
					//skip blocks which may cause a cave-in
					if(aboveBlock.typeId == Material.SAND.getId() && underBlock.typeId == Material.AIR.getId()) continue;
					
					//count adjacent non-air/non-leaf blocks
					if(	leftBlock.typeId == Material.SAND.getId() || 
						rightBlock.typeId == Material.SAND.getId() ||
						upBlock.typeId == Material.SAND.getId() ||
						downBlock.typeId == Material.SAND.getId() ||
						aboveBlock.typeId == Material.SAND.getId() ||
						underBlock.typeId == Material.SAND.getId())
					{
						snapshots[x][y][z].typeId = Material.SAND.getId();
					}
					else
					{
						snapshots[x][y][z].typeId = Material.AIR.getId();
					}					
				}				
			}
		}
	}
	
	private void reduceStone()
	{
		if(this.seaLevel < 1) return;
	    
	    for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				int thisy = this.highestY(x, z, true);
				
				while(thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].typeId == Material.STONE.getId() || this.snapshots[x][thisy][z].typeId == Material.SANDSTONE.getId()))
				{
					BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z];
					BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1];
					
					//count adjacent non-air/non-leaf blocks
					byte adjacentBlockCount = 0;
					if(leftBlock.typeId != Material.AIR.getId() && leftBlock.typeId != Material.LEAVES.getId() && leftBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(rightBlock.typeId != Material.AIR.getId() && rightBlock.typeId != Material.LEAVES.getId() && rightBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(downBlock.typeId != Material.AIR.getId() && downBlock.typeId != Material.LEAVES.getId() && downBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					if(upBlock.typeId != Material.AIR.getId() && upBlock.typeId != Material.LEAVES.getId() && upBlock.typeId != Material.VINE.getId())
					{
						adjacentBlockCount++;
					}
					
					if(adjacentBlockCount < 3)
					{
						this.snapshots[x][thisy][z].typeId = Material.AIR.getId();
					}

					thisy--;
				}				
			}
		}
	}
	
	private void reduceLogs()
	{
		if(this.seaLevel < 1) return;
	    
	    boolean jungleBiome = this.biome == Biome.JUNGLE || this.biome == Biome.JUNGLE_HILLS;
		
		//scan all blocks above sea level
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = this.seaLevel - 1; y < snapshots[0].length; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					
					//skip non-logs
					if(block.typeId != Material.LOG.getId()) continue;
					
					//if in jungle biome, skip jungle logs
					if(jungleBiome && block.data == 3) continue;
				
					//examine adjacent blocks for logs
					BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
					BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
					BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
					BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
					
					//if any, remove the log
					if(leftBlock.typeId == Material.LOG.getId() || rightBlock.typeId == Material.LOG.getId() || upBlock.typeId == Material.LOG.getId() || downBlock.typeId == Material.LOG.getId())
					{
						this.snapshots[x][y][z].typeId = Material.AIR.getId();
					}
				}				
			}
		}
	}
	
	private void removePlayerBlocks()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		//remove all player blocks
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					if(this.playerBlocks.contains(block.typeId))
					{
						block.typeId = Material.AIR.getId();
					}
				}
			}
		}				
	}
	
	private void removeHanging()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = snapshots[x][y][z];
					BlockSnapshot underBlock = snapshots[x][y - 1][z];
					
					if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.STATIONARY_WATER.getId() || underBlock.typeId == Material.STATIONARY_LAVA.getId() || underBlock.typeId == Material.LEAVES.getId())
					{
						if(this.notAllowedToHang.contains(block.typeId))
						{
							block.typeId = Material.AIR.getId();
						}
					}
				}
			}
		}
	}
	
	private void removeWallsAndTowers()
	{
		int [] excludedBlocksArray = new int []
		{
			Material.CACTUS.getId(),
			Material.LONG_GRASS.getId(),
			Material.RED_MUSHROOM.getId(),
			Material.BROWN_MUSHROOM.getId(),
			Material.DEAD_BUSH.getId(),
			Material.SAPLING.getId(),
			Material.YELLOW_FLOWER.getId(),
			Material.RED_ROSE.getId(),
			Material.SUGAR_CANE_BLOCK.getId(),
			Material.VINE.getId(),
			Material.PUMPKIN.getId(),
			Material.WATER_LILY.getId(),
			Material.LEAVES.getId()
		};
		
		ArrayList<Integer> excludedBlocks = new ArrayList<Integer>();
		for(int i = 0; i < excludedBlocksArray.length; i++) excludedBlocks.add(excludedBlocksArray[i]);
		
		boolean changed;
		do
		{
			changed = false;
			for(int x = 1; x < snapshots.length - 1; x++)
			{
				for(int z = 1; z < snapshots[0][0].length - 1; z++)
				{
					int thisy = this.highestY(x, z, false);
					if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue;
						
					int righty = this.highestY(x + 1, z, false);
					int lefty = this.highestY(x - 1, z, false);
					while(lefty < thisy && righty < thisy)
					{
						this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
						changed = true;
					}
					
					int upy = this.highestY(x, z + 1, false);
					int downy = this.highestY(x, z - 1, false);
					while(upy < thisy && downy < thisy)
					{
						this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
						changed = true;
					}
				}
			}
		}while(changed);
	}
	
	private void coverSurfaceStone()
	{
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				int y = this.highestY(x, z, true);
				BlockSnapshot block = snapshots[x][y][z];
				
				if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.SOIL.getId() || block.typeId == Material.DIRT.getId() || block.typeId == Material.SANDSTONE.getId())
				{
					if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH)
					{
						this.snapshots[x][y][z].typeId = Material.SAND.getId();
					}
					else
					{
						this.snapshots[x][y][z].typeId = Material.GRASS.getId();
					}
				}
			}
		}
	}
	
	private void fillHolesAndTrenches()
	{
		ArrayList<Integer> fillableBlocks = new ArrayList<Integer>();
		fillableBlocks.add(Material.AIR.getId());
		fillableBlocks.add(Material.STATIONARY_WATER.getId());
		fillableBlocks.add(Material.STATIONARY_LAVA.getId());
		fillableBlocks.add(Material.LONG_GRASS.getId());
		
		ArrayList<Integer> notSuitableForFillBlocks = new ArrayList<Integer>();
		notSuitableForFillBlocks.add(Material.LONG_GRASS.getId());
		notSuitableForFillBlocks.add(Material.CACTUS.getId());
		notSuitableForFillBlocks.add(Material.STATIONARY_WATER.getId());
		notSuitableForFillBlocks.add(Material.STATIONARY_LAVA.getId());
		
		boolean changed;
		do
		{
			changed = false;
			for(int x = 1; x < snapshots.length - 1; x++)
			{
				for(int z = 1; z < snapshots[0][0].length - 1; z++)
				{
					for(int y = 0; y < snapshots[0].length - 1; y++)
					{
						BlockSnapshot block = this.snapshots[x][y][z];
						if(!fillableBlocks.contains(block.typeId)) continue;
							
						BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
						BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
						
						if(!fillableBlocks.contains(leftBlock.typeId) && !fillableBlocks.contains(rightBlock.typeId))
						{
							if(!notSuitableForFillBlocks.contains(rightBlock.typeId))
							{
								block.typeId = rightBlock.typeId;
								changed = true;
							}
						}
					
						BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
						BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
						
						if(!fillableBlocks.contains(upBlock.typeId) && !fillableBlocks.contains(downBlock.typeId))
						{	
							if(!notSuitableForFillBlocks.contains(downBlock.typeId))
							{
								block.typeId = downBlock.typeId;
								changed = true;
							}
						}
					}
				}
			}
		}while(changed);
	}	
	
	private void fixWater()
	{
		int miny = this.miny;
		if(miny < 1) miny = 1;
		
		boolean changed;
		
		//remove hanging water or lava
		for(int x = 1; x < snapshots.length - 1; x++)
		{
			for(int z = 1; z < snapshots[0][0].length - 1; z++)
			{
				for(int y = miny; y < snapshots[0].length - 1; y++)
				{
					BlockSnapshot block = this.snapshots[x][y][z];
					BlockSnapshot underBlock = this.snapshots[x][y][z];
					if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId())
					{
						if(underBlock.typeId == Material.AIR.getId() || (underBlock.data != 0))
						{
							block.typeId = Material.AIR.getId();
						}
					}
				}
			}
		}
		
		//fill water depressions
		do
		{
			changed = false;		
			for(int y = Math.max(this.seaLevel - 10, 0); y <= this.seaLevel; y++)			
			{
				for(int x = 1; x < snapshots.length - 1; x++)				
				{
					for(int z = 1; z < snapshots[0][0].length - 1; z++)
					{
						BlockSnapshot block = snapshots[x][y][z];
						
						//only consider air blocks and flowing water blocks for upgrade to water source blocks
						if(block.typeId == Material.AIR.getId() || (block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0))
						{
							BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
							BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
							BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
							BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
							BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
							
							//block underneath MUST be source water
							if(underBlock.typeId != Material.STATIONARY_WATER.getId() || underBlock.data != 0) continue;
							
							//count adjacent source water blocks
							byte adjacentSourceWaterCount = 0;
							if(leftBlock.typeId == Material.STATIONARY_WATER.getId() && leftBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(rightBlock.typeId == Material.STATIONARY_WATER.getId() && rightBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(upBlock.typeId == Material.STATIONARY_WATER.getId() && upBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							if(downBlock.typeId == Material.STATIONARY_WATER.getId() && downBlock.data == 0)
							{
								adjacentSourceWaterCount++;
							}
							
							//at least two adjacent blocks must be source water
							if(adjacentSourceWaterCount >= 2)
							{
								block.typeId = Material.STATIONARY_WATER.getId();
								block.data = 0;
								changed = true;
							}
						}
					}
				}
			}
		}while(changed);
	}
	
	private void removeDumpedFluids()
	{
		if(this.seaLevel < 1) return;
	    
	    //remove any surface water or lava above sea level, presumed to be placed by players
		//sometimes, this is naturally generated.  but replacing it is very easy with a bucket, so overall this is a good plan
		if(this.environment == Environment.NETHER) return;
{'fraction_non_alphanumeric': 0.13871516485715715, 'fraction_numerical': 0.00815530094561465, 'mean_word_length': 3.0583756345177666, 'pattern_counts': {'":': 0, '<': 114, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 2, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5044891', 'n_tokens_mistral': 16536, 'n_tokens_neox': 13188, 'n_words': 2946}