diff --git a/plugin.yml b/plugin.yml index ca55e53..980d34e 100644 --- a/plugin.yml +++ b/plugin.yml @@ -2,7 +2,7 @@ name: GriefPrevention main: me.ryanhamshire.GriefPrevention.GriefPrevention softdepend: [Vault, Multiverse-Core, My Worlds, MystCraft, Transporter] dev-url: http://dev.bukkit.org/server-mods/grief-prevention -version: 7.2 +version: 7.2.2 commands: abandonclaim: description: Deletes a claim. diff --git a/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java index b1431c3..867790a 100644 --- a/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java @@ -1,743 +1,769 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - 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 . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; -import java.util.List; - -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.Chest; -import org.bukkit.entity.Arrow; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockDamageEvent; -import org.bukkit.event.block.BlockDispenseEvent; -import org.bukkit.event.block.BlockFromToEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; -import org.bukkit.event.block.BlockPistonExtendEvent; -import org.bukkit.event.block.BlockPistonRetractEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.block.BlockSpreadEvent; -import org.bukkit.event.block.SignChangeEvent; -import org.bukkit.event.entity.ProjectileHitEvent; -import org.bukkit.event.world.StructureGrowEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.util.Vector; - -//event handlers related to blocks -public class BlockEventHandler implements Listener -{ - //convenience reference to singleton datastore - private DataStore dataStore; - - private ArrayList trashBlocks; - - //constructor - public BlockEventHandler(DataStore dataStore) - { - this.dataStore = dataStore; - - //create the list of blocks which will not trigger a warning when they're placed outside of land claims - this.trashBlocks = new ArrayList(); - this.trashBlocks.add(Material.COBBLESTONE); - this.trashBlocks.add(Material.TORCH); - this.trashBlocks.add(Material.DIRT); - this.trashBlocks.add(Material.SAPLING); - this.trashBlocks.add(Material.GRAVEL); - this.trashBlocks.add(Material.SAND); - this.trashBlocks.add(Material.TNT); - this.trashBlocks.add(Material.WORKBENCH); - } - - //when a wooden button is triggered by an arrow... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onProjectileHit(ProjectileHitEvent event) - { - Projectile projectile = event.getEntity(); - Location location = projectile.getLocation(); - Block block = location.getBlock(); - - //only care about wooden buttons - if(block.getType() != Material.WOOD_BUTTON) return; - - //only care about arrows - if(projectile instanceof Arrow) - { - Arrow arrow = (Arrow)projectile; - LivingEntity shooterEntity = arrow.getShooter(); - - //player arrows only trigger buttons when they have permission - if(shooterEntity instanceof Player) - { - //Player player = (Player)shooterEntity; - } - - //other arrows don't trigger buttons, could be used as a workaround to get access to areas without permission - else - { - } - } - } - - //when a block is damaged... - @EventHandler(ignoreCancelled = true) - public void onBlockDamaged(BlockDamageEvent event) - { - //if placing items in protected chests isn't enabled, none of this code needs to run - if(!GriefPrevention.instance.config_addItemsToClaimedChests) return; - - Block block = event.getBlock(); - Player player = event.getPlayer(); - - //only care about player-damaged blocks - if(player == null) return; - - //FEATURE: players may add items to a chest they don't have permission for by hitting it - - //if it's a chest - if(block.getType() == Material.CHEST) - { - //only care about non-creative mode players, since those would outright break the box in one hit - if(player.getGameMode() == GameMode.CREATIVE) return; - - //only care if the player has an itemstack in hand - PlayerInventory playerInventory = player.getInventory(); - ItemStack stackInHand = playerInventory.getItemInHand(); - if(stackInHand == null || stackInHand.getType() == Material.AIR) return; - - //only care if the chest is in a claim, and the player does not have access to the chest - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim == null || claim.allowContainers(player) == null) return; - - //if the player is under siege, he can't give away items - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); - event.setCancelled(true); - return; - } - - //if a player is in pvp combat, he can't give away items - if(playerData.inPvpCombat()) return; - - //NOTE: to eliminate accidental give-aways, first hit on a chest displays a confirmation message - //subsequent hits donate item to the chest - - //if first time damaging this chest, show confirmation message - if(playerData.lastChestDamageLocation == null || !block.getLocation().equals(playerData.lastChestDamageLocation)) - { - //remember this location - playerData.lastChestDamageLocation = block.getLocation(); - - //give the player instructions - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DonateItemsInstruction); - } - - //otherwise, try to donate the item stack in hand - else - { - //look for empty slot in chest - Chest chest = (Chest)block.getState(); - Inventory chestInventory = chest.getInventory(); - int availableSlot = chestInventory.firstEmpty(); - - //if there isn't one - if(availableSlot < 0) - { - //tell the player and stop here - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ChestFull); - - return; - } - - //otherwise, transfer item stack from player to chest - //NOTE: Inventory.addItem() is smart enough to add items to existing stacks, making filling a chest with garbage as a grief very difficult - chestInventory.addItem(stackInHand); - playerInventory.setItemInHand(new ItemStack(Material.AIR)); - - //and confirm for the player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DonationSuccess); - } - } - } - - //when a player breaks a block... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockBreak(BlockBreakEvent breakEvent) - { - Player player = breakEvent.getPlayer(); - Block block = breakEvent.getBlock(); - - //make sure the player is allowed to break at the location - String noBuildReason = GriefPrevention.instance.allowBreak(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - breakEvent.setCancelled(true); - return; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - - //if there's a claim here - if(claim != null) - { - //if breaking UNDER the claim and the player has permission to build in the claim - if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) - { - //extend the claim downward beyond the breakage point - this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); - } - } - - //FEATURE: automatically clean up hanging treetops - //if it's a log - if(block.getType() == Material.LOG && GriefPrevention.instance.config_trees_removeFloatingTreetops) - { - //run the specialized code for treetop removal (see below) - GriefPrevention.instance.handleLogBroken(block); - } - } - - //when a player places a sign... - @EventHandler(ignoreCancelled = true) - public void onSignChanged(SignChangeEvent event) - { - Player player = event.getPlayer(); - if(player == null) return; - - StringBuilder lines = new StringBuilder(); - boolean notEmpty = false; - for(int i = 0; i < event.getLines().length; i++) - { - if(event.getLine(i).length() != 0) notEmpty = true; - lines.append(event.getLine(i) + ";"); - } - - String signMessage = lines.toString(); - - //if not empty and wasn't the same as the last sign, log it and remember it for later - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if(notEmpty && playerData.lastMessage != null && !playerData.lastMessage.equals(signMessage)) - { - GriefPrevention.AddLogEntry("[Sign Placement] <" + player.getName() + "> " + lines.toString() + " @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); - playerData.lastMessage = signMessage; - } - } - - //when a player places a block... - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) - public void onBlockPlace(BlockPlaceEvent placeEvent) - { - Player player = placeEvent.getPlayer(); - Block block = placeEvent.getBlock(); - - //FEATURE: limit fire placement, to prevent PvP-by-fire - - //if placed block is fire and pvp is off, apply rules for proximity to other players - if(block.getType() == Material.FIRE && !GriefPrevention.instance.config_pvp_enabledWorlds.contains(block.getWorld()) && !player.hasPermission("griefprevention.lava")) - { - List players = block.getWorld().getPlayers(); - for(int i = 0; i < players.size(); i++) - { - Player otherPlayer = players.get(i); - Location location = otherPlayer.getLocation(); - if(!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire, otherPlayer.getName()); - placeEvent.setCancelled(true); - return; - } - } - } - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - placeEvent.setCancelled(true); - return; - } - - //if the block is being placed within an existing claim - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - if(claim != null) - { - //warn about TNT not destroying claimed blocks - if(block.getType() == Material.TNT) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); - } - - //if the player has permission for the claim and he's placing UNDER the claim - if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) - { - //extend the claim downward - this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); - } - - //reset the counter for warning the player when he places outside his claims - playerData.unclaimedBlockPlacementsUntilWarning = 1; - } - - //FEATURE: automatically create a claim when a player who has no claims places a chest - - //otherwise if there's no claim, the player is placing a chest, and new player automatic claims are enabled - else if(block.getType() == Material.CHEST && GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) - { - //if the chest is too deep underground, don't create the claim and explain why - if(GriefPrevention.instance.config_claims_preventTheft && block.getY() < GriefPrevention.instance.config_claims_maxDepth) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.TooDeepToClaim); - return; - } - - int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; - - //if the player doesn't have any claims yet, automatically create a claim centered at the chest - if(playerData.claims.size() == 0) - { - //radius == 0 means protect ONLY the chest - if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius == 0) - { - this.dataStore.createClaim(block.getWorld(), block.getX(), block.getX(), block.getY(), block.getY(), block.getZ(), block.getZ(), player.getName(), null, null); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); - } - - //otherwise, create a claim in the area around the chest - else - { - //as long as the automatic claim overlaps another existing claim, shrink it - //note that since the player had permission to place the chest, at the very least, the automatic claim will include the chest - while(radius >= 0 && !this.dataStore.createClaim(block.getWorld(), - block.getX() - radius, block.getX() + radius, - block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, block.getY(), - block.getZ() - radius, block.getZ() + radius, - player.getName(), - null, null).succeeded) - { - radius--; - } - - //notify and explain to player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); - - //show the player the protected area - Claim newClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); - Visualization visualization = Visualization.FromClaim(newClaim, block.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - - //instructions for using /trust - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TrustCommandAdvertisement); - - //unless special permission is required to create a claim with the shovel, educate the player about the shovel - if(!GriefPrevention.instance.config_claims_creationRequiresPermission) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.GoldenShovelAdvertisement); - } - } - - //check to see if this chest is in a claim, and warn when it isn't - if(GriefPrevention.instance.config_claims_preventTheft && this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim) == null) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnprotectedChestWarning); - } - } - - //FEATURE: limit wilderness tree planting to grass, or dirt with more blocks beneath it - else if(block.getType() == Material.SAPLING && GriefPrevention.instance.config_blockSkyTrees && GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) - { - Block earthBlock = placeEvent.getBlockAgainst(); - if(earthBlock.getType() != Material.GRASS) - { - if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR || - earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR) - { - placeEvent.setCancelled(true); - } - } - } - - //FEATURE: warn players when they're placing non-trash blocks outside of their claimed areas - else if(GriefPrevention.instance.config_claims_warnOnBuildOutside && !this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld()) && playerData.claims.size() > 0) - { - if(--playerData.unclaimedBlockPlacementsUntilWarning <= 0) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); - playerData.unclaimedBlockPlacementsUntilWarning = 15; - - if(playerData.lastClaim != null && playerData.lastClaim.allowBuild(player) == null) - { - Visualization visualization = Visualization.FromClaim(playerData.lastClaim, block.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - } - } - - //warn players when they place TNT above sea level, since it doesn't destroy blocks there - if( GriefPrevention.instance.config_blockSurfaceOtherExplosions && block.getType() == Material.TNT && - block.getWorld().getEnvironment() != Environment.NETHER && - block.getY() > GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); - } - } - - //blocks "pushing" other players' blocks around (pistons) - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonExtend (BlockPistonExtendEvent event) - { - List blocks = event.getBlocks(); - - //if no blocks moving, then only check to make sure we're not pushing into a claim from outside - //this avoids pistons breaking non-solids just inside a claim, like torches, doors, and touchplates - if(blocks.size() == 0) - { - Block pistonBlock = event.getBlock(); - Block invadedBlock = pistonBlock.getRelative(event.getDirection()); - - if( this.dataStore.getClaimAt(pistonBlock.getLocation(), false, null) == null && - this.dataStore.getClaimAt(invadedBlock.getLocation(), false, null) != null) - { - event.setCancelled(true); - } - - return; - } - - //who owns the piston, if anyone? - String pistonClaimOwnerName = "_"; - Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null); - if(claim != null) pistonClaimOwnerName = claim.getOwnerName(); - - //which blocks are being pushed? - for(int i = 0; i < blocks.size(); i++) - { - //if ANY of the pushed blocks are owned by someone other than the piston owner, cancel the event - Block block = blocks.get(i); - claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim != null && !claim.getOwnerName().equals(pistonClaimOwnerName)) - { - event.setCancelled(true); - event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); - event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); - event.getBlock().setType(Material.AIR); - return; - } - } - - //which direction? note we're ignoring vertical push - int xchange = 0; - int zchange = 0; - - Block piston = event.getBlock(); - Block firstBlock = blocks.get(0); - - if(firstBlock.getX() > piston.getX()) - { - xchange = 1; - } - else if(firstBlock.getX() < piston.getX()) - { - xchange = -1; - } - else if(firstBlock.getZ() > piston.getZ()) - { - zchange = 1; - } - else if(firstBlock.getZ() < piston.getZ()) - { - zchange = -1; - } - - //if horizontal movement - if(xchange != 0 || zchange != 0) - { - for(int i = 0; i < blocks.size(); i++) - { - Block block = blocks.get(i); - Claim originalClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); - String originalOwnerName = ""; - if(originalClaim != null) - { - originalOwnerName = originalClaim.getOwnerName(); - } - - Claim newClaim = this.dataStore.getClaimAt(block.getLocation().add(xchange, 0, zchange), false, null); - String newOwnerName = ""; - if(newClaim != null) - { - newOwnerName = newClaim.getOwnerName(); - } - - //if pushing this block will change ownership, cancel the event and take away the piston (for performance reasons) - if(!newOwnerName.equals(originalOwnerName)) - { - event.setCancelled(true); - event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); - event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); - event.getBlock().setType(Material.AIR); - return; - } - - } - } - } - - //blocks theft by pulling blocks out of a claim (again pistons) - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonRetract (BlockPistonRetractEvent event) - { - //we only care about sticky pistons - if(!event.isSticky()) return; - - //who owns the moving block, if anyone? - String movingBlockOwnerName = "_"; - Claim movingBlockClaim = this.dataStore.getClaimAt(event.getRetractLocation(), false, null); - if(movingBlockClaim != null) movingBlockOwnerName = movingBlockClaim.getOwnerName(); - - //who owns the piston, if anyone? - String pistonOwnerName = "_"; - Location pistonLocation = event.getBlock().getLocation(); - Claim pistonClaim = this.dataStore.getClaimAt(pistonLocation, false, null); - if(pistonClaim != null) pistonOwnerName = pistonClaim.getOwnerName(); - - //if there are owners for the blocks, they must be the same player - //otherwise cancel the event - if(!pistonOwnerName.equals(movingBlockOwnerName)) - { - event.setCancelled(true); - } - } - - //blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockIgnite (BlockIgniteEvent igniteEvent) - { - if(!GriefPrevention.instance.config_fireSpreads && igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && igniteEvent.getCause() != IgniteCause.LIGHTNING) - { - igniteEvent.setCancelled(true); - } - } - - //fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example) - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockSpread (BlockSpreadEvent spreadEvent) - { - if(spreadEvent.getSource().getType() != Material.FIRE) return; - - if(!GriefPrevention.instance.config_fireSpreads) - { - spreadEvent.setCancelled(true); - - Block underBlock = spreadEvent.getSource().getRelative(BlockFace.DOWN); - if(underBlock.getType() != Material.NETHERRACK) - { - spreadEvent.getSource().setType(Material.AIR); - } - - return; - } - - //never spread into a claimed area, regardless of settings - if(this.dataStore.getClaimAt(spreadEvent.getBlock().getLocation(), false, null) != null) - { - spreadEvent.setCancelled(true); - - //if the source of the spread is not fire on netherrack, put out that source fire to save cpu cycles - Block source = spreadEvent.getSource(); - if(source.getType() == Material.FIRE && source.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK) - { - source.setType(Material.AIR); - } - } - } - - //blocks are not destroyed by fire, unless configured to do so - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockBurn (BlockBurnEvent burnEvent) - { - if(!GriefPrevention.instance.config_fireDestroys) - { - burnEvent.setCancelled(true); - return; - } - - //never burn claimed blocks, regardless of settings - if(this.dataStore.getClaimAt(burnEvent.getBlock().getLocation(), false, null) != null) - { - burnEvent.setCancelled(true); - } - } - - //ensures fluids don't flow out of claims, unless into another claim where the owner is trusted to build - private Claim lastSpreadClaim = null; - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockFromTo (BlockFromToEvent spreadEvent) - { - //don't track fluid movement in worlds where claims are not enabled - if(!GriefPrevention.instance.config_claims_enabledWorlds.contains(spreadEvent.getBlock().getWorld())) return; - - //always allow fluids to flow straight down - if(spreadEvent.getFace() == BlockFace.DOWN) return; - - //from where? - Block fromBlock = spreadEvent.getBlock(); - Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, this.lastSpreadClaim); - if(fromClaim != null) - { - this.lastSpreadClaim = fromClaim; - } - - //where to? - Block toBlock = spreadEvent.getToBlock(); - Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); - - //if it's within the same claim or wilderness to wilderness, allow it - if(fromClaim == toClaim) return; - - //block any spread into the wilderness from a claim - if(fromClaim != null && toClaim == null) - { - spreadEvent.setCancelled(true); - return; - } - - //if spreading into a claim - else if(toClaim != null) - { - //who owns the spreading block, if anyone? - OfflinePlayer fromOwner = null; - if(fromClaim != null) - { - fromOwner = GriefPrevention.instance.getServer().getOfflinePlayer(fromClaim.ownerName); - } - - //cancel unless the owner of the spreading block is allowed to build in the receiving claim - if(fromOwner == null || fromOwner.getPlayer() == null || toClaim.allowBuild(fromOwner.getPlayer()) != null) - { - spreadEvent.setCancelled(true); - } - } - } - - //ensures dispensers can't be used to dispense a block(like water or lava) or item across a claim boundary - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onDispense(BlockDispenseEvent dispenseEvent) - { - //from where? - Block fromBlock = dispenseEvent.getBlock(); - - //to where? - Vector velocity = dispenseEvent.getVelocity(); - int xChange = 0; - int zChange = 0; - if(Math.abs(velocity.getX()) > Math.abs(velocity.getZ())) - { - if(velocity.getX() > 0) xChange = 1; - else xChange = -1; - } - else - { - if(velocity.getZ() > 0) zChange = 1; - else zChange = -1; - } - - Block toBlock = fromBlock.getRelative(xChange, 0, zChange); - - Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); - Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); - - //into wilderness is NOT OK when surface buckets are limited - Material materialDispensed = dispenseEvent.getItem().getType(); - if((materialDispensed == Material.WATER_BUCKET || materialDispensed == Material.LAVA_BUCKET) && GriefPrevention.instance.config_blockWildernessWaterBuckets && GriefPrevention.instance.claimsEnabledForWorld(fromBlock.getWorld()) && toClaim == null) - { - dispenseEvent.setCancelled(true); - return; - } - - //wilderness to wilderness is OK - if(fromClaim == null && toClaim == null) return; - - //within claim is OK - if(fromClaim == toClaim) return; - - //everything else is NOT OK - dispenseEvent.setCancelled(true); - } - - @EventHandler(ignoreCancelled = true) - public void onTreeGrow (StructureGrowEvent growEvent) - { - Location rootLocation = growEvent.getLocation(); - Claim rootClaim = this.dataStore.getClaimAt(rootLocation, false, null); - String rootOwnerName = null; - - //who owns the spreading block, if anyone? - if(rootClaim != null) - { - //tree growth in subdivisions is dependent on who owns the top level claim - if(rootClaim.parent != null) rootClaim = rootClaim.parent; - - //if an administrative claim, just let the tree grow where it wants - if(rootClaim.isAdminClaim()) return; - - //otherwise, note the owner of the claim - rootOwnerName = rootClaim.getOwnerName(); - } - - //for each block growing - for(int i = 0; i < growEvent.getBlocks().size(); i++) - { - BlockState block = growEvent.getBlocks().get(i); - Claim blockClaim = this.dataStore.getClaimAt(block.getLocation(), false, rootClaim); - - //if it's growing into a claim - if(blockClaim != null) - { - //if there's no owner for the new tree, or the owner for the new tree is different from the owner of the claim - if(rootOwnerName == null || !rootOwnerName.equals(blockClaim.getOwnerName())) - { - growEvent.getBlocks().remove(i--); - } - } - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + 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 . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Chest; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockDamageEvent; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.entity.ProjectileHitEvent; +import org.bukkit.event.world.StructureGrowEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.util.Vector; + +//event handlers related to blocks +public class BlockEventHandler implements Listener +{ + //convenience reference to singleton datastore + private DataStore dataStore; + + private ArrayList trashBlocks; + + //constructor + public BlockEventHandler(DataStore dataStore) + { + this.dataStore = dataStore; + + //create the list of blocks which will not trigger a warning when they're placed outside of land claims + this.trashBlocks = new ArrayList(); + this.trashBlocks.add(Material.COBBLESTONE); + this.trashBlocks.add(Material.TORCH); + this.trashBlocks.add(Material.DIRT); + this.trashBlocks.add(Material.SAPLING); + this.trashBlocks.add(Material.GRAVEL); + this.trashBlocks.add(Material.SAND); + this.trashBlocks.add(Material.TNT); + this.trashBlocks.add(Material.WORKBENCH); + } + + //when a wooden button is triggered by an arrow... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onProjectileHit(ProjectileHitEvent event) + { + Projectile projectile = event.getEntity(); + Location location = projectile.getLocation(); + Block block = location.getBlock(); + + //only care about wooden buttons + if(block.getType() != Material.WOOD_BUTTON) return; + + //only care about arrows + if(projectile instanceof Arrow) + { + Arrow arrow = (Arrow)projectile; + LivingEntity shooterEntity = arrow.getShooter(); + + //player arrows only trigger buttons when they have permission + if(shooterEntity instanceof Player) + { + //Player player = (Player)shooterEntity; + } + + //other arrows don't trigger buttons, could be used as a workaround to get access to areas without permission + else + { + } + } + } + + //when a block is damaged... + @EventHandler(ignoreCancelled = true) + public void onBlockDamaged(BlockDamageEvent event) + { + //if placing items in protected chests isn't enabled, none of this code needs to run + if(!GriefPrevention.instance.config_addItemsToClaimedChests) return; + + Block block = event.getBlock(); + Player player = event.getPlayer(); + + //only care about player-damaged blocks + if(player == null) return; + + //FEATURE: players may add items to a chest they don't have permission for by hitting it + + //if it's a chest + if(block.getType() == Material.CHEST) + { + //only care about non-creative mode players, since those would outright break the box in one hit + if(player.getGameMode() == GameMode.CREATIVE) return; + + //only care if the player has an itemstack in hand + PlayerInventory playerInventory = player.getInventory(); + ItemStack stackInHand = playerInventory.getItemInHand(); + if(stackInHand == null || stackInHand.getType() == Material.AIR) return; + + //only care if the chest is in a claim, and the player does not have access to the chest + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim == null || claim.allowContainers(player) == null) return; + + //if the player is under siege, he can't give away items + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); + event.setCancelled(true); + return; + } + + //if a player is in pvp combat, he can't give away items + if(playerData.inPvpCombat()) return; + + //NOTE: to eliminate accidental give-aways, first hit on a chest displays a confirmation message + //subsequent hits donate item to the chest + + //if first time damaging this chest, show confirmation message + if(playerData.lastChestDamageLocation == null || !block.getLocation().equals(playerData.lastChestDamageLocation)) + { + //remember this location + playerData.lastChestDamageLocation = block.getLocation(); + + //give the player instructions + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DonateItemsInstruction); + } + + //otherwise, try to donate the item stack in hand + else + { + //look for empty slot in chest + Chest chest = (Chest)block.getState(); + Inventory chestInventory = chest.getInventory(); + int availableSlot = chestInventory.firstEmpty(); + + //if there isn't one + if(availableSlot < 0) + { + //tell the player and stop here + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ChestFull); + + return; + } + + //otherwise, transfer item stack from player to chest + //NOTE: Inventory.addItem() is smart enough to add items to existing stacks, making filling a chest with garbage as a grief very difficult + chestInventory.addItem(stackInHand); + playerInventory.setItemInHand(new ItemStack(Material.AIR)); + + //and confirm for the player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DonationSuccess); + } + } + } + + //when a player breaks a block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockBreak(BlockBreakEvent breakEvent) + { + Player player = breakEvent.getPlayer(); + Block block = breakEvent.getBlock(); + + //make sure the player is allowed to break at the location + String noBuildReason = GriefPrevention.instance.allowBreak(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + breakEvent.setCancelled(true); + return; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + + //if there's a claim here + if(claim != null) + { + //if breaking UNDER the claim and the player has permission to build in the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) + { + //extend the claim downward beyond the breakage point + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + } + + //FEATURE: automatically clean up hanging treetops + //if it's a log + if(block.getType() == Material.LOG && GriefPrevention.instance.config_trees_removeFloatingTreetops) + { + //run the specialized code for treetop removal (see below) + GriefPrevention.instance.handleLogBroken(block); + } + } + + //when a player places a sign... + @EventHandler(ignoreCancelled = true) + public void onSignChanged(SignChangeEvent event) + { + Player player = event.getPlayer(); + if(player == null) return; + + StringBuilder lines = new StringBuilder(); + boolean notEmpty = false; + for(int i = 0; i < event.getLines().length; i++) + { + if(event.getLine(i).length() != 0) notEmpty = true; + lines.append(event.getLine(i) + ";"); + } + + String signMessage = lines.toString(); + + //if not empty and wasn't the same as the last sign, log it and remember it for later + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(notEmpty && playerData.lastMessage != null && !playerData.lastMessage.equals(signMessage)) + { + GriefPrevention.AddLogEntry("[Sign Placement] <" + player.getName() + "> " + lines.toString() + " @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); + playerData.lastMessage = signMessage; + } + } + + //when a player places a block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + public void onBlockPlace(BlockPlaceEvent placeEvent) + { + Player player = placeEvent.getPlayer(); + Block block = placeEvent.getBlock(); + + //FEATURE: limit fire placement, to prevent PvP-by-fire + + //if placed block is fire and pvp is off, apply rules for proximity to other players + if(block.getType() == Material.FIRE && !GriefPrevention.instance.config_pvp_enabledWorlds.contains(block.getWorld()) && !player.hasPermission("griefprevention.lava")) + { + List players = block.getWorld().getPlayers(); + for(int i = 0; i < players.size(); i++) + { + Player otherPlayer = players.get(i); + Location location = otherPlayer.getLocation(); + if(!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire, otherPlayer.getName()); + placeEvent.setCancelled(true); + return; + } + } + } + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + placeEvent.setCancelled(true); + return; + } + + //if the block is being placed within an existing claim + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + if(claim != null) + { + //warn about TNT not destroying claimed blocks + if(block.getType() == Material.TNT) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); + } + + //if the player has permission for the claim and he's placing UNDER the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) + { + //extend the claim downward + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + + //reset the counter for warning the player when he places outside his claims + playerData.unclaimedBlockPlacementsUntilWarning = 1; + } + + //FEATURE: automatically create a claim when a player who has no claims places a chest + + //otherwise if there's no claim, the player is placing a chest, and new player automatic claims are enabled + else if(block.getType() == Material.CHEST && GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) + { + //if the chest is too deep underground, don't create the claim and explain why + if(GriefPrevention.instance.config_claims_preventTheft && block.getY() < GriefPrevention.instance.config_claims_maxDepth) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.TooDeepToClaim); + return; + } + + int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; + + //if the player doesn't have any claims yet, automatically create a claim centered at the chest + if(playerData.claims.size() == 0) + { + //radius == 0 means protect ONLY the chest + if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius == 0) + { + this.dataStore.createClaim(block.getWorld(), block.getX(), block.getX(), block.getY(), block.getY(), block.getZ(), block.getZ(), player.getName(), null, null); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); + } + + //otherwise, create a claim in the area around the chest + else + { + //as long as the automatic claim overlaps another existing claim, shrink it + //note that since the player had permission to place the chest, at the very least, the automatic claim will include the chest + while(radius >= 0 && !this.dataStore.createClaim(block.getWorld(), + block.getX() - radius, block.getX() + radius, + block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, block.getY(), + block.getZ() - radius, block.getZ() + radius, + player.getName(), + null, null).succeeded) + { + radius--; + } + + //notify and explain to player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); + + //show the player the protected area + Claim newClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); + Visualization visualization = Visualization.FromClaim(newClaim, block.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + + //instructions for using /trust + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TrustCommandAdvertisement); + + //unless special permission is required to create a claim with the shovel, educate the player about the shovel + if(!GriefPrevention.instance.config_claims_creationRequiresPermission) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.GoldenShovelAdvertisement); + } + } + + //check to see if this chest is in a claim, and warn when it isn't + if(GriefPrevention.instance.config_claims_preventTheft && this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim) == null) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnprotectedChestWarning); + } + } + + //FEATURE: limit wilderness tree planting to grass, or dirt with more blocks beneath it + else if(block.getType() == Material.SAPLING && GriefPrevention.instance.config_blockSkyTrees && GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) + { + Block earthBlock = placeEvent.getBlockAgainst(); + if(earthBlock.getType() != Material.GRASS) + { + if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR || + earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR) + { + placeEvent.setCancelled(true); + } + } + } + + //FEATURE: warn players when they're placing non-trash blocks outside of their claimed areas + else if(GriefPrevention.instance.config_claims_warnOnBuildOutside && !this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld()) && playerData.claims.size() > 0) + { + if(--playerData.unclaimedBlockPlacementsUntilWarning <= 0) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); + playerData.unclaimedBlockPlacementsUntilWarning = 15; + + if(playerData.lastClaim != null && playerData.lastClaim.allowBuild(player) == null) + { + Visualization visualization = Visualization.FromClaim(playerData.lastClaim, block.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + } + } + + //warn players when they place TNT above sea level, since it doesn't destroy blocks there + if( GriefPrevention.instance.config_blockSurfaceOtherExplosions && block.getType() == Material.TNT && + block.getWorld().getEnvironment() != Environment.NETHER && + block.getY() > GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); + } + } + + //blocks "pushing" other players' blocks around (pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonExtend (BlockPistonExtendEvent event) + { + List blocks = event.getBlocks(); + + //if no blocks moving, then only check to make sure we're not pushing into a claim from outside + //this avoids pistons breaking non-solids just inside a claim, like torches, doors, and touchplates + if(blocks.size() == 0) + { + Block pistonBlock = event.getBlock(); + Block invadedBlock = pistonBlock.getRelative(event.getDirection()); + + if( this.dataStore.getClaimAt(pistonBlock.getLocation(), false, null) == null && + this.dataStore.getClaimAt(invadedBlock.getLocation(), false, null) != null) + { + event.setCancelled(true); + } + + return; + } + + //who owns the piston, if anyone? + String pistonClaimOwnerName = "_"; + Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null); + if(claim != null) pistonClaimOwnerName = claim.getOwnerName(); + + //which blocks are being pushed? + for(int i = 0; i < blocks.size(); i++) + { + //if ANY of the pushed blocks are owned by someone other than the piston owner, cancel the event + Block block = blocks.get(i); + claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null && !claim.getOwnerName().equals(pistonClaimOwnerName)) + { + event.setCancelled(true); + event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); + event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); + event.getBlock().setType(Material.AIR); + return; + } + } + + //which direction? note we're ignoring vertical push + int xchange = 0; + int zchange = 0; + + Block piston = event.getBlock(); + Block firstBlock = blocks.get(0); + + if(firstBlock.getX() > piston.getX()) + { + xchange = 1; + } + else if(firstBlock.getX() < piston.getX()) + { + xchange = -1; + } + else if(firstBlock.getZ() > piston.getZ()) + { + zchange = 1; + } + else if(firstBlock.getZ() < piston.getZ()) + { + zchange = -1; + } + + //if horizontal movement + if(xchange != 0 || zchange != 0) + { + for(int i = 0; i < blocks.size(); i++) + { + Block block = blocks.get(i); + Claim originalClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); + String originalOwnerName = ""; + if(originalClaim != null) + { + originalOwnerName = originalClaim.getOwnerName(); + } + + Claim newClaim = this.dataStore.getClaimAt(block.getLocation().add(xchange, 0, zchange), false, null); + String newOwnerName = ""; + if(newClaim != null) + { + newOwnerName = newClaim.getOwnerName(); + } + + //if pushing this block will change ownership, cancel the event and take away the piston (for performance reasons) + if(!newOwnerName.equals(originalOwnerName)) + { + event.setCancelled(true); + event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); + event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); + event.getBlock().setType(Material.AIR); + return; + } + + } + } + } + + //blocks theft by pulling blocks out of a claim (again pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonRetract (BlockPistonRetractEvent event) + { + //we only care about sticky pistons + if(!event.isSticky()) return; + + //who owns the moving block, if anyone? + String movingBlockOwnerName = "_"; + Claim movingBlockClaim = this.dataStore.getClaimAt(event.getRetractLocation(), false, null); + if(movingBlockClaim != null) movingBlockOwnerName = movingBlockClaim.getOwnerName(); + + //who owns the piston, if anyone? + String pistonOwnerName = "_"; + Location pistonLocation = event.getBlock().getLocation(); + Claim pistonClaim = this.dataStore.getClaimAt(pistonLocation, false, null); + if(pistonClaim != null) pistonOwnerName = pistonClaim.getOwnerName(); + + //if there are owners for the blocks, they must be the same player + //otherwise cancel the event + if(!pistonOwnerName.equals(movingBlockOwnerName)) + { + event.setCancelled(true); + } + } + + //blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockIgnite (BlockIgniteEvent igniteEvent) + { + if(!GriefPrevention.instance.config_fireSpreads && igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && igniteEvent.getCause() != IgniteCause.LIGHTNING) + { + igniteEvent.setCancelled(true); + } + } + + //fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example) + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockSpread (BlockSpreadEvent spreadEvent) + { + if(spreadEvent.getSource().getType() != Material.FIRE) return; + + if(!GriefPrevention.instance.config_fireSpreads) + { + spreadEvent.setCancelled(true); + + Block underBlock = spreadEvent.getSource().getRelative(BlockFace.DOWN); + if(underBlock.getType() != Material.NETHERRACK) + { + spreadEvent.getSource().setType(Material.AIR); + } + + return; + } + + //never spread into a claimed area, regardless of settings + if(this.dataStore.getClaimAt(spreadEvent.getBlock().getLocation(), false, null) != null) + { + spreadEvent.setCancelled(true); + + //if the source of the spread is not fire on netherrack, put out that source fire to save cpu cycles + Block source = spreadEvent.getSource(); + if(source.getType() == Material.FIRE && source.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK) + { + source.setType(Material.AIR); + } + } + } + + //blocks are not destroyed by fire, unless configured to do so + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockBurn (BlockBurnEvent burnEvent) + { + if(!GriefPrevention.instance.config_fireDestroys) + { + burnEvent.setCancelled(true); + Block block = burnEvent.getBlock(); + Block [] adjacentBlocks = new Block [] + { + block.getRelative(BlockFace.UP), + block.getRelative(BlockFace.DOWN), + block.getRelative(BlockFace.NORTH), + block.getRelative(BlockFace.SOUTH), + block.getRelative(BlockFace.EAST), + block.getRelative(BlockFace.WEST) + }; + + //pro-actively put out any fires adjacent the burning block, to reduce future processing here + for(int i = 0; i < adjacentBlocks.length; i++) + { + Block adjacentBlock = adjacentBlocks[i]; + if(adjacentBlock.getType() == Material.FIRE && adjacentBlock.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK) + { + adjacentBlock.setType(Material.AIR); + } + } + + Block aboveBlock = block.getRelative(BlockFace.UP); + if(aboveBlock.getType() == Material.FIRE) + { + aboveBlock.setType(Material.AIR); + } + return; + } + + //never burn claimed blocks, regardless of settings + if(this.dataStore.getClaimAt(burnEvent.getBlock().getLocation(), false, null) != null) + { + burnEvent.setCancelled(true); + } + } + + //ensures fluids don't flow out of claims, unless into another claim where the owner is trusted to build + private Claim lastSpreadClaim = null; + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockFromTo (BlockFromToEvent spreadEvent) + { + //don't track fluid movement in worlds where claims are not enabled + if(!GriefPrevention.instance.config_claims_enabledWorlds.contains(spreadEvent.getBlock().getWorld())) return; + + //always allow fluids to flow straight down + if(spreadEvent.getFace() == BlockFace.DOWN) return; + + //from where? + Block fromBlock = spreadEvent.getBlock(); + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, this.lastSpreadClaim); + if(fromClaim != null) + { + this.lastSpreadClaim = fromClaim; + } + + //where to? + Block toBlock = spreadEvent.getToBlock(); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); + + //if it's within the same claim or wilderness to wilderness, allow it + if(fromClaim == toClaim) return; + + //block any spread into the wilderness from a claim + if(fromClaim != null && toClaim == null) + { + spreadEvent.setCancelled(true); + return; + } + + //if spreading into a claim + else if(toClaim != null) + { + //who owns the spreading block, if anyone? + OfflinePlayer fromOwner = null; + if(fromClaim != null) + { + fromOwner = GriefPrevention.instance.getServer().getOfflinePlayer(fromClaim.ownerName); + } + + //cancel unless the owner of the spreading block is allowed to build in the receiving claim + if(fromOwner == null || fromOwner.getPlayer() == null || toClaim.allowBuild(fromOwner.getPlayer()) != null) + { + spreadEvent.setCancelled(true); + } + } + } + + //ensures dispensers can't be used to dispense a block(like water or lava) or item across a claim boundary + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onDispense(BlockDispenseEvent dispenseEvent) + { + //from where? + Block fromBlock = dispenseEvent.getBlock(); + + //to where? + Vector velocity = dispenseEvent.getVelocity(); + int xChange = 0; + int zChange = 0; + if(Math.abs(velocity.getX()) > Math.abs(velocity.getZ())) + { + if(velocity.getX() > 0) xChange = 1; + else xChange = -1; + } + else + { + if(velocity.getZ() > 0) zChange = 1; + else zChange = -1; + } + + Block toBlock = fromBlock.getRelative(xChange, 0, zChange); + + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); + + //into wilderness is NOT OK when surface buckets are limited + Material materialDispensed = dispenseEvent.getItem().getType(); + if((materialDispensed == Material.WATER_BUCKET || materialDispensed == Material.LAVA_BUCKET) && GriefPrevention.instance.config_blockWildernessWaterBuckets && GriefPrevention.instance.claimsEnabledForWorld(fromBlock.getWorld()) && toClaim == null) + { + dispenseEvent.setCancelled(true); + return; + } + + //wilderness to wilderness is OK + if(fromClaim == null && toClaim == null) return; + + //within claim is OK + if(fromClaim == toClaim) return; + + //everything else is NOT OK + dispenseEvent.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onTreeGrow (StructureGrowEvent growEvent) + { + Location rootLocation = growEvent.getLocation(); + Claim rootClaim = this.dataStore.getClaimAt(rootLocation, false, null); + String rootOwnerName = null; + + //who owns the spreading block, if anyone? + if(rootClaim != null) + { + //tree growth in subdivisions is dependent on who owns the top level claim + if(rootClaim.parent != null) rootClaim = rootClaim.parent; + + //if an administrative claim, just let the tree grow where it wants + if(rootClaim.isAdminClaim()) return; + + //otherwise, note the owner of the claim + rootOwnerName = rootClaim.getOwnerName(); + } + + //for each block growing + for(int i = 0; i < growEvent.getBlocks().size(); i++) + { + BlockState block = growEvent.getBlocks().get(i); + Claim blockClaim = this.dataStore.getClaimAt(block.getLocation(), false, rootClaim); + + //if it's growing into a claim + if(blockClaim != null) + { + //if there's no owner for the new tree, or the owner for the new tree is different from the owner of the claim + if(rootOwnerName == null || !rootOwnerName.equals(blockClaim.getOwnerName())) + { + growEvent.getBlocks().remove(i--); + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/DataStore.java b/src/me/ryanhamshire/GriefPrevention/DataStore.java index 4b4ee9a..370a1ce 100644 --- a/src/me/ryanhamshire/GriefPrevention/DataStore.java +++ b/src/me/ryanhamshire/GriefPrevention/DataStore.java @@ -20,6 +20,7 @@ package me.ryanhamshire.GriefPrevention; import java.io.*; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.*; import org.bukkit.configuration.file.FileConfiguration; @@ -31,10 +32,10 @@ import org.bukkit.inventory.ItemStack; public abstract class DataStore { //in-memory cache for player data - protected HashMap playerNameToPlayerDataMap = new HashMap(); + protected ConcurrentHashMap playerNameToPlayerDataMap = new ConcurrentHashMap(); //in-memory cache for group (permission-based) data - protected HashMap permissionToBonusBlocksMap = new HashMap(); + protected ConcurrentHashMap permissionToBonusBlocksMap = new ConcurrentHashMap(); //in-memory cache for claim data ArrayList claims = new ArrayList(); diff --git a/src/me/ryanhamshire/GriefPrevention/DatabaseDataStore.java b/src/me/ryanhamshire/GriefPrevention/DatabaseDataStore.java index 0ad388e..604a1c7 100644 --- a/src/me/ryanhamshire/GriefPrevention/DatabaseDataStore.java +++ b/src/me/ryanhamshire/GriefPrevention/DatabaseDataStore.java @@ -62,13 +62,7 @@ public class DatabaseDataStore extends DataStore try { - //set username/pass properties - Properties connectionProps = new Properties(); - connectionProps.put("user", this.userName); - connectionProps.put("password", this.password); - - //establish connection - this.databaseConnection = DriverManager.getConnection(this.databaseUrl, connectionProps); + this.refreshDataConnection(); } catch(Exception e2) { @@ -240,6 +234,8 @@ public class DatabaseDataStore extends DataStore { try { + this.refreshDataConnection(); + //wipe out any existing data about this claim this.deleteClaimFromSecondaryStorage(claim); @@ -320,6 +316,8 @@ public class DatabaseDataStore extends DataStore try { + this.refreshDataConnection(); + Statement statement = databaseConnection.createStatement(); statement.execute("INSERT INTO griefprevention_claimdata VALUES(" + id + ", '" + @@ -346,6 +344,8 @@ public class DatabaseDataStore extends DataStore { try { + this.refreshDataConnection(); + Statement statement = this.databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_claimdata WHERE id=" + claim.id + ";"); statement.execute("DELETE FROM griefprevention_claimdata WHERE parentid=" + claim.id + ";"); @@ -365,6 +365,8 @@ public class DatabaseDataStore extends DataStore try { + this.refreshDataConnection(); + Statement statement = this.databaseConnection.createStatement(); ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_playerdata WHERE name='" + playerName + "';"); @@ -400,6 +402,8 @@ public class DatabaseDataStore extends DataStore try { + this.refreshDataConnection(); + SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateString = sqlFormat.format(playerData.lastLogin); @@ -427,6 +431,8 @@ public class DatabaseDataStore extends DataStore try { + this.refreshDataConnection(); + Statement statement = databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_nextclaimid;"); statement.execute("INSERT INTO griefprevention_nextclaimid VALUES (" + nextID + ");"); @@ -457,11 +463,28 @@ public class DatabaseDataStore extends DataStore { try { - this.databaseConnection.close(); + if(!this.databaseConnection.isClosed()) + { + this.databaseConnection.close(); + } } catch(SQLException e){}; + } + + this.databaseConnection = null; + } + + private void refreshDataConnection() throws SQLException + { + if(this.databaseConnection == null || this.databaseConnection.isClosed()) + { + //set username/pass properties + Properties connectionProps = new Properties(); + connectionProps.put("user", this.userName); + connectionProps.put("password", this.password); - this.databaseConnection = null; + //establish connection + this.databaseConnection = DriverManager.getConnection(this.databaseUrl, connectionProps); } } } diff --git a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java index 7c7d673..343f593 100644 --- a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java +++ b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java @@ -2254,8 +2254,8 @@ public class GriefPrevention extends JavaPlugin if(hasLeaves) { //schedule a cleanup task for later, in case the player leaves part of this tree hanging in the air - TreeCleanupTask cleanupTask = new TreeCleanupTask(block, rootBlock, treeBlocks); - + TreeCleanupTask cleanupTask = new TreeCleanupTask(block, rootBlock, treeBlocks, rootBlock.getData()); + //20L ~ 1 second, so 2 mins = 120 seconds ~ 2400L GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, cleanupTask, 2400L); } diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java index c15f7a5..8e29960 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -235,15 +235,18 @@ class PlayerEventHandler implements Listener //log entry GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); - //ban - GriefPrevention.instance.getServer().getOfflinePlayer(player.getName()).setBanned(true); - - //kick - player.kickPlayer(GriefPrevention.instance.config_spam_banMessage); - } + //kick and ban + PlayerKickBanTask task = new PlayerKickBanTask(player, GriefPrevention.instance.config_spam_banMessage); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 1L); + } else { - player.kickPlayer(""); + //log entry + GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); + + //just kick + PlayerKickBanTask task = new PlayerKickBanTask(player, null); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 1L); } return true; diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerKickBanTask.java b/src/me/ryanhamshire/GriefPrevention/PlayerKickBanTask.java new file mode 100644 index 0000000..0e5a117 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/PlayerKickBanTask.java @@ -0,0 +1,59 @@ +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + 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 . + */ + +package me.ryanhamshire.GriefPrevention; + +import org.bukkit.entity.Player; + +//kicks or bans a player +//need a task for this because async threads (like the chat event handlers) can't kick or ban. +//but they CAN schedule a task to run in the main thread to do that job +class PlayerKickBanTask implements Runnable +{ + //player to kick or ban + private Player player; + + //ban message. if null, don't ban + private String banReason; + + public PlayerKickBanTask(Player player, String banReason) + { + this.player = player; + this.banReason = banReason; + } + + @Override + public void run() + { + if(this.banReason != null) + { + //ban + GriefPrevention.instance.getServer().getOfflinePlayer(this.player.getName()).setBanned(true); + + //kick + if(this.player.isOnline()) + { + this.player.kickPlayer(this.banReason); + } + } + else if(this.player.isOnline()) + { + this.player.kickPlayer(""); + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java index dacaa4f..919cfed 100644 --- a/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java +++ b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java @@ -1,749 +1,750 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - 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 . - */ - -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 notAllowedToHang; //natural blocks which don't naturally hang in their air - private ArrayList 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; - 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(); - 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(); - 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 excludedBlocks = new ArrayList(); - 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 fillableBlocks = new ArrayList(); - fillableBlocks.add(Material.AIR.getId()); - fillableBlocks.add(Material.STATIONARY_WATER.getId()); - fillableBlocks.add(Material.STATIONARY_LAVA.getId()); - fillableBlocks.add(Material.LONG_GRASS.getId()); - - ArrayList notSuitableForFillBlocks = new ArrayList(); - 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; - 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 - 1; y++) - { - BlockSnapshot block = snapshots[x][y][z]; - if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId() || - block.typeId == Material.WATER.getId() || block.typeId == Material.LAVA.getId()) - { - block.typeId = Material.AIR.getId(); - } - } - } - } - } - - private int highestY(int x, int z, boolean ignoreLeaves) - { - int y; - for(y = snapshots[0].length - 1; y > 0; y--) - { - BlockSnapshot block = this.snapshots[x][y][z]; - if(block.typeId != Material.AIR.getId() && - !(ignoreLeaves && block.typeId == Material.SNOW.getId()) && - !(ignoreLeaves && block.typeId == Material.LEAVES.getId()) && - !(block.typeId == Material.STATIONARY_WATER.getId()) && - !(block.typeId == Material.WATER.getId()) && - !(block.typeId == Material.LAVA.getId()) && - !(block.typeId == Material.STATIONARY_LAVA.getId())) - { - return y; - } - } - - return y; - } - - static ArrayList getPlayerBlocks(Environment environment, Biome biome) - { - //NOTE on this list. why not make a list of natural blocks? - //answer: better to leave a few player blocks than to remove too many natural blocks. remember we're "restoring nature" - //a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal - ArrayList playerBlocks = new ArrayList(); - playerBlocks.add(Material.FIRE.getId()); - playerBlocks.add(Material.BED_BLOCK.getId()); - playerBlocks.add(Material.WOOD.getId()); - playerBlocks.add(Material.BOOKSHELF.getId()); - playerBlocks.add(Material.BREWING_STAND.getId()); - playerBlocks.add(Material.BRICK.getId()); - playerBlocks.add(Material.COBBLESTONE.getId()); - playerBlocks.add(Material.GLASS.getId()); - playerBlocks.add(Material.LAPIS_BLOCK.getId()); - playerBlocks.add(Material.DISPENSER.getId()); - playerBlocks.add(Material.NOTE_BLOCK.getId()); - playerBlocks.add(Material.POWERED_RAIL.getId()); - playerBlocks.add(Material.DETECTOR_RAIL.getId()); - playerBlocks.add(Material.PISTON_STICKY_BASE.getId()); - playerBlocks.add(Material.PISTON_BASE.getId()); - playerBlocks.add(Material.PISTON_EXTENSION.getId()); - playerBlocks.add(Material.WOOL.getId()); - playerBlocks.add(Material.PISTON_MOVING_PIECE.getId()); - playerBlocks.add(Material.GOLD_BLOCK.getId()); - playerBlocks.add(Material.IRON_BLOCK.getId()); - playerBlocks.add(Material.DOUBLE_STEP.getId()); - playerBlocks.add(Material.STEP.getId()); - playerBlocks.add(Material.CROPS.getId()); - playerBlocks.add(Material.TNT.getId()); - playerBlocks.add(Material.MOSSY_COBBLESTONE.getId()); - playerBlocks.add(Material.TORCH.getId()); - playerBlocks.add(Material.FIRE.getId()); - playerBlocks.add(Material.WOOD_STAIRS.getId()); - playerBlocks.add(Material.CHEST.getId()); - playerBlocks.add(Material.REDSTONE_WIRE.getId()); - playerBlocks.add(Material.DIAMOND_BLOCK.getId()); - playerBlocks.add(Material.WORKBENCH.getId()); - playerBlocks.add(Material.FURNACE.getId()); - playerBlocks.add(Material.BURNING_FURNACE.getId()); - playerBlocks.add(Material.WOODEN_DOOR.getId()); - playerBlocks.add(Material.SIGN_POST.getId()); - playerBlocks.add(Material.LADDER.getId()); - playerBlocks.add(Material.RAILS.getId()); - playerBlocks.add(Material.COBBLESTONE_STAIRS.getId()); - playerBlocks.add(Material.WALL_SIGN.getId()); - playerBlocks.add(Material.STONE_PLATE.getId()); - playerBlocks.add(Material.LEVER.getId()); - playerBlocks.add(Material.IRON_DOOR_BLOCK.getId()); - playerBlocks.add(Material.WOOD_PLATE.getId()); - playerBlocks.add(Material.REDSTONE_TORCH_ON.getId()); - playerBlocks.add(Material.REDSTONE_TORCH_OFF.getId()); - playerBlocks.add(Material.STONE_BUTTON.getId()); - playerBlocks.add(Material.SNOW_BLOCK.getId()); - playerBlocks.add(Material.JUKEBOX.getId()); - playerBlocks.add(Material.FENCE.getId()); - playerBlocks.add(Material.PORTAL.getId()); - playerBlocks.add(Material.JACK_O_LANTERN.getId()); - playerBlocks.add(Material.CAKE_BLOCK.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_OFF.getId()); - playerBlocks.add(Material.TRAP_DOOR.getId()); - playerBlocks.add(Material.SMOOTH_BRICK.getId()); - playerBlocks.add(Material.HUGE_MUSHROOM_1.getId()); - playerBlocks.add(Material.HUGE_MUSHROOM_2.getId()); - playerBlocks.add(Material.IRON_FENCE.getId()); - playerBlocks.add(Material.THIN_GLASS.getId()); - playerBlocks.add(Material.MELON_STEM.getId()); - playerBlocks.add(Material.FENCE_GATE.getId()); - playerBlocks.add(Material.BRICK_STAIRS.getId()); - playerBlocks.add(Material.SMOOTH_STAIRS.getId()); - playerBlocks.add(Material.ENCHANTMENT_TABLE.getId()); - playerBlocks.add(Material.BREWING_STAND.getId()); - playerBlocks.add(Material.CAULDRON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.WEB.getId()); - playerBlocks.add(Material.SPONGE.getId()); - playerBlocks.add(Material.GRAVEL.getId()); - playerBlocks.add(Material.EMERALD_BLOCK.getId()); - playerBlocks.add(Material.SANDSTONE.getId()); - playerBlocks.add(Material.WOOD_STEP.getId()); - playerBlocks.add(Material.WOOD_DOUBLE_STEP.getId()); - playerBlocks.add(Material.ENDER_CHEST.getId()); - playerBlocks.add(Material.SANDSTONE_STAIRS.getId()); - playerBlocks.add(Material.SPRUCE_WOOD_STAIRS.getId()); - playerBlocks.add(Material.JUNGLE_WOOD_STAIRS.getId()); - playerBlocks.add(Material.COMMAND.getId()); - playerBlocks.add(Material.BEACON.getId()); - playerBlocks.add(Material.COBBLE_WALL.getId()); - playerBlocks.add(Material.FLOWER_POT.getId()); - playerBlocks.add(Material.CARROT.getId()); - playerBlocks.add(Material.POTATO.getId()); - playerBlocks.add(Material.WOOD_BUTTON.getId()); - playerBlocks.add(Material.SKULL.getId()); - playerBlocks.add(Material.ANVIL.getId()); - - - //these are unnatural in the standard world, but not in the nether - if(environment != Environment.NETHER) - { - playerBlocks.add(Material.NETHERRACK.getId()); - playerBlocks.add(Material.SOUL_SAND.getId()); - playerBlocks.add(Material.GLOWSTONE.getId()); - playerBlocks.add(Material.NETHER_BRICK.getId()); - playerBlocks.add(Material.NETHER_FENCE.getId()); - playerBlocks.add(Material.NETHER_BRICK_STAIRS.getId()); - } - - //these are unnatural in the standard and nether worlds, but not in the end - if(environment != Environment.THE_END) - { - playerBlocks.add(Material.OBSIDIAN.getId()); - playerBlocks.add(Material.ENDER_STONE.getId()); - playerBlocks.add(Material.ENDER_PORTAL_FRAME.getId()); - } - - //these are unnatural in sandy biomes, but not elsewhere - if(biome == Biome.DESERT || biome == Biome.DESERT_HILLS || biome == Biome.BEACH || environment != Environment.NORMAL) - { - playerBlocks.add(Material.LEAVES.getId()); - playerBlocks.add(Material.LOG.getId()); - } - - return playerBlocks; - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + 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 . + */ + +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 notAllowedToHang; //natural blocks which don't naturally hang in their air + private ArrayList 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(); + 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(); + 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 excludedBlocks = new ArrayList(); + 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 fillableBlocks = new ArrayList(); + fillableBlocks.add(Material.AIR.getId()); + fillableBlocks.add(Material.STATIONARY_WATER.getId()); + fillableBlocks.add(Material.STATIONARY_LAVA.getId()); + fillableBlocks.add(Material.LONG_GRASS.getId()); + + ArrayList notSuitableForFillBlocks = new ArrayList(); + 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; + 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 - 1; y++) + { + BlockSnapshot block = snapshots[x][y][z]; + if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId() || + block.typeId == Material.WATER.getId() || block.typeId == Material.LAVA.getId()) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + + private int highestY(int x, int z, boolean ignoreLeaves) + { + int y; + for(y = snapshots[0].length - 1; y > 0; y--) + { + BlockSnapshot block = this.snapshots[x][y][z]; + if(block.typeId != Material.AIR.getId() && + !(ignoreLeaves && block.typeId == Material.SNOW.getId()) && + !(ignoreLeaves && block.typeId == Material.LEAVES.getId()) && + !(block.typeId == Material.STATIONARY_WATER.getId()) && + !(block.typeId == Material.WATER.getId()) && + !(block.typeId == Material.LAVA.getId()) && + !(block.typeId == Material.STATIONARY_LAVA.getId())) + { + return y; + } + } + + return y; + } + + static ArrayList getPlayerBlocks(Environment environment, Biome biome) + { + //NOTE on this list. why not make a list of natural blocks? + //answer: better to leave a few player blocks than to remove too many natural blocks. remember we're "restoring nature" + //a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal + ArrayList playerBlocks = new ArrayList(); + playerBlocks.add(Material.FIRE.getId()); + playerBlocks.add(Material.BED_BLOCK.getId()); + playerBlocks.add(Material.WOOD.getId()); + playerBlocks.add(Material.BOOKSHELF.getId()); + playerBlocks.add(Material.BREWING_STAND.getId()); + playerBlocks.add(Material.BRICK.getId()); + playerBlocks.add(Material.COBBLESTONE.getId()); + playerBlocks.add(Material.GLASS.getId()); + playerBlocks.add(Material.LAPIS_BLOCK.getId()); + playerBlocks.add(Material.DISPENSER.getId()); + playerBlocks.add(Material.NOTE_BLOCK.getId()); + playerBlocks.add(Material.POWERED_RAIL.getId()); + playerBlocks.add(Material.DETECTOR_RAIL.getId()); + playerBlocks.add(Material.PISTON_STICKY_BASE.getId()); + playerBlocks.add(Material.PISTON_BASE.getId()); + playerBlocks.add(Material.PISTON_EXTENSION.getId()); + playerBlocks.add(Material.WOOL.getId()); + playerBlocks.add(Material.PISTON_MOVING_PIECE.getId()); + playerBlocks.add(Material.GOLD_BLOCK.getId()); + playerBlocks.add(Material.IRON_BLOCK.getId()); + playerBlocks.add(Material.DOUBLE_STEP.getId()); + playerBlocks.add(Material.STEP.getId()); + playerBlocks.add(Material.CROPS.getId()); + playerBlocks.add(Material.TNT.getId()); + playerBlocks.add(Material.MOSSY_COBBLESTONE.getId()); + playerBlocks.add(Material.TORCH.getId()); + playerBlocks.add(Material.FIRE.getId()); + playerBlocks.add(Material.WOOD_STAIRS.getId()); + playerBlocks.add(Material.CHEST.getId()); + playerBlocks.add(Material.REDSTONE_WIRE.getId()); + playerBlocks.add(Material.DIAMOND_BLOCK.getId()); + playerBlocks.add(Material.WORKBENCH.getId()); + playerBlocks.add(Material.FURNACE.getId()); + playerBlocks.add(Material.BURNING_FURNACE.getId()); + playerBlocks.add(Material.WOODEN_DOOR.getId()); + playerBlocks.add(Material.SIGN_POST.getId()); + playerBlocks.add(Material.LADDER.getId()); + playerBlocks.add(Material.RAILS.getId()); + playerBlocks.add(Material.COBBLESTONE_STAIRS.getId()); + playerBlocks.add(Material.WALL_SIGN.getId()); + playerBlocks.add(Material.STONE_PLATE.getId()); + playerBlocks.add(Material.LEVER.getId()); + playerBlocks.add(Material.IRON_DOOR_BLOCK.getId()); + playerBlocks.add(Material.WOOD_PLATE.getId()); + playerBlocks.add(Material.REDSTONE_TORCH_ON.getId()); + playerBlocks.add(Material.REDSTONE_TORCH_OFF.getId()); + playerBlocks.add(Material.STONE_BUTTON.getId()); + playerBlocks.add(Material.SNOW_BLOCK.getId()); + playerBlocks.add(Material.JUKEBOX.getId()); + playerBlocks.add(Material.FENCE.getId()); + playerBlocks.add(Material.PORTAL.getId()); + playerBlocks.add(Material.JACK_O_LANTERN.getId()); + playerBlocks.add(Material.CAKE_BLOCK.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_OFF.getId()); + playerBlocks.add(Material.TRAP_DOOR.getId()); + playerBlocks.add(Material.SMOOTH_BRICK.getId()); + playerBlocks.add(Material.HUGE_MUSHROOM_1.getId()); + playerBlocks.add(Material.HUGE_MUSHROOM_2.getId()); + playerBlocks.add(Material.IRON_FENCE.getId()); + playerBlocks.add(Material.THIN_GLASS.getId()); + playerBlocks.add(Material.MELON_STEM.getId()); + playerBlocks.add(Material.FENCE_GATE.getId()); + playerBlocks.add(Material.BRICK_STAIRS.getId()); + playerBlocks.add(Material.SMOOTH_STAIRS.getId()); + playerBlocks.add(Material.ENCHANTMENT_TABLE.getId()); + playerBlocks.add(Material.BREWING_STAND.getId()); + playerBlocks.add(Material.CAULDRON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.WEB.getId()); + playerBlocks.add(Material.SPONGE.getId()); + playerBlocks.add(Material.GRAVEL.getId()); + playerBlocks.add(Material.EMERALD_BLOCK.getId()); + playerBlocks.add(Material.SANDSTONE.getId()); + playerBlocks.add(Material.WOOD_STEP.getId()); + playerBlocks.add(Material.WOOD_DOUBLE_STEP.getId()); + playerBlocks.add(Material.ENDER_CHEST.getId()); + playerBlocks.add(Material.SANDSTONE_STAIRS.getId()); + playerBlocks.add(Material.SPRUCE_WOOD_STAIRS.getId()); + playerBlocks.add(Material.JUNGLE_WOOD_STAIRS.getId()); + playerBlocks.add(Material.COMMAND.getId()); + playerBlocks.add(Material.BEACON.getId()); + playerBlocks.add(Material.COBBLE_WALL.getId()); + playerBlocks.add(Material.FLOWER_POT.getId()); + playerBlocks.add(Material.CARROT.getId()); + playerBlocks.add(Material.POTATO.getId()); + playerBlocks.add(Material.WOOD_BUTTON.getId()); + playerBlocks.add(Material.SKULL.getId()); + playerBlocks.add(Material.ANVIL.getId()); + + + //these are unnatural in the standard world, but not in the nether + if(environment != Environment.NETHER) + { + playerBlocks.add(Material.NETHERRACK.getId()); + playerBlocks.add(Material.SOUL_SAND.getId()); + playerBlocks.add(Material.GLOWSTONE.getId()); + playerBlocks.add(Material.NETHER_BRICK.getId()); + playerBlocks.add(Material.NETHER_FENCE.getId()); + playerBlocks.add(Material.NETHER_BRICK_STAIRS.getId()); + } + + //these are unnatural in the standard and nether worlds, but not in the end + if(environment != Environment.THE_END) + { + playerBlocks.add(Material.OBSIDIAN.getId()); + playerBlocks.add(Material.ENDER_STONE.getId()); + playerBlocks.add(Material.ENDER_PORTAL_FRAME.getId()); + } + + //these are unnatural in sandy biomes, but not elsewhere + if(biome == Biome.DESERT || biome == Biome.DESERT_HILLS || biome == Biome.BEACH || environment != Environment.NORMAL) + { + playerBlocks.add(Material.LEAVES.getId()); + playerBlocks.add(Material.LOG.getId()); + } + + return playerBlocks; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java b/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java index 60426ad..3beff33 100644 --- a/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java +++ b/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java @@ -1,100 +1,102 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - 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 . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; - -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; - -//FEATURE: treetops left unnaturally hanging will be automatically cleaned up - -//this main thread task revisits the location of a partially chopped tree from several minutes ago -//if any part of the tree is still there and nothing else has been built in its place, remove the remaining parts -class TreeCleanupTask implements Runnable -{ - private Block originalChoppedBlock; //first block chopped in the tree - private Block originalRootBlock; //where the root of the tree used to be - private ArrayList originalTreeBlocks; //a list of other log blocks determined to be part of this tree - - public TreeCleanupTask(Block originalChoppedBlock, Block originalRootBlock, ArrayList originalTreeBlocks) - { - this.originalChoppedBlock = originalChoppedBlock; - this.originalRootBlock = originalRootBlock; - this.originalTreeBlocks = originalTreeBlocks; - } - - @Override - public void run() - { - //if this chunk is no longer loaded, load it and come back in a few seconds - Chunk chunk = this.originalChoppedBlock.getWorld().getChunkAt(this.originalChoppedBlock); - if(!chunk.isLoaded()) - { - chunk.load(); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, this, 100L); - return; - } - - //if the block originally chopped has been replaced with anything but air, something has been built (or has grown here) - //in that case, don't do any cleanup - if(this.originalChoppedBlock.getWorld().getBlockAt(this.originalChoppedBlock.getLocation()).getType() != Material.AIR) return; - - //scan the original tree block locations to see if any of them have been replaced - for(int i = 0; i < this.originalTreeBlocks.size(); i++) - { - Location location = this.originalTreeBlocks.get(i).getLocation(); - Block currentBlock = location.getBlock(); - - //if the block has been replaced, stop here, we won't do any cleanup - if(currentBlock.getType() != Material.LOG && currentBlock.getType() != Material.AIR) - { - return; - } - } - - //otherwise scan again, this time removing any remaining log blocks - boolean logsRemaining = false; - for(int i = 0; i < this.originalTreeBlocks.size(); i++) - { - Location location = this.originalTreeBlocks.get(i).getLocation(); - Block currentBlock = location.getBlock(); - if(currentBlock.getType() == Material.LOG) - { - logsRemaining = true; - currentBlock.setType(Material.AIR); - } - } - - //if any were actually removed and we're set to automatically replant griefed trees, place a sapling where the root block was previously - if(logsRemaining && GriefPrevention.instance.config_trees_regrowGriefedTrees) - { - Block currentBlock = this.originalRootBlock.getLocation().getBlock(); - //make sure there's grass or dirt underneath - if(currentBlock.getType() == Material.AIR && (currentBlock.getRelative(BlockFace.DOWN).getType() == Material.DIRT || currentBlock.getRelative(BlockFace.DOWN).getType() == Material.GRASS)) - { - currentBlock.setType(Material.SAPLING); - currentBlock.setData(this.originalRootBlock.getData()); //makes the sapling type match the original tree type - } - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + 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 . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.ArrayList; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; + +//FEATURE: treetops left unnaturally hanging will be automatically cleaned up + +//this main thread task revisits the location of a partially chopped tree from several minutes ago +//if any part of the tree is still there and nothing else has been built in its place, remove the remaining parts +class TreeCleanupTask implements Runnable +{ + private Block originalChoppedBlock; //first block chopped in the tree + private Block originalRootBlock; //where the root of the tree used to be + private byte originalRootBlockData; //data value of that root block (TYPE of log) + private ArrayList originalTreeBlocks; //a list of other log blocks determined to be part of this tree + + public TreeCleanupTask(Block originalChoppedBlock, Block originalRootBlock, ArrayList originalTreeBlocks, byte originalRootBlockData) + { + this.originalChoppedBlock = originalChoppedBlock; + this.originalRootBlock = originalRootBlock; + this.originalTreeBlocks = originalTreeBlocks; + this.originalRootBlockData = originalRootBlockData; + } + + @Override + public void run() + { + //if this chunk is no longer loaded, load it and come back in a few seconds + Chunk chunk = this.originalChoppedBlock.getWorld().getChunkAt(this.originalChoppedBlock); + if(!chunk.isLoaded()) + { + chunk.load(); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, this, 100L); + return; + } + + //if the block originally chopped has been replaced with anything but air, something has been built (or has grown here) + //in that case, don't do any cleanup + if(this.originalChoppedBlock.getWorld().getBlockAt(this.originalChoppedBlock.getLocation()).getType() != Material.AIR) return; + + //scan the original tree block locations to see if any of them have been replaced + for(int i = 0; i < this.originalTreeBlocks.size(); i++) + { + Location location = this.originalTreeBlocks.get(i).getLocation(); + Block currentBlock = location.getBlock(); + + //if the block has been replaced, stop here, we won't do any cleanup + if(currentBlock.getType() != Material.LOG && currentBlock.getType() != Material.AIR) + { + return; + } + } + + //otherwise scan again, this time removing any remaining log blocks + boolean logsRemaining = false; + for(int i = 0; i < this.originalTreeBlocks.size(); i++) + { + Location location = this.originalTreeBlocks.get(i).getLocation(); + Block currentBlock = location.getBlock(); + if(currentBlock.getType() == Material.LOG) + { + logsRemaining = true; + currentBlock.setType(Material.AIR); + } + } + + //if any were actually removed and we're set to automatically replant griefed trees, place a sapling where the root block was previously + if(logsRemaining && GriefPrevention.instance.config_trees_regrowGriefedTrees) + { + Block currentBlock = this.originalRootBlock.getLocation().getBlock(); + //make sure there's grass or dirt underneath + if(currentBlock.getType() == Material.AIR && (currentBlock.getRelative(BlockFace.DOWN).getType() == Material.DIRT || currentBlock.getRelative(BlockFace.DOWN).getType() == Material.GRASS)) + { + currentBlock.setType(Material.SAPLING); + currentBlock.setData(this.originalRootBlockData); //makes the sapling type match the original tree type + } + } + } +}