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
+ }
+ }
+ }
+}