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