From 9a70c2218b32e2e357d7fe9262909e50bba1b130 Mon Sep 17 00:00:00 2001 From: Len <40720638+destro174@users.noreply.github.com> Date: Sat, 4 Jun 2022 21:26:09 +0200 Subject: [PATCH] Only warn once when placing tnt --- .../GriefPrevention/BlockEventHandler.java | 1959 +++++++++-------- .../GriefPrevention/PlayerData.java | 677 +++--- 2 files changed, 1320 insertions(+), 1316 deletions(-) diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java b/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java index 40fd756..23f15a9 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/BlockEventHandler.java @@ -1,979 +1,980 @@ -/* - 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 me.ryanhamshire.GriefPrevention.util.BoundingBox; -import org.bukkit.ChatColor; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.Tag; -import org.bukkit.World; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.Hopper; -import org.bukkit.block.PistonMoveReaction; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Chest; -import org.bukkit.block.data.type.Dispenser; -import org.bukkit.entity.Fireball; -import org.bukkit.entity.Item; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.entity.minecart.HopperMinecart; -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.BlockDispenseEvent; -import org.bukkit.event.block.BlockFormEvent; -import org.bukkit.event.block.BlockFromToEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; -import org.bukkit.event.block.BlockMultiPlaceEvent; -import org.bukkit.event.block.BlockPistonEvent; -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.hanging.HangingBreakEvent; -import org.bukkit.event.inventory.InventoryPickupItemEvent; -import org.bukkit.event.world.StructureGrowEvent; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.metadata.MetadataValue; -import org.bukkit.projectiles.BlockProjectileSource; -import org.bukkit.projectiles.ProjectileSource; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.function.BiPredicate; -import java.util.function.Supplier; - -//event handlers related to blocks -public class BlockEventHandler implements Listener -{ - //convenience reference to singleton datastore - private final DataStore dataStore; - - private final EnumSet 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 = EnumSet.noneOf(Material.class); - this.trashBlocks.add(Material.COBBLESTONE); - this.trashBlocks.add(Material.TORCH); - this.trashBlocks.add(Material.DIRT); - this.trashBlocks.add(Material.OAK_SAPLING); - this.trashBlocks.add(Material.SPRUCE_SAPLING); - this.trashBlocks.add(Material.BIRCH_SAPLING); - this.trashBlocks.add(Material.JUNGLE_SAPLING); - this.trashBlocks.add(Material.ACACIA_SAPLING); - this.trashBlocks.add(Material.DARK_OAK_SAPLING); - this.trashBlocks.add(Material.GRAVEL); - this.trashBlocks.add(Material.SAND); - this.trashBlocks.add(Material.TNT); - this.trashBlocks.add(Material.CRAFTING_TABLE); - this.trashBlocks.add(Material.TUFF); - this.trashBlocks.add(Material.COBBLED_DEEPSLATE); - } - - //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, block.getLocation(), breakEvent); - if (noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - breakEvent.setCancelled(true); - return; - } - } - - //when a player changes the text of a sign... - @EventHandler(ignoreCancelled = true) - public void onSignChanged(SignChangeEvent event) - { - Player player = event.getPlayer(); - Block sign = event.getBlock(); - - if (player == null || sign == null) return; - - String noBuildReason = GriefPrevention.instance.allowBuild(player, sign.getLocation(), sign.getType()); - if (noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - return; - } - - //send sign content to online administrators - if (!GriefPrevention.instance.config_signNotifications) return; - - StringBuilder lines = new StringBuilder(" placed a sign @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); - boolean notEmpty = false; - for (int i = 0; i < event.getLines().length; i++) - { - String withoutSpaces = event.getLine(i).replace(" ", ""); - if (!withoutSpaces.isEmpty()) - { - notEmpty = true; - lines.append("\n ").append(event.getLine(i)); - } - } - - String signMessage = lines.toString(); - - //prevent signs with blocked IP addresses - if (!player.hasPermission("griefprevention.spam") && GriefPrevention.instance.containsBlockedIP(signMessage)) - { - event.setCancelled(true); - return; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - //if not empty and wasn't the same as the last sign, log it and remember it for later - //This has been temporarily removed since `signMessage` includes location, not just the message. Waste of memory IMO - //if(notEmpty && (playerData.lastSignMessage == null || !playerData.lastSignMessage.equals(signMessage))) - if (notEmpty) - { - GriefPrevention.AddLogEntry(player.getName() + lines.toString().replace("\n ", ";"), null); - PlayerEventHandler.makeSocialLogEntry(player.getName(), signMessage); - //playerData.lastSignMessage = signMessage; - - if (!player.hasPermission("griefprevention.eavesdropsigns")) - { - @SuppressWarnings("unchecked") - Collection players = (Collection) GriefPrevention.instance.getServer().getOnlinePlayers(); - for (Player otherPlayer : players) - { - if (otherPlayer.hasPermission("griefprevention.eavesdropsigns")) - { - otherPlayer.sendMessage(ChatColor.GRAY + player.getName() + signMessage); - } - } - } - } - } - - //when a player places multiple blocks... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlocksPlace(BlockMultiPlaceEvent placeEvent) - { - Player player = placeEvent.getPlayer(); - - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return; - - //make sure the player is allowed to build at the location - for (BlockState block : placeEvent.getReplacedBlockStates()) - { - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType()); - if (noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - placeEvent.setCancelled(true); - return; - } - } - } - - private boolean doesAllowFireProximityInWorld(World world) - { - if (GriefPrevention.instance.pvpRulesApply(world)) - { - return GriefPrevention.instance.config_pvp_allowFireNearPlayers; - } - else - { - return GriefPrevention.instance.config_pvp_allowFireNearPlayers_NonPvp; - } - } - - //when a player places a block... - @SuppressWarnings("null") - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - 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 && !doesAllowFireProximityInWorld(block.getWorld())) - { - List players = block.getWorld().getPlayers(); - for (Player otherPlayer : players) - { - // Ignore players in creative or spectator mode to avoid users from checking if someone is spectating near them - if (otherPlayer.getGameMode() == GameMode.CREATIVE || otherPlayer.getGameMode() == GameMode.SPECTATOR) - { - continue; - } - - Location location = otherPlayer.getLocation(); - if (!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9 && player.canSee(otherPlayer)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire2); - placeEvent.setCancelled(true); - return; - } - } - } - - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return; - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType()); - if (noBuildReason != null) - { - // Allow players with container trust to place books in lecterns - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - if (block.getType() == Material.LECTERN && placeEvent.getBlockReplacedState().getType() == Material.LECTERN) - { - if (claim != null) - { - playerData.lastClaim = claim; - Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Inventory, placeEvent); - if (noContainerReason == null) - return; - - placeEvent.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get()); - return; - } - } - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - placeEvent.setCancelled(true); - return; - } - - //if the block is being placed within or under an existing claim - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - - //If block is a chest, don't allow a DoubleChest to form across a claim boundary - denyConnectingDoubleChestsAcrossClaimBoundary(claim, block, player); - - if (claim != null) - { - playerData.lastClaim = claim; - - //warn about TNT not destroying claimed blocks - if (block.getType() == Material.TNT && !claim.areExplosivesAllowed) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimExplosivesAdvertisement); - } - - //if the player has permission for the claim and he's placing UNDER the claim - if (block.getY() <= claim.lesserBoundaryCorner.getBlockY() && claim.checkPermission(player, ClaimPermission.Build, placeEvent) == null) - { - //extend the claim downward - this.dataStore.extendClaim(claim, block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); - } - - //allow for a build warning in the future - playerData.warnedAboutBuildingOutsideClaims = false; - } - - //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 (GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && player.hasPermission("griefprevention.createclaims") && block.getType() == Material.CHEST) - { - //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.getClaims().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.getUniqueId(), null, null, player); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); - } - - //otherwise, create a claim in the area around the chest - else - { - //if failure due to insufficient claim blocks available - if (playerData.getRemainingClaimBlocks() < Math.pow(1 + 2 * GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadiusMin, 2)) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoEnoughBlocksForChestClaim); - return; - } - - //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 - CreateClaimResult result = null; - while (radius >= GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadiusMin) - { - int area = (radius * 2 + 1) * (radius * 2 + 1); - if (playerData.getRemainingClaimBlocks() >= area) - { - result = 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.getUniqueId(), - null, null, - player); - - if (result.succeeded) break; - } - - radius--; - } - - if (result != null && result.claim != null) - { - if (result.succeeded) - { - //notify and explain to player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); - - //show the player the protected area - Visualization visualization = Visualization.FromClaim(result.claim, block.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - else - { - //notify and explain to player - GriefPrevention.sendMessage(player, TextMode.Err, Messages.AutomaticClaimOtherClaimTooClose); - - //show the player the protected area - Visualization visualization = Visualization.FromClaim(result.claim, block.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - } - } - - } - - //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 (Tag.SAPLINGS.isTagged(block.getType()) && 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 (!this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) - { - if (!playerData.warnedAboutBuildingOutsideClaims && !player.hasPermission("griefprevention.adminclaims") - && player.hasPermission("griefprevention.createclaims") && ((playerData.lastClaim == null - && playerData.getClaims().size() == 0) || (playerData.lastClaim != null - && playerData.lastClaim.isNear(player.getLocation(), 15)))) - { - Long now = null; - if (playerData.buildWarningTimestamp == null || (now = System.currentTimeMillis()) - playerData.buildWarningTimestamp > 600000) //10 minute cooldown - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); - playerData.warnedAboutBuildingOutsideClaims = true; - - if (now == null) now = System.currentTimeMillis(); - playerData.buildWarningTimestamp = now; - - if (playerData.lastClaim != 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 && - claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); - } - - //warn players about disabled pistons outside of land claims - if (GriefPrevention.instance.config_pistonMovement == PistonMode.CLAIMS_ONLY && - (block.getType() == Material.PISTON || block.getType() == Material.STICKY_PISTON) && - claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoPistonsOutsideClaims); - } - } - - static boolean isActiveBlock(Block block) - { - return isActiveBlock(block.getType()); - } - - static boolean isActiveBlock(BlockState state) - { - return isActiveBlock(state.getType()); - } - - static boolean isActiveBlock(Material type) - { - if (type == Material.HOPPER || type == Material.BEACON || type == Material.SPAWNER) return true; - return false; - } - - private static final BlockFace[] HORIZONTAL_DIRECTIONS = new BlockFace[] { - BlockFace.NORTH, - BlockFace.EAST, - BlockFace.SOUTH, - BlockFace.WEST - }; - private void denyConnectingDoubleChestsAcrossClaimBoundary(Claim claim, Block block, Player player) - { - UUID claimOwner = null; - if (claim != null) - claimOwner = claim.getOwnerID(); - - // Check for double chests placed just outside the claim boundary - if (block.getBlockData() instanceof Chest) - { - for (BlockFace face : HORIZONTAL_DIRECTIONS) - { - Block relative = block.getRelative(face); - if (!(relative.getBlockData() instanceof Chest)) continue; - - Claim relativeClaim = this.dataStore.getClaimAt(relative.getLocation(), true, claim); - UUID relativeClaimOwner = relativeClaim == null ? null : relativeClaim.getOwnerID(); - - // Chests outside claims should connect (both null) - // and chests inside the same claim should connect (equal) - if (Objects.equals(claimOwner, relativeClaimOwner)) break; - - // Change both chests to singular chests - Chest chest = (Chest) block.getBlockData(); - chest.setType(Chest.Type.SINGLE); - block.setBlockData(chest); - - Chest relativeChest = (Chest) relative.getBlockData(); - relativeChest.setType(Chest.Type.SINGLE); - relative.setBlockData(relativeChest); - - // Resend relative chest block to prevent visual bug - player.sendBlockChange(relative.getLocation(), relativeChest); - break; - } - } - } - - // Prevent pistons pushing blocks into or out of claims. - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonExtend(BlockPistonExtendEvent event) - { - onPistonEvent(event, event.getBlocks(), false); - } - - // Prevent pistons pulling blocks into or out of claims. - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonRetract(BlockPistonRetractEvent event) - { - onPistonEvent(event, event.getBlocks(), true); - } - - // Handle piston push and pulls. - private void onPistonEvent(BlockPistonEvent event, List blocks, boolean isRetract) - { - PistonMode pistonMode = GriefPrevention.instance.config_pistonMovement; - // Return if piston movements are ignored. - if (pistonMode == PistonMode.IGNORED) return; - - // Don't check in worlds where claims are not enabled. - if (!GriefPrevention.instance.claimsEnabledForWorld(event.getBlock().getWorld())) return; - - BlockFace direction = event.getDirection(); - Block pistonBlock = event.getBlock(); - Claim pistonClaim = this.dataStore.getClaimAt(pistonBlock.getLocation(), false, - pistonMode != PistonMode.CLAIMS_ONLY, null); - - // A claim is required, but the piston is not inside a claim. - if (pistonClaim == null && pistonMode == PistonMode.CLAIMS_ONLY) - { - event.setCancelled(true); - return; - } - - // If no blocks are moving, quickly check if another claim's boundaries are violated. - if (blocks.isEmpty()) - { - // No block and retraction is always safe. - if (isRetract) return; - - Block invadedBlock = pistonBlock.getRelative(direction); - Claim invadedClaim = this.dataStore.getClaimAt(invadedBlock.getLocation(), false, - pistonMode != PistonMode.CLAIMS_ONLY, pistonClaim); - if (invadedClaim != null && (pistonClaim == null || !Objects.equals(pistonClaim.getOwnerID(), invadedClaim.getOwnerID()))) - { - event.setCancelled(true); - } - - return; - } - - // Create bounding box for moved blocks. - BoundingBox movedBlocks = BoundingBox.ofBlocks(blocks); - // Expand to include invaded zone. - movedBlocks.resize(direction, 1); - - if (pistonClaim != null) - { - // If blocks are all inside the same claim as the piston, allow. - if (new BoundingBox(pistonClaim).contains(movedBlocks)) return; - - /* - * In claims-only mode, all moved blocks must be inside of the owning claim. - * From BigScary: - * - Could push into another land claim, don't want to spend CPU checking for that - * - Push ice out, place torch, get water outside the claim - */ - if (pistonMode == PistonMode.CLAIMS_ONLY) - { - event.setCancelled(true); - return; - } - } - - // Check if blocks are in line vertically. - if (movedBlocks.getLength() == 1 && movedBlocks.getWidth() == 1) - { - // Pulling up is always safe. The claim may not contain the area pulled from, but claims cannot stack. - if (isRetract && direction == BlockFace.UP) return; - - // Pushing down is always safe. The claim may not contain the area pushed into, but claims cannot stack. - if (!isRetract && direction == BlockFace.DOWN) return; - } - - // Assemble list of potentially intersecting claims from chunks interacted with. - ArrayList intersectable = new ArrayList<>(); - int chunkXMax = movedBlocks.getMaxX() >> 4; - int chunkZMax = movedBlocks.getMaxZ() >> 4; - - for (int chunkX = movedBlocks.getMinX() >> 4; chunkX <= chunkXMax; ++chunkX) - { - for (int chunkZ = movedBlocks.getMinZ() >> 4; chunkZ <= chunkZMax; ++chunkZ) - { - ArrayList chunkClaims = dataStore.chunksToClaimsMap.get(DataStore.getChunkHash(chunkX, chunkZ)); - if (chunkClaims == null) continue; - - for (Claim claim : chunkClaims) - { - // Ensure claim is not piston claim and is in same world. - if (pistonClaim != claim && pistonBlock.getWorld().equals(claim.getLesserBoundaryCorner().getWorld())) - intersectable.add(claim); - } - } - } - - BiPredicate intersectionHandler; - final Claim finalPistonClaim = pistonClaim; - - // Fast mode: Bounding box intersection always causes a conflict, even if blocks do not conflict. - if (pistonMode == PistonMode.EVERYWHERE_SIMPLE) - { - intersectionHandler = (claim, claimBoundingBox) -> - { - // If owners are different, cancel. - if (finalPistonClaim == null || !Objects.equals(finalPistonClaim.getOwnerID(), claim.getOwnerID())) - { - event.setCancelled(true); - return true; - } - - // Otherwise, proceed to next claim. - return false; - }; - } - // Precise mode: Bounding box intersection may not yield a conflict. Individual blocks must be considered. - else - { - // Set up list of affected blocks. - HashSet checkBlocks = new HashSet<>(blocks); - - // Add all blocks that will be occupied after the shift. - for (Block block : blocks) - if (block.getPistonMoveReaction() != PistonMoveReaction.BREAK) - checkBlocks.add(block.getRelative(direction)); - - intersectionHandler = (claim, claimBoundingBox) -> - { - // Ensure that the claim contains an affected block. - if (checkBlocks.stream().noneMatch(claimBoundingBox::contains)) return false; - - // If pushing this block will change ownership, cancel the event and take away the piston (for performance reasons). - if (finalPistonClaim == null || !Objects.equals(finalPistonClaim.getOwnerID(), claim.getOwnerID())) - { - event.setCancelled(true); - if (GriefPrevention.instance.config_pistonExplosionSound) - { - pistonBlock.getWorld().createExplosion(pistonBlock.getLocation(), 0); - } - pistonBlock.getWorld().dropItem(pistonBlock.getLocation(), new ItemStack(event.isSticky() ? Material.STICKY_PISTON : Material.PISTON)); - pistonBlock.setType(Material.AIR); - return true; - } - - // Otherwise, proceed to next claim. - return false; - }; - } - - for (Claim claim : intersectable) - { - BoundingBox claimBoundingBox = new BoundingBox(claim); - - // Ensure claim intersects with block bounding box. - if (!claimBoundingBox.intersects(movedBlocks)) continue; - - // Do additional mode-based handling. - if (intersectionHandler.test(claim, claimBoundingBox)) return; - } - } - - //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) - { - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(igniteEvent.getBlock().getWorld())) return; - - if (igniteEvent.getCause() == IgniteCause.LIGHTNING && GriefPrevention.instance.dataStore.getClaimAt(igniteEvent.getIgnitingEntity().getLocation(), false, null) != null) - { - igniteEvent.setCancelled(true); //BlockIgniteEvent is called before LightningStrikeEvent. See #532. However, see #1125 for further discussion on detecting trident-caused lightning. - } - - // If a fire is started by a fireball from a dispenser, allow it if the dispenser is in the same claim. - if (igniteEvent.getCause() == IgniteCause.FIREBALL && igniteEvent.getIgnitingEntity() instanceof Fireball) - { - ProjectileSource shooter = ((Fireball) igniteEvent.getIgnitingEntity()).getShooter(); - if (shooter instanceof BlockProjectileSource) - { - Claim claim = GriefPrevention.instance.dataStore.getClaimAt(igniteEvent.getBlock().getLocation(), false, null); - if (claim != null && GriefPrevention.instance.dataStore.getClaimAt(((BlockProjectileSource) shooter).getBlock().getLocation(), false, claim) == claim) - { - return; - } - } - } - - // Arrow ignition is handled by the EntityChangeBlockEvent. - if (igniteEvent.getCause() == IgniteCause.ARROW) - { - return; - } - - 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; - - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) 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) - { - if (GriefPrevention.instance.config_claims_firespreads) return; - 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.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) - { - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(burnEvent.getBlock().getWorld())) return; - - 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 (Block adjacentBlock : adjacentBlocks) - { - 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) - { - if (GriefPrevention.instance.config_claims_firedamages) return; - burnEvent.setCancelled(true); - } - } - - - //ensures fluids don't flow into land claims from outside - private Claim lastSpreadClaim = null; - - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockFromTo(BlockFromToEvent spreadEvent) - { - //always allow fluids to flow straight down - if (spreadEvent.getFace() == BlockFace.DOWN) return; - - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) return; - - //where to? - Block toBlock = spreadEvent.getToBlock(); - Location toLocation = toBlock.getLocation(); - Claim toClaim = this.dataStore.getClaimAt(toLocation, false, lastSpreadClaim); - - //if into a land claim, it must be from the same land claim - if (toClaim != null) - { - this.lastSpreadClaim = toClaim; - if (!toClaim.contains(spreadEvent.getBlock().getLocation(), false, true)) - { - //exception: from parent into subdivision - if (toClaim.parent == null || !toClaim.parent.contains(spreadEvent.getBlock().getLocation(), false, false)) - { - spreadEvent.setCancelled(true); - } - } - } - - } - - //Stop projectiles from destroying blocks that don't fire a proper event - @EventHandler(ignoreCancelled = true) - private void chorusFlower(ProjectileHitEvent event) - { - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(event.getEntity().getWorld())) return; - - Block block = event.getHitBlock(); - - // Ensure projectile affects block. - if (block == null || block.getType() != Material.CHORUS_FLOWER) - return; - - Claim claim = dataStore.getClaimAt(block.getLocation(), false, null); - if (claim == null) - return; - - Player shooter = null; - Projectile projectile = event.getEntity(); - - if (projectile.getShooter() instanceof Player) - shooter = (Player) projectile.getShooter(); - - if (shooter == null) - { - event.setCancelled(true); - return; - } - - Supplier allowContainer = claim.checkPermission(shooter, ClaimPermission.Inventory, event); - - if (allowContainer != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(shooter, TextMode.Err, allowContainer.get()); - return; - } - } - - //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) - { - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(dispenseEvent.getBlock().getWorld())) return; - - //from where? - Block fromBlock = dispenseEvent.getBlock(); - BlockData fromData = fromBlock.getBlockData(); - if (!(fromData instanceof Dispenser)) return; - Dispenser dispenser = (Dispenser) fromData; - - //to where? - Block toBlock = fromBlock.getRelative(dispenser.getFacing()); - Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); - Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); - - //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) - { - //only take these potentially expensive steps if configured to do so - if (!GriefPrevention.instance.config_limitTreeGrowth) return; - - //don't track in worlds where claims are not enabled - if (!GriefPrevention.instance.claimsEnabledForWorld(growEvent.getWorld())) return; - - 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--); - } - } - } - } - - @EventHandler(ignoreCancelled = true) - public void onItemFrameBrokenByBoat(final HangingBreakEvent event) - { - // Checks if the event is caused by physics - 90% of cases caused by a boat (other 10% would be block, - // however since it's in a claim, unless you use a TNT block we don't need to worry about it). - if (event.getCause() != HangingBreakEvent.RemoveCause.PHYSICS) - { - return; - } - - // Cancels the event if in a claim, as we can not efficiently retrieve the person/entity who broke the Item Frame/Hangable Item. - if (this.dataStore.getClaimAt(event.getEntity().getLocation(), false, null) != null) - { - event.setCancelled(true); - } - } -} +/* + 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 me.ryanhamshire.GriefPrevention.util.BoundingBox; +import org.bukkit.ChatColor; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Hopper; +import org.bukkit.block.PistonMoveReaction; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Chest; +import org.bukkit.block.data.type.Dispenser; +import org.bukkit.entity.Fireball; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.entity.minecart.HopperMinecart; +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.BlockDispenseEvent; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; +import org.bukkit.event.block.BlockMultiPlaceEvent; +import org.bukkit.event.block.BlockPistonEvent; +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.hanging.HangingBreakEvent; +import org.bukkit.event.inventory.InventoryPickupItemEvent; +import org.bukkit.event.world.StructureGrowEvent; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.projectiles.BlockProjectileSource; +import org.bukkit.projectiles.ProjectileSource; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiPredicate; +import java.util.function.Supplier; + +//event handlers related to blocks +public class BlockEventHandler implements Listener +{ + //convenience reference to singleton datastore + private final DataStore dataStore; + + private final EnumSet 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 = EnumSet.noneOf(Material.class); + this.trashBlocks.add(Material.COBBLESTONE); + this.trashBlocks.add(Material.TORCH); + this.trashBlocks.add(Material.DIRT); + this.trashBlocks.add(Material.OAK_SAPLING); + this.trashBlocks.add(Material.SPRUCE_SAPLING); + this.trashBlocks.add(Material.BIRCH_SAPLING); + this.trashBlocks.add(Material.JUNGLE_SAPLING); + this.trashBlocks.add(Material.ACACIA_SAPLING); + this.trashBlocks.add(Material.DARK_OAK_SAPLING); + this.trashBlocks.add(Material.GRAVEL); + this.trashBlocks.add(Material.SAND); + this.trashBlocks.add(Material.TNT); + this.trashBlocks.add(Material.CRAFTING_TABLE); + this.trashBlocks.add(Material.TUFF); + this.trashBlocks.add(Material.COBBLED_DEEPSLATE); + } + + //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, block.getLocation(), breakEvent); + if (noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + breakEvent.setCancelled(true); + return; + } + } + + //when a player changes the text of a sign... + @EventHandler(ignoreCancelled = true) + public void onSignChanged(SignChangeEvent event) + { + Player player = event.getPlayer(); + Block sign = event.getBlock(); + + if (player == null || sign == null) return; + + String noBuildReason = GriefPrevention.instance.allowBuild(player, sign.getLocation(), sign.getType()); + if (noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + return; + } + + //send sign content to online administrators + if (!GriefPrevention.instance.config_signNotifications) return; + + StringBuilder lines = new StringBuilder(" placed a sign @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); + boolean notEmpty = false; + for (int i = 0; i < event.getLines().length; i++) + { + String withoutSpaces = event.getLine(i).replace(" ", ""); + if (!withoutSpaces.isEmpty()) + { + notEmpty = true; + lines.append("\n ").append(event.getLine(i)); + } + } + + String signMessage = lines.toString(); + + //prevent signs with blocked IP addresses + if (!player.hasPermission("griefprevention.spam") && GriefPrevention.instance.containsBlockedIP(signMessage)) + { + event.setCancelled(true); + return; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + //if not empty and wasn't the same as the last sign, log it and remember it for later + //This has been temporarily removed since `signMessage` includes location, not just the message. Waste of memory IMO + //if(notEmpty && (playerData.lastSignMessage == null || !playerData.lastSignMessage.equals(signMessage))) + if (notEmpty) + { + GriefPrevention.AddLogEntry(player.getName() + lines.toString().replace("\n ", ";"), null); + PlayerEventHandler.makeSocialLogEntry(player.getName(), signMessage); + //playerData.lastSignMessage = signMessage; + + if (!player.hasPermission("griefprevention.eavesdropsigns")) + { + @SuppressWarnings("unchecked") + Collection players = (Collection) GriefPrevention.instance.getServer().getOnlinePlayers(); + for (Player otherPlayer : players) + { + if (otherPlayer.hasPermission("griefprevention.eavesdropsigns")) + { + otherPlayer.sendMessage(ChatColor.GRAY + player.getName() + signMessage); + } + } + } + } + } + + //when a player places multiple blocks... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlocksPlace(BlockMultiPlaceEvent placeEvent) + { + Player player = placeEvent.getPlayer(); + + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return; + + //make sure the player is allowed to build at the location + for (BlockState block : placeEvent.getReplacedBlockStates()) + { + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType()); + if (noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + placeEvent.setCancelled(true); + return; + } + } + } + + private boolean doesAllowFireProximityInWorld(World world) + { + if (GriefPrevention.instance.pvpRulesApply(world)) + { + return GriefPrevention.instance.config_pvp_allowFireNearPlayers; + } + else + { + return GriefPrevention.instance.config_pvp_allowFireNearPlayers_NonPvp; + } + } + + //when a player places a block... + @SuppressWarnings("null") + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + 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 && !doesAllowFireProximityInWorld(block.getWorld())) + { + List players = block.getWorld().getPlayers(); + for (Player otherPlayer : players) + { + // Ignore players in creative or spectator mode to avoid users from checking if someone is spectating near them + if (otherPlayer.getGameMode() == GameMode.CREATIVE || otherPlayer.getGameMode() == GameMode.SPECTATOR) + { + continue; + } + + Location location = otherPlayer.getLocation(); + if (!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9 && player.canSee(otherPlayer)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire2); + placeEvent.setCancelled(true); + return; + } + } + } + + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return; + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType()); + if (noBuildReason != null) + { + // Allow players with container trust to place books in lecterns + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + if (block.getType() == Material.LECTERN && placeEvent.getBlockReplacedState().getType() == Material.LECTERN) + { + if (claim != null) + { + playerData.lastClaim = claim; + Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Inventory, placeEvent); + if (noContainerReason == null) + return; + + placeEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get()); + return; + } + } + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + placeEvent.setCancelled(true); + return; + } + + //if the block is being placed within or under an existing claim + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + + //If block is a chest, don't allow a DoubleChest to form across a claim boundary + denyConnectingDoubleChestsAcrossClaimBoundary(claim, block, player); + + if (claim != null) + { + playerData.lastClaim = claim; + + //warn about TNT not destroying claimed blocks + if (block.getType() == Material.TNT && !claim.areExplosivesAllowed && !playerData.warnedAboutPlacingTnt) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimExplosivesAdvertisement); + playerData.warnedAboutPlacingTnt = true; + } + + //if the player has permission for the claim and he's placing UNDER the claim + if (block.getY() <= claim.lesserBoundaryCorner.getBlockY() && claim.checkPermission(player, ClaimPermission.Build, placeEvent) == null) + { + //extend the claim downward + this.dataStore.extendClaim(claim, block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + + //allow for a build warning in the future + playerData.warnedAboutBuildingOutsideClaims = false; + } + + //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 (GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && player.hasPermission("griefprevention.createclaims") && block.getType() == Material.CHEST) + { + //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.getClaims().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.getUniqueId(), null, null, player); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); + } + + //otherwise, create a claim in the area around the chest + else + { + //if failure due to insufficient claim blocks available + if (playerData.getRemainingClaimBlocks() < Math.pow(1 + 2 * GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadiusMin, 2)) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoEnoughBlocksForChestClaim); + return; + } + + //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 + CreateClaimResult result = null; + while (radius >= GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadiusMin) + { + int area = (radius * 2 + 1) * (radius * 2 + 1); + if (playerData.getRemainingClaimBlocks() >= area) + { + result = 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.getUniqueId(), + null, null, + player); + + if (result.succeeded) break; + } + + radius--; + } + + if (result != null && result.claim != null) + { + if (result.succeeded) + { + //notify and explain to player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); + + //show the player the protected area + Visualization visualization = Visualization.FromClaim(result.claim, block.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + else + { + //notify and explain to player + GriefPrevention.sendMessage(player, TextMode.Err, Messages.AutomaticClaimOtherClaimTooClose); + + //show the player the protected area + Visualization visualization = Visualization.FromClaim(result.claim, block.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + } + } + + } + + //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 (Tag.SAPLINGS.isTagged(block.getType()) && 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 (!this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) + { + if (!playerData.warnedAboutBuildingOutsideClaims && !player.hasPermission("griefprevention.adminclaims") + && player.hasPermission("griefprevention.createclaims") && ((playerData.lastClaim == null + && playerData.getClaims().size() == 0) || (playerData.lastClaim != null + && playerData.lastClaim.isNear(player.getLocation(), 15)))) + { + Long now = null; + if (playerData.buildWarningTimestamp == null || (now = System.currentTimeMillis()) - playerData.buildWarningTimestamp > 600000) //10 minute cooldown + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); + playerData.warnedAboutBuildingOutsideClaims = true; + + if (now == null) now = System.currentTimeMillis(); + playerData.buildWarningTimestamp = now; + + if (playerData.lastClaim != 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 && + claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); + } + + //warn players about disabled pistons outside of land claims + if (GriefPrevention.instance.config_pistonMovement == PistonMode.CLAIMS_ONLY && + (block.getType() == Material.PISTON || block.getType() == Material.STICKY_PISTON) && + claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoPistonsOutsideClaims); + } + } + + static boolean isActiveBlock(Block block) + { + return isActiveBlock(block.getType()); + } + + static boolean isActiveBlock(BlockState state) + { + return isActiveBlock(state.getType()); + } + + static boolean isActiveBlock(Material type) + { + if (type == Material.HOPPER || type == Material.BEACON || type == Material.SPAWNER) return true; + return false; + } + + private static final BlockFace[] HORIZONTAL_DIRECTIONS = new BlockFace[] { + BlockFace.NORTH, + BlockFace.EAST, + BlockFace.SOUTH, + BlockFace.WEST + }; + private void denyConnectingDoubleChestsAcrossClaimBoundary(Claim claim, Block block, Player player) + { + UUID claimOwner = null; + if (claim != null) + claimOwner = claim.getOwnerID(); + + // Check for double chests placed just outside the claim boundary + if (block.getBlockData() instanceof Chest) + { + for (BlockFace face : HORIZONTAL_DIRECTIONS) + { + Block relative = block.getRelative(face); + if (!(relative.getBlockData() instanceof Chest)) continue; + + Claim relativeClaim = this.dataStore.getClaimAt(relative.getLocation(), true, claim); + UUID relativeClaimOwner = relativeClaim == null ? null : relativeClaim.getOwnerID(); + + // Chests outside claims should connect (both null) + // and chests inside the same claim should connect (equal) + if (Objects.equals(claimOwner, relativeClaimOwner)) break; + + // Change both chests to singular chests + Chest chest = (Chest) block.getBlockData(); + chest.setType(Chest.Type.SINGLE); + block.setBlockData(chest); + + Chest relativeChest = (Chest) relative.getBlockData(); + relativeChest.setType(Chest.Type.SINGLE); + relative.setBlockData(relativeChest); + + // Resend relative chest block to prevent visual bug + player.sendBlockChange(relative.getLocation(), relativeChest); + break; + } + } + } + + // Prevent pistons pushing blocks into or out of claims. + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonExtend(BlockPistonExtendEvent event) + { + onPistonEvent(event, event.getBlocks(), false); + } + + // Prevent pistons pulling blocks into or out of claims. + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonRetract(BlockPistonRetractEvent event) + { + onPistonEvent(event, event.getBlocks(), true); + } + + // Handle piston push and pulls. + private void onPistonEvent(BlockPistonEvent event, List blocks, boolean isRetract) + { + PistonMode pistonMode = GriefPrevention.instance.config_pistonMovement; + // Return if piston movements are ignored. + if (pistonMode == PistonMode.IGNORED) return; + + // Don't check in worlds where claims are not enabled. + if (!GriefPrevention.instance.claimsEnabledForWorld(event.getBlock().getWorld())) return; + + BlockFace direction = event.getDirection(); + Block pistonBlock = event.getBlock(); + Claim pistonClaim = this.dataStore.getClaimAt(pistonBlock.getLocation(), false, + pistonMode != PistonMode.CLAIMS_ONLY, null); + + // A claim is required, but the piston is not inside a claim. + if (pistonClaim == null && pistonMode == PistonMode.CLAIMS_ONLY) + { + event.setCancelled(true); + return; + } + + // If no blocks are moving, quickly check if another claim's boundaries are violated. + if (blocks.isEmpty()) + { + // No block and retraction is always safe. + if (isRetract) return; + + Block invadedBlock = pistonBlock.getRelative(direction); + Claim invadedClaim = this.dataStore.getClaimAt(invadedBlock.getLocation(), false, + pistonMode != PistonMode.CLAIMS_ONLY, pistonClaim); + if (invadedClaim != null && (pistonClaim == null || !Objects.equals(pistonClaim.getOwnerID(), invadedClaim.getOwnerID()))) + { + event.setCancelled(true); + } + + return; + } + + // Create bounding box for moved blocks. + BoundingBox movedBlocks = BoundingBox.ofBlocks(blocks); + // Expand to include invaded zone. + movedBlocks.resize(direction, 1); + + if (pistonClaim != null) + { + // If blocks are all inside the same claim as the piston, allow. + if (new BoundingBox(pistonClaim).contains(movedBlocks)) return; + + /* + * In claims-only mode, all moved blocks must be inside of the owning claim. + * From BigScary: + * - Could push into another land claim, don't want to spend CPU checking for that + * - Push ice out, place torch, get water outside the claim + */ + if (pistonMode == PistonMode.CLAIMS_ONLY) + { + event.setCancelled(true); + return; + } + } + + // Check if blocks are in line vertically. + if (movedBlocks.getLength() == 1 && movedBlocks.getWidth() == 1) + { + // Pulling up is always safe. The claim may not contain the area pulled from, but claims cannot stack. + if (isRetract && direction == BlockFace.UP) return; + + // Pushing down is always safe. The claim may not contain the area pushed into, but claims cannot stack. + if (!isRetract && direction == BlockFace.DOWN) return; + } + + // Assemble list of potentially intersecting claims from chunks interacted with. + ArrayList intersectable = new ArrayList<>(); + int chunkXMax = movedBlocks.getMaxX() >> 4; + int chunkZMax = movedBlocks.getMaxZ() >> 4; + + for (int chunkX = movedBlocks.getMinX() >> 4; chunkX <= chunkXMax; ++chunkX) + { + for (int chunkZ = movedBlocks.getMinZ() >> 4; chunkZ <= chunkZMax; ++chunkZ) + { + ArrayList chunkClaims = dataStore.chunksToClaimsMap.get(DataStore.getChunkHash(chunkX, chunkZ)); + if (chunkClaims == null) continue; + + for (Claim claim : chunkClaims) + { + // Ensure claim is not piston claim and is in same world. + if (pistonClaim != claim && pistonBlock.getWorld().equals(claim.getLesserBoundaryCorner().getWorld())) + intersectable.add(claim); + } + } + } + + BiPredicate intersectionHandler; + final Claim finalPistonClaim = pistonClaim; + + // Fast mode: Bounding box intersection always causes a conflict, even if blocks do not conflict. + if (pistonMode == PistonMode.EVERYWHERE_SIMPLE) + { + intersectionHandler = (claim, claimBoundingBox) -> + { + // If owners are different, cancel. + if (finalPistonClaim == null || !Objects.equals(finalPistonClaim.getOwnerID(), claim.getOwnerID())) + { + event.setCancelled(true); + return true; + } + + // Otherwise, proceed to next claim. + return false; + }; + } + // Precise mode: Bounding box intersection may not yield a conflict. Individual blocks must be considered. + else + { + // Set up list of affected blocks. + HashSet checkBlocks = new HashSet<>(blocks); + + // Add all blocks that will be occupied after the shift. + for (Block block : blocks) + if (block.getPistonMoveReaction() != PistonMoveReaction.BREAK) + checkBlocks.add(block.getRelative(direction)); + + intersectionHandler = (claim, claimBoundingBox) -> + { + // Ensure that the claim contains an affected block. + if (checkBlocks.stream().noneMatch(claimBoundingBox::contains)) return false; + + // If pushing this block will change ownership, cancel the event and take away the piston (for performance reasons). + if (finalPistonClaim == null || !Objects.equals(finalPistonClaim.getOwnerID(), claim.getOwnerID())) + { + event.setCancelled(true); + if (GriefPrevention.instance.config_pistonExplosionSound) + { + pistonBlock.getWorld().createExplosion(pistonBlock.getLocation(), 0); + } + pistonBlock.getWorld().dropItem(pistonBlock.getLocation(), new ItemStack(event.isSticky() ? Material.STICKY_PISTON : Material.PISTON)); + pistonBlock.setType(Material.AIR); + return true; + } + + // Otherwise, proceed to next claim. + return false; + }; + } + + for (Claim claim : intersectable) + { + BoundingBox claimBoundingBox = new BoundingBox(claim); + + // Ensure claim intersects with block bounding box. + if (!claimBoundingBox.intersects(movedBlocks)) continue; + + // Do additional mode-based handling. + if (intersectionHandler.test(claim, claimBoundingBox)) return; + } + } + + //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) + { + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(igniteEvent.getBlock().getWorld())) return; + + if (igniteEvent.getCause() == IgniteCause.LIGHTNING && GriefPrevention.instance.dataStore.getClaimAt(igniteEvent.getIgnitingEntity().getLocation(), false, null) != null) + { + igniteEvent.setCancelled(true); //BlockIgniteEvent is called before LightningStrikeEvent. See #532. However, see #1125 for further discussion on detecting trident-caused lightning. + } + + // If a fire is started by a fireball from a dispenser, allow it if the dispenser is in the same claim. + if (igniteEvent.getCause() == IgniteCause.FIREBALL && igniteEvent.getIgnitingEntity() instanceof Fireball) + { + ProjectileSource shooter = ((Fireball) igniteEvent.getIgnitingEntity()).getShooter(); + if (shooter instanceof BlockProjectileSource) + { + Claim claim = GriefPrevention.instance.dataStore.getClaimAt(igniteEvent.getBlock().getLocation(), false, null); + if (claim != null && GriefPrevention.instance.dataStore.getClaimAt(((BlockProjectileSource) shooter).getBlock().getLocation(), false, claim) == claim) + { + return; + } + } + } + + // Arrow ignition is handled by the EntityChangeBlockEvent. + if (igniteEvent.getCause() == IgniteCause.ARROW) + { + return; + } + + 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; + + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) 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) + { + if (GriefPrevention.instance.config_claims_firespreads) return; + 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.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) + { + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(burnEvent.getBlock().getWorld())) return; + + 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 (Block adjacentBlock : adjacentBlocks) + { + 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) + { + if (GriefPrevention.instance.config_claims_firedamages) return; + burnEvent.setCancelled(true); + } + } + + + //ensures fluids don't flow into land claims from outside + private Claim lastSpreadClaim = null; + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockFromTo(BlockFromToEvent spreadEvent) + { + //always allow fluids to flow straight down + if (spreadEvent.getFace() == BlockFace.DOWN) return; + + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) return; + + //where to? + Block toBlock = spreadEvent.getToBlock(); + Location toLocation = toBlock.getLocation(); + Claim toClaim = this.dataStore.getClaimAt(toLocation, false, lastSpreadClaim); + + //if into a land claim, it must be from the same land claim + if (toClaim != null) + { + this.lastSpreadClaim = toClaim; + if (!toClaim.contains(spreadEvent.getBlock().getLocation(), false, true)) + { + //exception: from parent into subdivision + if (toClaim.parent == null || !toClaim.parent.contains(spreadEvent.getBlock().getLocation(), false, false)) + { + spreadEvent.setCancelled(true); + } + } + } + + } + + //Stop projectiles from destroying blocks that don't fire a proper event + @EventHandler(ignoreCancelled = true) + private void chorusFlower(ProjectileHitEvent event) + { + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(event.getEntity().getWorld())) return; + + Block block = event.getHitBlock(); + + // Ensure projectile affects block. + if (block == null || block.getType() != Material.CHORUS_FLOWER) + return; + + Claim claim = dataStore.getClaimAt(block.getLocation(), false, null); + if (claim == null) + return; + + Player shooter = null; + Projectile projectile = event.getEntity(); + + if (projectile.getShooter() instanceof Player) + shooter = (Player) projectile.getShooter(); + + if (shooter == null) + { + event.setCancelled(true); + return; + } + + Supplier allowContainer = claim.checkPermission(shooter, ClaimPermission.Inventory, event); + + if (allowContainer != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(shooter, TextMode.Err, allowContainer.get()); + return; + } + } + + //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) + { + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(dispenseEvent.getBlock().getWorld())) return; + + //from where? + Block fromBlock = dispenseEvent.getBlock(); + BlockData fromData = fromBlock.getBlockData(); + if (!(fromData instanceof Dispenser)) return; + Dispenser dispenser = (Dispenser) fromData; + + //to where? + Block toBlock = fromBlock.getRelative(dispenser.getFacing()); + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); + + //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) + { + //only take these potentially expensive steps if configured to do so + if (!GriefPrevention.instance.config_limitTreeGrowth) return; + + //don't track in worlds where claims are not enabled + if (!GriefPrevention.instance.claimsEnabledForWorld(growEvent.getWorld())) return; + + 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--); + } + } + } + } + + @EventHandler(ignoreCancelled = true) + public void onItemFrameBrokenByBoat(final HangingBreakEvent event) + { + // Checks if the event is caused by physics - 90% of cases caused by a boat (other 10% would be block, + // however since it's in a claim, unless you use a TNT block we don't need to worry about it). + if (event.getCause() != HangingBreakEvent.RemoveCause.PHYSICS) + { + return; + } + + // Cancels the event if in a claim, as we can not efficiently retrieve the person/entity who broke the Item Frame/Hangable Item. + if (this.dataStore.getClaimAt(event.getEntity().getLocation(), false, null) != null) + { + event.setCancelled(true); + } + } +} diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerData.java b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerData.java index ce695eb..aa1b1d5 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerData.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerData.java @@ -1,338 +1,341 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 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.Location; -import org.bukkit.OfflinePlayer; - -import java.net.InetAddress; -import java.util.Calendar; -import java.util.UUID; -import java.util.Vector; -import java.util.concurrent.ConcurrentHashMap; - -//holds all of GriefPrevention's player-tied data -public class PlayerData -{ - //the player's ID - public UUID playerID; - - //the player's claims - private Vector claims = null; - - //how many claim blocks the player has earned via play time - private Integer accruedClaimBlocks = null; - - //temporary holding area to avoid opening data files too early - private int newlyAccruedClaimBlocks = 0; - - //where this player was the last time we checked on him for earning claim blocks - public Location lastAfkCheckLocation = null; - - //how many claim blocks the player has been gifted by admins, or purchased via economy integration - private Integer bonusClaimBlocks = null; - - //what "mode" the shovel is in determines what it will do when it's used - public ShovelMode shovelMode = ShovelMode.Basic; - - //radius for restore nature fill mode - int fillRadius = 0; - - //last place the player used the shovel, useful in creating and resizing claims, - //because the player must use the shovel twice in those instances - public Location lastShovelLocation = null; - - //the claim this player is currently resizing - public Claim claimResizing = null; - - //the claim this player is currently subdividing - public Claim claimSubdividing = null; - - //whether or not the player has a pending /trapped rescue - public boolean pendingTrapped = false; - - //whether this player was recently warned about building outside land claims - boolean warnedAboutBuildingOutsideClaims = false; - - //whether the player was kicked (set and used during logout) - boolean wasKicked = false; - - //visualization - public Visualization currentVisualization = null; - - //anti-camping pvp protection - public boolean pvpImmune = false; - public long lastSpawn = 0; - - //ignore claims mode - public boolean ignoreClaims = false; - - //the last claim this player was in, that we know of - public Claim lastClaim = null; - - //pvp - public long lastPvpTimestamp = 0; - public String lastPvpPlayer = ""; - - //safety confirmation for deleting multi-subdivision claims - public boolean warnedAboutMajorDeletion = false; - - public InetAddress ipAddress; - - //for addons to set per-player claim limits. Any negative value will use config's value - private int AccruedClaimBlocksLimit = -1; - - //whether or not this player has received a message about unlocking death drops since his last death - boolean receivedDropUnlockAdvertisement = false; - - //whether or not this player's dropped items (on death) are unlocked for other players to pick up - boolean dropsAreUnlocked = false; - - //message to send to player after he respawns - String messageOnRespawn = null; - - //player which a pet will be given to when it's right-clicked - OfflinePlayer petGiveawayRecipient = null; - - //timestamp for last "you're building outside your land claims" message - Long buildWarningTimestamp = null; - - //spot where a player can't talk, used to mute new players until they've moved a little - //this is an anti-bot strategy. - Location noChatLocation = null; - - //ignore list - //true means invisible (admin-forced ignore), false means player-created ignore - public ConcurrentHashMap ignoredPlayers = new ConcurrentHashMap<>(); - public boolean ignoreListChanged = false; - - //profanity warning, once per play session - boolean profanityWarned = false; - - //whether or not this player is "in" pvp combat - public boolean inPvpCombat() - { - if (this.lastPvpTimestamp == 0) return false; - - long now = Calendar.getInstance().getTimeInMillis(); - - long elapsed = now - this.lastPvpTimestamp; - - if (elapsed > GriefPrevention.instance.config_pvp_combatTimeoutSeconds * 1000) //X seconds - { - this.lastPvpTimestamp = 0; - return false; - } - - return true; - } - - //the number of claim blocks a player has available for claiming land - public int getRemainingClaimBlocks() - { - int remainingBlocks = this.getAccruedClaimBlocks() + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); - for (int i = 0; i < this.getClaims().size(); i++) - { - Claim claim = this.getClaims().get(i); - remainingBlocks -= claim.getArea(); - } - - return remainingBlocks; - } - - //don't load data from secondary storage until it's needed - public synchronized int getAccruedClaimBlocks() - { - if (this.accruedClaimBlocks == null) this.loadDataFromSecondaryStorage(); - - //update claim blocks with any he has accrued during his current play session - if (this.newlyAccruedClaimBlocks > 0) - { - int accruedLimit = this.getAccruedClaimBlocksLimit(); - - //if over the limit before adding blocks, leave it as-is, because the limit may have changed AFTER he accrued the blocks - if (this.accruedClaimBlocks < accruedLimit) - { - //move any in the holding area - int newTotal = this.accruedClaimBlocks + this.newlyAccruedClaimBlocks; - - //respect limits - this.accruedClaimBlocks = Math.min(newTotal, accruedLimit); - } - - this.newlyAccruedClaimBlocks = 0; - return this.accruedClaimBlocks; - } - - return accruedClaimBlocks; - } - - public void setAccruedClaimBlocks(Integer accruedClaimBlocks) - { - this.accruedClaimBlocks = accruedClaimBlocks; - this.newlyAccruedClaimBlocks = 0; - } - - public int getBonusClaimBlocks() - { - if (this.bonusClaimBlocks == null) this.loadDataFromSecondaryStorage(); - return bonusClaimBlocks; - } - - public void setBonusClaimBlocks(Integer bonusClaimBlocks) - { - this.bonusClaimBlocks = bonusClaimBlocks; - } - - private void loadDataFromSecondaryStorage() - { - //reach out to secondary storage to get any data there - PlayerData storageData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(this.playerID); - - if (this.accruedClaimBlocks == null) - { - if (storageData.accruedClaimBlocks != null) - { - this.accruedClaimBlocks = storageData.accruedClaimBlocks; - - //ensure at least minimum accrued are accrued (in case of settings changes to increase initial amount) - if (GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && (this.accruedClaimBlocks < GriefPrevention.instance.config_claims_initialBlocks)) - { - this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks; - } - - } - else - { - this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks; - } - } - - if (this.bonusClaimBlocks == null) - { - if (storageData.bonusClaimBlocks != null) - { - this.bonusClaimBlocks = storageData.bonusClaimBlocks; - } - else - { - this.bonusClaimBlocks = 0; - } - } - } - - public Vector getClaims() - { - if (this.claims == null) - { - this.claims = new Vector<>(); - - //find all the claims belonging to this player and note them for future reference - DataStore dataStore = GriefPrevention.instance.dataStore; - int totalClaimsArea = 0; - for (int i = 0; i < dataStore.claims.size(); i++) - { - Claim claim = dataStore.claims.get(i); - if (!claim.inDataStore) - { - dataStore.claims.remove(i--); - continue; - } - if (playerID.equals(claim.ownerID)) - { - this.claims.add(claim); - totalClaimsArea += claim.getArea(); - } - } - - //ensure player has claim blocks for his claims, and at least the minimum accrued - this.loadDataFromSecondaryStorage(); - - //if total claimed area is more than total blocks available - int totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); - if (GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && totalBlocks < totalClaimsArea) - { - OfflinePlayer player = GriefPrevention.instance.getServer().getOfflinePlayer(this.playerID); - GriefPrevention.AddLogEntry(player.getName() + " has more claimed land than blocks available. Adding blocks to fix.", CustomLogEntryTypes.Debug, true); - GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks(), CustomLogEntryTypes.Debug, true); - GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true); - for (Claim claim : this.claims) - { - if (!claim.inDataStore) continue; - GriefPrevention.AddLogEntry( - GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " // " - + GriefPrevention.getfriendlyLocationString(claim.getGreaterBoundaryCorner()) + " = " - + claim.getArea() - , CustomLogEntryTypes.Debug, true); - } - - //try to fix it by adding to accrued blocks - this.accruedClaimBlocks = totalClaimsArea; //Set accrued blocks to equal total claims - int accruedLimit = this.getAccruedClaimBlocksLimit(); - this.accruedClaimBlocks = Math.min(accruedLimit, this.accruedClaimBlocks); //set accrued blocks to maximum limit, if it's smaller - GriefPrevention.AddLogEntry("New accrued blocks: " + this.accruedClaimBlocks, CustomLogEntryTypes.Debug, true); - - //Recalculate total blocks (accrued + bonus + permission group bonus) - totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); - GriefPrevention.AddLogEntry("New total blocks: " + totalBlocks, CustomLogEntryTypes.Debug, true); - - //if that didn't fix it, then make up the difference with bonus blocks - if (totalBlocks < totalClaimsArea) - { - int bonusBlocksToAdd = totalClaimsArea - totalBlocks; - this.bonusClaimBlocks += bonusBlocksToAdd; - GriefPrevention.AddLogEntry("Accrued blocks weren't enough. Adding " + bonusBlocksToAdd + " bonus blocks.", CustomLogEntryTypes.Debug, true); - } - GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks() + " Group Bonus Blocks: " + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID), CustomLogEntryTypes.Debug, true); - //Recalculate total blocks (accrued + bonus + permission group bonus) - totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); - GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true); - GriefPrevention.AddLogEntry("Remaining claim blocks to use: " + this.getRemainingClaimBlocks() + " (should be 0)", CustomLogEntryTypes.Debug, true); - } - } - - for (int i = 0; i < this.claims.size(); i++) - { - if (!claims.get(i).inDataStore) - { - claims.remove(i--); - } - } - - return claims; - } - - //Limit can be changed by addons - public int getAccruedClaimBlocksLimit() - { - if (this.AccruedClaimBlocksLimit < 0) - return GriefPrevention.instance.config_claims_maxAccruedBlocks_default; - return this.AccruedClaimBlocksLimit; - } - - public void setAccruedClaimBlocksLimit(int limit) - { - this.AccruedClaimBlocksLimit = limit; - } - - public void accrueBlocks(int howMany) - { - this.newlyAccruedClaimBlocks += howMany; - } +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 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.Location; +import org.bukkit.OfflinePlayer; + +import java.net.InetAddress; +import java.util.Calendar; +import java.util.UUID; +import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; + +//holds all of GriefPrevention's player-tied data +public class PlayerData +{ + //the player's ID + public UUID playerID; + + //the player's claims + private Vector claims = null; + + //how many claim blocks the player has earned via play time + private Integer accruedClaimBlocks = null; + + //temporary holding area to avoid opening data files too early + private int newlyAccruedClaimBlocks = 0; + + //where this player was the last time we checked on him for earning claim blocks + public Location lastAfkCheckLocation = null; + + //how many claim blocks the player has been gifted by admins, or purchased via economy integration + private Integer bonusClaimBlocks = null; + + //what "mode" the shovel is in determines what it will do when it's used + public ShovelMode shovelMode = ShovelMode.Basic; + + //radius for restore nature fill mode + int fillRadius = 0; + + //last place the player used the shovel, useful in creating and resizing claims, + //because the player must use the shovel twice in those instances + public Location lastShovelLocation = null; + + //the claim this player is currently resizing + public Claim claimResizing = null; + + //the claim this player is currently subdividing + public Claim claimSubdividing = null; + + //whether or not the player has a pending /trapped rescue + public boolean pendingTrapped = false; + + //whether this player was recently warned about building outside land claims + boolean warnedAboutBuildingOutsideClaims = false; + + //whether this player was recently warned about placing tnt + boolean warnedAboutPlacingTnt = false; + + //whether the player was kicked (set and used during logout) + boolean wasKicked = false; + + //visualization + public Visualization currentVisualization = null; + + //anti-camping pvp protection + public boolean pvpImmune = false; + public long lastSpawn = 0; + + //ignore claims mode + public boolean ignoreClaims = false; + + //the last claim this player was in, that we know of + public Claim lastClaim = null; + + //pvp + public long lastPvpTimestamp = 0; + public String lastPvpPlayer = ""; + + //safety confirmation for deleting multi-subdivision claims + public boolean warnedAboutMajorDeletion = false; + + public InetAddress ipAddress; + + //for addons to set per-player claim limits. Any negative value will use config's value + private int AccruedClaimBlocksLimit = -1; + + //whether or not this player has received a message about unlocking death drops since his last death + boolean receivedDropUnlockAdvertisement = false; + + //whether or not this player's dropped items (on death) are unlocked for other players to pick up + boolean dropsAreUnlocked = false; + + //message to send to player after he respawns + String messageOnRespawn = null; + + //player which a pet will be given to when it's right-clicked + OfflinePlayer petGiveawayRecipient = null; + + //timestamp for last "you're building outside your land claims" message + Long buildWarningTimestamp = null; + + //spot where a player can't talk, used to mute new players until they've moved a little + //this is an anti-bot strategy. + Location noChatLocation = null; + + //ignore list + //true means invisible (admin-forced ignore), false means player-created ignore + public ConcurrentHashMap ignoredPlayers = new ConcurrentHashMap<>(); + public boolean ignoreListChanged = false; + + //profanity warning, once per play session + boolean profanityWarned = false; + + //whether or not this player is "in" pvp combat + public boolean inPvpCombat() + { + if (this.lastPvpTimestamp == 0) return false; + + long now = Calendar.getInstance().getTimeInMillis(); + + long elapsed = now - this.lastPvpTimestamp; + + if (elapsed > GriefPrevention.instance.config_pvp_combatTimeoutSeconds * 1000) //X seconds + { + this.lastPvpTimestamp = 0; + return false; + } + + return true; + } + + //the number of claim blocks a player has available for claiming land + public int getRemainingClaimBlocks() + { + int remainingBlocks = this.getAccruedClaimBlocks() + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); + for (int i = 0; i < this.getClaims().size(); i++) + { + Claim claim = this.getClaims().get(i); + remainingBlocks -= claim.getArea(); + } + + return remainingBlocks; + } + + //don't load data from secondary storage until it's needed + public synchronized int getAccruedClaimBlocks() + { + if (this.accruedClaimBlocks == null) this.loadDataFromSecondaryStorage(); + + //update claim blocks with any he has accrued during his current play session + if (this.newlyAccruedClaimBlocks > 0) + { + int accruedLimit = this.getAccruedClaimBlocksLimit(); + + //if over the limit before adding blocks, leave it as-is, because the limit may have changed AFTER he accrued the blocks + if (this.accruedClaimBlocks < accruedLimit) + { + //move any in the holding area + int newTotal = this.accruedClaimBlocks + this.newlyAccruedClaimBlocks; + + //respect limits + this.accruedClaimBlocks = Math.min(newTotal, accruedLimit); + } + + this.newlyAccruedClaimBlocks = 0; + return this.accruedClaimBlocks; + } + + return accruedClaimBlocks; + } + + public void setAccruedClaimBlocks(Integer accruedClaimBlocks) + { + this.accruedClaimBlocks = accruedClaimBlocks; + this.newlyAccruedClaimBlocks = 0; + } + + public int getBonusClaimBlocks() + { + if (this.bonusClaimBlocks == null) this.loadDataFromSecondaryStorage(); + return bonusClaimBlocks; + } + + public void setBonusClaimBlocks(Integer bonusClaimBlocks) + { + this.bonusClaimBlocks = bonusClaimBlocks; + } + + private void loadDataFromSecondaryStorage() + { + //reach out to secondary storage to get any data there + PlayerData storageData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(this.playerID); + + if (this.accruedClaimBlocks == null) + { + if (storageData.accruedClaimBlocks != null) + { + this.accruedClaimBlocks = storageData.accruedClaimBlocks; + + //ensure at least minimum accrued are accrued (in case of settings changes to increase initial amount) + if (GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && (this.accruedClaimBlocks < GriefPrevention.instance.config_claims_initialBlocks)) + { + this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks; + } + + } + else + { + this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks; + } + } + + if (this.bonusClaimBlocks == null) + { + if (storageData.bonusClaimBlocks != null) + { + this.bonusClaimBlocks = storageData.bonusClaimBlocks; + } + else + { + this.bonusClaimBlocks = 0; + } + } + } + + public Vector getClaims() + { + if (this.claims == null) + { + this.claims = new Vector<>(); + + //find all the claims belonging to this player and note them for future reference + DataStore dataStore = GriefPrevention.instance.dataStore; + int totalClaimsArea = 0; + for (int i = 0; i < dataStore.claims.size(); i++) + { + Claim claim = dataStore.claims.get(i); + if (!claim.inDataStore) + { + dataStore.claims.remove(i--); + continue; + } + if (playerID.equals(claim.ownerID)) + { + this.claims.add(claim); + totalClaimsArea += claim.getArea(); + } + } + + //ensure player has claim blocks for his claims, and at least the minimum accrued + this.loadDataFromSecondaryStorage(); + + //if total claimed area is more than total blocks available + int totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); + if (GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && totalBlocks < totalClaimsArea) + { + OfflinePlayer player = GriefPrevention.instance.getServer().getOfflinePlayer(this.playerID); + GriefPrevention.AddLogEntry(player.getName() + " has more claimed land than blocks available. Adding blocks to fix.", CustomLogEntryTypes.Debug, true); + GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks(), CustomLogEntryTypes.Debug, true); + GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true); + for (Claim claim : this.claims) + { + if (!claim.inDataStore) continue; + GriefPrevention.AddLogEntry( + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " // " + + GriefPrevention.getfriendlyLocationString(claim.getGreaterBoundaryCorner()) + " = " + + claim.getArea() + , CustomLogEntryTypes.Debug, true); + } + + //try to fix it by adding to accrued blocks + this.accruedClaimBlocks = totalClaimsArea; //Set accrued blocks to equal total claims + int accruedLimit = this.getAccruedClaimBlocksLimit(); + this.accruedClaimBlocks = Math.min(accruedLimit, this.accruedClaimBlocks); //set accrued blocks to maximum limit, if it's smaller + GriefPrevention.AddLogEntry("New accrued blocks: " + this.accruedClaimBlocks, CustomLogEntryTypes.Debug, true); + + //Recalculate total blocks (accrued + bonus + permission group bonus) + totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); + GriefPrevention.AddLogEntry("New total blocks: " + totalBlocks, CustomLogEntryTypes.Debug, true); + + //if that didn't fix it, then make up the difference with bonus blocks + if (totalBlocks < totalClaimsArea) + { + int bonusBlocksToAdd = totalClaimsArea - totalBlocks; + this.bonusClaimBlocks += bonusBlocksToAdd; + GriefPrevention.AddLogEntry("Accrued blocks weren't enough. Adding " + bonusBlocksToAdd + " bonus blocks.", CustomLogEntryTypes.Debug, true); + } + GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks() + " Group Bonus Blocks: " + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID), CustomLogEntryTypes.Debug, true); + //Recalculate total blocks (accrued + bonus + permission group bonus) + totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID); + GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true); + GriefPrevention.AddLogEntry("Remaining claim blocks to use: " + this.getRemainingClaimBlocks() + " (should be 0)", CustomLogEntryTypes.Debug, true); + } + } + + for (int i = 0; i < this.claims.size(); i++) + { + if (!claims.get(i).inDataStore) + { + claims.remove(i--); + } + } + + return claims; + } + + //Limit can be changed by addons + public int getAccruedClaimBlocksLimit() + { + if (this.AccruedClaimBlocksLimit < 0) + return GriefPrevention.instance.config_claims_maxAccruedBlocks_default; + return this.AccruedClaimBlocksLimit; + } + + public void setAccruedClaimBlocksLimit(int limit) + { + this.AccruedClaimBlocksLimit = limit; + } + + public void accrueBlocks(int howMany) + { + this.newlyAccruedClaimBlocks += howMany; + } } \ No newline at end of file