From 9fe7f8258d05a5c57501234d4823a6842caa52fc Mon Sep 17 00:00:00 2001
From: Len <40720638+destro174@users.noreply.github.com>
Date: Thu, 9 Jun 2022 11:05:13 +0200
Subject: [PATCH] Require container trust to let pets sit down
---
.../GriefPrevention/PlayerEventHandler.java | 4106 +++++++++--------
1 file changed, 2062 insertions(+), 2044 deletions(-)
diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java
index ace1bf9..97070a1 100644
--- a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java
+++ b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java
@@ -1,2044 +1,2062 @@
-/*
- 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 me.ryanhamshire.GriefPrevention.events.ClaimInspectionEvent;
-import me.ryanhamshire.GriefPrevention.events.VisualizationEvent;
-import org.bukkit.BanList;
-import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
-import org.bukkit.Chunk;
-import org.bukkit.GameMode;
-import org.bukkit.Location;
-import org.bukkit.Material;
-import org.bukkit.Nameable;
-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.data.BlockData;
-import org.bukkit.block.data.Levelled;
-import org.bukkit.block.data.Waterlogged;
-import org.bukkit.command.Command;
-import org.bukkit.entity.AbstractHorse;
-import org.bukkit.entity.Animals;
-import org.bukkit.entity.Creature;
-import org.bukkit.entity.Donkey;
-import org.bukkit.entity.Entity;
-import org.bukkit.entity.EntityType;
-import org.bukkit.entity.Fish;
-import org.bukkit.entity.Hanging;
-import org.bukkit.entity.Item;
-import org.bukkit.entity.Llama;
-import org.bukkit.entity.Mule;
-import org.bukkit.entity.Player;
-import org.bukkit.entity.Tameable;
-import org.bukkit.entity.Vehicle;
-import org.bukkit.entity.Villager;
-import org.bukkit.entity.minecart.PoweredMinecart;
-import org.bukkit.entity.minecart.StorageMinecart;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.block.Action;
-import org.bukkit.event.block.CauldronLevelChangeEvent;
-import org.bukkit.event.entity.EntityDamageByEntityEvent;
-import org.bukkit.event.entity.EntityDamageEvent;
-import org.bukkit.event.entity.PlayerDeathEvent;
-import org.bukkit.event.player.PlayerBucketEmptyEvent;
-import org.bukkit.event.player.PlayerBucketFillEvent;
-import org.bukkit.event.player.PlayerCommandPreprocessEvent;
-import org.bukkit.event.player.PlayerDropItemEvent;
-import org.bukkit.event.player.PlayerEggThrowEvent;
-import org.bukkit.event.player.PlayerFishEvent;
-import org.bukkit.event.player.PlayerInteractAtEntityEvent;
-import org.bukkit.event.player.PlayerInteractEntityEvent;
-import org.bukkit.event.player.PlayerInteractEvent;
-import org.bukkit.event.player.PlayerItemHeldEvent;
-import org.bukkit.event.player.PlayerJoinEvent;
-import org.bukkit.event.player.PlayerKickEvent;
-import org.bukkit.event.player.PlayerPickupItemEvent;
-import org.bukkit.event.player.PlayerPortalEvent;
-import org.bukkit.event.player.PlayerQuitEvent;
-import org.bukkit.event.player.PlayerRespawnEvent;
-import org.bukkit.event.player.PlayerTakeLecternBookEvent;
-import org.bukkit.event.player.PlayerTeleportEvent;
-import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
-import org.bukkit.event.raid.RaidTriggerEvent;
-import org.bukkit.inventory.EquipmentSlot;
-import org.bukkit.inventory.InventoryHolder;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.metadata.MetadataValue;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.util.BlockIterator;
-
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
-import java.util.regex.Pattern;
-
-class PlayerEventHandler implements Listener
-{
- private final DataStore dataStore;
- private final GriefPrevention instance;
-
- //number of milliseconds in a day
- private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;
-
- //timestamps of login and logout notifications in the last minute
- private final ArrayList recentLoginLogoutNotifications = new ArrayList<>();
-
- //regex pattern for the "how do i claim land?" scanner
- private Pattern howToClaimPattern = null;
-
- //matcher for banned words
- private final WordFinder bannedWordFinder;
-
- //typical constructor, yawn
- PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
- {
- this.dataStore = dataStore;
- this.instance = plugin;
- bannedWordFinder = new WordFinder(instance.dataStore.loadBannedWords());
- }
-
- //when a player uses a slash command...
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- synchronized void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event)
- {
- String message = event.getMessage();
- String[] args = message.split(" ");
- String command = args[0];
-
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId());
-
- //if the slash command used is in the list of monitored commands, treat it like a chat message (see above)
- boolean isMonitoredCommand = false;
- for (String monitoredCommand : instance.config_claims_commandsRequiringAccessTrust)
- {
- if (command.equalsIgnoreCase(monitoredCommand))
- {
- isMonitoredCommand = true;
- break;
- }
- }
-
- if (isMonitoredCommand)
- {
- Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
- Supplier reason = claim.checkPermission(player, ClaimPermission.Access, event);
- if (reason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
- event.setCancelled(true);
- }
- }
- }
- }
-
- static int longestNameLength = 10;
-
- static void makeSocialLogEntry(String name, String message)
- {
- StringBuilder entryBuilder = new StringBuilder(name);
- for (int i = name.length(); i < longestNameLength; i++)
- {
- entryBuilder.append(' ');
- }
- entryBuilder.append(": ").append(message);
-
- longestNameLength = Math.max(longestNameLength, name.length());
- //TODO: cleanup static
- GriefPrevention.AddLogEntry(entryBuilder.toString(), CustomLogEntryTypes.SocialActivity, true);
- }
-
- private final ConcurrentHashMap lastLoginThisServerSessionMap = new ConcurrentHashMap<>();
-
- //when a player successfully joins the server...
-
- @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
- void onPlayerJoin(PlayerJoinEvent event)
- {
- Player player = event.getPlayer();
- UUID playerID = player.getUniqueId();
-
- //note login time
- Date nowDate = new Date();
- long now = nowDate.getTime();
- PlayerData playerData = this.dataStore.getPlayerData(playerID);
- playerData.lastSpawn = now;
- this.lastLoginThisServerSessionMap.put(playerID, nowDate);
-
- //if newish, prevent chat until he's moved a bit to prove he's not a bot
- if (GriefPrevention.isNewToServer(player))
- {
- playerData.noChatLocation = player.getLocation();
- }
-
- //silence notifications when they're coming too fast
- if (event.getJoinMessage() != null && this.shouldSilenceNotification())
- {
- event.setJoinMessage(null);
- }
-
- //in case player has changed his name, on successful login, update UUID > Name mapping
- GriefPrevention.cacheUUIDNamePair(player.getUniqueId(), player.getName());
-
- //ensure we're not over the limit for this IP address
- InetAddress ipAddress = playerData.ipAddress;
- if (ipAddress != null)
- {
- int ipLimit = instance.config_ipLimit;
- if (ipLimit > 0 && GriefPrevention.isNewToServer(player))
- {
- int ipCount = 0;
-
- @SuppressWarnings("unchecked")
- Collection players = (Collection) instance.getServer().getOnlinePlayers();
- for (Player onlinePlayer : players)
- {
- if (onlinePlayer.getUniqueId().equals(player.getUniqueId())) continue;
-
- PlayerData otherData = instance.dataStore.getPlayerData(onlinePlayer.getUniqueId());
- if (ipAddress.equals(otherData.ipAddress) && GriefPrevention.isNewToServer(onlinePlayer))
- {
- ipCount++;
- }
- }
-
- if (ipCount >= ipLimit)
- {
- //kick player
- PlayerKickBanTask task = new PlayerKickBanTask(player, instance.dataStore.getMessage(Messages.TooMuchIpOverlap), "GriefPrevention IP-sharing limit.", false);
- instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 100L);
-
- //silence join message
- event.setJoinMessage(null);
- return;
- }
- }
- }
-
- //is he stuck in a portal frame?
- if (player.hasMetadata("GP_PORTALRESCUE"))
- {
- //If so, let him know and rescue him in 10 seconds. If he is in fact not trapped, hopefully chunks will have loaded by this time so he can walk out.
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.NetherPortalTrapDetectionMessage, 20L);
- new BukkitRunnable()
- {
- @Override
- public void run()
- {
- if (player.getPortalCooldown() > 8 && player.hasMetadata("GP_PORTALRESCUE"))
- {
- GriefPrevention.AddLogEntry("Rescued " + player.getName() + " from a nether portal.\nTeleported from " + player.getLocation().toString() + " to " + ((Location) player.getMetadata("GP_PORTALRESCUE").get(0).value()).toString(), CustomLogEntryTypes.Debug);
- player.teleport((Location) player.getMetadata("GP_PORTALRESCUE").get(0).value());
- player.removeMetadata("GP_PORTALRESCUE", instance);
- }
- }
- }.runTaskLater(instance, 200L);
- }
- //Otherwise just reset cooldown, just in case they happened to logout again...
- else
- player.setPortalCooldown(0);
-
-
- //if we're holding a logout message for this player, don't send that or this event's join message
- if (instance.config_spam_logoutMessageDelaySeconds > 0)
- {
- String joinMessage = event.getJoinMessage();
- if (joinMessage != null && !joinMessage.isEmpty())
- {
- Integer taskID = this.heldLogoutMessages.get(player.getUniqueId());
- if (taskID != null && Bukkit.getScheduler().isQueued(taskID))
- {
- Bukkit.getScheduler().cancelTask(taskID);
- player.sendMessage(event.getJoinMessage());
- event.setJoinMessage("");
- }
- }
- }
- }
-
- //when a player spawns, conditionally apply temporary pvp protection
- @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
- void onPlayerRespawn(PlayerRespawnEvent event)
- {
- Player player = event.getPlayer();
- PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId());
- playerData.lastSpawn = Calendar.getInstance().getTimeInMillis();
- playerData.lastPvpTimestamp = 0; //no longer in pvp combat
-
- //also send him any messaged from grief prevention he would have received while dead
- if (playerData.messageOnRespawn != null)
- {
- GriefPrevention.sendMessage(player, ChatColor.RESET /*color is alrady embedded in message in this case*/, playerData.messageOnRespawn, 40L);
- playerData.messageOnRespawn = null;
- }
-
- instance.checkPvpProtectionNeeded(player);
- }
-
- //when a player dies...
- private final HashMap deathTimestamps = new HashMap<>();
-
- @EventHandler(priority = EventPriority.HIGHEST)
- void onPlayerDeath(PlayerDeathEvent event)
- {
- //FEATURE: prevent death message spam by implementing a "cooldown period" for death messages
- Player player = event.getEntity();
- Long lastDeathTime = this.deathTimestamps.get(player.getUniqueId());
- long now = Calendar.getInstance().getTimeInMillis();
- if (lastDeathTime != null && now - lastDeathTime < instance.config_spam_deathMessageCooldownSeconds * 1000)
- {
- player.sendMessage(event.getDeathMessage()); //let the player assume his death message was broadcasted to everyone
- event.setDeathMessage("");
- }
-
- this.deathTimestamps.put(player.getUniqueId(), now);
- }
-
- //when a player gets kicked...
- @EventHandler(priority = EventPriority.HIGHEST)
- void onPlayerKicked(PlayerKickEvent event)
- {
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
- playerData.wasKicked = true;
-
- UUID playerID = player.getUniqueId();
- if(instance.ignoreClaimWarningTasks.containsKey(playerID))
- {
- instance.ignoreClaimWarningTasks.get(playerID).cancel();
- instance.ignoreClaimWarningTasks.remove(playerID);
- }
- }
-
- //when a player quits...
- private final HashMap heldLogoutMessages = new HashMap<>();
-
- @EventHandler(priority = EventPriority.HIGHEST)
- void onPlayerQuit(PlayerQuitEvent event)
- {
- Player player = event.getPlayer();
- UUID playerID = player.getUniqueId();
- PlayerData playerData = this.dataStore.getPlayerData(playerID);
-
- //If player is not trapped in a portal and has a pending rescue task, remove the associated metadata
- //Why 9? No idea why, but this is decremented by 1 when the player disconnects.
- if (player.getPortalCooldown() < 9)
- {
- player.removeMetadata("GP_PORTALRESCUE", instance);
- }
-
- //silence notifications when they're coming too fast
- if (event.getQuitMessage() != null && this.shouldSilenceNotification())
- {
- event.setQuitMessage(null);
- }
-
- //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately
- else
- {
- this.dataStore.savePlayerData(player.getUniqueId(), playerData);
- }
-
- //FEATURE: players in pvp combat when they log out will die
- if (instance.config_pvp_punishLogout && playerData.inPvpCombat())
- {
- player.setHealth(0);
- }
-
- //drop data about this player
- this.dataStore.clearCachedPlayerData(playerID);
-
- //send quit message later, but only if the player stays offline
- if (instance.config_spam_logoutMessageDelaySeconds > 0)
- {
- String quitMessage = event.getQuitMessage();
- if (quitMessage != null && !quitMessage.isEmpty())
- {
- BroadcastMessageTask task = new BroadcastMessageTask(quitMessage);
- int taskID = Bukkit.getScheduler().scheduleSyncDelayedTask(instance, task, 20L * instance.config_spam_logoutMessageDelaySeconds);
- this.heldLogoutMessages.put(playerID, taskID);
- event.setQuitMessage("");
- }
- }
- if(instance.ignoreClaimWarningTasks.containsKey(playerID))
- {
- instance.ignoreClaimWarningTasks.get(playerID).cancel();
- instance.ignoreClaimWarningTasks.remove(playerID);
- }
- }
-
- //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute
- private boolean shouldSilenceNotification()
- {
- if (instance.config_spam_loginLogoutNotificationsPerMinute <= 0)
- {
- return false; // not silencing login/logout notifications
- }
-
- final long ONE_MINUTE = 60000;
- Long now = Calendar.getInstance().getTimeInMillis();
-
- //eliminate any expired entries (longer than a minute ago)
- for (int i = 0; i < this.recentLoginLogoutNotifications.size(); i++)
- {
- Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i);
- if (now - notificationTimestamp > ONE_MINUTE)
- {
- this.recentLoginLogoutNotifications.remove(i--);
- }
- else
- {
- break;
- }
- }
-
- //add the new entry
- this.recentLoginLogoutNotifications.add(now);
-
- return this.recentLoginLogoutNotifications.size() > instance.config_spam_loginLogoutNotificationsPerMinute;
- }
-
- //when a player drops an item
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerDropItem(PlayerDropItemEvent event)
- {
- Player player = event.getPlayer();
-
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
-
- //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide
- //them or give them away to other players before they are defeated
-
- //if in combat, don't let him drop it
- if (!instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat())
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop);
- event.setCancelled(true);
- }
- }
-
- //when a player teleports via a portal
- @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
- void onPlayerPortal(PlayerPortalEvent event)
- {
- //if the player isn't going anywhere, take no action
- if (event.getTo() == null || event.getTo().getWorld() == null) return;
-
- Player player = event.getPlayer();
- if (event.getCause() == TeleportCause.NETHER_PORTAL)
- {
- //FEATURE: when players get trapped in a nether portal, send them back through to the other side
- instance.startRescueTask(player, player.getLocation());
-
- //don't track in worlds where claims are not enabled
- if (!instance.claimsEnabledForWorld(event.getTo().getWorld())) return;
- }
- }
-
- //when a player teleports
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerTeleport(PlayerTeleportEvent event)
- {
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
-
- //FEATURE: prevent players from using ender pearls to gain access to secured claims
- TeleportCause cause = event.getCause();
- if (cause == TeleportCause.CHORUS_FRUIT || (cause == TeleportCause.ENDER_PEARL && instance.config_claims_enderPearlsRequireAccessTrust))
- {
- Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim);
- if (toClaim != null)
- {
- playerData.lastClaim = toClaim;
- Supplier noAccessReason = toClaim.checkPermission(player, ClaimPermission.Access, event);
- if (noAccessReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
- event.setCancelled(true);
- if (cause == TeleportCause.ENDER_PEARL)
- player.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
- }
- }
- }
- }
-
- //when a player triggers a raid (in a claim)
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerTriggerRaid(RaidTriggerEvent event)
- {
-// if (!instance.config_claims_raidTriggersRequireBuildTrust)
-// return;
-
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
-
- Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim);
- if (claim == null)
- return;
-
- playerData.lastClaim = claim;
-// if (claim.checkPermission(player, ClaimPermission.Build, event) == null)
- if (claim.checkPermission(player, ClaimPermission.Access, event) == null)
- return;
-
- event.setCancelled(true);
- }
-
- //when a player interacts with a specific part of entity...
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event)
- {
- //treat it the same as interacting with an entity in general
- if (event.getRightClicked().getType() == EntityType.ARMOR_STAND)
- {
- this.onPlayerInteractEntity((PlayerInteractEntityEvent) event);
- }
- }
-
- //when a player interacts with an entity...
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerInteractEntity(PlayerInteractEntityEvent event)
- {
- Player player = event.getPlayer();
- Entity entity = event.getRightClicked();
-
- if (!instance.claimsEnabledForWorld(entity.getWorld())) return;
-
- //allow horse protection to be overridden to allow management from other plugins
- if (!instance.config_claims_protectHorses && entity instanceof AbstractHorse) return;
- if (!instance.config_claims_protectDonkeys && entity instanceof Donkey) return;
- if (!instance.config_claims_protectDonkeys && entity instanceof Mule) return;
- if (!instance.config_claims_protectLlamas && entity instanceof Llama) return;
-
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
-
- //if entity is tameable and has an owner, apply special rules
- if (entity instanceof Tameable)
- {
- Tameable tameable = (Tameable) entity;
- if (tameable.isTamed())
- {
- if (tameable.getOwner() != null)
- {
- UUID ownerID = tameable.getOwner().getUniqueId();
-
- //if the player interacting is the owner or an admin in ignore claims mode, always allow
- if (player.getUniqueId().equals(ownerID) || playerData.ignoreClaims)
- {
- //if giving away pet, do that instead
- if (playerData.petGiveawayRecipient != null)
- {
- tameable.setOwner(playerData.petGiveawayRecipient);
- playerData.petGiveawayRecipient = null;
- GriefPrevention.sendMessage(player, TextMode.Success, Messages.PetGiveawayConfirmation);
- event.setCancelled(true);
- }
-
- return;
- }
- if (!instance.pvpRulesApply(entity.getLocation().getWorld()) || instance.config_pvp_protectPets)
- {
- //otherwise disallow
- OfflinePlayer owner = instance.getServer().getOfflinePlayer(ownerID);
- String ownerName = owner.getName();
- if (ownerName == null) ownerName = "someone";
- String message = instance.dataStore.getMessage(Messages.NotYourPet, ownerName);
- if (player.hasPermission("griefprevention.ignoreclaims"))
- message += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
- GriefPrevention.sendMessage(player, TextMode.Err, message);
- event.setCancelled(true);
- return;
- }
- }
- }
- else //world repair code for a now-fixed GP bug //TODO: necessary anymore?
- {
- //ensure this entity can be tamed by players
- tameable.setOwner(null);
- if (tameable instanceof InventoryHolder)
- {
- InventoryHolder holder = (InventoryHolder) tameable;
- holder.getInventory().clear();
- }
- }
- }
-
- //don't allow interaction with item frames or armor stands in claimed areas without build permission
- if (entity.getType() == EntityType.ARMOR_STAND || entity instanceof Hanging)
- {
- String noBuildReason = instance.allowBuild(player, entity.getLocation(), Material.ITEM_FRAME);
- if (noBuildReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
- event.setCancelled(true);
- return;
- }
- }
-
- //always allow interactions when player is in ignore claims mode
- if (playerData.ignoreClaims) return;
-
- //don't allow container access during pvp combat
- if ((entity instanceof StorageMinecart || entity instanceof PoweredMinecart))
- {
- if (playerData.inPvpCombat())
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers);
- event.setCancelled(true);
- return;
- }
- }
-
- //if the entity is a vehicle and we're preventing theft in claims
- if (instance.config_claims_preventTheft && entity instanceof Vehicle)
- {
- //if the entity is in a claim
- Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null);
- if (claim != null)
- {
- //for storage entities, apply container rules (this is a potential theft)
- if (entity instanceof InventoryHolder)
- {
- Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (noContainersReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
- event.setCancelled(true);
- return;
- }
- }
- }
- }
-
- //if the entity is an animal, apply container rules
- if ((instance.config_claims_preventTheft && (entity instanceof Animals || entity instanceof Fish)) || (entity.getType() == EntityType.VILLAGER && instance.config_claims_villagerTradingRequiresTrust))
- {
- if (entity.getType() == EntityType.VILLAGER) {
- // early check for custom name to not waste time on getting the claim if not needed?
- Villager villager = (Villager) entity;
- if (villager.getCustomName() != null)
- {
- if (ChatColor.stripColor(villager.getCustomName()).equalsIgnoreCase("public"))
- {
- return;
- }
- }
- }
- //if the entity is in a claim
- Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null);
- if (claim != null)
- {
- Supplier override = () ->
- {
- String message = instance.dataStore.getMessage(Messages.NoDamageClaimedEntity, claim.getOwnerName());
- if (player.hasPermission("griefprevention.ignoreclaims"))
- message += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
-
- return message;
- };
- final Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event, override);
- if (noContainersReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
- event.setCancelled(true);
- return;
- }
- }
- }
-
- ItemStack itemInHand = instance.getItemInHand(player, event.getHand());
-
- //if preventing theft, prevent leashing claimed creatures
- if (instance.config_claims_preventTheft && entity instanceof Creature && itemInHand.getType() == Material.LEAD)
- {
- Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- Supplier failureReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (failureReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, failureReason.get());
- return;
- }
- }
- }
- // This change breaks mcmmo where it adds hearts as health bar to mobs...
- // Name tags may only be used on entities that the player is allowed to kill.
- /*if (itemInHand.getType() == Material.NAME_TAG)
- {
- EntityDamageByEntityEvent damageEvent = new EntityDamageByEntityEvent(player, entity, EntityDamageEvent.DamageCause.CUSTOM, 0);
- instance.entityEventHandler.onEntityDamage(damageEvent);
- if (damageEvent.isCancelled())
- {
- event.setCancelled(true);
- // Don't print message - damage event handler should have handled it.
- return;
- }
- }*/
- }
-
- //when a player throws an egg
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerThrowEgg(PlayerEggThrowEvent event)
- {
- if (!event.isHatching()) return;
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(event.getEgg().getLocation(), false, playerData.lastClaim);
-
- //allow throw egg if player is in ignore claims mode
- if (playerData.ignoreClaims || claim == null) return;
-
-// Supplier failureReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- Supplier failureReason = claim.checkPermission(player, ClaimPermission.Build, event);
- if (failureReason != null)
- {
- String reason = failureReason.get();
- if (player.hasPermission("griefprevention.ignoreclaims"))
- {
- reason += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
- }
-
- GriefPrevention.sendMessage(player, TextMode.Err, reason);
-
- //cancel the event by preventing hatching
- event.setHatching(false);
-
- //only give the egg back if player is in survival or adventure
- if (player.getGameMode() == GameMode.SURVIVAL || player.getGameMode() == GameMode.ADVENTURE)
- {
- player.getInventory().addItem(event.getEgg().getItem());
- }
- }
- }
-
- //when a player reels in his fishing rod
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerFish(PlayerFishEvent event)
- {
- Entity entity = event.getCaught();
- if (entity == null) return; //if nothing pulled, uninteresting event
-
- //if should be protected from pulling in land claims without permission
- if (entity.getType() == EntityType.ARMOR_STAND || entity instanceof Animals)
- {
- Player player = event.getPlayer();
- PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = instance.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- //if no permission, cancel
- Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (errorMessage != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName());
- return;
- }
- }
- }
- }
-
- //when a player picks up an item...
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerPickupItem(PlayerPickupItemEvent event)
- {
- Player player = event.getPlayer();
-
- //the rest of this code is specific to pvp worlds
- if (!instance.pvpRulesApply(player.getWorld())) return;
-
- //if we're preventing spawn camping and the player was previously empty handed...
- if (instance.config_pvp_protectFreshSpawns && (instance.getItemInHand(player, EquipmentSlot.HAND).getType() == Material.AIR))
- {
- //if that player is currently immune to pvp
- PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId());
- if (playerData.pvpImmune)
- {
- //if it's been less than 10 seconds since the last time he spawned, don't pick up the item
- long now = Calendar.getInstance().getTimeInMillis();
- long elapsedSinceLastSpawn = now - playerData.lastSpawn;
- if (elapsedSinceLastSpawn < 10000)
- {
- event.setCancelled(true);
- return;
- }
-
- //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot
- playerData.pvpImmune = false;
- GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
- }
- }
- }
-
- //when a player switches in-hand items
- @EventHandler(ignoreCancelled = true)
- public void onItemHeldChange(PlayerItemHeldEvent event)
- {
- Player player = event.getPlayer();
-
- //if he's switching to the golden shovel
- int newSlot = event.getNewSlot();
- ItemStack newItemStack = player.getInventory().getItem(newSlot);
- if (newItemStack != null && newItemStack.getType() == instance.config_claims_modificationTool)
- {
- //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam
- if (instance.claimsEnabledForWorld(player.getWorld()))
- {
- EquipShovelProcessingTask task = new EquipShovelProcessingTask(player);
- instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 15L); //15L is approx. 3/4 of a second
- }
- }
- }
-
- //block use of buckets within other players' claims
- private final Set commonAdjacentBlocks_water = EnumSet.of(Material.WATER, Material.FARMLAND, Material.DIRT, Material.STONE);
- private final Set commonAdjacentBlocks_lava = EnumSet.of(Material.LAVA, Material.DIRT, Material.STONE);
-
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerBucketEmpty(PlayerBucketEmptyEvent bucketEvent)
- {
- if (!instance.claimsEnabledForWorld(bucketEvent.getBlockClicked().getWorld())) return;
-
- Player player = bucketEvent.getPlayer();
- Block block = bucketEvent.getBlockClicked().getRelative(bucketEvent.getBlockFace());
- int minLavaDistance = 10;
-
- // Fixes #1155:
- // Prevents waterlogging blocks placed on a claim's edge.
- // Waterlogging a block affects the clicked block, and NOT the adjacent location relative to it.
- if (bucketEvent.getBucket() == Material.WATER_BUCKET
- && bucketEvent.getBlockClicked().getBlockData() instanceof Waterlogged)
- {
- block = bucketEvent.getBlockClicked();
- }
-
- //make sure the player is allowed to build at the location
- String noBuildReason = instance.allowBuild(player, block.getLocation(), Material.WATER);
- if (noBuildReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
- bucketEvent.setCancelled(true);
- return;
- }
-
- //if the bucket is being used in a claim, allow for dumping lava closer to other players
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- minLavaDistance = 3;
- }
-
- //lava buckets can't be dumped near other players unless pvp is on
- if (!doesAllowLavaProximityInWorld(block.getWorld()) && !player.hasPermission("griefprevention.lava"))
- {
- if (bucketEvent.getBucket() == Material.LAVA_BUCKET)
- {
- List players = block.getWorld().getPlayers();
- for (Player otherPlayer : players)
- {
- if (isVanished(otherPlayer)) continue;
- Location location = otherPlayer.getLocation();
- if (!otherPlayer.equals(player) && otherPlayer.getGameMode() == GameMode.SURVIVAL && player.canSee(otherPlayer) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, "another player");
- bucketEvent.setCancelled(true);
- return;
- }
- }
- }
- }
-
- //log any suspicious placements (check sea level, world type, and adjacent blocks)
- if (block.getY() >= instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava") && block.getWorld().getEnvironment() != Environment.NETHER)
- {
- //if certain blocks are nearby, it's less suspicious and not worth logging
- Set exclusionAdjacentTypes;
- if (bucketEvent.getBucket() == Material.WATER_BUCKET)
- exclusionAdjacentTypes = this.commonAdjacentBlocks_water;
- else
- exclusionAdjacentTypes = this.commonAdjacentBlocks_lava;
-
- boolean makeLogEntry = true;
- BlockFace[] adjacentDirections = new BlockFace[]{BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.DOWN};
- for (BlockFace direction : adjacentDirections)
- {
- Material adjacentBlockType = block.getRelative(direction).getType();
- if (exclusionAdjacentTypes.contains(adjacentBlockType))
- {
- makeLogEntry = false;
- break;
- }
- }
-
- if (makeLogEntry)
- {
- GriefPrevention.AddLogEntry(player.getName() + " placed suspicious " + bucketEvent.getBucket().name() + " @ " + GriefPrevention.getfriendlyLocationString(block.getLocation()), CustomLogEntryTypes.SuspiciousActivity, true);
- }
- }
- }
-
- private boolean doesAllowLavaProximityInWorld(World world)
- {
- if (GriefPrevention.instance.pvpRulesApply(world))
- {
- return GriefPrevention.instance.config_pvp_allowLavaNearPlayers;
- }
- else
- {
- return GriefPrevention.instance.config_pvp_allowLavaNearPlayers_NonPvp;
- }
- }
-
- //see above
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerBucketFill(PlayerBucketFillEvent bucketEvent)
- {
- Player player = bucketEvent.getPlayer();
- Block block = bucketEvent.getBlockClicked();
-
- if (!instance.claimsEnabledForWorld(block.getWorld())) return;
-
- //make sure the player is allowed to build at the location
- String noBuildReason = instance.allowBuild(player, block.getLocation(), Material.AIR);
- if (noBuildReason != null)
- {
- //exemption for cow milking (permissions will be handled by player interact with entity event instead)
- Material blockType = block.getType();
- if (blockType == Material.AIR)
- return;
- if (blockType.isSolid())
- {
- BlockData blockData = block.getBlockData();
- if (!(blockData instanceof Waterlogged) || !((Waterlogged) blockData).isWaterlogged())
- return;
- }
-
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
- bucketEvent.setCancelled(true);
- return;
- }
- }
-
- @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
- public void onPlayerBucketatCauldron(CauldronLevelChangeEvent event)
- {
- if (!instance.claimsEnabledForWorld(event.getBlock().getWorld())) return;
- Entity entity = event.getEntity();
- Block block = event.getBlock();
- if (entity == null) return;
- if (entity instanceof Player player) {
- Claim claim = dataStore.getClaimAt(block.getLocation(), false, null);
- if (claim == null)
- return;
-
- Supplier allowContainer = claim.checkPermission(player, ClaimPermission.Inventory, event);
-
- if (allowContainer != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, allowContainer.get());
- return;
- }
- }
- }
-
- //when a player interacts with the world
- @EventHandler(priority = EventPriority.LOWEST)
- void onPlayerInteract(PlayerInteractEvent event)
- {
- //not interested in left-click-on-air actions
- Action action = event.getAction();
- if (action == Action.LEFT_CLICK_AIR) return;
-
- Player player = event.getPlayer();
- Block clickedBlock = event.getClickedBlock(); //null returned here means interacting with air
-
- Material clickedBlockType = null;
- if (clickedBlock != null)
- {
- clickedBlockType = clickedBlock.getType();
- }
- else
- {
- clickedBlockType = Material.AIR;
- }
-
- PlayerData playerData = null;
-
- //Turtle eggs
- if (action == Action.PHYSICAL)
- {
- if (clickedBlockType != Material.TURTLE_EGG)
- return;
- playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Build, event);
- if (noAccessReason != null)
- {
- event.setCancelled(true);
- return;
- }
- }
- return;
- }
-
- //don't care about left-clicking on most blocks, this is probably a break action
- if (action == Action.LEFT_CLICK_BLOCK && clickedBlock != null)
- {
- if (clickedBlock.getY() < clickedBlock.getWorld().getMaxHeight() - 1 || event.getBlockFace() != BlockFace.UP)
- {
- Block adjacentBlock = clickedBlock.getRelative(event.getBlockFace());
- byte lightLevel = adjacentBlock.getLightFromBlocks();
- if (lightLevel == 15 && adjacentBlock.getType() == Material.FIRE)
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, event);
- if (noBuildReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
- player.sendBlockChange(adjacentBlock.getLocation(), adjacentBlock.getType(), adjacentBlock.getData());
- return;
- }
- }
- }
- }
-
- //exception for blocks on a specific watch list
- if (!this.onLeftClickWatchList(clickedBlockType))
- {
- return;
- }
- }
-
- //apply rules for containers and crafting blocks
- if (clickedBlock != null && instance.config_claims_preventTheft && (
- event.getAction() == Action.RIGHT_CLICK_BLOCK && (
- (this.isInventoryHolder(clickedBlock) && clickedBlock.getType() != Material.LECTERN) ||
- clickedBlockType == Material.ANVIL ||
- clickedBlockType == Material.BEACON ||
- clickedBlockType == Material.BEE_NEST ||
- clickedBlockType == Material.BEEHIVE ||
- clickedBlockType == Material.BELL ||
- clickedBlockType == Material.CAKE ||
- clickedBlockType == Material.CARTOGRAPHY_TABLE ||
- clickedBlockType == Material.CAULDRON ||
- clickedBlockType == Material.CAVE_VINES ||
- clickedBlockType == Material.CAVE_VINES_PLANT ||
- clickedBlockType == Material.CHIPPED_ANVIL ||
- clickedBlockType == Material.DAMAGED_ANVIL ||
- clickedBlockType == Material.GRINDSTONE ||
- clickedBlockType == Material.JUKEBOX ||
- clickedBlockType == Material.LOOM ||
- clickedBlockType == Material.PUMPKIN ||
- clickedBlockType == Material.RESPAWN_ANCHOR ||
- clickedBlockType == Material.ROOTED_DIRT ||
- clickedBlockType == Material.STONECUTTER ||
- clickedBlockType == Material.SWEET_BERRY_BUSH ||
- clickedBlockType == Material.GLOW_BERRIES ||
- Tag.CANDLES.isTagged(clickedBlockType) ||
- Tag.CANDLE_CAKES.isTagged(clickedBlockType)
- )))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
-
- //block container use during pvp combat, same reason
- if (playerData.inPvpCombat())
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers);
- event.setCancelled(true);
- return;
- }
-
- // there's no need to check for claims as this is a claiming bypass?
- Block block = event.getClickedBlock();
- // TODO : doesn't cover cartography tables add these...
- /* This should cover all chests, enderchests, barrels, furnacetypes, lectern, ....*/
- if(block.getState() instanceof InventoryHolder) {
- if (block.getState() instanceof Nameable) {
- Nameable nameable = (Nameable) block.getState();
- if (nameable.getCustomName() != null) {
- if (ChatColor.stripColor(nameable.getCustomName()).equalsIgnoreCase("public")) {
- return;
- }
- }
- }
- }
-
- //otherwise check permissions for the claim the player is in
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (noContainersReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
- return;
- }
- }
-
- //if the event hasn't been cancelled, then the player is allowed to use the container
- //so drop any pvp protection
- if (playerData.pvpImmune)
- {
- playerData.pvpImmune = false;
- GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
- }
- }
-
- //otherwise apply rules for doors and beds, if configured that way
- else if (clickedBlock != null &&
-
- (instance.config_claims_lockWoodenDoors && Tag.WOODEN_DOORS.isTagged(clickedBlockType) ||
-
- instance.config_claims_preventButtonsSwitches && Tag.BEDS.isTagged(clickedBlockType) ||
-
- instance.config_claims_lockTrapDoors && Tag.WOODEN_TRAPDOORS.isTagged(clickedBlockType) ||
-
- instance.config_claims_lecternReadingRequiresAccessTrust && clickedBlockType == Material.LECTERN ||
-
- instance.config_claims_lockFenceGates && Tag.FENCE_GATES.isTagged(clickedBlockType)))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Access, event);
- if (noAccessReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
- return;
- }
- }
- }
-
- //otherwise apply rules for buttons and switches
- else if (clickedBlock != null && instance.config_claims_preventButtonsSwitches && (Tag.BUTTONS.isTagged(clickedBlockType) || clickedBlockType == Material.LEVER))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Access, event);
- if (noAccessReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
- return;
- }
- }
- }
-
- //otherwise apply rule for cake
- else if (clickedBlock != null && instance.config_claims_preventTheft && (clickedBlockType == Material.CAKE || Tag.CANDLE_CAKES.isTagged(clickedBlockType)))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
-
- Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Access, event);
- if (noContainerReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get());
- return;
- }
- }
- }
-
- //apply rule for note blocks and repeaters and daylight sensors //RoboMWM: Include flower pots
- else if (clickedBlock != null &&
- (
- clickedBlockType == Material.NOTE_BLOCK ||
- clickedBlockType == Material.REPEATER ||
- clickedBlockType == Material.DRAGON_EGG ||
- clickedBlockType == Material.DAYLIGHT_DETECTOR ||
- clickedBlockType == Material.COMPARATOR ||
- clickedBlockType == Material.REDSTONE_WIRE ||
- Tag.FLOWER_POTS.isTagged(clickedBlockType)
- ))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, event);
- if (noBuildReason != null)
- {
- event.setCancelled(true);
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
- return;
- }
- }
- }
-
- //otherwise handle right click (shovel, string, bonemeal) //RoboMWM: flint and steel
- else
- {
- //ignore all actions except right-click on a block or in the air
- if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return;
-
- //what's the player holding?
- EquipmentSlot hand = event.getHand();
- ItemStack itemInHand = instance.getItemInHand(player, hand);
- Material materialInHand = itemInHand.getType();
-
- Set spawn_eggs = new HashSet<>();
- Set dyes = new HashSet<>();
-
- for (Material material : Material.values())
- {
- if (material.isLegacy()) continue;
- if (material.name().endsWith("_SPAWN_EGG"))
- spawn_eggs.add(material);
- else if (material.name().endsWith("_DYE"))
- dyes.add(material);
- }
-
- //if it's bonemeal, armor stand, spawn egg, etc - check for build permission //RoboMWM: also check flint and steel to stop TNT ignition
- //add glowing ink sac and ink sac, due to their usage on signs
- if (clickedBlock != null && (materialInHand == Material.BONE_MEAL
- || materialInHand == Material.ARMOR_STAND
- || (spawn_eggs.contains(materialInHand) && GriefPrevention.instance.config_claims_preventGlobalMonsterEggs)
- || materialInHand == Material.END_CRYSTAL
- || materialInHand == Material.FLINT_AND_STEEL
- || materialInHand == Material.INK_SAC
- || materialInHand == Material.GLOW_INK_SAC
- || dyes.contains(materialInHand)))
- {
- String noBuildReason = instance
- .allowBuild(player, clickedBlock
- .getLocation(),
- clickedBlockType);
- if (noBuildReason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
- event.setCancelled(true);
- }
-
- return;
- }
- else if (clickedBlock != null && Tag.ITEMS_BOATS.isTagged(materialInHand))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- Supplier reason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (reason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
- event.setCancelled(true);
- }
- }
-
- return;
- }
-
- //survival world minecart placement requires container trust, which is the permission required to remove the minecart later
- else if (clickedBlock != null &&
- (materialInHand == Material.MINECART ||
- materialInHand == Material.FURNACE_MINECART ||
- materialInHand == Material.CHEST_MINECART ||
- materialInHand == Material.TNT_MINECART ||
- materialInHand == Material.HOPPER_MINECART))
- {
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- Supplier reason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (reason != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
- event.setCancelled(true);
- }
- }
-
- return;
- }
-
- //if he's investigating a claim
- else if (materialInHand == instance.config_claims_investigationTool && hand == EquipmentSlot.HAND)
- {
- //if claims are disabled in this world, do nothing
- if (!instance.claimsEnabledForWorld(player.getWorld())) return;
-
- //if holding shift (sneaking), show all claims in area
- if (player.isSneaking() && player.hasPermission("griefprevention.visualizenearbyclaims"))
- {
- //find nearby claims
- Set claims = this.dataStore.getNearbyClaims(player.getLocation());
-
- // alert plugins of a claim inspection, return if cancelled
- ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, null, claims, true);
- Bukkit.getPluginManager().callEvent(inspectionEvent);
- if (inspectionEvent.isCancelled()) return;
-
- //visualize boundaries
- Visualization visualization = Visualization.fromClaims(claims, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claims, true));
-
- Visualization.Apply(player, visualization);
-
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShowNearbyClaims, String.valueOf(claims.size()));
-
- return;
- }
-
- //FEATURE: shovel and stick can be used from a distance away
- if (action == Action.RIGHT_CLICK_AIR)
- {
- //try to find a far away non-air block along line of sight
- clickedBlock = getTargetBlock(player, 100);
- clickedBlockType = clickedBlock.getType();
- }
-
- //if no block, stop here
- if (clickedBlock == null)
- {
- return;
- }
-
- //air indicates too far away
- if (clickedBlockType == Material.AIR)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway);
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, null, Collections.emptySet()));
-
- Visualization.Revert(player);
- return;
- }
-
- if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, playerData.lastClaim);
-
- //no claim case
- if (claim == null)
- {
- // alert plugins of a claim inspection, return if cancelled
- ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, clickedBlock, null);
- Bukkit.getPluginManager().callEvent(inspectionEvent);
- if (inspectionEvent.isCancelled()) return;
-
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockNotClaimed);
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, null, Collections.emptySet()));
-
- Visualization.Revert(player);
- }
-
- //claim case
- else
- {
- // alert plugins of a claim inspection, return if cancelled
- ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, clickedBlock, claim);
- Bukkit.getPluginManager().callEvent(inspectionEvent);
- if (inspectionEvent.isCancelled()) return;
-
- playerData.lastClaim = claim;
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockClaimed, claim.getOwnerName());
-
- //visualize boundary
- Visualization visualization = Visualization.FromClaim(claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
-
- Visualization.Apply(player, visualization);
-
- if (player.hasPermission("griefprevention.seeclaimsize"))
- {
- GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea());
- }
-
- //if permission, tell about the player's offline time
- if (!claim.isAdminClaim() && (player.hasPermission("griefprevention.deleteclaims") || player.hasPermission("griefprevention.seeinactivity")))
- {
- if (claim.parent != null)
- {
- claim = claim.parent;
- }
- Date lastLogin = new Date(Bukkit.getOfflinePlayer(claim.ownerID).getLastPlayed());
- Date now = new Date();
- long daysElapsed = (now.getTime() - lastLogin.getTime()) / (1000 * 60 * 60 * 24);
-
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.PlayerOfflineTime, String.valueOf(daysElapsed));
-
- //drop the data we just loaded, if the player isn't online
- if (instance.getServer().getPlayer(claim.ownerID) == null)
- this.dataStore.clearCachedPlayerData(claim.ownerID);
- }
- }
-
- return;
- }
-
- //if it's a golden shovel
- else if (materialInHand != instance.config_claims_modificationTool || hand != EquipmentSlot.HAND) return;
-
- event.setCancelled(true); //GriefPrevention exclusively reserves this tool (e.g. no grass path creation for golden shovel)
-
- //FEATURE: shovel and stick can be used from a distance away
- if (action == Action.RIGHT_CLICK_AIR)
- {
- //try to find a far away non-air block along line of sight
- clickedBlock = getTargetBlock(player, 100);
- clickedBlockType = clickedBlock.getType();
- }
-
- //if no block, stop here
- if (clickedBlock == null)
- {
- return;
- }
-
- //can't use the shovel from too far away
- if (clickedBlockType == Material.AIR)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway);
- return;
- }
-
- //if the player is in restore nature mode, do only that
- UUID playerID = player.getUniqueId();
- playerData = this.dataStore.getPlayerData(player.getUniqueId());
- if (playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive)
- {
- //if the clicked block is in a claim, visualize that claim and deliver an error message
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName());
- Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
-
- Visualization.Apply(player, visualization);
-
- return;
- }
-
- //figure out which chunk to repair
- Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation());
- //start the repair process
-
- //set boundaries for processing
- int miny = clickedBlock.getY();
-
- //if not in aggressive mode, extend the selection down to a little below sea level
- if (!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive))
- {
- if (miny > instance.getSeaLevel(chunk.getWorld()) - 10)
- {
- miny = instance.getSeaLevel(chunk.getWorld()) - 10;
- }
- }
-
- instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player);
-
- return;
- }
-
- //if in restore nature fill mode
- if (playerData.shovelMode == ShovelMode.RestoreNatureFill)
- {
- ArrayList allowedFillBlocks = new ArrayList<>();
- Environment environment = clickedBlock.getWorld().getEnvironment();
- if (environment == Environment.NETHER)
- {
- allowedFillBlocks.add(Material.NETHERRACK);
- }
- else if (environment == Environment.THE_END)
- {
- allowedFillBlocks.add(Material.END_STONE);
- }
- else
- {
- allowedFillBlocks.add(Material.GRASS);
- allowedFillBlocks.add(Material.DIRT);
- allowedFillBlocks.add(Material.STONE);
- allowedFillBlocks.add(Material.SAND);
- allowedFillBlocks.add(Material.SANDSTONE);
- allowedFillBlocks.add(Material.ICE);
- }
-
- Block centerBlock = clickedBlock;
-
- int maxHeight = centerBlock.getY();
- int minx = centerBlock.getX() - playerData.fillRadius;
- int maxx = centerBlock.getX() + playerData.fillRadius;
- int minz = centerBlock.getZ() - playerData.fillRadius;
- int maxz = centerBlock.getZ() + playerData.fillRadius;
- int minHeight = maxHeight - 10;
- minHeight = Math.max(minHeight, clickedBlock.getWorld().getMinHeight());
-
- Claim cachedClaim = null;
- for (int x = minx; x <= maxx; x++)
- {
- for (int z = minz; z <= maxz; z++)
- {
- //circular brush
- Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z);
- if (location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue;
-
- //default fill block is initially the first from the allowed fill blocks list above
- Material defaultFiller = allowedFillBlocks.get(0);
-
- //prefer to use the block the player clicked on, if it's an acceptable fill block
- if (allowedFillBlocks.contains(centerBlock.getType()))
- {
- defaultFiller = centerBlock.getType();
- }
-
- //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler
- else if (centerBlock.getType() == Material.WATER)
- {
- Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation());
- while (!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10)
- {
- block = block.getRelative(BlockFace.DOWN);
- }
- if (allowedFillBlocks.contains(block.getType()))
- {
- defaultFiller = block.getType();
- }
- }
-
- //fill bottom to top
- for (int y = minHeight; y <= maxHeight; y++)
- {
- Block block = centerBlock.getWorld().getBlockAt(x, y, z);
-
- //respect claims
- Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim);
- if (claim != null)
- {
- cachedClaim = claim;
- break;
- }
-
- //only replace air, spilling water, snow, long grass
- if (block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.WATER && ((Levelled) block.getBlockData()).getLevel() != 0) || block.getType() == Material.GRASS)
- {
- //if the top level, always use the default filler picked above
- if (y == maxHeight)
- {
- block.setType(defaultFiller);
- }
-
- //otherwise look to neighbors for an appropriate fill block
- else
- {
- Block eastBlock = block.getRelative(BlockFace.EAST);
- Block westBlock = block.getRelative(BlockFace.WEST);
- Block northBlock = block.getRelative(BlockFace.NORTH);
- Block southBlock = block.getRelative(BlockFace.SOUTH);
-
- //first, check lateral neighbors (ideally, want to keep natural layers)
- if (allowedFillBlocks.contains(eastBlock.getType()))
- {
- block.setType(eastBlock.getType());
- }
- else if (allowedFillBlocks.contains(westBlock.getType()))
- {
- block.setType(westBlock.getType());
- }
- else if (allowedFillBlocks.contains(northBlock.getType()))
- {
- block.setType(northBlock.getType());
- }
- else if (allowedFillBlocks.contains(southBlock.getType()))
- {
- block.setType(southBlock.getType());
- }
-
- //if all else fails, use the default filler selected above
- else
- {
- block.setType(defaultFiller);
- }
- }
- }
- }
- }
- }
-
- return;
- }
-
- //if the player doesn't have claims permission, don't do anything
- if (!player.hasPermission("griefprevention.createclaims"))
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission);
- return;
- }
-
- //if he's resizing a claim and that claim hasn't been deleted since he started resizing it
- if (playerData.claimResizing != null && playerData.claimResizing.inDataStore)
- {
- if (clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return;
-
- //figure out what the coords of his new claim would be
- int newx1, newx2, newz1, newz2, newy1, newy2;
- if (playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX())
- {
- newx1 = clickedBlock.getX();
- newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX();
- }
- else
- {
- newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX();
- newx2 = clickedBlock.getX();
- }
-
- if (playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ())
- {
- newz1 = clickedBlock.getZ();
- newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ();
- }
- else
- {
- newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ();
- newz2 = clickedBlock.getZ();
- }
-
- newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY();
- newy2 = clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance;
-
- this.dataStore.resizeClaimWithChecks(player, playerData, newx1, newx2, newy1, newy2, newz1, newz2);
-
- return;
- }
-
- //otherwise, since not currently resizing a claim, must be starting a resize, creating a new claim, or creating a subdivision
- Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), true /*ignore height*/, playerData.lastClaim);
-
- //if within an existing claim, he's not creating a new one
- if (claim != null)
- {
- //if the player has permission to edit the claim or subdivision
- Supplier noEditReason = claim.checkPermission(player, ClaimPermission.Edit, event, () -> instance.dataStore.getMessage(Messages.CreateClaimFailOverlapOtherPlayer, claim.getOwnerName()));
- if (noEditReason == null)
- {
- //if he clicked on a corner, start resizing it
- if ((clickedBlock.getX() == claim.getLesserBoundaryCorner().getBlockX() || clickedBlock.getX() == claim.getGreaterBoundaryCorner().getBlockX()) && (clickedBlock.getZ() == claim.getLesserBoundaryCorner().getBlockZ() || clickedBlock.getZ() == claim.getGreaterBoundaryCorner().getBlockZ()))
- {
- playerData.claimResizing = claim;
- playerData.lastShovelLocation = clickedBlock.getLocation();
- GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ResizeStart);
- }
-
- //if he didn't click on a corner and is in subdivision mode, he's creating a new subdivision
- else if (playerData.shovelMode == ShovelMode.Subdivide)
- {
- //if it's the first click, he's trying to start a new subdivision
- if (playerData.lastShovelLocation == null)
- {
- //if the clicked claim was a subdivision, tell him he can't start a new subdivision here
- if (claim.parent != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapSubdivision);
- }
-
- //otherwise start a new subdivision
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionStart);
- playerData.lastShovelLocation = clickedBlock.getLocation();
- playerData.claimSubdividing = claim;
- }
- }
-
- //otherwise, he's trying to finish creating a subdivision by setting the other boundary corner
- else
- {
- //if last shovel location was in a different world, assume the player is starting the create-claim workflow over
- if (!playerData.lastShovelLocation.getWorld().equals(clickedBlock.getWorld()))
- {
- playerData.lastShovelLocation = null;
- this.onPlayerInteract(event);
- return;
- }
-
- //try to create a new claim (will return null if this subdivision overlaps another)
- CreateClaimResult result = this.dataStore.createClaim(
- player.getWorld(),
- playerData.lastShovelLocation.getBlockX(), clickedBlock.getX(),
- playerData.lastShovelLocation.getBlockY() - instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance,
- playerData.lastShovelLocation.getBlockZ(), clickedBlock.getZ(),
- null, //owner is not used for subdivisions
- playerData.claimSubdividing,
- null, player);
-
- //if it didn't succeed, tell the player why
- if (!result.succeeded)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap);
-
- Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
-
- Visualization.Apply(player, visualization);
-
- return;
- }
-
- //otherwise, advise him on the /trust command and show him his new subdivision
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess);
- Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
-
- Visualization.Apply(player, visualization);
- playerData.lastShovelLocation = null;
- playerData.claimSubdividing = null;
- }
- }
- }
-
- //otherwise tell him he can't create a claim here, and show him the existing claim
- //also advise him to consider /abandonclaim or resizing the existing claim
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlap);
- Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
-
- Visualization.Apply(player, visualization);
- }
- }
-
- //otherwise tell the player he can't claim here because it's someone else's claim, and show him the claim
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Err, noEditReason.get());
- Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
-
- Visualization.Apply(player, visualization);
- }
-
- return;
- }
-
- //otherwise, the player isn't in an existing claim!
-
- //if he hasn't already start a claim with a previous shovel action
- Location lastShovelLocation = playerData.lastShovelLocation;
- if (lastShovelLocation == null)
- {
- //if claims are not enabled in this world and it's not an administrative claim, display an error message and stop
- if (!instance.claimsEnabledForWorld(player.getWorld()))
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld);
- return;
- }
-
- //if he's at the claim count per player limit already and doesn't have permission to bypass, display an error message
- if (instance.config_claims_maxClaimsPerPlayer > 0 &&
- !player.hasPermission("griefprevention.overrideclaimcountlimit") &&
- playerData.getClaims().size() >= instance.config_claims_maxClaimsPerPlayer)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimCreationFailedOverClaimCountLimit);
- return;
- }
-
- //remember it, and start him on the new claim
- playerData.lastShovelLocation = clickedBlock.getLocation();
- GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimStart);
-
- //show him where he's working
- Claim newClaim = new Claim(clickedBlock.getLocation(), clickedBlock.getLocation(), null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null);
- Visualization visualization = Visualization.FromClaim(newClaim, clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, newClaim));
-
- Visualization.Apply(player, visualization);
- }
-
- //otherwise, he's trying to finish creating a claim by setting the other boundary corner
- else
- {
- //if last shovel location was in a different world, assume the player is starting the create-claim workflow over
- if (!lastShovelLocation.getWorld().equals(clickedBlock.getWorld()))
- {
- playerData.lastShovelLocation = null;
- this.onPlayerInteract(event);
- return;
- }
-
- //apply pvp rule
- if (playerData.inPvpCombat())
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoClaimDuringPvP);
- return;
- }
-
- //apply minimum claim dimensions rule
- int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1;
- int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1;
-
- if (playerData.shovelMode != ShovelMode.Admin)
- {
- if (newClaimWidth < instance.config_claims_minWidth || newClaimHeight < instance.config_claims_minWidth)
- {
- //this IF block is a workaround for craftbukkit bug which fires two events for one interaction
- if (newClaimWidth != 1 && newClaimHeight != 1)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooNarrow, String.valueOf(instance.config_claims_minWidth));
- }
- return;
- }
-
- int newArea = newClaimWidth * newClaimHeight;
- if (newArea < instance.config_claims_minArea)
- {
- if (newArea != 1)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimInsufficientArea, String.valueOf(instance.config_claims_minArea));
- }
-
- return;
- }
- }
-
- //if not an administrative claim, verify the player has enough claim blocks for this new claim
- if (playerData.shovelMode != ShovelMode.Admin)
- {
- int newClaimArea = newClaimWidth * newClaimHeight;
- int remainingBlocks = playerData.getRemainingClaimBlocks();
- if (newClaimArea > remainingBlocks)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks));
- instance.dataStore.tryAdvertiseAdminAlternatives(player);
- return;
- }
- }
- else
- {
- playerID = null;
- }
-
- //try to create a new claim
- CreateClaimResult result = this.dataStore.createClaim(
- player.getWorld(),
- lastShovelLocation.getBlockX(), clickedBlock.getX(),
- lastShovelLocation.getBlockY() - instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance,
- lastShovelLocation.getBlockZ(), clickedBlock.getZ(),
- playerID,
- null, null,
- player);
-
- //if it didn't succeed, tell the player why
- if (!result.succeeded)
- {
- if (result.claim != null)
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort);
-
- Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
-
- Visualization.Apply(player, visualization);
- }
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion);
- }
-
- return;
- }
-
- //otherwise, advise him on the /trust command and show him his new claim
- else
- {
- GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess);
- Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
-
- // alert plugins of a visualization
- Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
-
- Visualization.Apply(player, visualization);
- playerData.lastShovelLocation = null;
-
- //if it's a big claim, tell the player about subdivisions
- if (!player.hasPermission("griefprevention.adminclaims") && result.claim.getArea() >= 1000)
- {
- GriefPrevention.sendMessage(player, TextMode.Info, Messages.BecomeMayor, 200L);
- }
-
- instance.autoExtendClaim(result.claim);
- }
- }
- }
- }
-
- // Stops an untrusted player from removing a book from a lectern
- @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
- void onTakeBook(PlayerTakeLecternBookEvent event)
- {
- Player player = event.getPlayer();
- PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
- Claim claim = this.dataStore.getClaimAt(event.getLectern().getLocation(), false, playerData.lastClaim);
- if (claim != null)
- {
- playerData.lastClaim = claim;
- Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
- if (noContainerReason != null)
- {
- event.setCancelled(true);
- player.closeInventory();
- GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get());
- }
- }
- }
-
- //determines whether a block type is an inventory holder. uses a caching strategy to save cpu time
- private final ConcurrentHashMap inventoryHolderCache = new ConcurrentHashMap<>();
-
- private boolean isInventoryHolder(Block clickedBlock)
- {
-
- Material cacheKey = clickedBlock.getType();
- Boolean cachedValue = this.inventoryHolderCache.get(cacheKey);
- if (cachedValue != null)
- {
- return cachedValue.booleanValue();
-
- }
- else
- {
- boolean isHolder = clickedBlock.getState() instanceof InventoryHolder;
- this.inventoryHolderCache.put(cacheKey, isHolder);
- return isHolder;
- }
- }
-
- private boolean onLeftClickWatchList(Material material)
- {
- switch (material)
- {
- case OAK_BUTTON:
- case SPRUCE_BUTTON:
- case BIRCH_BUTTON:
- case JUNGLE_BUTTON:
- case ACACIA_BUTTON:
- case DARK_OAK_BUTTON:
- case STONE_BUTTON:
- case LEVER:
- case REPEATER:
- case CAKE:
- case DRAGON_EGG:
- return true;
- default:
- return false;
- }
- }
-
- static Block getTargetBlock(Player player, int maxDistance) throws IllegalStateException
- {
- Location eye = player.getEyeLocation();
- Material eyeMaterial = eye.getBlock().getType();
- boolean passThroughWater = (eyeMaterial == Material.WATER);
- BlockIterator iterator = new BlockIterator(player.getLocation(), player.getEyeHeight(), maxDistance);
- Block result = player.getLocation().getBlock().getRelative(BlockFace.UP);
- while (iterator.hasNext())
- {
- result = iterator.next();
- Material type = result.getType();
- if (type != Material.AIR &&
- (!passThroughWater || type != Material.WATER) &&
- type != Material.GRASS &&
- type != Material.SNOW) return result;
- }
-
- return result;
- }
-
- private boolean isVanished(Player player) {
- for (MetadataValue meta : player.getMetadata("vanished")) {
- if (meta.asBoolean()) return true;
- }
- return false;
- }
-}
+/*
+ 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 me.ryanhamshire.GriefPrevention.events.ClaimInspectionEvent;
+import me.ryanhamshire.GriefPrevention.events.VisualizationEvent;
+import org.bukkit.BanList;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Chunk;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.Nameable;
+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.data.BlockData;
+import org.bukkit.block.data.Levelled;
+import org.bukkit.block.data.Waterlogged;
+import org.bukkit.command.Command;
+import org.bukkit.entity.AbstractHorse;
+import org.bukkit.entity.Animals;
+import org.bukkit.entity.Creature;
+import org.bukkit.entity.Donkey;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Fish;
+import org.bukkit.entity.Hanging;
+import org.bukkit.entity.Item;
+import org.bukkit.entity.Llama;
+import org.bukkit.entity.Mule;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Sittable;
+import org.bukkit.entity.Tameable;
+import org.bukkit.entity.Vehicle;
+import org.bukkit.entity.Villager;
+import org.bukkit.entity.minecart.PoweredMinecart;
+import org.bukkit.entity.minecart.StorageMinecart;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.block.CauldronLevelChangeEvent;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.entity.PlayerDeathEvent;
+import org.bukkit.event.player.PlayerBucketEmptyEvent;
+import org.bukkit.event.player.PlayerBucketFillEvent;
+import org.bukkit.event.player.PlayerCommandPreprocessEvent;
+import org.bukkit.event.player.PlayerDropItemEvent;
+import org.bukkit.event.player.PlayerEggThrowEvent;
+import org.bukkit.event.player.PlayerFishEvent;
+import org.bukkit.event.player.PlayerInteractAtEntityEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerItemHeldEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerKickEvent;
+import org.bukkit.event.player.PlayerPickupItemEvent;
+import org.bukkit.event.player.PlayerPortalEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.event.player.PlayerTakeLecternBookEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
+import org.bukkit.event.raid.RaidTriggerEvent;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.metadata.MetadataValue;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.util.BlockIterator;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+class PlayerEventHandler implements Listener
+{
+ private final DataStore dataStore;
+ private final GriefPrevention instance;
+
+ //number of milliseconds in a day
+ private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;
+
+ //timestamps of login and logout notifications in the last minute
+ private final ArrayList recentLoginLogoutNotifications = new ArrayList<>();
+
+ //regex pattern for the "how do i claim land?" scanner
+ private Pattern howToClaimPattern = null;
+
+ //matcher for banned words
+ private final WordFinder bannedWordFinder;
+
+ //typical constructor, yawn
+ PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
+ {
+ this.dataStore = dataStore;
+ this.instance = plugin;
+ bannedWordFinder = new WordFinder(instance.dataStore.loadBannedWords());
+ }
+
+ //when a player uses a slash command...
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ synchronized void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event)
+ {
+ String message = event.getMessage();
+ String[] args = message.split(" ");
+ String command = args[0];
+
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId());
+
+ //if the slash command used is in the list of monitored commands, treat it like a chat message (see above)
+ boolean isMonitoredCommand = false;
+ for (String monitoredCommand : instance.config_claims_commandsRequiringAccessTrust)
+ {
+ if (command.equalsIgnoreCase(monitoredCommand))
+ {
+ isMonitoredCommand = true;
+ break;
+ }
+ }
+
+ if (isMonitoredCommand)
+ {
+ Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+ Supplier reason = claim.checkPermission(player, ClaimPermission.Access, event);
+ if (reason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
+ event.setCancelled(true);
+ }
+ }
+ }
+ }
+
+ static int longestNameLength = 10;
+
+ static void makeSocialLogEntry(String name, String message)
+ {
+ StringBuilder entryBuilder = new StringBuilder(name);
+ for (int i = name.length(); i < longestNameLength; i++)
+ {
+ entryBuilder.append(' ');
+ }
+ entryBuilder.append(": ").append(message);
+
+ longestNameLength = Math.max(longestNameLength, name.length());
+ //TODO: cleanup static
+ GriefPrevention.AddLogEntry(entryBuilder.toString(), CustomLogEntryTypes.SocialActivity, true);
+ }
+
+ private final ConcurrentHashMap lastLoginThisServerSessionMap = new ConcurrentHashMap<>();
+
+ //when a player successfully joins the server...
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
+ void onPlayerJoin(PlayerJoinEvent event)
+ {
+ Player player = event.getPlayer();
+ UUID playerID = player.getUniqueId();
+
+ //note login time
+ Date nowDate = new Date();
+ long now = nowDate.getTime();
+ PlayerData playerData = this.dataStore.getPlayerData(playerID);
+ playerData.lastSpawn = now;
+ this.lastLoginThisServerSessionMap.put(playerID, nowDate);
+
+ //if newish, prevent chat until he's moved a bit to prove he's not a bot
+ if (GriefPrevention.isNewToServer(player))
+ {
+ playerData.noChatLocation = player.getLocation();
+ }
+
+ //silence notifications when they're coming too fast
+ if (event.getJoinMessage() != null && this.shouldSilenceNotification())
+ {
+ event.setJoinMessage(null);
+ }
+
+ //in case player has changed his name, on successful login, update UUID > Name mapping
+ GriefPrevention.cacheUUIDNamePair(player.getUniqueId(), player.getName());
+
+ //ensure we're not over the limit for this IP address
+ InetAddress ipAddress = playerData.ipAddress;
+ if (ipAddress != null)
+ {
+ int ipLimit = instance.config_ipLimit;
+ if (ipLimit > 0 && GriefPrevention.isNewToServer(player))
+ {
+ int ipCount = 0;
+
+ @SuppressWarnings("unchecked")
+ Collection players = (Collection) instance.getServer().getOnlinePlayers();
+ for (Player onlinePlayer : players)
+ {
+ if (onlinePlayer.getUniqueId().equals(player.getUniqueId())) continue;
+
+ PlayerData otherData = instance.dataStore.getPlayerData(onlinePlayer.getUniqueId());
+ if (ipAddress.equals(otherData.ipAddress) && GriefPrevention.isNewToServer(onlinePlayer))
+ {
+ ipCount++;
+ }
+ }
+
+ if (ipCount >= ipLimit)
+ {
+ //kick player
+ PlayerKickBanTask task = new PlayerKickBanTask(player, instance.dataStore.getMessage(Messages.TooMuchIpOverlap), "GriefPrevention IP-sharing limit.", false);
+ instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 100L);
+
+ //silence join message
+ event.setJoinMessage(null);
+ return;
+ }
+ }
+ }
+
+ //is he stuck in a portal frame?
+ if (player.hasMetadata("GP_PORTALRESCUE"))
+ {
+ //If so, let him know and rescue him in 10 seconds. If he is in fact not trapped, hopefully chunks will have loaded by this time so he can walk out.
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.NetherPortalTrapDetectionMessage, 20L);
+ new BukkitRunnable()
+ {
+ @Override
+ public void run()
+ {
+ if (player.getPortalCooldown() > 8 && player.hasMetadata("GP_PORTALRESCUE"))
+ {
+ GriefPrevention.AddLogEntry("Rescued " + player.getName() + " from a nether portal.\nTeleported from " + player.getLocation().toString() + " to " + ((Location) player.getMetadata("GP_PORTALRESCUE").get(0).value()).toString(), CustomLogEntryTypes.Debug);
+ player.teleport((Location) player.getMetadata("GP_PORTALRESCUE").get(0).value());
+ player.removeMetadata("GP_PORTALRESCUE", instance);
+ }
+ }
+ }.runTaskLater(instance, 200L);
+ }
+ //Otherwise just reset cooldown, just in case they happened to logout again...
+ else
+ player.setPortalCooldown(0);
+
+
+ //if we're holding a logout message for this player, don't send that or this event's join message
+ if (instance.config_spam_logoutMessageDelaySeconds > 0)
+ {
+ String joinMessage = event.getJoinMessage();
+ if (joinMessage != null && !joinMessage.isEmpty())
+ {
+ Integer taskID = this.heldLogoutMessages.get(player.getUniqueId());
+ if (taskID != null && Bukkit.getScheduler().isQueued(taskID))
+ {
+ Bukkit.getScheduler().cancelTask(taskID);
+ player.sendMessage(event.getJoinMessage());
+ event.setJoinMessage("");
+ }
+ }
+ }
+ }
+
+ //when a player spawns, conditionally apply temporary pvp protection
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
+ void onPlayerRespawn(PlayerRespawnEvent event)
+ {
+ Player player = event.getPlayer();
+ PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId());
+ playerData.lastSpawn = Calendar.getInstance().getTimeInMillis();
+ playerData.lastPvpTimestamp = 0; //no longer in pvp combat
+
+ //also send him any messaged from grief prevention he would have received while dead
+ if (playerData.messageOnRespawn != null)
+ {
+ GriefPrevention.sendMessage(player, ChatColor.RESET /*color is alrady embedded in message in this case*/, playerData.messageOnRespawn, 40L);
+ playerData.messageOnRespawn = null;
+ }
+
+ instance.checkPvpProtectionNeeded(player);
+ }
+
+ //when a player dies...
+ private final HashMap deathTimestamps = new HashMap<>();
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ void onPlayerDeath(PlayerDeathEvent event)
+ {
+ //FEATURE: prevent death message spam by implementing a "cooldown period" for death messages
+ Player player = event.getEntity();
+ Long lastDeathTime = this.deathTimestamps.get(player.getUniqueId());
+ long now = Calendar.getInstance().getTimeInMillis();
+ if (lastDeathTime != null && now - lastDeathTime < instance.config_spam_deathMessageCooldownSeconds * 1000)
+ {
+ player.sendMessage(event.getDeathMessage()); //let the player assume his death message was broadcasted to everyone
+ event.setDeathMessage("");
+ }
+
+ this.deathTimestamps.put(player.getUniqueId(), now);
+ }
+
+ //when a player gets kicked...
+ @EventHandler(priority = EventPriority.HIGHEST)
+ void onPlayerKicked(PlayerKickEvent event)
+ {
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ playerData.wasKicked = true;
+
+ UUID playerID = player.getUniqueId();
+ if(instance.ignoreClaimWarningTasks.containsKey(playerID))
+ {
+ instance.ignoreClaimWarningTasks.get(playerID).cancel();
+ instance.ignoreClaimWarningTasks.remove(playerID);
+ }
+ }
+
+ //when a player quits...
+ private final HashMap heldLogoutMessages = new HashMap<>();
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ void onPlayerQuit(PlayerQuitEvent event)
+ {
+ Player player = event.getPlayer();
+ UUID playerID = player.getUniqueId();
+ PlayerData playerData = this.dataStore.getPlayerData(playerID);
+
+ //If player is not trapped in a portal and has a pending rescue task, remove the associated metadata
+ //Why 9? No idea why, but this is decremented by 1 when the player disconnects.
+ if (player.getPortalCooldown() < 9)
+ {
+ player.removeMetadata("GP_PORTALRESCUE", instance);
+ }
+
+ //silence notifications when they're coming too fast
+ if (event.getQuitMessage() != null && this.shouldSilenceNotification())
+ {
+ event.setQuitMessage(null);
+ }
+
+ //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately
+ else
+ {
+ this.dataStore.savePlayerData(player.getUniqueId(), playerData);
+ }
+
+ //FEATURE: players in pvp combat when they log out will die
+ if (instance.config_pvp_punishLogout && playerData.inPvpCombat())
+ {
+ player.setHealth(0);
+ }
+
+ //drop data about this player
+ this.dataStore.clearCachedPlayerData(playerID);
+
+ //send quit message later, but only if the player stays offline
+ if (instance.config_spam_logoutMessageDelaySeconds > 0)
+ {
+ String quitMessage = event.getQuitMessage();
+ if (quitMessage != null && !quitMessage.isEmpty())
+ {
+ BroadcastMessageTask task = new BroadcastMessageTask(quitMessage);
+ int taskID = Bukkit.getScheduler().scheduleSyncDelayedTask(instance, task, 20L * instance.config_spam_logoutMessageDelaySeconds);
+ this.heldLogoutMessages.put(playerID, taskID);
+ event.setQuitMessage("");
+ }
+ }
+ if(instance.ignoreClaimWarningTasks.containsKey(playerID))
+ {
+ instance.ignoreClaimWarningTasks.get(playerID).cancel();
+ instance.ignoreClaimWarningTasks.remove(playerID);
+ }
+ }
+
+ //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute
+ private boolean shouldSilenceNotification()
+ {
+ if (instance.config_spam_loginLogoutNotificationsPerMinute <= 0)
+ {
+ return false; // not silencing login/logout notifications
+ }
+
+ final long ONE_MINUTE = 60000;
+ Long now = Calendar.getInstance().getTimeInMillis();
+
+ //eliminate any expired entries (longer than a minute ago)
+ for (int i = 0; i < this.recentLoginLogoutNotifications.size(); i++)
+ {
+ Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i);
+ if (now - notificationTimestamp > ONE_MINUTE)
+ {
+ this.recentLoginLogoutNotifications.remove(i--);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ //add the new entry
+ this.recentLoginLogoutNotifications.add(now);
+
+ return this.recentLoginLogoutNotifications.size() > instance.config_spam_loginLogoutNotificationsPerMinute;
+ }
+
+ //when a player drops an item
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPlayerDropItem(PlayerDropItemEvent event)
+ {
+ Player player = event.getPlayer();
+
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+
+ //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide
+ //them or give them away to other players before they are defeated
+
+ //if in combat, don't let him drop it
+ if (!instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat())
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop);
+ event.setCancelled(true);
+ }
+ }
+
+ //when a player teleports via a portal
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
+ void onPlayerPortal(PlayerPortalEvent event)
+ {
+ //if the player isn't going anywhere, take no action
+ if (event.getTo() == null || event.getTo().getWorld() == null) return;
+
+ Player player = event.getPlayer();
+ if (event.getCause() == TeleportCause.NETHER_PORTAL)
+ {
+ //FEATURE: when players get trapped in a nether portal, send them back through to the other side
+ instance.startRescueTask(player, player.getLocation());
+
+ //don't track in worlds where claims are not enabled
+ if (!instance.claimsEnabledForWorld(event.getTo().getWorld())) return;
+ }
+ }
+
+ //when a player teleports
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPlayerTeleport(PlayerTeleportEvent event)
+ {
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+
+ //FEATURE: prevent players from using ender pearls to gain access to secured claims
+ TeleportCause cause = event.getCause();
+ if (cause == TeleportCause.CHORUS_FRUIT || (cause == TeleportCause.ENDER_PEARL && instance.config_claims_enderPearlsRequireAccessTrust))
+ {
+ Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim);
+ if (toClaim != null)
+ {
+ playerData.lastClaim = toClaim;
+ Supplier noAccessReason = toClaim.checkPermission(player, ClaimPermission.Access, event);
+ if (noAccessReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
+ event.setCancelled(true);
+ if (cause == TeleportCause.ENDER_PEARL)
+ player.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
+ }
+ }
+ }
+ }
+
+ //when a player triggers a raid (in a claim)
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPlayerTriggerRaid(RaidTriggerEvent event)
+ {
+// if (!instance.config_claims_raidTriggersRequireBuildTrust)
+// return;
+
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+
+ Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim);
+ if (claim == null)
+ return;
+
+ playerData.lastClaim = claim;
+// if (claim.checkPermission(player, ClaimPermission.Build, event) == null)
+ if (claim.checkPermission(player, ClaimPermission.Access, event) == null)
+ return;
+
+ event.setCancelled(true);
+ }
+
+ //when a player interacts with a specific part of entity...
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event)
+ {
+ //treat it the same as interacting with an entity in general
+ if (event.getRightClicked().getType() == EntityType.ARMOR_STAND)
+ {
+ this.onPlayerInteractEntity((PlayerInteractEntityEvent) event);
+ }
+ Player player = event.getPlayer();
+ Entity entity = event.getRightClicked();
+ if (entity instanceof Sittable sittable)
+ {
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ final Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (noContainersReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
+ event.setCancelled(true);
+ sittable.setSitting(false);
+ }
+ }
+ }
+ }
+
+ //when a player interacts with an entity...
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerInteractEntity(PlayerInteractEntityEvent event)
+ {
+ Player player = event.getPlayer();
+ Entity entity = event.getRightClicked();
+
+ if (!instance.claimsEnabledForWorld(entity.getWorld())) return;
+
+ //allow horse protection to be overridden to allow management from other plugins
+ if (!instance.config_claims_protectHorses && entity instanceof AbstractHorse) return;
+ if (!instance.config_claims_protectDonkeys && entity instanceof Donkey) return;
+ if (!instance.config_claims_protectDonkeys && entity instanceof Mule) return;
+ if (!instance.config_claims_protectLlamas && entity instanceof Llama) return;
+
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+
+ //if entity is tameable and has an owner, apply special rules
+ if (entity instanceof Tameable)
+ {
+ Tameable tameable = (Tameable) entity;
+ if (tameable.isTamed())
+ {
+ if (tameable.getOwner() != null)
+ {
+ UUID ownerID = tameable.getOwner().getUniqueId();
+
+ //if the player interacting is the owner or an admin in ignore claims mode, always allow
+ if (player.getUniqueId().equals(ownerID) || playerData.ignoreClaims)
+ {
+ //if giving away pet, do that instead
+ if (playerData.petGiveawayRecipient != null)
+ {
+ tameable.setOwner(playerData.petGiveawayRecipient);
+ playerData.petGiveawayRecipient = null;
+ GriefPrevention.sendMessage(player, TextMode.Success, Messages.PetGiveawayConfirmation);
+ event.setCancelled(true);
+ }
+
+ return;
+ }
+ if (!instance.pvpRulesApply(entity.getLocation().getWorld()) || instance.config_pvp_protectPets)
+ {
+ //otherwise disallow
+ OfflinePlayer owner = instance.getServer().getOfflinePlayer(ownerID);
+ String ownerName = owner.getName();
+ if (ownerName == null) ownerName = "someone";
+ String message = instance.dataStore.getMessage(Messages.NotYourPet, ownerName);
+ if (player.hasPermission("griefprevention.ignoreclaims"))
+ message += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
+ GriefPrevention.sendMessage(player, TextMode.Err, message);
+ event.setCancelled(true);
+ return;
+ }
+ }
+ }
+ else //world repair code for a now-fixed GP bug //TODO: necessary anymore?
+ {
+ //ensure this entity can be tamed by players
+ tameable.setOwner(null);
+ if (tameable instanceof InventoryHolder)
+ {
+ InventoryHolder holder = (InventoryHolder) tameable;
+ holder.getInventory().clear();
+ }
+ }
+ }
+
+ //don't allow interaction with item frames or armor stands in claimed areas without build permission
+ if (entity.getType() == EntityType.ARMOR_STAND || entity instanceof Hanging)
+ {
+ String noBuildReason = instance.allowBuild(player, entity.getLocation(), Material.ITEM_FRAME);
+ if (noBuildReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ //always allow interactions when player is in ignore claims mode
+ if (playerData.ignoreClaims) return;
+
+ //don't allow container access during pvp combat
+ if ((entity instanceof StorageMinecart || entity instanceof PoweredMinecart))
+ {
+ if (playerData.inPvpCombat())
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers);
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ //if the entity is a vehicle and we're preventing theft in claims
+ if (instance.config_claims_preventTheft && entity instanceof Vehicle)
+ {
+ //if the entity is in a claim
+ Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null);
+ if (claim != null)
+ {
+ //for storage entities, apply container rules (this is a potential theft)
+ if (entity instanceof InventoryHolder)
+ {
+ Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (noContainersReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
+ event.setCancelled(true);
+ return;
+ }
+ }
+ }
+ }
+
+ //if the entity is an animal, apply container rules
+ if ((instance.config_claims_preventTheft && (entity instanceof Animals || entity instanceof Fish)) || (entity.getType() == EntityType.VILLAGER && instance.config_claims_villagerTradingRequiresTrust))
+ {
+ if (entity.getType() == EntityType.VILLAGER) {
+ // early check for custom name to not waste time on getting the claim if not needed?
+ Villager villager = (Villager) entity;
+ if (villager.getCustomName() != null)
+ {
+ if (ChatColor.stripColor(villager.getCustomName()).equalsIgnoreCase("public"))
+ {
+ return;
+ }
+ }
+ }
+ //if the entity is in a claim
+ Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null);
+ if (claim != null)
+ {
+ Supplier override = () ->
+ {
+ String message = instance.dataStore.getMessage(Messages.NoDamageClaimedEntity, claim.getOwnerName());
+ if (player.hasPermission("griefprevention.ignoreclaims"))
+ message += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
+
+ return message;
+ };
+ final Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event, override);
+ if (noContainersReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
+ event.setCancelled(true);
+ return;
+ }
+ }
+ }
+
+ ItemStack itemInHand = instance.getItemInHand(player, event.getHand());
+
+ //if preventing theft, prevent leashing claimed creatures
+ if (instance.config_claims_preventTheft && entity instanceof Creature && itemInHand.getType() == Material.LEAD)
+ {
+ Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ Supplier failureReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (failureReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, failureReason.get());
+ return;
+ }
+ }
+ }
+ // This change breaks mcmmo where it adds hearts as health bar to mobs...
+ // Name tags may only be used on entities that the player is allowed to kill.
+ /*if (itemInHand.getType() == Material.NAME_TAG)
+ {
+ EntityDamageByEntityEvent damageEvent = new EntityDamageByEntityEvent(player, entity, EntityDamageEvent.DamageCause.CUSTOM, 0);
+ instance.entityEventHandler.onEntityDamage(damageEvent);
+ if (damageEvent.isCancelled())
+ {
+ event.setCancelled(true);
+ // Don't print message - damage event handler should have handled it.
+ return;
+ }
+ }*/
+ }
+
+ //when a player throws an egg
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPlayerThrowEgg(PlayerEggThrowEvent event)
+ {
+ if (!event.isHatching()) return;
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(event.getEgg().getLocation(), false, playerData.lastClaim);
+
+ //allow throw egg if player is in ignore claims mode
+ if (playerData.ignoreClaims || claim == null) return;
+
+// Supplier failureReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ Supplier failureReason = claim.checkPermission(player, ClaimPermission.Build, event);
+ if (failureReason != null)
+ {
+ String reason = failureReason.get();
+ if (player.hasPermission("griefprevention.ignoreclaims"))
+ {
+ reason += " " + instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
+ }
+
+ GriefPrevention.sendMessage(player, TextMode.Err, reason);
+
+ //cancel the event by preventing hatching
+ event.setHatching(false);
+
+ //only give the egg back if player is in survival or adventure
+ if (player.getGameMode() == GameMode.SURVIVAL || player.getGameMode() == GameMode.ADVENTURE)
+ {
+ player.getInventory().addItem(event.getEgg().getItem());
+ }
+ }
+ }
+
+ //when a player reels in his fishing rod
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerFish(PlayerFishEvent event)
+ {
+ Entity entity = event.getCaught();
+ if (entity == null) return; //if nothing pulled, uninteresting event
+
+ //if should be protected from pulling in land claims without permission
+ if (entity.getType() == EntityType.ARMOR_STAND || entity instanceof Animals)
+ {
+ Player player = event.getPlayer();
+ PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = instance.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ //if no permission, cancel
+ Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (errorMessage != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName());
+ return;
+ }
+ }
+ }
+ }
+
+ //when a player picks up an item...
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerPickupItem(PlayerPickupItemEvent event)
+ {
+ Player player = event.getPlayer();
+
+ //the rest of this code is specific to pvp worlds
+ if (!instance.pvpRulesApply(player.getWorld())) return;
+
+ //if we're preventing spawn camping and the player was previously empty handed...
+ if (instance.config_pvp_protectFreshSpawns && (instance.getItemInHand(player, EquipmentSlot.HAND).getType() == Material.AIR))
+ {
+ //if that player is currently immune to pvp
+ PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId());
+ if (playerData.pvpImmune)
+ {
+ //if it's been less than 10 seconds since the last time he spawned, don't pick up the item
+ long now = Calendar.getInstance().getTimeInMillis();
+ long elapsedSinceLastSpawn = now - playerData.lastSpawn;
+ if (elapsedSinceLastSpawn < 10000)
+ {
+ event.setCancelled(true);
+ return;
+ }
+
+ //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot
+ playerData.pvpImmune = false;
+ GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
+ }
+ }
+ }
+
+ //when a player switches in-hand items
+ @EventHandler(ignoreCancelled = true)
+ public void onItemHeldChange(PlayerItemHeldEvent event)
+ {
+ Player player = event.getPlayer();
+
+ //if he's switching to the golden shovel
+ int newSlot = event.getNewSlot();
+ ItemStack newItemStack = player.getInventory().getItem(newSlot);
+ if (newItemStack != null && newItemStack.getType() == instance.config_claims_modificationTool)
+ {
+ //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam
+ if (instance.claimsEnabledForWorld(player.getWorld()))
+ {
+ EquipShovelProcessingTask task = new EquipShovelProcessingTask(player);
+ instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 15L); //15L is approx. 3/4 of a second
+ }
+ }
+ }
+
+ //block use of buckets within other players' claims
+ private final Set commonAdjacentBlocks_water = EnumSet.of(Material.WATER, Material.FARMLAND, Material.DIRT, Material.STONE);
+ private final Set commonAdjacentBlocks_lava = EnumSet.of(Material.LAVA, Material.DIRT, Material.STONE);
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerBucketEmpty(PlayerBucketEmptyEvent bucketEvent)
+ {
+ if (!instance.claimsEnabledForWorld(bucketEvent.getBlockClicked().getWorld())) return;
+
+ Player player = bucketEvent.getPlayer();
+ Block block = bucketEvent.getBlockClicked().getRelative(bucketEvent.getBlockFace());
+ int minLavaDistance = 10;
+
+ // Fixes #1155:
+ // Prevents waterlogging blocks placed on a claim's edge.
+ // Waterlogging a block affects the clicked block, and NOT the adjacent location relative to it.
+ if (bucketEvent.getBucket() == Material.WATER_BUCKET
+ && bucketEvent.getBlockClicked().getBlockData() instanceof Waterlogged)
+ {
+ block = bucketEvent.getBlockClicked();
+ }
+
+ //make sure the player is allowed to build at the location
+ String noBuildReason = instance.allowBuild(player, block.getLocation(), Material.WATER);
+ if (noBuildReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
+ bucketEvent.setCancelled(true);
+ return;
+ }
+
+ //if the bucket is being used in a claim, allow for dumping lava closer to other players
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ minLavaDistance = 3;
+ }
+
+ //lava buckets can't be dumped near other players unless pvp is on
+ if (!doesAllowLavaProximityInWorld(block.getWorld()) && !player.hasPermission("griefprevention.lava"))
+ {
+ if (bucketEvent.getBucket() == Material.LAVA_BUCKET)
+ {
+ List players = block.getWorld().getPlayers();
+ for (Player otherPlayer : players)
+ {
+ if (isVanished(otherPlayer)) continue;
+ Location location = otherPlayer.getLocation();
+ if (!otherPlayer.equals(player) && otherPlayer.getGameMode() == GameMode.SURVIVAL && player.canSee(otherPlayer) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, "another player");
+ bucketEvent.setCancelled(true);
+ return;
+ }
+ }
+ }
+ }
+
+ //log any suspicious placements (check sea level, world type, and adjacent blocks)
+ if (block.getY() >= instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava") && block.getWorld().getEnvironment() != Environment.NETHER)
+ {
+ //if certain blocks are nearby, it's less suspicious and not worth logging
+ Set exclusionAdjacentTypes;
+ if (bucketEvent.getBucket() == Material.WATER_BUCKET)
+ exclusionAdjacentTypes = this.commonAdjacentBlocks_water;
+ else
+ exclusionAdjacentTypes = this.commonAdjacentBlocks_lava;
+
+ boolean makeLogEntry = true;
+ BlockFace[] adjacentDirections = new BlockFace[]{BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.DOWN};
+ for (BlockFace direction : adjacentDirections)
+ {
+ Material adjacentBlockType = block.getRelative(direction).getType();
+ if (exclusionAdjacentTypes.contains(adjacentBlockType))
+ {
+ makeLogEntry = false;
+ break;
+ }
+ }
+
+ if (makeLogEntry)
+ {
+ GriefPrevention.AddLogEntry(player.getName() + " placed suspicious " + bucketEvent.getBucket().name() + " @ " + GriefPrevention.getfriendlyLocationString(block.getLocation()), CustomLogEntryTypes.SuspiciousActivity, true);
+ }
+ }
+ }
+
+ private boolean doesAllowLavaProximityInWorld(World world)
+ {
+ if (GriefPrevention.instance.pvpRulesApply(world))
+ {
+ return GriefPrevention.instance.config_pvp_allowLavaNearPlayers;
+ }
+ else
+ {
+ return GriefPrevention.instance.config_pvp_allowLavaNearPlayers_NonPvp;
+ }
+ }
+
+ //see above
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerBucketFill(PlayerBucketFillEvent bucketEvent)
+ {
+ Player player = bucketEvent.getPlayer();
+ Block block = bucketEvent.getBlockClicked();
+
+ if (!instance.claimsEnabledForWorld(block.getWorld())) return;
+
+ //make sure the player is allowed to build at the location
+ String noBuildReason = instance.allowBuild(player, block.getLocation(), Material.AIR);
+ if (noBuildReason != null)
+ {
+ //exemption for cow milking (permissions will be handled by player interact with entity event instead)
+ Material blockType = block.getType();
+ if (blockType == Material.AIR)
+ return;
+ if (blockType.isSolid())
+ {
+ BlockData blockData = block.getBlockData();
+ if (!(blockData instanceof Waterlogged) || !((Waterlogged) blockData).isWaterlogged())
+ return;
+ }
+
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
+ bucketEvent.setCancelled(true);
+ return;
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlayerBucketatCauldron(CauldronLevelChangeEvent event)
+ {
+ if (!instance.claimsEnabledForWorld(event.getBlock().getWorld())) return;
+ Entity entity = event.getEntity();
+ Block block = event.getBlock();
+ if (entity == null) return;
+ if (entity instanceof Player player) {
+ Claim claim = dataStore.getClaimAt(block.getLocation(), false, null);
+ if (claim == null)
+ return;
+
+ Supplier allowContainer = claim.checkPermission(player, ClaimPermission.Inventory, event);
+
+ if (allowContainer != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, allowContainer.get());
+ return;
+ }
+ }
+ }
+
+ //when a player interacts with the world
+ @EventHandler(priority = EventPriority.LOWEST)
+ void onPlayerInteract(PlayerInteractEvent event)
+ {
+ //not interested in left-click-on-air actions
+ Action action = event.getAction();
+ if (action == Action.LEFT_CLICK_AIR) return;
+
+ Player player = event.getPlayer();
+ Block clickedBlock = event.getClickedBlock(); //null returned here means interacting with air
+
+ Material clickedBlockType = null;
+ if (clickedBlock != null)
+ {
+ clickedBlockType = clickedBlock.getType();
+ }
+ else
+ {
+ clickedBlockType = Material.AIR;
+ }
+
+ PlayerData playerData = null;
+
+ //Turtle eggs
+ if (action == Action.PHYSICAL)
+ {
+ if (clickedBlockType != Material.TURTLE_EGG)
+ return;
+ playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Build, event);
+ if (noAccessReason != null)
+ {
+ event.setCancelled(true);
+ return;
+ }
+ }
+ return;
+ }
+
+ //don't care about left-clicking on most blocks, this is probably a break action
+ if (action == Action.LEFT_CLICK_BLOCK && clickedBlock != null)
+ {
+ if (clickedBlock.getY() < clickedBlock.getWorld().getMaxHeight() - 1 || event.getBlockFace() != BlockFace.UP)
+ {
+ Block adjacentBlock = clickedBlock.getRelative(event.getBlockFace());
+ byte lightLevel = adjacentBlock.getLightFromBlocks();
+ if (lightLevel == 15 && adjacentBlock.getType() == Material.FIRE)
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, event);
+ if (noBuildReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
+ player.sendBlockChange(adjacentBlock.getLocation(), adjacentBlock.getType(), adjacentBlock.getData());
+ return;
+ }
+ }
+ }
+ }
+
+ //exception for blocks on a specific watch list
+ if (!this.onLeftClickWatchList(clickedBlockType))
+ {
+ return;
+ }
+ }
+
+ //apply rules for containers and crafting blocks
+ if (clickedBlock != null && instance.config_claims_preventTheft && (
+ event.getAction() == Action.RIGHT_CLICK_BLOCK && (
+ (this.isInventoryHolder(clickedBlock) && clickedBlock.getType() != Material.LECTERN) ||
+ clickedBlockType == Material.ANVIL ||
+ clickedBlockType == Material.BEACON ||
+ clickedBlockType == Material.BEE_NEST ||
+ clickedBlockType == Material.BEEHIVE ||
+ clickedBlockType == Material.BELL ||
+ clickedBlockType == Material.CAKE ||
+ clickedBlockType == Material.CARTOGRAPHY_TABLE ||
+ clickedBlockType == Material.CAULDRON ||
+ clickedBlockType == Material.CAVE_VINES ||
+ clickedBlockType == Material.CAVE_VINES_PLANT ||
+ clickedBlockType == Material.CHIPPED_ANVIL ||
+ clickedBlockType == Material.DAMAGED_ANVIL ||
+ clickedBlockType == Material.GRINDSTONE ||
+ clickedBlockType == Material.JUKEBOX ||
+ clickedBlockType == Material.LOOM ||
+ clickedBlockType == Material.PUMPKIN ||
+ clickedBlockType == Material.RESPAWN_ANCHOR ||
+ clickedBlockType == Material.ROOTED_DIRT ||
+ clickedBlockType == Material.STONECUTTER ||
+ clickedBlockType == Material.SWEET_BERRY_BUSH ||
+ clickedBlockType == Material.GLOW_BERRIES ||
+ Tag.CANDLES.isTagged(clickedBlockType) ||
+ Tag.CANDLE_CAKES.isTagged(clickedBlockType)
+ )))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+
+ //block container use during pvp combat, same reason
+ if (playerData.inPvpCombat())
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers);
+ event.setCancelled(true);
+ return;
+ }
+
+ // there's no need to check for claims as this is a claiming bypass?
+ Block block = event.getClickedBlock();
+ // TODO : doesn't cover cartography tables add these...
+ /* This should cover all chests, enderchests, barrels, furnacetypes, lectern, ....*/
+ if(block.getState() instanceof InventoryHolder) {
+ if (block.getState() instanceof Nameable) {
+ Nameable nameable = (Nameable) block.getState();
+ if (nameable.getCustomName() != null) {
+ if (ChatColor.stripColor(nameable.getCustomName()).equalsIgnoreCase("public")) {
+ return;
+ }
+ }
+ }
+ }
+
+ //otherwise check permissions for the claim the player is in
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (noContainersReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get());
+ return;
+ }
+ }
+
+ //if the event hasn't been cancelled, then the player is allowed to use the container
+ //so drop any pvp protection
+ if (playerData.pvpImmune)
+ {
+ playerData.pvpImmune = false;
+ GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
+ }
+ }
+
+ //otherwise apply rules for doors and beds, if configured that way
+ else if (clickedBlock != null &&
+
+ (instance.config_claims_lockWoodenDoors && Tag.WOODEN_DOORS.isTagged(clickedBlockType) ||
+
+ instance.config_claims_preventButtonsSwitches && Tag.BEDS.isTagged(clickedBlockType) ||
+
+ instance.config_claims_lockTrapDoors && Tag.WOODEN_TRAPDOORS.isTagged(clickedBlockType) ||
+
+ instance.config_claims_lecternReadingRequiresAccessTrust && clickedBlockType == Material.LECTERN ||
+
+ instance.config_claims_lockFenceGates && Tag.FENCE_GATES.isTagged(clickedBlockType)))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Access, event);
+ if (noAccessReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
+ return;
+ }
+ }
+ }
+
+ //otherwise apply rules for buttons and switches
+ else if (clickedBlock != null && instance.config_claims_preventButtonsSwitches && (Tag.BUTTONS.isTagged(clickedBlockType) || clickedBlockType == Material.LEVER))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noAccessReason = claim.checkPermission(player, ClaimPermission.Access, event);
+ if (noAccessReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason.get());
+ return;
+ }
+ }
+ }
+
+ //otherwise apply rule for cake
+ else if (clickedBlock != null && instance.config_claims_preventTheft && (clickedBlockType == Material.CAKE || Tag.CANDLE_CAKES.isTagged(clickedBlockType)))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+
+ Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Access, event);
+ if (noContainerReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get());
+ return;
+ }
+ }
+ }
+
+ //apply rule for note blocks and repeaters and daylight sensors //RoboMWM: Include flower pots
+ else if (clickedBlock != null &&
+ (
+ clickedBlockType == Material.NOTE_BLOCK ||
+ clickedBlockType == Material.REPEATER ||
+ clickedBlockType == Material.DRAGON_EGG ||
+ clickedBlockType == Material.DAYLIGHT_DETECTOR ||
+ clickedBlockType == Material.COMPARATOR ||
+ clickedBlockType == Material.REDSTONE_WIRE ||
+ Tag.FLOWER_POTS.isTagged(clickedBlockType)
+ ))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, event);
+ if (noBuildReason != null)
+ {
+ event.setCancelled(true);
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
+ return;
+ }
+ }
+ }
+
+ //otherwise handle right click (shovel, string, bonemeal) //RoboMWM: flint and steel
+ else
+ {
+ //ignore all actions except right-click on a block or in the air
+ if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return;
+
+ //what's the player holding?
+ EquipmentSlot hand = event.getHand();
+ ItemStack itemInHand = instance.getItemInHand(player, hand);
+ Material materialInHand = itemInHand.getType();
+
+ Set spawn_eggs = new HashSet<>();
+ Set dyes = new HashSet<>();
+
+ for (Material material : Material.values())
+ {
+ if (material.isLegacy()) continue;
+ if (material.name().endsWith("_SPAWN_EGG"))
+ spawn_eggs.add(material);
+ else if (material.name().endsWith("_DYE"))
+ dyes.add(material);
+ }
+
+ //if it's bonemeal, armor stand, spawn egg, etc - check for build permission //RoboMWM: also check flint and steel to stop TNT ignition
+ //add glowing ink sac and ink sac, due to their usage on signs
+ if (clickedBlock != null && (materialInHand == Material.BONE_MEAL
+ || materialInHand == Material.ARMOR_STAND
+ || (spawn_eggs.contains(materialInHand) && GriefPrevention.instance.config_claims_preventGlobalMonsterEggs)
+ || materialInHand == Material.END_CRYSTAL
+ || materialInHand == Material.FLINT_AND_STEEL
+ || materialInHand == Material.INK_SAC
+ || materialInHand == Material.GLOW_INK_SAC
+ || dyes.contains(materialInHand)))
+ {
+ String noBuildReason = instance
+ .allowBuild(player, clickedBlock
+ .getLocation(),
+ clickedBlockType);
+ if (noBuildReason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
+ event.setCancelled(true);
+ }
+
+ return;
+ }
+ else if (clickedBlock != null && Tag.ITEMS_BOATS.isTagged(materialInHand))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ Supplier reason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (reason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
+ event.setCancelled(true);
+ }
+ }
+
+ return;
+ }
+
+ //survival world minecart placement requires container trust, which is the permission required to remove the minecart later
+ else if (clickedBlock != null &&
+ (materialInHand == Material.MINECART ||
+ materialInHand == Material.FURNACE_MINECART ||
+ materialInHand == Material.CHEST_MINECART ||
+ materialInHand == Material.TNT_MINECART ||
+ materialInHand == Material.HOPPER_MINECART))
+ {
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ Supplier reason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (reason != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, reason.get());
+ event.setCancelled(true);
+ }
+ }
+
+ return;
+ }
+
+ //if he's investigating a claim
+ else if (materialInHand == instance.config_claims_investigationTool && hand == EquipmentSlot.HAND)
+ {
+ //if claims are disabled in this world, do nothing
+ if (!instance.claimsEnabledForWorld(player.getWorld())) return;
+
+ //if holding shift (sneaking), show all claims in area
+ if (player.isSneaking() && player.hasPermission("griefprevention.visualizenearbyclaims"))
+ {
+ //find nearby claims
+ Set claims = this.dataStore.getNearbyClaims(player.getLocation());
+
+ // alert plugins of a claim inspection, return if cancelled
+ ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, null, claims, true);
+ Bukkit.getPluginManager().callEvent(inspectionEvent);
+ if (inspectionEvent.isCancelled()) return;
+
+ //visualize boundaries
+ Visualization visualization = Visualization.fromClaims(claims, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claims, true));
+
+ Visualization.Apply(player, visualization);
+
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShowNearbyClaims, String.valueOf(claims.size()));
+
+ return;
+ }
+
+ //FEATURE: shovel and stick can be used from a distance away
+ if (action == Action.RIGHT_CLICK_AIR)
+ {
+ //try to find a far away non-air block along line of sight
+ clickedBlock = getTargetBlock(player, 100);
+ clickedBlockType = clickedBlock.getType();
+ }
+
+ //if no block, stop here
+ if (clickedBlock == null)
+ {
+ return;
+ }
+
+ //air indicates too far away
+ if (clickedBlockType == Material.AIR)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway);
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, null, Collections.emptySet()));
+
+ Visualization.Revert(player);
+ return;
+ }
+
+ if (playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, playerData.lastClaim);
+
+ //no claim case
+ if (claim == null)
+ {
+ // alert plugins of a claim inspection, return if cancelled
+ ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, clickedBlock, null);
+ Bukkit.getPluginManager().callEvent(inspectionEvent);
+ if (inspectionEvent.isCancelled()) return;
+
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockNotClaimed);
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, null, Collections.emptySet()));
+
+ Visualization.Revert(player);
+ }
+
+ //claim case
+ else
+ {
+ // alert plugins of a claim inspection, return if cancelled
+ ClaimInspectionEvent inspectionEvent = new ClaimInspectionEvent(player, clickedBlock, claim);
+ Bukkit.getPluginManager().callEvent(inspectionEvent);
+ if (inspectionEvent.isCancelled()) return;
+
+ playerData.lastClaim = claim;
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockClaimed, claim.getOwnerName());
+
+ //visualize boundary
+ Visualization visualization = Visualization.FromClaim(claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
+
+ Visualization.Apply(player, visualization);
+
+ if (player.hasPermission("griefprevention.seeclaimsize"))
+ {
+ GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea());
+ }
+
+ //if permission, tell about the player's offline time
+ if (!claim.isAdminClaim() && (player.hasPermission("griefprevention.deleteclaims") || player.hasPermission("griefprevention.seeinactivity")))
+ {
+ if (claim.parent != null)
+ {
+ claim = claim.parent;
+ }
+ Date lastLogin = new Date(Bukkit.getOfflinePlayer(claim.ownerID).getLastPlayed());
+ Date now = new Date();
+ long daysElapsed = (now.getTime() - lastLogin.getTime()) / (1000 * 60 * 60 * 24);
+
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.PlayerOfflineTime, String.valueOf(daysElapsed));
+
+ //drop the data we just loaded, if the player isn't online
+ if (instance.getServer().getPlayer(claim.ownerID) == null)
+ this.dataStore.clearCachedPlayerData(claim.ownerID);
+ }
+ }
+
+ return;
+ }
+
+ //if it's a golden shovel
+ else if (materialInHand != instance.config_claims_modificationTool || hand != EquipmentSlot.HAND) return;
+
+ event.setCancelled(true); //GriefPrevention exclusively reserves this tool (e.g. no grass path creation for golden shovel)
+
+ //FEATURE: shovel and stick can be used from a distance away
+ if (action == Action.RIGHT_CLICK_AIR)
+ {
+ //try to find a far away non-air block along line of sight
+ clickedBlock = getTargetBlock(player, 100);
+ clickedBlockType = clickedBlock.getType();
+ }
+
+ //if no block, stop here
+ if (clickedBlock == null)
+ {
+ return;
+ }
+
+ //can't use the shovel from too far away
+ if (clickedBlockType == Material.AIR)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway);
+ return;
+ }
+
+ //if the player is in restore nature mode, do only that
+ UUID playerID = player.getUniqueId();
+ playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ if (playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive)
+ {
+ //if the clicked block is in a claim, visualize that claim and deliver an error message
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName());
+ Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
+
+ Visualization.Apply(player, visualization);
+
+ return;
+ }
+
+ //figure out which chunk to repair
+ Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation());
+ //start the repair process
+
+ //set boundaries for processing
+ int miny = clickedBlock.getY();
+
+ //if not in aggressive mode, extend the selection down to a little below sea level
+ if (!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive))
+ {
+ if (miny > instance.getSeaLevel(chunk.getWorld()) - 10)
+ {
+ miny = instance.getSeaLevel(chunk.getWorld()) - 10;
+ }
+ }
+
+ instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player);
+
+ return;
+ }
+
+ //if in restore nature fill mode
+ if (playerData.shovelMode == ShovelMode.RestoreNatureFill)
+ {
+ ArrayList allowedFillBlocks = new ArrayList<>();
+ Environment environment = clickedBlock.getWorld().getEnvironment();
+ if (environment == Environment.NETHER)
+ {
+ allowedFillBlocks.add(Material.NETHERRACK);
+ }
+ else if (environment == Environment.THE_END)
+ {
+ allowedFillBlocks.add(Material.END_STONE);
+ }
+ else
+ {
+ allowedFillBlocks.add(Material.GRASS);
+ allowedFillBlocks.add(Material.DIRT);
+ allowedFillBlocks.add(Material.STONE);
+ allowedFillBlocks.add(Material.SAND);
+ allowedFillBlocks.add(Material.SANDSTONE);
+ allowedFillBlocks.add(Material.ICE);
+ }
+
+ Block centerBlock = clickedBlock;
+
+ int maxHeight = centerBlock.getY();
+ int minx = centerBlock.getX() - playerData.fillRadius;
+ int maxx = centerBlock.getX() + playerData.fillRadius;
+ int minz = centerBlock.getZ() - playerData.fillRadius;
+ int maxz = centerBlock.getZ() + playerData.fillRadius;
+ int minHeight = maxHeight - 10;
+ minHeight = Math.max(minHeight, clickedBlock.getWorld().getMinHeight());
+
+ Claim cachedClaim = null;
+ for (int x = minx; x <= maxx; x++)
+ {
+ for (int z = minz; z <= maxz; z++)
+ {
+ //circular brush
+ Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z);
+ if (location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue;
+
+ //default fill block is initially the first from the allowed fill blocks list above
+ Material defaultFiller = allowedFillBlocks.get(0);
+
+ //prefer to use the block the player clicked on, if it's an acceptable fill block
+ if (allowedFillBlocks.contains(centerBlock.getType()))
+ {
+ defaultFiller = centerBlock.getType();
+ }
+
+ //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler
+ else if (centerBlock.getType() == Material.WATER)
+ {
+ Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation());
+ while (!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10)
+ {
+ block = block.getRelative(BlockFace.DOWN);
+ }
+ if (allowedFillBlocks.contains(block.getType()))
+ {
+ defaultFiller = block.getType();
+ }
+ }
+
+ //fill bottom to top
+ for (int y = minHeight; y <= maxHeight; y++)
+ {
+ Block block = centerBlock.getWorld().getBlockAt(x, y, z);
+
+ //respect claims
+ Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim);
+ if (claim != null)
+ {
+ cachedClaim = claim;
+ break;
+ }
+
+ //only replace air, spilling water, snow, long grass
+ if (block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.WATER && ((Levelled) block.getBlockData()).getLevel() != 0) || block.getType() == Material.GRASS)
+ {
+ //if the top level, always use the default filler picked above
+ if (y == maxHeight)
+ {
+ block.setType(defaultFiller);
+ }
+
+ //otherwise look to neighbors for an appropriate fill block
+ else
+ {
+ Block eastBlock = block.getRelative(BlockFace.EAST);
+ Block westBlock = block.getRelative(BlockFace.WEST);
+ Block northBlock = block.getRelative(BlockFace.NORTH);
+ Block southBlock = block.getRelative(BlockFace.SOUTH);
+
+ //first, check lateral neighbors (ideally, want to keep natural layers)
+ if (allowedFillBlocks.contains(eastBlock.getType()))
+ {
+ block.setType(eastBlock.getType());
+ }
+ else if (allowedFillBlocks.contains(westBlock.getType()))
+ {
+ block.setType(westBlock.getType());
+ }
+ else if (allowedFillBlocks.contains(northBlock.getType()))
+ {
+ block.setType(northBlock.getType());
+ }
+ else if (allowedFillBlocks.contains(southBlock.getType()))
+ {
+ block.setType(southBlock.getType());
+ }
+
+ //if all else fails, use the default filler selected above
+ else
+ {
+ block.setType(defaultFiller);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return;
+ }
+
+ //if the player doesn't have claims permission, don't do anything
+ if (!player.hasPermission("griefprevention.createclaims"))
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission);
+ return;
+ }
+
+ //if he's resizing a claim and that claim hasn't been deleted since he started resizing it
+ if (playerData.claimResizing != null && playerData.claimResizing.inDataStore)
+ {
+ if (clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return;
+
+ //figure out what the coords of his new claim would be
+ int newx1, newx2, newz1, newz2, newy1, newy2;
+ if (playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX())
+ {
+ newx1 = clickedBlock.getX();
+ newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX();
+ }
+ else
+ {
+ newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX();
+ newx2 = clickedBlock.getX();
+ }
+
+ if (playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ())
+ {
+ newz1 = clickedBlock.getZ();
+ newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ();
+ }
+ else
+ {
+ newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ();
+ newz2 = clickedBlock.getZ();
+ }
+
+ newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY();
+ newy2 = clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance;
+
+ this.dataStore.resizeClaimWithChecks(player, playerData, newx1, newx2, newy1, newy2, newz1, newz2);
+
+ return;
+ }
+
+ //otherwise, since not currently resizing a claim, must be starting a resize, creating a new claim, or creating a subdivision
+ Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), true /*ignore height*/, playerData.lastClaim);
+
+ //if within an existing claim, he's not creating a new one
+ if (claim != null)
+ {
+ //if the player has permission to edit the claim or subdivision
+ Supplier noEditReason = claim.checkPermission(player, ClaimPermission.Edit, event, () -> instance.dataStore.getMessage(Messages.CreateClaimFailOverlapOtherPlayer, claim.getOwnerName()));
+ if (noEditReason == null)
+ {
+ //if he clicked on a corner, start resizing it
+ if ((clickedBlock.getX() == claim.getLesserBoundaryCorner().getBlockX() || clickedBlock.getX() == claim.getGreaterBoundaryCorner().getBlockX()) && (clickedBlock.getZ() == claim.getLesserBoundaryCorner().getBlockZ() || clickedBlock.getZ() == claim.getGreaterBoundaryCorner().getBlockZ()))
+ {
+ playerData.claimResizing = claim;
+ playerData.lastShovelLocation = clickedBlock.getLocation();
+ GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ResizeStart);
+ }
+
+ //if he didn't click on a corner and is in subdivision mode, he's creating a new subdivision
+ else if (playerData.shovelMode == ShovelMode.Subdivide)
+ {
+ //if it's the first click, he's trying to start a new subdivision
+ if (playerData.lastShovelLocation == null)
+ {
+ //if the clicked claim was a subdivision, tell him he can't start a new subdivision here
+ if (claim.parent != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapSubdivision);
+ }
+
+ //otherwise start a new subdivision
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionStart);
+ playerData.lastShovelLocation = clickedBlock.getLocation();
+ playerData.claimSubdividing = claim;
+ }
+ }
+
+ //otherwise, he's trying to finish creating a subdivision by setting the other boundary corner
+ else
+ {
+ //if last shovel location was in a different world, assume the player is starting the create-claim workflow over
+ if (!playerData.lastShovelLocation.getWorld().equals(clickedBlock.getWorld()))
+ {
+ playerData.lastShovelLocation = null;
+ this.onPlayerInteract(event);
+ return;
+ }
+
+ //try to create a new claim (will return null if this subdivision overlaps another)
+ CreateClaimResult result = this.dataStore.createClaim(
+ player.getWorld(),
+ playerData.lastShovelLocation.getBlockX(), clickedBlock.getX(),
+ playerData.lastShovelLocation.getBlockY() - instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance,
+ playerData.lastShovelLocation.getBlockZ(), clickedBlock.getZ(),
+ null, //owner is not used for subdivisions
+ playerData.claimSubdividing,
+ null, player);
+
+ //if it didn't succeed, tell the player why
+ if (!result.succeeded)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap);
+
+ Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
+
+ Visualization.Apply(player, visualization);
+
+ return;
+ }
+
+ //otherwise, advise him on the /trust command and show him his new subdivision
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess);
+ Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
+
+ Visualization.Apply(player, visualization);
+ playerData.lastShovelLocation = null;
+ playerData.claimSubdividing = null;
+ }
+ }
+ }
+
+ //otherwise tell him he can't create a claim here, and show him the existing claim
+ //also advise him to consider /abandonclaim or resizing the existing claim
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlap);
+ Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
+
+ Visualization.Apply(player, visualization);
+ }
+ }
+
+ //otherwise tell the player he can't claim here because it's someone else's claim, and show him the claim
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, noEditReason.get());
+ Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, claim));
+
+ Visualization.Apply(player, visualization);
+ }
+
+ return;
+ }
+
+ //otherwise, the player isn't in an existing claim!
+
+ //if he hasn't already start a claim with a previous shovel action
+ Location lastShovelLocation = playerData.lastShovelLocation;
+ if (lastShovelLocation == null)
+ {
+ //if claims are not enabled in this world and it's not an administrative claim, display an error message and stop
+ if (!instance.claimsEnabledForWorld(player.getWorld()))
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld);
+ return;
+ }
+
+ //if he's at the claim count per player limit already and doesn't have permission to bypass, display an error message
+ if (instance.config_claims_maxClaimsPerPlayer > 0 &&
+ !player.hasPermission("griefprevention.overrideclaimcountlimit") &&
+ playerData.getClaims().size() >= instance.config_claims_maxClaimsPerPlayer)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimCreationFailedOverClaimCountLimit);
+ return;
+ }
+
+ //remember it, and start him on the new claim
+ playerData.lastShovelLocation = clickedBlock.getLocation();
+ GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimStart);
+
+ //show him where he's working
+ Claim newClaim = new Claim(clickedBlock.getLocation(), clickedBlock.getLocation(), null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null);
+ Visualization visualization = Visualization.FromClaim(newClaim, clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, newClaim));
+
+ Visualization.Apply(player, visualization);
+ }
+
+ //otherwise, he's trying to finish creating a claim by setting the other boundary corner
+ else
+ {
+ //if last shovel location was in a different world, assume the player is starting the create-claim workflow over
+ if (!lastShovelLocation.getWorld().equals(clickedBlock.getWorld()))
+ {
+ playerData.lastShovelLocation = null;
+ this.onPlayerInteract(event);
+ return;
+ }
+
+ //apply pvp rule
+ if (playerData.inPvpCombat())
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoClaimDuringPvP);
+ return;
+ }
+
+ //apply minimum claim dimensions rule
+ int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1;
+ int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1;
+
+ if (playerData.shovelMode != ShovelMode.Admin)
+ {
+ if (newClaimWidth < instance.config_claims_minWidth || newClaimHeight < instance.config_claims_minWidth)
+ {
+ //this IF block is a workaround for craftbukkit bug which fires two events for one interaction
+ if (newClaimWidth != 1 && newClaimHeight != 1)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooNarrow, String.valueOf(instance.config_claims_minWidth));
+ }
+ return;
+ }
+
+ int newArea = newClaimWidth * newClaimHeight;
+ if (newArea < instance.config_claims_minArea)
+ {
+ if (newArea != 1)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimInsufficientArea, String.valueOf(instance.config_claims_minArea));
+ }
+
+ return;
+ }
+ }
+
+ //if not an administrative claim, verify the player has enough claim blocks for this new claim
+ if (playerData.shovelMode != ShovelMode.Admin)
+ {
+ int newClaimArea = newClaimWidth * newClaimHeight;
+ int remainingBlocks = playerData.getRemainingClaimBlocks();
+ if (newClaimArea > remainingBlocks)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks));
+ instance.dataStore.tryAdvertiseAdminAlternatives(player);
+ return;
+ }
+ }
+ else
+ {
+ playerID = null;
+ }
+
+ //try to create a new claim
+ CreateClaimResult result = this.dataStore.createClaim(
+ player.getWorld(),
+ lastShovelLocation.getBlockX(), clickedBlock.getX(),
+ lastShovelLocation.getBlockY() - instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - instance.config_claims_claimsExtendIntoGroundDistance,
+ lastShovelLocation.getBlockZ(), clickedBlock.getZ(),
+ playerID,
+ null, null,
+ player);
+
+ //if it didn't succeed, tell the player why
+ if (!result.succeeded)
+ {
+ if (result.claim != null)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort);
+
+ Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
+
+ Visualization.Apply(player, visualization);
+ }
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion);
+ }
+
+ return;
+ }
+
+ //otherwise, advise him on the /trust command and show him his new claim
+ else
+ {
+ GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess);
+ Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation());
+
+ // alert plugins of a visualization
+ Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, visualization, result.claim));
+
+ Visualization.Apply(player, visualization);
+ playerData.lastShovelLocation = null;
+
+ //if it's a big claim, tell the player about subdivisions
+ if (!player.hasPermission("griefprevention.adminclaims") && result.claim.getArea() >= 1000)
+ {
+ GriefPrevention.sendMessage(player, TextMode.Info, Messages.BecomeMayor, 200L);
+ }
+
+ instance.autoExtendClaim(result.claim);
+ }
+ }
+ }
+ }
+
+ // Stops an untrusted player from removing a book from a lectern
+ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+ void onTakeBook(PlayerTakeLecternBookEvent event)
+ {
+ Player player = event.getPlayer();
+ PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
+ Claim claim = this.dataStore.getClaimAt(event.getLectern().getLocation(), false, playerData.lastClaim);
+ if (claim != null)
+ {
+ playerData.lastClaim = claim;
+ Supplier noContainerReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
+ if (noContainerReason != null)
+ {
+ event.setCancelled(true);
+ player.closeInventory();
+ GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason.get());
+ }
+ }
+ }
+
+ //determines whether a block type is an inventory holder. uses a caching strategy to save cpu time
+ private final ConcurrentHashMap inventoryHolderCache = new ConcurrentHashMap<>();
+
+ private boolean isInventoryHolder(Block clickedBlock)
+ {
+
+ Material cacheKey = clickedBlock.getType();
+ Boolean cachedValue = this.inventoryHolderCache.get(cacheKey);
+ if (cachedValue != null)
+ {
+ return cachedValue.booleanValue();
+
+ }
+ else
+ {
+ boolean isHolder = clickedBlock.getState() instanceof InventoryHolder;
+ this.inventoryHolderCache.put(cacheKey, isHolder);
+ return isHolder;
+ }
+ }
+
+ private boolean onLeftClickWatchList(Material material)
+ {
+ switch (material)
+ {
+ case OAK_BUTTON:
+ case SPRUCE_BUTTON:
+ case BIRCH_BUTTON:
+ case JUNGLE_BUTTON:
+ case ACACIA_BUTTON:
+ case DARK_OAK_BUTTON:
+ case STONE_BUTTON:
+ case LEVER:
+ case REPEATER:
+ case CAKE:
+ case DRAGON_EGG:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static Block getTargetBlock(Player player, int maxDistance) throws IllegalStateException
+ {
+ Location eye = player.getEyeLocation();
+ Material eyeMaterial = eye.getBlock().getType();
+ boolean passThroughWater = (eyeMaterial == Material.WATER);
+ BlockIterator iterator = new BlockIterator(player.getLocation(), player.getEyeHeight(), maxDistance);
+ Block result = player.getLocation().getBlock().getRelative(BlockFace.UP);
+ while (iterator.hasNext())
+ {
+ result = iterator.next();
+ Material type = result.getType();
+ if (type != Material.AIR &&
+ (!passThroughWater || type != Material.WATER) &&
+ type != Material.GRASS &&
+ type != Material.SNOW) return result;
+ }
+
+ return result;
+ }
+
+ private boolean isVanished(Player player) {
+ for (MetadataValue meta : player.getMetadata("vanished")) {
+ if (meta.asBoolean()) return true;
+ }
+ return false;
+ }
+}