AlttdGriefPrevention/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java
2024-08-03 14:32:34 +02:00

2082 lines
97 KiB
Java

/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import com.destroystokyo.paper.loottable.LootableBlockInventory;
import com.griefprevention.protection.ProtectionHelper;
import me.ryanhamshire.GriefPrevention.events.ClaimInspectionEvent;
import me.ryanhamshire.GriefPrevention.events.VisualizationEvent;
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.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.ItemFrame;
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.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.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<Long> 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<String> 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<UUID, Date> 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<Player> players = (Collection<Player>) 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<UUID, Long> 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<UUID, Integer> 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<String> 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<String> 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 abandoning a pet, do that instead
if (playerData.petAbandonment)
{
tameable.setOwner(null);
playerData.petAbandonment = false;
GriefPrevention.sendMessage(player, TextMode.Success, Messages.PetGiveawayConfirmation);
event.setCancelled(true);
}
//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 && !(entity instanceof ItemFrame))
{
Supplier<String> noBuildReason = ProtectionHelper.checkPermission(player, entity.getLocation(), ClaimPermission.Build, event);
if (noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
event.setCancelled(true);
return;
}
}
//don't allow interaction with item frames in claimed areas without container permission
if ((entity instanceof ItemFrame) && !player.getInventory().getItem(event.getHand()).getType().equals(Material.DIAMOND)) {
Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
Supplier<String> stringSupplier = claim.checkPermission(player, ClaimPermission.Access, event);
if (stringSupplier != null) {
instance.sendMessage(player, TextMode.Err, stringSupplier.get());
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<String> 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<String> 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<String> 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<String> 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<String> failureReason = claim.checkPermission(player, ClaimPermission.Inventory, event);
Supplier<String> 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<String> 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<Material> commonAdjacentBlocks_water = EnumSet.of(Material.WATER, Material.FARMLAND, Material.DIRT, Material.STONE);
private final Set<Material> 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
Supplier<String> noBuildReason = ProtectionHelper.checkPermission(player, block.getLocation(), ClaimPermission.Build, bucketEvent);
if (noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
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<Player> 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<Material> 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;
//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;
//make sure the player is allowed to build at the location
Supplier<String> noBuildReason = ProtectionHelper.checkPermission(player, block.getLocation(), ClaimPermission.Build, bucketEvent);
if (noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
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<String> 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<String> 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<String> 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.WATER_CAULDRON ||
clickedBlockType == Material.LAVA_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 ||
clickedBlockType == Material.CRAFTER ||
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) {
if (nameable.getCustomName() != null) {
String customName = ChatColor.stripColor(nameable.getCustomName());
if (customName.equalsIgnoreCase("public") || customName.equalsIgnoreCase(player.getName())) {
return;
}
}
}
}
if(block.getState() instanceof LootableBlockInventory lootable) {
if (lootable.hasLootTable())
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<String> 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<String> 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<String> 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<String> 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 ||
clickedBlockType == Material.CRAFTER ||
Tag.FLOWER_POTS.isTagged(clickedBlockType) ||
Tag.CANDLES.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<String> 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<Material> spawn_eggs = new HashSet<>();
Set<Material> 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
|| materialInHand == Material.HONEYCOMB
|| dyes.contains(materialInHand)))
{
Supplier<String> noBuildReason = ProtectionHelper.checkPermission(player, event.getClickedBlock().getLocation(), ClaimPermission.Build, event);
if (noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get());
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<String> 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<String> 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<Claim> 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.<Claim>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.<Claim>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<Material> 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.SHORT_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.SHORT_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<String> 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<String> 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<Material, Boolean> 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 (!Tag.REPLACEABLE.isTagged(type) || (!passThroughWater && type == Material.WATER)) return result;
}
return result;
}
private boolean isVanished(Player player) {
for (MetadataValue meta : player.getMetadata("vanished")) {
if (meta.asBoolean()) return true;
}
return false;
}
}