diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..70cb064d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +# Declare text file types just in case +*.java text +*.yml text +*.xml text +*.md text + +# Image files are treated as binary by default +#*.jpg binary +#*.png binary +#*.gif binary diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java index 8eceb6d85..4a4a61677 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -1,2482 +1,2482 @@ -/* - 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 com.griefprevention.util.command.MonitorableCommand; -import com.griefprevention.util.command.MonitoredCommands; -import com.griefprevention.visualization.BoundaryVisualization; -import com.griefprevention.visualization.VisualizationType; -import me.ryanhamshire.GriefPrevention.events.ClaimInspectionEvent; -import me.ryanhamshire.GriefPrevention.util.BoundingBox; -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.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.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.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.entity.PlayerDeathEvent; -import org.bukkit.event.player.AsyncPlayerChatEvent; -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.PlayerEvent; -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.PlayerLoginEvent; -import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.event.player.PlayerPortalEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerRespawnEvent; -import org.bukkit.event.player.PlayerSignOpenEvent; -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.scheduler.BukkitRunnable; -import org.bukkit.util.BlockIterator; -import org.jetbrains.annotations.NotNull; - -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -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; - - //list of temporarily banned ip's - private final ArrayList tempBannedIps = new ArrayList<>(); - - //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 WordFinder bannedWordFinder; - private MonitoredCommands pvpBlockedCommands; - private MonitoredCommands accessTrustCommands; - private MonitoredCommands chatCommands; - private MonitoredCommands whisperCommands; - - //spam tracker - SpamDetector spamDetector = new SpamDetector(); - // Definitions for specific material groups that do not have a tag - private final Set spawnEggs; - private final Set dyes; - - //typical constructor, yawn - PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) - { - this.dataStore = dataStore; - this.instance = plugin; - // Initialize empty on load so never null just in case. Reload after plugins enable. - this.bannedWordFinder = new WordFinder(List.of()); - this.pvpBlockedCommands = new MonitoredCommands(List.of()); - this.accessTrustCommands = new MonitoredCommands(List.of()); - this.chatCommands = new MonitoredCommands(List.of()); - this.whisperCommands = new MonitoredCommands(List.of()); - - spawnEggs = new HashSet<>(); - dyes = new HashSet<>(); - for (Material material : Material.values()) - { - if (material.name().endsWith("_SPAWN_EGG")) - spawnEggs.add(material); - else if (material.name().endsWith("_DYE")) - dyes.add(material); - } - - reload(); - } - - protected void reload() - { - this.howToClaimPattern = null; - this.bannedWordFinder = new WordFinder(instance.dataStore.loadBannedWords()); - this.pvpBlockedCommands = new MonitoredCommands(instance.config_pvp_blockedCommands); - this.accessTrustCommands = new MonitoredCommands(instance.config_claims_commandsRequiringAccessTrust); - this.chatCommands = new MonitoredCommands(instance.config_spam_monitorSlashCommands); - this.whisperCommands = new MonitoredCommands(instance.config_eavesdrop_whisperCommands); - } - - //when a player chats, monitor for spam - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - synchronized void onPlayerChat(AsyncPlayerChatEvent event) - { - Player player = event.getPlayer(); - if (!player.isOnline()) - { - event.setCancelled(true); - return; - } - - String message = event.getMessage(); - - boolean muted = this.handlePlayerChat(player, message, event); - Set recipients = event.getRecipients(); - - //muted messages go out to only the sender - if (muted) - { - recipients.clear(); - recipients.add(player); - } - - //soft muted messages go out to all soft muted players - else if (this.dataStore.isSoftMuted(player.getUniqueId())) - { - String notificationMessage = "(Muted " + player.getName() + "): " + message; - Set recipientsToKeep = new HashSet<>(); - for (Player recipient : recipients) - { - if (this.dataStore.isSoftMuted(recipient.getUniqueId())) - { - recipientsToKeep.add(recipient); - } - else if (recipient.hasPermission("griefprevention.eavesdrop")) - { - recipient.sendMessage(ChatColor.GRAY + notificationMessage); - } - } - recipients.clear(); - recipients.addAll(recipientsToKeep); - - GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.MutedChat, false); - } - - //troll and excessive profanity filter - else if (!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(message)) - { - //allow admins to see the soft-muted text - String notificationMessage = "(Muted " + player.getName() + "): " + message; - for (Player recipient : recipients) - { - if (recipient.hasPermission("griefprevention.eavesdrop")) - { - recipient.sendMessage(ChatColor.GRAY + notificationMessage); - } - } - - //limit recipients to sender - recipients.clear(); - recipients.add(player); - - //if player not new warn for the first infraction per play session. - if (!GriefPrevention.isNewToServer(player)) - { - PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); - if (!playerData.profanityWarned) - { - playerData.profanityWarned = true; - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoProfanity); - event.setCancelled(true); - return; - } - } - - //otherwise assume chat troll and mute all chat from this sender until an admin says otherwise - else if (instance.config_trollFilterEnabled) - { - GriefPrevention.AddLogEntry("Auto-muted new player " + player.getName() + " for profanity shortly after join. Use /SoftMute to undo.", CustomLogEntryTypes.AdminActivity); - GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.MutedChat, false); - instance.dataStore.toggleSoftMute(player.getUniqueId()); - } - } - - //remaining messages - else - { - //enter in abridged chat logs - makeSocialLogEntry(player.getName(), message); - - //based on ignore lists, remove some of the audience - if (!player.hasPermission("griefprevention.notignorable")) - { - Set recipientsToRemove = new HashSet<>(); - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - for (Player recipient : recipients) - { - if (!recipient.hasPermission("griefprevention.notignorable")) - { - if (playerData.ignoredPlayers.containsKey(recipient.getUniqueId())) - { - recipientsToRemove.add(recipient); - } - else - { - PlayerData targetPlayerData = this.dataStore.getPlayerData(recipient.getUniqueId()); - if (targetPlayerData.ignoredPlayers.containsKey(player.getUniqueId())) - { - recipientsToRemove.add(recipient); - } - } - } - } - - recipients.removeAll(recipientsToRemove); - } - } - } - - //returns true if the message should be muted, true if it should be sent - private boolean handlePlayerChat(Player player, String message, PlayerEvent event) - { - //FEATURE: automatically educate players about claiming land - //watching for message format how*claim*, and will send a link to the basics video - if (this.howToClaimPattern == null) - { - this.howToClaimPattern = Pattern.compile(this.dataStore.getMessage(Messages.HowToClaimRegex), Pattern.CASE_INSENSITIVE); - } - - if (this.howToClaimPattern.matcher(message).matches()) - { - if (instance.creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.CreativeBasicsVideo2, 10L, DataStore.CREATIVE_VIDEO_URL); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsVideo2, 10L, DataStore.SURVIVAL_VIDEO_URL); - } - } - - //FEATURE: automatically educate players about the /trapped command - //check for "trapped" or "stuck" to educate players about the /trapped command - String trappedwords = this.dataStore.getMessage( - Messages.TrappedChatKeyword - ); - if (!trappedwords.isEmpty()) - { - String[] checkWords = trappedwords.split(";"); - - for (String checkWord : checkWords) - { - if (!message.contains("/trapped") - && message.contains(checkWord)) - { - GriefPrevention.sendMessage( - player, - TextMode.Info, - Messages.TrappedInstructions, - 10L - ); - break; - } - } - } - - //FEATURE: monitor for chat and command spam - - if (!instance.config_spam_enabled) return false; - - //if the player has permission to spam, don't bother even examining the message - if (player.hasPermission("griefprevention.spam")) return false; - - //examine recent messages to detect spam - SpamAnalysisResult result = this.spamDetector.AnalyzeMessage(player.getUniqueId(), message, System.currentTimeMillis()); - - //apply any needed changes to message (like lowercasing all-caps) - if (event instanceof AsyncPlayerChatEvent) - { - ((AsyncPlayerChatEvent) event).setMessage(result.finalMessage); - } - - //don't allow new players to chat after logging in until they move - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - if (playerData.noChatLocation != null) - { - Location currentLocation = player.getLocation(); - if (currentLocation.getBlockX() == playerData.noChatLocation.getBlockX() && - currentLocation.getBlockZ() == playerData.noChatLocation.getBlockZ()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoChatUntilMove, 10L); - result.muteReason = "pre-movement chat"; - } - else - { - playerData.noChatLocation = null; - } - } - - //filter IP addresses - if (result.muteReason == null) - { - if (instance.containsBlockedIP(message)) - { - //block message - result.muteReason = "IP address"; - } - } - - //take action based on spam detector results - if (result.shouldBanChatter) - { - if (instance.config_spam_banOffenders) - { - //log entry - GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam.", CustomLogEntryTypes.AdminActivity); - - //kick and ban - PlayerKickBanTask task = new PlayerKickBanTask(player, instance.config_spam_banMessage, "GriefPrevention Anti-Spam", true); - instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 1L); - } - else - { - //log entry - GriefPrevention.AddLogEntry("Kicking " + player.getName() + " for spam.", CustomLogEntryTypes.AdminActivity); - - //just kick - PlayerKickBanTask task = new PlayerKickBanTask(player, "", "GriefPrevention Anti-Spam", false); - instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 1L); - } - } - else if (result.shouldWarnChatter) - { - //warn and log - GriefPrevention.sendMessage(player, TextMode.Warn, instance.config_spam_warningMessage, 10L); - GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties.", CustomLogEntryTypes.Debug, true); - } - - if (result.muteReason != null) - { - //mute and log - GriefPrevention.AddLogEntry("Muted " + result.muteReason + "."); - GriefPrevention.AddLogEntry("Muted " + player.getName() + " " + result.muteReason + ":" + message, CustomLogEntryTypes.Debug, true); - - return true; - } - - return false; - } - - //when a player uses a slash command... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - synchronized void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) - { - MonitorableCommand command = new MonitorableCommand(event.getMessage()); - - CommandCategory category = this.getCommandCategory(command); - - Player player = event.getPlayer(); - PlayerData playerData = null; - - //if a whisper - if (category == CommandCategory.Whisper && command.getArgumentCount() > 1) - { - //determine target player, might be NULL - - Player targetPlayer = instance.getServer().getPlayer(command.getArgument(0)); - - //softmute feature - if (this.dataStore.isSoftMuted(player.getUniqueId()) && targetPlayer != null && !this.dataStore.isSoftMuted(targetPlayer.getUniqueId())) - { - event.setCancelled(true); - return; - } - - //if eavesdrop enabled and sender doesn't have the eavesdrop immunity permission, eavesdrop - if (instance.config_whisperNotifications && !player.hasPermission("griefprevention.eavesdropimmune")) - { - //except for when the recipient has eavesdrop immunity - if (targetPlayer == null || !targetPlayer.hasPermission("griefprevention.eavesdropimmune")) - { - - String logMessage = "[[" + event.getPlayer().getName() + "]] " + - command.getCommand().substring(command.getCommand(0).length() + 1); - - @SuppressWarnings("unchecked") - Collection players = (Collection) instance.getServer().getOnlinePlayers(); - for (Player onlinePlayer : players) - { - if (onlinePlayer.hasPermission("griefprevention.eavesdrop") && !onlinePlayer.equals(targetPlayer) && !onlinePlayer.equals(player)) - { - onlinePlayer.sendMessage(ChatColor.GRAY + logMessage); - } - } - } - } - - //ignore feature - if (targetPlayer != null && targetPlayer.isOnline()) - { - //if either is ignoring the other, cancel this command - playerData = this.dataStore.getPlayerData(player.getUniqueId()); - if (playerData.ignoredPlayers.containsKey(targetPlayer.getUniqueId()) && !targetPlayer.hasPermission("griefprevention.notignorable")) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, Messages.IsIgnoringYou); - return; - } - - PlayerData targetPlayerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); - if (targetPlayerData.ignoredPlayers.containsKey(player.getUniqueId()) && !player.hasPermission("griefprevention.notignorable")) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, Messages.IsIgnoringYou); - return; - } - } - } - - //if in pvp, block any pvp-banned slash commands - if (playerData == null) playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId()); - - if ((playerData.inPvpCombat()) && pvpBlockedCommands.isMonitoredCommand(command)) - { - event.setCancelled(true); - GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); - return; - } - - //soft mute for chat slash commands - if (category == CommandCategory.Chat && this.dataStore.isSoftMuted(player.getUniqueId())) - { - event.setCancelled(true); - return; - } - - //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) - boolean isMonitoredCommand = (category == CommandCategory.Chat || category == CommandCategory.Whisper); - if (isMonitoredCommand) - { - //if anti spam enabled, check for spam - if (instance.config_spam_enabled) - { - event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); - } - - if (!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(event.getMessage())) - { - event.setCancelled(true); - } - - //unless cancelled, log in abridged logs - if (!event.isCancelled()) - { - makeSocialLogEntry(event.getPlayer().getName(), event.getMessage()); - } - } - - //if requires access trust, check for permission - if (accessTrustCommands.isMonitoredCommand(command)) - { - 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); - } - } - } - } - - private CommandCategory getCommandCategory(MonitorableCommand command) - { - if (whisperCommands.isMonitoredCommand(command)) return CommandCategory.Whisper; - if (chatCommands.isMonitoredCommand(command)) return CommandCategory.Chat; - return CommandCategory.None; - } - - 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 attempts to join the server... - @EventHandler(priority = EventPriority.HIGHEST) - void onPlayerLogin(PlayerLoginEvent event) - { - Player player = event.getPlayer(); - - //all this is anti-spam code - if (instance.config_spam_enabled) - { - //FEATURE: login cooldown to prevent login/logout spam with custom clients - long now = Calendar.getInstance().getTimeInMillis(); - - //if allowed to join and login cooldown enabled - if (instance.config_spam_loginCooldownSeconds > 0 && event.getResult() == Result.ALLOWED && !player.hasPermission("griefprevention.spam")) - { - //determine how long since last login and cooldown remaining - Date lastLoginThisSession = lastLoginThisServerSessionMap.get(player.getUniqueId()); - if (lastLoginThisSession != null) - { - long millisecondsSinceLastLogin = now - lastLoginThisSession.getTime(); - long secondsSinceLastLogin = millisecondsSinceLastLogin / 1000; - long cooldownRemaining = instance.config_spam_loginCooldownSeconds - secondsSinceLastLogin; - - //if cooldown remaining - if (cooldownRemaining > 0) - { - //DAS BOOT! - event.setResult(Result.KICK_OTHER); - event.setKickMessage("You must wait " + cooldownRemaining + " seconds before logging-in again."); - event.disallow(event.getResult(), event.getKickMessage()); - return; - } - } - } - - //if logging-in account is banned, remember IP address for later - if (instance.config_smartBan && event.getResult() == Result.KICK_BANNED) - { - this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); - } - } - - //remember the player's ip address - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.ipAddress = event.getAddress(); - } - - //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) && !player.hasPermission("griefprevention.premovementchat")) - { - playerData.noChatLocation = player.getLocation(); - } - - //if player has never played on the server before... - if (!player.hasPlayedBefore()) - { - //may need pvp protection - instance.checkPvpProtectionNeeded(player); - - //if in survival claims mode, send a message about the claim basics video (except for admins - assumed experts) - if (instance.config_claims_worldModes.get(player.getWorld()) == ClaimsMode.Survival && !player.hasPermission("griefprevention.adminclaims") && this.dataStore.claims.size() > 10) - { - WelcomeTask task = new WelcomeTask(player); - Bukkit.getScheduler().scheduleSyncDelayedTask(instance, task, instance.config_claims_manualDeliveryDelaySeconds * 20L); - } - } - - //silence notifications when they're coming too fast - if (event.getJoinMessage() != null && this.shouldSilenceNotification()) - { - event.setJoinMessage(null); - } - - //FEATURE: auto-ban accounts who use an IP address which was very recently used by another banned account - if (instance.config_smartBan && !player.hasPlayedBefore()) - { - //search temporarily banned IP addresses for this one - for (int i = 0; i < this.tempBannedIps.size(); i++) - { - IpBanInfo info = this.tempBannedIps.get(i); - String address = info.address.toString(); - - //eliminate any expired entries - if (now > info.expirationTimestamp) - { - this.tempBannedIps.remove(i--); - } - - //if we find a match - else if (address.equals(playerData.ipAddress.toString())) - { - //if the account associated with the IP ban has been pardoned, remove all ip bans for that ip and we're done - OfflinePlayer bannedPlayer = instance.getServer().getOfflinePlayer(info.bannedAccountName); - if (!bannedPlayer.isBanned()) - { - for (int j = 0; j < this.tempBannedIps.size(); j++) - { - IpBanInfo info2 = this.tempBannedIps.get(j); - if (info2.address.toString().equals(address)) - { - OfflinePlayer bannedAccount = instance.getServer().getOfflinePlayer(info2.bannedAccountName); - instance.getServer().getBanList(BanList.Type.NAME).pardon(bannedAccount.getName()); - this.tempBannedIps.remove(j--); - } - } - - break; - } - - //otherwise if that account is still banned, ban this account, too - else - { - GriefPrevention.AddLogEntry("Auto-banned new player " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ").", CustomLogEntryTypes.AdminActivity); - - //notify any online ops - @SuppressWarnings("unchecked") - Collection players = (Collection) instance.getServer().getOnlinePlayers(); - for (Player otherPlayer : players) - { - if (otherPlayer.isOp()) - { - GriefPrevention.sendMessage(otherPlayer, TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); - } - } - - //ban player - PlayerKickBanTask task = new PlayerKickBanTask(player, "", "GriefPrevention Smart Ban - Shared Login:" + info.bannedAccountName, true); - instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 10L); - - //silence join message - event.setJoinMessage(""); - - break; - } - } - } - } - - //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; - } - } - } - - //create a thread to load ignore information - new IgnoreLoaderThread(playerID, playerData.ignoredPlayers).start(); - - //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 && event.getDeathMessage() != null) - { - player.sendMessage(event.getDeathMessage()); //let the player assume his death message was broadcasted to everyone - event.setDeathMessage(null); - } - - this.deathTimestamps.put(player.getUniqueId(), now); - - //these are related to locking dropped items on death to prevent theft - PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); - playerData.dropsAreUnlocked = false; - playerData.receivedDropUnlockAdvertisement = false; - } - - //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; - } - - //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); - boolean isBanned; - - //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); - } - - if (playerData.wasKicked) - { - isBanned = player.isBanned(); - } - else - { - isBanned = false; - } - - //if banned, add IP to the temporary IP ban list - if (isBanned && playerData.ipAddress != null) - { - long now = Calendar.getInstance().getTimeInMillis(); - this.tempBannedIps.add(new IpBanInfo(playerData.ipAddress, now + this.MILLISECONDS_IN_DAY, player.getName())); - } - - //silence notifications when they're coming too fast - if (event.getQuitMessage() != null && this.shouldSilenceNotification()) - { - event.setQuitMessage(null); - } - - //silence notifications when the player is banned - if (isBanned && instance.config_silenceBans) - { - 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(""); - } - } - } - - //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(); - - //in creative worlds, dropping items is blocked - if (instance.creativeRulesApply(player.getLocation())) - { - event.setCancelled(true); - return; - } - - 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() && !player.isDead()) - { - 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) - { - //FEATURE: prevent players from using ender pearls or chorus fruit to gain access to secured claims - if(!instance.config_claims_enderPearlsRequireAccessTrust) return; - - TeleportCause cause = event.getCause(); - if(cause != TeleportCause.CHORUS_FRUIT && cause != TeleportCause.ENDER_PEARL) return; - - Player player = event.getPlayer(); - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim); - if(toClaim == null) return; - - playerData.lastClaim = toClaim; - Supplier noAccessReason = toClaim.checkPermission(player, ClaimPermission.Access, event); - if(noAccessReason == null) return; - - 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.Access, event) == null) - return; - - event.setCancelled(true); - } - - //when a player interacts with a specific part of entity... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) - 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.LOW) - 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 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; - } - } - } - - // Name tags may only be used on entities that the player is allowed to kill. - if (itemInHand.getType() == Material.NAME_TAG) - { - //don't track in worlds where claims are not enabled - if (!instance.claimsEnabledForWorld(entity.getWorld())) return; - - Claim cachedClaim = playerData.lastClaim;; - Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, cachedClaim); - - // Require a claim to handle. - if (claim == null) return; - - Supplier override = () -> - { - String message = dataStore.getMessage(Messages.NoDamageClaimedEntity, claim.getOwnerName()); - if (player.hasPermission("griefprevention.ignoreclaims")) - message += " " + dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); - return message; - }; - - // Check for permission to access containers. - Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event, override); - - // If player has permission, action is allowed. - if (noContainersReason == null) return; - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get()); - } - } - - - - //when a player throws an egg - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerThrowEgg(PlayerEggThrowEvent event) - { - 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); - 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 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 = Set.of(Material.WATER, Material.FARMLAND, Material.DIRT, Material.STONE); - private final Set commonAdjacentBlocks_lava = Set.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; - } - - //otherwise no wilderness dumping in creative mode worlds - else if (instance.creativeRulesApply(block.getLocation())) - { - if (block.getY() >= instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava")) - { - if (bucketEvent.getBucket() == Material.LAVA_BUCKET) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); - bucketEvent.setCancelled(true); - return; - } - } - } - - //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) - { - 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(priority = EventPriority.LOW) - void onPlayerSignOpen(@NotNull PlayerSignOpenEvent event) - { - if (event.getCause() != PlayerSignOpenEvent.Cause.INTERACT || event.getSign().getBlock().getType() != event.getSign().getType()) - { - // If the sign is not opened by interaction or the corresponding block is no longer a sign, - // it is either the initial sign placement or another plugin is at work. Do not interfere. - return; - } - - Player player = event.getPlayer(); - String denial = instance.allowBuild(player, event.getSign().getLocation(), event.getSign().getType()); - - // If user is allowed to build, do nothing. - if (denial == null) - return; - - // If user is not allowed to build, prevent sign UI opening and send message. - GriefPrevention.sendMessage(player, TextMode.Err, denial); - event.setCancelled(true); - } - - //when a player interacts with the world - @EventHandler(priority = EventPriority.LOW) - 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.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.DECORATED_POT - ))) - { - 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; - } - - //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.DOORS.isTagged(clickedBlockType) || - - instance.config_claims_preventButtonsSwitches && Tag.BEDS.isTagged(clickedBlockType) || - - instance.config_claims_lockTrapDoors && Tag.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 redstone and various decor blocks that require full trust - 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) || - 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 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(); - - // Require build permission for items that may have an effect on the world when used. - if (clickedBlock != null && (materialInHand == Material.BONE_MEAL - || materialInHand == Material.ARMOR_STAND - || (spawnEggs.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))) - { - 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) && - !instance.creativeRulesApply(clickedBlock.getLocation())) - { - 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 investigation tool is on cooldown, do nothing. - if (player.getCooldown(instance.config_claims_investigationTool) > 0) return; - // Set investigation tool on cooldown to prevent spamming. - player.setCooldown(instance.config_claims_investigationTool, 1); - - //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 - BoundaryVisualization.visualizeNearbyClaims(player, inspectionEvent.getClaims(), player.getEyeLocation().getBlockY()); - 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; - } - - playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - //air indicates too far away - if (clickedBlockType == Material.AIR) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); - - // Remove visualizations - playerData.setVisibleBoundaries(null); - return; - } - - 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); - - playerData.setVisibleBoundaries(null); - } - - //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 - BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CLAIM); - - 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()); - BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CONFLICT_ZONE, clickedBlock); - 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.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 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 || result.claim == null) - { - if (result.claim != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap); - BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CONFLICT_ZONE, clickedBlock); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion); - } - - return; - } - - //otherwise, advise him on the /trust command and show him his new subdivision - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess); - BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CLAIM, clickedBlock); - 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); - BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CLAIM, clickedBlock); - } - } - - //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()); - BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CONFLICT_ZONE, clickedBlock); - } - - 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 - BoundaryVisualization.visualizeArea(player, new BoundingBox(clickedBlock), VisualizationType.INITIALIZE_ZONE); - } - - //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 || result.claim == null) - { - if (result.claim != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); - BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CONFLICT_ZONE, clickedBlock); - } - 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); - BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CLAIM, clickedBlock); - 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); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionVideo2, 201L, DataStore.SUBDIVISION_VIDEO_URL); - } - - AutoExtendClaimTask.scheduleAsync(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.SHORT_GRASS && - type != Material.SNOW) return result; - } - - return result; - } -} +/* + 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 com.griefprevention.util.command.MonitorableCommand; +import com.griefprevention.util.command.MonitoredCommands; +import com.griefprevention.visualization.BoundaryVisualization; +import com.griefprevention.visualization.VisualizationType; +import me.ryanhamshire.GriefPrevention.events.ClaimInspectionEvent; +import me.ryanhamshire.GriefPrevention.util.BoundingBox; +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.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.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.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.entity.PlayerDeathEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +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.PlayerEvent; +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.PlayerLoginEvent; +import org.bukkit.event.player.PlayerLoginEvent.Result; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerSignOpenEvent; +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.scheduler.BukkitRunnable; +import org.bukkit.util.BlockIterator; +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +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; + + //list of temporarily banned ip's + private final ArrayList tempBannedIps = new ArrayList<>(); + + //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 WordFinder bannedWordFinder; + private MonitoredCommands pvpBlockedCommands; + private MonitoredCommands accessTrustCommands; + private MonitoredCommands chatCommands; + private MonitoredCommands whisperCommands; + + //spam tracker + SpamDetector spamDetector = new SpamDetector(); + // Definitions for specific material groups that do not have a tag + private final Set spawnEggs; + private final Set dyes; + + //typical constructor, yawn + PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) + { + this.dataStore = dataStore; + this.instance = plugin; + // Initialize empty on load so never null just in case. Reload after plugins enable. + this.bannedWordFinder = new WordFinder(List.of()); + this.pvpBlockedCommands = new MonitoredCommands(List.of()); + this.accessTrustCommands = new MonitoredCommands(List.of()); + this.chatCommands = new MonitoredCommands(List.of()); + this.whisperCommands = new MonitoredCommands(List.of()); + + spawnEggs = new HashSet<>(); + dyes = new HashSet<>(); + for (Material material : Material.values()) + { + if (material.name().endsWith("_SPAWN_EGG")) + spawnEggs.add(material); + else if (material.name().endsWith("_DYE")) + dyes.add(material); + } + + reload(); + } + + protected void reload() + { + this.howToClaimPattern = null; + this.bannedWordFinder = new WordFinder(instance.dataStore.loadBannedWords()); + this.pvpBlockedCommands = new MonitoredCommands(instance.config_pvp_blockedCommands); + this.accessTrustCommands = new MonitoredCommands(instance.config_claims_commandsRequiringAccessTrust); + this.chatCommands = new MonitoredCommands(instance.config_spam_monitorSlashCommands); + this.whisperCommands = new MonitoredCommands(instance.config_eavesdrop_whisperCommands); + } + + //when a player chats, monitor for spam + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + synchronized void onPlayerChat(AsyncPlayerChatEvent event) + { + Player player = event.getPlayer(); + if (!player.isOnline()) + { + event.setCancelled(true); + return; + } + + String message = event.getMessage(); + + boolean muted = this.handlePlayerChat(player, message, event); + Set recipients = event.getRecipients(); + + //muted messages go out to only the sender + if (muted) + { + recipients.clear(); + recipients.add(player); + } + + //soft muted messages go out to all soft muted players + else if (this.dataStore.isSoftMuted(player.getUniqueId())) + { + String notificationMessage = "(Muted " + player.getName() + "): " + message; + Set recipientsToKeep = new HashSet<>(); + for (Player recipient : recipients) + { + if (this.dataStore.isSoftMuted(recipient.getUniqueId())) + { + recipientsToKeep.add(recipient); + } + else if (recipient.hasPermission("griefprevention.eavesdrop")) + { + recipient.sendMessage(ChatColor.GRAY + notificationMessage); + } + } + recipients.clear(); + recipients.addAll(recipientsToKeep); + + GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.MutedChat, false); + } + + //troll and excessive profanity filter + else if (!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(message)) + { + //allow admins to see the soft-muted text + String notificationMessage = "(Muted " + player.getName() + "): " + message; + for (Player recipient : recipients) + { + if (recipient.hasPermission("griefprevention.eavesdrop")) + { + recipient.sendMessage(ChatColor.GRAY + notificationMessage); + } + } + + //limit recipients to sender + recipients.clear(); + recipients.add(player); + + //if player not new warn for the first infraction per play session. + if (!GriefPrevention.isNewToServer(player)) + { + PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); + if (!playerData.profanityWarned) + { + playerData.profanityWarned = true; + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoProfanity); + event.setCancelled(true); + return; + } + } + + //otherwise assume chat troll and mute all chat from this sender until an admin says otherwise + else if (instance.config_trollFilterEnabled) + { + GriefPrevention.AddLogEntry("Auto-muted new player " + player.getName() + " for profanity shortly after join. Use /SoftMute to undo.", CustomLogEntryTypes.AdminActivity); + GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.MutedChat, false); + instance.dataStore.toggleSoftMute(player.getUniqueId()); + } + } + + //remaining messages + else + { + //enter in abridged chat logs + makeSocialLogEntry(player.getName(), message); + + //based on ignore lists, remove some of the audience + if (!player.hasPermission("griefprevention.notignorable")) + { + Set recipientsToRemove = new HashSet<>(); + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + for (Player recipient : recipients) + { + if (!recipient.hasPermission("griefprevention.notignorable")) + { + if (playerData.ignoredPlayers.containsKey(recipient.getUniqueId())) + { + recipientsToRemove.add(recipient); + } + else + { + PlayerData targetPlayerData = this.dataStore.getPlayerData(recipient.getUniqueId()); + if (targetPlayerData.ignoredPlayers.containsKey(player.getUniqueId())) + { + recipientsToRemove.add(recipient); + } + } + } + } + + recipients.removeAll(recipientsToRemove); + } + } + } + + //returns true if the message should be muted, true if it should be sent + private boolean handlePlayerChat(Player player, String message, PlayerEvent event) + { + //FEATURE: automatically educate players about claiming land + //watching for message format how*claim*, and will send a link to the basics video + if (this.howToClaimPattern == null) + { + this.howToClaimPattern = Pattern.compile(this.dataStore.getMessage(Messages.HowToClaimRegex), Pattern.CASE_INSENSITIVE); + } + + if (this.howToClaimPattern.matcher(message).matches()) + { + if (instance.creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.CreativeBasicsVideo2, 10L, DataStore.CREATIVE_VIDEO_URL); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsVideo2, 10L, DataStore.SURVIVAL_VIDEO_URL); + } + } + + //FEATURE: automatically educate players about the /trapped command + //check for "trapped" or "stuck" to educate players about the /trapped command + String trappedwords = this.dataStore.getMessage( + Messages.TrappedChatKeyword + ); + if (!trappedwords.isEmpty()) + { + String[] checkWords = trappedwords.split(";"); + + for (String checkWord : checkWords) + { + if (!message.contains("/trapped") + && message.contains(checkWord)) + { + GriefPrevention.sendMessage( + player, + TextMode.Info, + Messages.TrappedInstructions, + 10L + ); + break; + } + } + } + + //FEATURE: monitor for chat and command spam + + if (!instance.config_spam_enabled) return false; + + //if the player has permission to spam, don't bother even examining the message + if (player.hasPermission("griefprevention.spam")) return false; + + //examine recent messages to detect spam + SpamAnalysisResult result = this.spamDetector.AnalyzeMessage(player.getUniqueId(), message, System.currentTimeMillis()); + + //apply any needed changes to message (like lowercasing all-caps) + if (event instanceof AsyncPlayerChatEvent) + { + ((AsyncPlayerChatEvent) event).setMessage(result.finalMessage); + } + + //don't allow new players to chat after logging in until they move + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + if (playerData.noChatLocation != null) + { + Location currentLocation = player.getLocation(); + if (currentLocation.getBlockX() == playerData.noChatLocation.getBlockX() && + currentLocation.getBlockZ() == playerData.noChatLocation.getBlockZ()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoChatUntilMove, 10L); + result.muteReason = "pre-movement chat"; + } + else + { + playerData.noChatLocation = null; + } + } + + //filter IP addresses + if (result.muteReason == null) + { + if (instance.containsBlockedIP(message)) + { + //block message + result.muteReason = "IP address"; + } + } + + //take action based on spam detector results + if (result.shouldBanChatter) + { + if (instance.config_spam_banOffenders) + { + //log entry + GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam.", CustomLogEntryTypes.AdminActivity); + + //kick and ban + PlayerKickBanTask task = new PlayerKickBanTask(player, instance.config_spam_banMessage, "GriefPrevention Anti-Spam", true); + instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 1L); + } + else + { + //log entry + GriefPrevention.AddLogEntry("Kicking " + player.getName() + " for spam.", CustomLogEntryTypes.AdminActivity); + + //just kick + PlayerKickBanTask task = new PlayerKickBanTask(player, "", "GriefPrevention Anti-Spam", false); + instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 1L); + } + } + else if (result.shouldWarnChatter) + { + //warn and log + GriefPrevention.sendMessage(player, TextMode.Warn, instance.config_spam_warningMessage, 10L); + GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties.", CustomLogEntryTypes.Debug, true); + } + + if (result.muteReason != null) + { + //mute and log + GriefPrevention.AddLogEntry("Muted " + result.muteReason + "."); + GriefPrevention.AddLogEntry("Muted " + player.getName() + " " + result.muteReason + ":" + message, CustomLogEntryTypes.Debug, true); + + return true; + } + + return false; + } + + //when a player uses a slash command... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + synchronized void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) + { + MonitorableCommand command = new MonitorableCommand(event.getMessage()); + + CommandCategory category = this.getCommandCategory(command); + + Player player = event.getPlayer(); + PlayerData playerData = null; + + //if a whisper + if (category == CommandCategory.Whisper && command.getArgumentCount() > 1) + { + //determine target player, might be NULL + + Player targetPlayer = instance.getServer().getPlayer(command.getArgument(0)); + + //softmute feature + if (this.dataStore.isSoftMuted(player.getUniqueId()) && targetPlayer != null && !this.dataStore.isSoftMuted(targetPlayer.getUniqueId())) + { + event.setCancelled(true); + return; + } + + //if eavesdrop enabled and sender doesn't have the eavesdrop immunity permission, eavesdrop + if (instance.config_whisperNotifications && !player.hasPermission("griefprevention.eavesdropimmune")) + { + //except for when the recipient has eavesdrop immunity + if (targetPlayer == null || !targetPlayer.hasPermission("griefprevention.eavesdropimmune")) + { + + String logMessage = "[[" + event.getPlayer().getName() + "]] " + + command.getCommand().substring(command.getCommand(0).length() + 1); + + @SuppressWarnings("unchecked") + Collection players = (Collection) instance.getServer().getOnlinePlayers(); + for (Player onlinePlayer : players) + { + if (onlinePlayer.hasPermission("griefprevention.eavesdrop") && !onlinePlayer.equals(targetPlayer) && !onlinePlayer.equals(player)) + { + onlinePlayer.sendMessage(ChatColor.GRAY + logMessage); + } + } + } + } + + //ignore feature + if (targetPlayer != null && targetPlayer.isOnline()) + { + //if either is ignoring the other, cancel this command + playerData = this.dataStore.getPlayerData(player.getUniqueId()); + if (playerData.ignoredPlayers.containsKey(targetPlayer.getUniqueId()) && !targetPlayer.hasPermission("griefprevention.notignorable")) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, Messages.IsIgnoringYou); + return; + } + + PlayerData targetPlayerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); + if (targetPlayerData.ignoredPlayers.containsKey(player.getUniqueId()) && !player.hasPermission("griefprevention.notignorable")) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, Messages.IsIgnoringYou); + return; + } + } + } + + //if in pvp, block any pvp-banned slash commands + if (playerData == null) playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId()); + + if ((playerData.inPvpCombat()) && pvpBlockedCommands.isMonitoredCommand(command)) + { + event.setCancelled(true); + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); + return; + } + + //soft mute for chat slash commands + if (category == CommandCategory.Chat && this.dataStore.isSoftMuted(player.getUniqueId())) + { + event.setCancelled(true); + return; + } + + //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) + boolean isMonitoredCommand = (category == CommandCategory.Chat || category == CommandCategory.Whisper); + if (isMonitoredCommand) + { + //if anti spam enabled, check for spam + if (instance.config_spam_enabled) + { + event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); + } + + if (!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(event.getMessage())) + { + event.setCancelled(true); + } + + //unless cancelled, log in abridged logs + if (!event.isCancelled()) + { + makeSocialLogEntry(event.getPlayer().getName(), event.getMessage()); + } + } + + //if requires access trust, check for permission + if (accessTrustCommands.isMonitoredCommand(command)) + { + 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); + } + } + } + } + + private CommandCategory getCommandCategory(MonitorableCommand command) + { + if (whisperCommands.isMonitoredCommand(command)) return CommandCategory.Whisper; + if (chatCommands.isMonitoredCommand(command)) return CommandCategory.Chat; + return CommandCategory.None; + } + + 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 attempts to join the server... + @EventHandler(priority = EventPriority.HIGHEST) + void onPlayerLogin(PlayerLoginEvent event) + { + Player player = event.getPlayer(); + + //all this is anti-spam code + if (instance.config_spam_enabled) + { + //FEATURE: login cooldown to prevent login/logout spam with custom clients + long now = Calendar.getInstance().getTimeInMillis(); + + //if allowed to join and login cooldown enabled + if (instance.config_spam_loginCooldownSeconds > 0 && event.getResult() == Result.ALLOWED && !player.hasPermission("griefprevention.spam")) + { + //determine how long since last login and cooldown remaining + Date lastLoginThisSession = lastLoginThisServerSessionMap.get(player.getUniqueId()); + if (lastLoginThisSession != null) + { + long millisecondsSinceLastLogin = now - lastLoginThisSession.getTime(); + long secondsSinceLastLogin = millisecondsSinceLastLogin / 1000; + long cooldownRemaining = instance.config_spam_loginCooldownSeconds - secondsSinceLastLogin; + + //if cooldown remaining + if (cooldownRemaining > 0) + { + //DAS BOOT! + event.setResult(Result.KICK_OTHER); + event.setKickMessage("You must wait " + cooldownRemaining + " seconds before logging-in again."); + event.disallow(event.getResult(), event.getKickMessage()); + return; + } + } + } + + //if logging-in account is banned, remember IP address for later + if (instance.config_smartBan && event.getResult() == Result.KICK_BANNED) + { + this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); + } + } + + //remember the player's ip address + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.ipAddress = event.getAddress(); + } + + //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) && !player.hasPermission("griefprevention.premovementchat")) + { + playerData.noChatLocation = player.getLocation(); + } + + //if player has never played on the server before... + if (!player.hasPlayedBefore()) + { + //may need pvp protection + instance.checkPvpProtectionNeeded(player); + + //if in survival claims mode, send a message about the claim basics video (except for admins - assumed experts) + if (instance.config_claims_worldModes.get(player.getWorld()) == ClaimsMode.Survival && !player.hasPermission("griefprevention.adminclaims") && this.dataStore.claims.size() > 10) + { + WelcomeTask task = new WelcomeTask(player); + Bukkit.getScheduler().scheduleSyncDelayedTask(instance, task, instance.config_claims_manualDeliveryDelaySeconds * 20L); + } + } + + //silence notifications when they're coming too fast + if (event.getJoinMessage() != null && this.shouldSilenceNotification()) + { + event.setJoinMessage(null); + } + + //FEATURE: auto-ban accounts who use an IP address which was very recently used by another banned account + if (instance.config_smartBan && !player.hasPlayedBefore()) + { + //search temporarily banned IP addresses for this one + for (int i = 0; i < this.tempBannedIps.size(); i++) + { + IpBanInfo info = this.tempBannedIps.get(i); + String address = info.address.toString(); + + //eliminate any expired entries + if (now > info.expirationTimestamp) + { + this.tempBannedIps.remove(i--); + } + + //if we find a match + else if (address.equals(playerData.ipAddress.toString())) + { + //if the account associated with the IP ban has been pardoned, remove all ip bans for that ip and we're done + OfflinePlayer bannedPlayer = instance.getServer().getOfflinePlayer(info.bannedAccountName); + if (!bannedPlayer.isBanned()) + { + for (int j = 0; j < this.tempBannedIps.size(); j++) + { + IpBanInfo info2 = this.tempBannedIps.get(j); + if (info2.address.toString().equals(address)) + { + OfflinePlayer bannedAccount = instance.getServer().getOfflinePlayer(info2.bannedAccountName); + instance.getServer().getBanList(BanList.Type.NAME).pardon(bannedAccount.getName()); + this.tempBannedIps.remove(j--); + } + } + + break; + } + + //otherwise if that account is still banned, ban this account, too + else + { + GriefPrevention.AddLogEntry("Auto-banned new player " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ").", CustomLogEntryTypes.AdminActivity); + + //notify any online ops + @SuppressWarnings("unchecked") + Collection players = (Collection) instance.getServer().getOnlinePlayers(); + for (Player otherPlayer : players) + { + if (otherPlayer.isOp()) + { + GriefPrevention.sendMessage(otherPlayer, TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); + } + } + + //ban player + PlayerKickBanTask task = new PlayerKickBanTask(player, "", "GriefPrevention Smart Ban - Shared Login:" + info.bannedAccountName, true); + instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, task, 10L); + + //silence join message + event.setJoinMessage(""); + + break; + } + } + } + } + + //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; + } + } + } + + //create a thread to load ignore information + new IgnoreLoaderThread(playerID, playerData.ignoredPlayers).start(); + + //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 && event.getDeathMessage() != null) + { + player.sendMessage(event.getDeathMessage()); //let the player assume his death message was broadcasted to everyone + event.setDeathMessage(null); + } + + this.deathTimestamps.put(player.getUniqueId(), now); + + //these are related to locking dropped items on death to prevent theft + PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); + playerData.dropsAreUnlocked = false; + playerData.receivedDropUnlockAdvertisement = false; + } + + //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; + } + + //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); + boolean isBanned; + + //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); + } + + if (playerData.wasKicked) + { + isBanned = player.isBanned(); + } + else + { + isBanned = false; + } + + //if banned, add IP to the temporary IP ban list + if (isBanned && playerData.ipAddress != null) + { + long now = Calendar.getInstance().getTimeInMillis(); + this.tempBannedIps.add(new IpBanInfo(playerData.ipAddress, now + this.MILLISECONDS_IN_DAY, player.getName())); + } + + //silence notifications when they're coming too fast + if (event.getQuitMessage() != null && this.shouldSilenceNotification()) + { + event.setQuitMessage(null); + } + + //silence notifications when the player is banned + if (isBanned && instance.config_silenceBans) + { + 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(""); + } + } + } + + //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(); + + //in creative worlds, dropping items is blocked + if (instance.creativeRulesApply(player.getLocation())) + { + event.setCancelled(true); + return; + } + + 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() && !player.isDead()) + { + 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) + { + //FEATURE: prevent players from using ender pearls or chorus fruit to gain access to secured claims + if(!instance.config_claims_enderPearlsRequireAccessTrust) return; + + TeleportCause cause = event.getCause(); + if(cause != TeleportCause.CHORUS_FRUIT && cause != TeleportCause.ENDER_PEARL) return; + + Player player = event.getPlayer(); + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim); + if(toClaim == null) return; + + playerData.lastClaim = toClaim; + Supplier noAccessReason = toClaim.checkPermission(player, ClaimPermission.Access, event); + if(noAccessReason == null) return; + + 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.Access, event) == null) + return; + + event.setCancelled(true); + } + + //when a player interacts with a specific part of entity... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + 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.LOW) + 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 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; + } + } + } + + // Name tags may only be used on entities that the player is allowed to kill. + if (itemInHand.getType() == Material.NAME_TAG) + { + //don't track in worlds where claims are not enabled + if (!instance.claimsEnabledForWorld(entity.getWorld())) return; + + Claim cachedClaim = playerData.lastClaim;; + Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, cachedClaim); + + // Require a claim to handle. + if (claim == null) return; + + Supplier override = () -> + { + String message = dataStore.getMessage(Messages.NoDamageClaimedEntity, claim.getOwnerName()); + if (player.hasPermission("griefprevention.ignoreclaims")) + message += " " + dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return message; + }; + + // Check for permission to access containers. + Supplier noContainersReason = claim.checkPermission(player, ClaimPermission.Inventory, event, override); + + // If player has permission, action is allowed. + if (noContainersReason == null) return; + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason.get()); + } + } + + + + //when a player throws an egg + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerThrowEgg(PlayerEggThrowEvent event) + { + 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); + 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 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 = Set.of(Material.WATER, Material.FARMLAND, Material.DIRT, Material.STONE); + private final Set commonAdjacentBlocks_lava = Set.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; + } + + //otherwise no wilderness dumping in creative mode worlds + else if (instance.creativeRulesApply(block.getLocation())) + { + if (block.getY() >= instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava")) + { + if (bucketEvent.getBucket() == Material.LAVA_BUCKET) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); + bucketEvent.setCancelled(true); + return; + } + } + } + + //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) + { + 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(priority = EventPriority.LOW) + void onPlayerSignOpen(@NotNull PlayerSignOpenEvent event) + { + if (event.getCause() != PlayerSignOpenEvent.Cause.INTERACT || event.getSign().getBlock().getType() != event.getSign().getType()) + { + // If the sign is not opened by interaction or the corresponding block is no longer a sign, + // it is either the initial sign placement or another plugin is at work. Do not interfere. + return; + } + + Player player = event.getPlayer(); + String denial = instance.allowBuild(player, event.getSign().getLocation(), event.getSign().getType()); + + // If user is allowed to build, do nothing. + if (denial == null) + return; + + // If user is not allowed to build, prevent sign UI opening and send message. + GriefPrevention.sendMessage(player, TextMode.Err, denial); + event.setCancelled(true); + } + + //when a player interacts with the world + @EventHandler(priority = EventPriority.LOW) + 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.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.DECORATED_POT + ))) + { + 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; + } + + //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.DOORS.isTagged(clickedBlockType) || + + instance.config_claims_preventButtonsSwitches && Tag.BEDS.isTagged(clickedBlockType) || + + instance.config_claims_lockTrapDoors && Tag.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 redstone and various decor blocks that require full trust + 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) || + 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 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(); + + // Require build permission for items that may have an effect on the world when used. + if (clickedBlock != null && (materialInHand == Material.BONE_MEAL + || materialInHand == Material.ARMOR_STAND + || (spawnEggs.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))) + { + 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) && + !instance.creativeRulesApply(clickedBlock.getLocation())) + { + 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 investigation tool is on cooldown, do nothing. + if (player.getCooldown(instance.config_claims_investigationTool) > 0) return; + // Set investigation tool on cooldown to prevent spamming. + player.setCooldown(instance.config_claims_investigationTool, 1); + + //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 + BoundaryVisualization.visualizeNearbyClaims(player, inspectionEvent.getClaims(), player.getEyeLocation().getBlockY()); + 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; + } + + playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + //air indicates too far away + if (clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); + + // Remove visualizations + playerData.setVisibleBoundaries(null); + return; + } + + 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); + + playerData.setVisibleBoundaries(null); + } + + //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 + BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CLAIM); + + 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()); + BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CONFLICT_ZONE, clickedBlock); + 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.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 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 || result.claim == null) + { + if (result.claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap); + BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CONFLICT_ZONE, clickedBlock); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion); + } + + return; + } + + //otherwise, advise him on the /trust command and show him his new subdivision + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess); + BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CLAIM, clickedBlock); + 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); + BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CLAIM, clickedBlock); + } + } + + //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()); + BoundaryVisualization.visualizeClaim(player, claim, VisualizationType.CONFLICT_ZONE, clickedBlock); + } + + 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 + BoundaryVisualization.visualizeArea(player, new BoundingBox(clickedBlock), VisualizationType.INITIALIZE_ZONE); + } + + //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 || result.claim == null) + { + if (result.claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); + BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CONFLICT_ZONE, clickedBlock); + } + 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); + BoundaryVisualization.visualizeClaim(player, result.claim, VisualizationType.CLAIM, clickedBlock); + 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); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionVideo2, 201L, DataStore.SUBDIVISION_VIDEO_URL); + } + + AutoExtendClaimTask.scheduleAsync(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.SHORT_GRASS && + type != Material.SNOW) return result; + } + + return result; + } +}