/* 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 java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bukkit.Achievement; 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.TravelAgent; import org.bukkit.World.Environment; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Animals; import org.bukkit.entity.Boat; import org.bukkit.entity.Creature; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Hanging; import org.bukkit.entity.Horse; import org.bukkit.entity.Item; 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.*; import org.bukkit.event.player.PlayerLoginEvent.Result; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.MetadataValue; import org.bukkit.util.BlockIterator; class PlayerEventHandler implements Listener { private DataStore dataStore; //list of temporarily banned ip's private 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 ArrayList recentLoginLogoutNotifications = new ArrayList(); //regex pattern for the "how do i claim land?" scanner private Pattern howToClaimPattern = null; //typical constructor, yawn PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) { this.dataStore = dataStore; } //when a player chats, monitor for spam @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) 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())) { 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 + "(Muted " + player.getName() + "): " + message); } } recipients.clear(); recipients.addAll(recipientsToKeep); } } //last chat message shown, regardless of who sent it private String lastChatMessage = ""; private long lastChatMessageTimestamp = 0; //number of identical messages in a row private int duplicateMessageCount = 0; //returns true if the message should be sent, false if it should be muted 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(GriefPrevention.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 if(!message.contains("/trapped") && (message.contains("trapped") || message.contains("stuck") || message.contains(this.dataStore.getMessage(Messages.TrappedChatKeyword)))) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrappedInstructions, 10L); } //FEATURE: monitor for chat and command spam if(!GriefPrevention.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; boolean spam = false; boolean muted = false; //prevent bots from chatting - require movement before talking for any newish players 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); spam = true; muted = true; } else { playerData.noChatLocation = null; } } //remedy any CAPS SPAM, exception for very short messages which could be emoticons like =D or XD if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message)) { //exception for strings containing forward slash to avoid changing a case-sensitive URL if(event instanceof AsyncPlayerChatEvent && !message.contains("/")) { ((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase()); } } //always mute an exact match to the last chat message long now = new Date().getTime(); if(message.equals(this.lastChatMessage) && now - this.lastChatMessageTimestamp < 750) { playerData.spamCount += ++this.duplicateMessageCount; spam = true; muted = true; } else { this.lastChatMessage = message; this.lastChatMessageTimestamp = now; this.duplicateMessageCount = 0; } //where other types of spam are concerned, casing isn't significant message = message.toLowerCase(); //check message content and timing long millisecondsSinceLastMessage = now - playerData.lastMessageTimestamp.getTime(); //if the message came too close to the last one if(millisecondsSinceLastMessage < 1500) { //increment the spam counter playerData.spamCount++; spam = true; } //if it's very similar to the last message from the same player and within 10 seconds of that message if(!muted && this.stringsAreSimilar(message, playerData.lastMessage) && now - playerData.lastMessageTimestamp.getTime() < 10000) { playerData.spamCount++; spam = true; muted = true; } //filter IP addresses if(!muted) { Pattern ipAddressPattern = Pattern.compile("([0-9]{1,3}\\.){3}[0-9]{1,3}"); Matcher matcher = ipAddressPattern.matcher(message); //if it looks like an IP address if(matcher.find()) { //and it's not in the list of allowed IP addresses if(!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) { //log entry GriefPrevention.AddLogEntry("Muted IP address from " + player.getName() + ": " + message); //spam notation playerData.spamCount++; spam = true; //block message muted = true; } } } //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish) if(!muted && message.length() > 5) { int symbolsCount = 0; int whitespaceCount = 0; for(int i = 0; i < message.length(); i++) { char character = message.charAt(i); if(!(Character.isLetterOrDigit(character))) { symbolsCount++; } if(Character.isWhitespace(character)) { whitespaceCount++; } } if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10)) { spam = true; if(playerData.spamCount > 0) muted = true; playerData.spamCount++; } } //very short messages close together are spam if(!muted && message.length() < 5 && millisecondsSinceLastMessage < 3000) { spam = true; playerData.spamCount++; } //if the message was determined to be a spam, consider taking action if(spam) { //anything above level 8 for a player which has received a warning... kick or if enabled, ban if(playerData.spamCount > 8 && playerData.spamWarned) { if(GriefPrevention.instance.config_spam_banOffenders) { //log entry GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); //kick and ban PlayerKickBanTask task = new PlayerKickBanTask(player, GriefPrevention.instance.config_spam_banMessage); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 1L); } else { //log entry GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); //just kick PlayerKickBanTask task = new PlayerKickBanTask(player, null); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 1L); } return true; } //cancel any messages while at or above the third spam level and issue warnings //anything above level 2, mute and warn if(playerData.spamCount >= 4) { muted = true; if(!playerData.spamWarned) { GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L); GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); playerData.spamWarned = true; } } if(muted) { //make a log entry GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); //cancelling the event guarantees other players don't receive the message return true; } } //otherwise if not a spam, reset the spam counter for this player else { playerData.spamCount = 0; playerData.spamWarned = false; } //in any case, record the timestamp of this message and also its content for next time playerData.lastMessageTimestamp = new Date(); playerData.lastMessage = message; return false; } //if two strings are 75% identical, they're too close to follow each other in the chat private boolean stringsAreSimilar(String message, String lastMessage) { //determine which is shorter String shorterString, longerString; if(lastMessage.length() < message.length()) { shorterString = lastMessage; longerString = message; } else { shorterString = message; longerString = lastMessage; } if(shorterString.length() <= 5) return shorterString.equals(longerString); //set similarity tolerance int maxIdenticalCharacters = longerString.length() - longerString.length() / 4; //trivial check on length if(shorterString.length() < maxIdenticalCharacters) return false; //compare forward int identicalCount = 0; int i; for(i = 0; i < shorterString.length(); i++) { if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++; if(identicalCount > maxIdenticalCharacters) return true; } //compare backward int j; for(j = 0; j < shorterString.length() - i; j++) { if(shorterString.charAt(shorterString.length() - j - 1) == longerString.charAt(longerString.length() - j - 1)) identicalCount++; if(identicalCount > maxIdenticalCharacters) return true; } return false; } //when a player uses a slash command... @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) synchronized void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) { String [] args = event.getMessage().split(" "); //if eavesdrop enabled, eavesdrop String command = args[0].toLowerCase(); if(GriefPrevention.instance.config_whisperNotifications && GriefPrevention.instance.config_eavesdrop_whisperCommands.contains(command) && !event.getPlayer().hasPermission("griefprevention.eavesdrop") && args.length > 1) { StringBuilder logMessageBuilder = new StringBuilder(); logMessageBuilder.append("[[").append(event.getPlayer().getName()).append("]] "); for(int i = 1; i < args.length; i++) { logMessageBuilder.append(args[i]).append(" "); } String logMessage = logMessageBuilder.toString(); Collection players = (Collection)GriefPrevention.instance.getServer().getOnlinePlayers(); for(Player player : players) { if(player.hasPermission("griefprevention.eavesdrop") && !player.getName().equalsIgnoreCase(args[1])) { player.sendMessage(ChatColor.GRAY + logMessage); } } } //if in pvp, block any pvp-banned slash commands PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId()); if((playerData.inPvpCombat() || playerData.siegeData != null) && GriefPrevention.instance.config_pvp_blockedCommands.contains(command)) { event.setCancelled(true); GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); return; } //if anti spam enabled, check for spam if(!GriefPrevention.instance.config_spam_enabled) return; //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) boolean isMonitoredCommand = false; for(String monitoredCommand : GriefPrevention.instance.config_spam_monitorSlashCommands) { if(args[0].equalsIgnoreCase(monitoredCommand)) { isMonitoredCommand = true; break; } } if(isMonitoredCommand) { event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); } } private 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(GriefPrevention.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(GriefPrevention.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 = GriefPrevention.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(GriefPrevention.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; playerData.setLastLogin(nowDate); this.lastLoginThisServerSessionMap.put(playerID, nowDate); //if newish, prevent chat until he's moved a bit to prove he's not a bot if(!player.hasAchievement(Achievement.MINE_WOOD)) { playerData.noChatLocation = player.getLocation(); } //if player has never played on the server before... if(!player.hasPlayedBefore()) { //may need pvp protection GriefPrevention.instance.checkPvpProtectionNeeded(player); //if in survival claims mode, send a message about the claim basics video (except for admins - assumed experts) if(GriefPrevention.instance.config_claims_worldModes.get(player.getWorld()) == ClaimsMode.Survival && !player.hasPermission("griefprevention.adminclaims") && this.dataStore.claims.size() > 10) { GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AvoidGriefClaimLand, 600L); GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SurvivalBasicsVideo2, 601L, DataStore.SURVIVAL_VIDEO_URL); } } //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(GriefPrevention.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 = GriefPrevention.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 = GriefPrevention.instance.getServer().getOfflinePlayer(info2.bannedAccountName); bannedAccount.setBanned(false); this.tempBannedIps.remove(j--); } } break; } //otherwise if that account is still banned, ban this account, too else { GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ")."); //notify any online ops Collection players = (Collection)GriefPrevention.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.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.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()); } //when a player spawns, conditionally apply temporary pvp protection @EventHandler(ignoreCancelled = true) void onPlayerRespawn (PlayerRespawnEvent event) { Player player = event.getPlayer(); PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()); playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); //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; } GriefPrevention.instance.checkPvpProtectionNeeded(player); } //when a player dies... @EventHandler(priority = EventPriority.LOWEST) void onPlayerDeath(PlayerDeathEvent event) { //FEATURE: prevent death message spam by implementing a "cooldown period" for death messages PlayerData playerData = this.dataStore.getPlayerData(event.getEntity().getUniqueId()); long now = Calendar.getInstance().getTimeInMillis(); if(now - playerData.lastDeathTimeStamp < GriefPrevention.instance.config_spam_deathMessageCooldownSeconds * 1000) { event.setDeathMessage(""); } playerData.lastDeathTimeStamp = now; //these are related to locking dropped items on death to prevent theft 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... @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(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) { 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(GriefPrevention.instance.config_pvp_punishLogout && playerData.inPvpCombat()) { player.setHealth(0); } //FEATURE: during a siege, any player who logs out dies and forfeits the siege //if player was involved in a siege, he forfeits if(playerData.siegeData != null) { if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message } //drop data about this player this.dataStore.clearCachedPlayerData(playerID); } //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute private boolean shouldSilenceNotification() { final long ONE_MINUTE = 60000; final int MAX_ALLOWED = 20; 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() > MAX_ALLOWED; } //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(GriefPrevention.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(!GriefPrevention.instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat()) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop); event.setCancelled(true); } //if he's under siege, don't let him drop it else if(playerData.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); 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; //don't track in worlds where claims are not enabled if(!GriefPrevention.instance.claimsEnabledForWorld(event.getTo().getWorld())) 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 CheckForPortalTrapTask task = new CheckForPortalTrapTask(player, event.getFrom()); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 100L); //FEATURE: if the player teleporting doesn't have permission to build a nether portal and none already exists at the destination, cancel the teleportation if(GriefPrevention.instance.config_claims_portalsRequirePermission) { Location destination = event.getTo(); if(event.useTravelAgent()) { if(event.getPortalTravelAgent().getCanCreatePortal()) { //hypothetically find where the portal would be created if it were TravelAgent agent = event.getPortalTravelAgent(); agent.setCanCreatePortal(false); destination = agent.findOrCreate(destination); agent.setCanCreatePortal(true); } else { //if not able to create a portal, we don't have to do anything here return; } } //if creating a new portal if(destination.getBlock().getType() != Material.PORTAL) { //check for a land claim and the player's permission that land claim Claim claim = this.dataStore.getClaimAt(destination, false, null); if(claim != null && claim.allowBuild(player, Material.PORTAL) != null) { //cancel and inform about the reason event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoBuildPortalPermission, claim.getOwnerName()); } } } } } //when a player teleports @EventHandler(priority = EventPriority.LOWEST) public void onPlayerTeleport(PlayerTeleportEvent event) { Player player = event.getPlayer(); PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); //FEATURE: prevent players from using ender pearls to gain access to secured claims if(event.getCause() == TeleportCause.ENDER_PEARL && GriefPrevention.instance.config_claims_enderPearlsRequireAccessTrust) { Claim toClaim = this.dataStore.getClaimAt(event.getTo(), false, playerData.lastClaim); if(toClaim != null) { playerData.lastClaim = toClaim; String noAccessReason = toClaim.allowAccess(player); if(noAccessReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); event.setCancelled(true); } } } //FEATURE: prevent teleport abuse to win sieges //these rules only apply to siege worlds only if(!GriefPrevention.instance.config_siege_enabledWorlds.contains(player.getWorld())) return; Location source = event.getFrom(); Claim sourceClaim = this.dataStore.getClaimAt(source, false, playerData.lastClaim); if(sourceClaim != null && sourceClaim.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoTeleport); event.setCancelled(true); return; } Location destination = event.getTo(); Claim destinationClaim = this.dataStore.getClaimAt(destination, false, null); if(destinationClaim != null && destinationClaim.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.BesiegedNoTeleport); event.setCancelled(true); return; } } //when a player interacts with a specific part of entity... @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) { //treat it the same as interacting with an entity in general if(event.getRightClicked().getType() == EntityType.ARMOR_STAND) { this.onPlayerInteractEntity((PlayerInteractEntityEvent)event); } } //when a player interacts with an entity... @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { Player player = event.getPlayer(); Entity entity = event.getRightClicked(); if(!GriefPrevention.instance.claimsEnabledForWorld(entity.getWorld())) return; //allow horse protection to be overridden to allow management from other plugins if (!GriefPrevention.instance.config_claims_protectHorses && entity instanceof Horse) 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() && 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(!GriefPrevention.instance.config_pvp_enabledWorlds.contains(entity.getLocation().getWorld())) { //otherwise disallow OfflinePlayer owner = GriefPrevention.instance.getServer().getOfflinePlayer(ownerID); String ownerName = owner.getName(); if(ownerName == null) ownerName = "someone"; String message = GriefPrevention.instance.dataStore.getMessage(Messages.NotYourPet, ownerName); if(player.hasPermission("griefprevention.ignoreclaims")) message += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); GriefPrevention.sendMessage(player, TextMode.Err, message); event.setCancelled(true); return; } } } //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 = GriefPrevention.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.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); event.setCancelled(true); return; } 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(GriefPrevention.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) { String noContainersReason = claim.allowContainers(player); if(noContainersReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); event.setCancelled(true); return; } } //for boats, apply access rules else if(entity instanceof Boat) { String noAccessReason = claim.allowAccess(player); if(noAccessReason != null) { player.sendMessage(noAccessReason); event.setCancelled(true); return; } } } } //if the entity is an animal, apply container rules if(GriefPrevention.instance.config_claims_preventTheft && (entity instanceof Animals || entity.getType() == EntityType.VILLAGER)) { //if the entity is in a claim Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null); if(claim != null) { if(claim.allowContainers(player) != null) { String message = GriefPrevention.instance.dataStore.getMessage(Messages.NoDamageClaimedEntity, claim.getOwnerName()); if(player.hasPermission("griefprevention.ignoreclaims")) message += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); GriefPrevention.sendMessage(player, TextMode.Err, message); event.setCancelled(true); return; } } } //if preventing theft, prevent leashing claimed creatures if(GriefPrevention.instance.config_claims_preventTheft && entity instanceof Creature && player.getItemInHand().getType() == Material.LEASH) { Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim); if(claim != null) { String failureReason = claim.allowContainers(player); if(failureReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, failureReason); return; } } } } //when a player picks up an item... @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerPickupItem(PlayerPickupItemEvent event) { Player player = event.getPlayer(); //FEATURE: lock dropped items to player who dropped them //who owns this stack? Item item = event.getItem(); List data = item.getMetadata("GP_ITEMOWNER"); if(data != null && data.size() > 0) { UUID ownerID = (UUID)data.get(0).value(); //has that player unlocked his drops? OfflinePlayer owner = GriefPrevention.instance.getServer().getOfflinePlayer(ownerID); String ownerName = GriefPrevention.lookupPlayerName(ownerID); if(owner.isOnline() && !player.equals(owner)) { PlayerData playerData = this.dataStore.getPlayerData(ownerID); //if locked, don't allow pickup if(!playerData.dropsAreUnlocked) { event.setCancelled(true); //if hasn't been instructed how to unlock, send explanatory messages if(!playerData.receivedDropUnlockAdvertisement) { GriefPrevention.sendMessage(owner.getPlayer(), TextMode.Instr, Messages.DropUnlockAdvertisement); GriefPrevention.sendMessage(player, TextMode.Err, Messages.PickupBlockedExplanation, ownerName); playerData.receivedDropUnlockAdvertisement = true; } } } } //the rest of this code is specific to pvp worlds if(!GriefPrevention.instance.config_pvp_enabledWorlds.contains(player.getWorld())) return; //if we're preventing spawn camping and the player was previously empty handed... if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (player.getItemInHand().getType() == Material.AIR)) { //if that player is currently immune to pvp PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId()); if(playerData.pvpImmune) { //if it's been less than 10 seconds since the last time he spawned, don't pick up the item long now = Calendar.getInstance().getTimeInMillis(); long elapsedSinceLastSpawn = now - playerData.lastSpawn; if(elapsedSinceLastSpawn < 10000) { event.setCancelled(true); return; } //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot playerData.pvpImmune = false; GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); } } } //when a player switches in-hand items @EventHandler(ignoreCancelled = true) public void onItemHeldChange(PlayerItemHeldEvent event) { Player player = event.getPlayer(); //if he's switching to the golden shovel ItemStack newItemStack = player.getInventory().getItem(event.getNewSlot()); if(newItemStack != null && newItemStack.getType() == GriefPrevention.instance.config_claims_modificationTool) { PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()); //always reset to basic claims mode if(playerData.shovelMode != ShovelMode.Basic) { playerData.shovelMode = ShovelMode.Basic; GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShovelBasicClaimMode); } //reset any work he might have been doing playerData.lastShovelLocation = null; playerData.claimResizing = null; //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(GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) { EquipShovelProcessingTask task = new EquipShovelProcessingTask(player); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 15L); //15L is approx. 3/4 of a second } } } //block use of buckets within other players' claims @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) { if(!GriefPrevention.instance.claimsEnabledForWorld(bucketEvent.getBlockClicked().getWorld())) return; Player player = bucketEvent.getPlayer(); Block block = bucketEvent.getBlockClicked().getRelative(bucketEvent.getBlockFace()); int minLavaDistance = 10; //make sure the player is allowed to build at the location String noBuildReason = GriefPrevention.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(GriefPrevention.instance.creativeRulesApply(block.getLocation())) { if(block.getY() >= GriefPrevention.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(!GriefPrevention.instance.config_pvp_enabledWorlds.contains(block.getWorld()) && !player.hasPermission("griefprevention.lava")) { if(bucketEvent.getBucket() == Material.LAVA_BUCKET) { List players = block.getWorld().getPlayers(); for(int i = 0; i < players.size(); i++) { Player otherPlayer = players.get(i); Location location = otherPlayer.getLocation(); if(!otherPlayer.equals(player) && otherPlayer.getGameMode() == GameMode.SURVIVAL && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, "another player"); bucketEvent.setCancelled(true); return; } } } } } //see above @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) { Player player = bucketEvent.getPlayer(); Block block = bucketEvent.getBlockClicked(); if(!GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) return; //make sure the player is allowed to build at the location String noBuildReason = GriefPrevention.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 || blockType.isSolid()) return; GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); bucketEvent.setCancelled(true); return; } } //when a player interacts with the world @EventHandler(priority = EventPriority.LOWEST) void onPlayerInteract(PlayerInteractEvent event) { //not interested in left-click-on-air actions Action action = event.getAction(); if(action == Action.LEFT_CLICK_AIR) return; if(action == Action.PHYSICAL) 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; } //don't care about left-clicking on most blocks, this is probably a break action PlayerData playerData = null; if(action == Action.LEFT_CLICK_BLOCK && clickedBlock != null) { //exception for blocks on a specific watch list if(!this.onLeftClickWatchList(clickedBlockType) && !GriefPrevention.instance.config_mods_accessTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))) { //and an exception for putting our fires if(GriefPrevention.instance.config_claims_protectFires && event.getClickedBlock() != null) { Block adjacentBlock = event.getClickedBlock().getRelative(event.getBlockFace()); if(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; String noBuildReason = claim.allowBuild(player, Material.AIR); if(noBuildReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); player.sendBlockChange(adjacentBlock.getLocation(), adjacentBlock.getTypeId(), adjacentBlock.getData()); return; } } } } return; } } //apply rules for containers and crafting blocks if( clickedBlock != null && GriefPrevention.instance.config_claims_preventTheft && ( event.getAction() == Action.RIGHT_CLICK_BLOCK && ( this.isInventoryHolder(clickedBlock) || clickedBlockType == Material.CAULDRON || clickedBlockType == Material.JUKEBOX || clickedBlockType == Material.ANVIL || GriefPrevention.instance.config_mods_containerTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))))) { if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); //block container use while under siege, so players can't hide items from attackers if(playerData.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); event.setCancelled(true); return; } //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; String noContainersReason = claim.allowContainers(player); if(noContainersReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); 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 && (GriefPrevention.instance.config_claims_lockWoodenDoors && ( clickedBlockType == Material.WOODEN_DOOR || clickedBlockType == Material.ACACIA_DOOR || clickedBlockType == Material.BIRCH_DOOR || clickedBlockType == Material.JUNGLE_DOOR || clickedBlockType == Material.SPRUCE_DOOR || clickedBlockType == Material.DARK_OAK_DOOR)) || (GriefPrevention.instance.config_claims_preventButtonsSwitches && clickedBlockType == Material.BED_BLOCK) || (GriefPrevention.instance.config_claims_lockTrapDoors && ( clickedBlockType == Material.TRAP_DOOR)) || (GriefPrevention.instance.config_claims_lockFenceGates && ( clickedBlockType == Material.FENCE_GATE || clickedBlockType == Material.ACACIA_FENCE_GATE || clickedBlockType == Material.BIRCH_FENCE_GATE || clickedBlockType == Material.JUNGLE_FENCE_GATE || clickedBlockType == Material.SPRUCE_FENCE_GATE || clickedBlockType == Material.DARK_OAK_FENCE_GATE))) { 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; String noAccessReason = claim.allowAccess(player); if(noAccessReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); return; } } } //otherwise apply rules for buttons and switches else if(clickedBlock != null && GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == null || clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.WOOD_BUTTON || clickedBlockType == Material.LEVER || GriefPrevention.instance.config_mods_accessTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null)))) { 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; String noAccessReason = claim.allowAccess(player); if(noAccessReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); return; } } } //apply rule for note blocks and repeaters and daylight sensors else if(clickedBlock != null && ( clickedBlockType == Material.NOTE_BLOCK || clickedBlockType == Material.DIODE_BLOCK_ON || clickedBlockType == Material.DIODE_BLOCK_OFF) || clickedBlockType == Material.DAYLIGHT_DETECTOR || clickedBlockType == Material.DAYLIGHT_DETECTOR_INVERTED ) { if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); if(claim != null) { String noBuildReason = claim.allowBuild(player, clickedBlockType); if(noBuildReason != null) { event.setCancelled(true); GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); return; } } } //otherwise handle right click (shovel, string, bonemeal) 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? ItemStack itemInHand = player.getItemInHand(); Material materialInHand = itemInHand.getType(); //if it's bonemeal or armor stand, check for build permission (ink sac == bone meal, must be a Bukkit bug?) if(clickedBlock != null && (materialInHand == Material.INK_SACK || materialInHand == Material.ARMOR_STAND)) { String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation(), clickedBlockType); if(noBuildReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); event.setCancelled(true); } return; } else if(clickedBlock != null && materialInHand == Material.BOAT) { if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); if(claim != null) { String noAccessReason = claim.allowAccess(player); if(noAccessReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); event.setCancelled(true); } } return; } //if it's a spawn egg, minecart, or boat, and this is a creative world, apply special rules else if(clickedBlock != null && (materialInHand == Material.MONSTER_EGG || materialInHand == Material.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.BOAT) && GriefPrevention.instance.creativeRulesApply(clickedBlock.getLocation())) { //player needs build permission at this location String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation(), Material.MINECART); if(noBuildReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); event.setCancelled(true); return; } //enforce limit on total number of entities in this claim if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); if(claim == null) return; String noEntitiesReason = claim.allowMoreEntities(); if(noEntitiesReason != null) { GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason); event.setCancelled(true); return; } return; } //if he's investigating a claim else if(materialInHand == GriefPrevention.instance.config_claims_investigationTool) { //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()); //visualize boundaries Visualization visualization = Visualization.fromClaims(claims, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShowNearbyClaims, String.valueOf(claims.size())); return; } //FEATURE: shovel and stick can be used from a distance away if(action == Action.RIGHT_CLICK_AIR) { //try to find a far away non-air block along line of sight clickedBlock = getTargetBlock(player, 100); clickedBlockType = clickedBlock.getType(); } //if no block, stop here if(clickedBlock == null) { return; } //air indicates too far away if(clickedBlockType == Material.AIR) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); Visualization.Revert(player); return; } if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, playerData.lastClaim); //no claim case if(claim == null) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockNotClaimed); Visualization.Revert(player); } //claim case else { playerData.lastClaim = claim; GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockClaimed, claim.getOwnerName()); //visualize boundary Visualization visualization = Visualization.FromClaim(claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); //if can resize this claim, tell about the boundaries if(claim.allowEdit(player) == null) { GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea()); } //if deleteclaims permission, tell about the player's offline time if(!claim.isAdminClaim() && player.hasPermission("griefprevention.deleteclaims")) { if(claim.parent != null) { claim = claim.parent; } PlayerData otherPlayerData = this.dataStore.getPlayerData(claim.ownerID); Date lastLogin = otherPlayerData.getLastLogin(); 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(GriefPrevention.instance.getServer().getPlayer(claim.ownerID) == null) this.dataStore.clearCachedPlayerData(claim.ownerID); } } return; } //if holding a non-vanilla item else if(Material.getMaterial(itemInHand.getTypeId()) == null) { //assume it's a long range tool and project out ahead if(action == Action.RIGHT_CLICK_AIR) { //try to find a far away non-air block along line of sight clickedBlock = getTargetBlock(player, 100); } //if target is claimed, require build trust permission if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); if(claim != null) { String reason = claim.allowBreak(player, Material.AIR); if(reason != null) { GriefPrevention.sendMessage(player, TextMode.Err, reason); event.setCancelled(true); return; } } return; } //if it's a golden shovel else if(materialInHand != GriefPrevention.instance.config_claims_modificationTool) return; //disable golden shovel while under siege if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId()); if(playerData.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoShovel); event.setCancelled(true); 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; } //can't use the shovel from too far away if(clickedBlockType == Material.AIR) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); return; } //if the player is in restore nature mode, do only that UUID playerID = player.getUniqueId(); playerData = this.dataStore.getPlayerData(player.getUniqueId()); if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive) { //if the clicked block is in a claim, visualize that claim and deliver an error message Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); if(claim != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName()); Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); Visualization.Apply(player, visualization); return; } //figure out which chunk to repair Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); //start the repair process //set boundaries for processing int miny = clickedBlock.getY(); //if not in aggressive mode, extend the selection down to a little below sea level if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive)) { if(miny > GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10) { miny = GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10; } } GriefPrevention.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.ENDER_STONE); } else { allowedFillBlocks.add(Material.GRASS); allowedFillBlocks.add(Material.DIRT); allowedFillBlocks.add(Material.STONE); allowedFillBlocks.add(Material.SAND); allowedFillBlocks.add(Material.SANDSTONE); allowedFillBlocks.add(Material.ICE); } Block centerBlock = clickedBlock; int maxHeight = centerBlock.getY(); int minx = centerBlock.getX() - playerData.fillRadius; int maxx = centerBlock.getX() + playerData.fillRadius; int minz = centerBlock.getZ() - playerData.fillRadius; int maxz = centerBlock.getZ() + playerData.fillRadius; int minHeight = maxHeight - 10; if(minHeight < 0) minHeight = 0; 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 || centerBlock.getType() == Material.STATIONARY_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.STATIONARY_WATER && block.getData() != 0) || block.getType() == Material.LONG_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(); } else { newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX(); } if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockX()) { newx2 = clickedBlock.getX(); } else { newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX(); } if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ()) { newz1 = clickedBlock.getZ(); } else { newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ(); } if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ()) { newz2 = clickedBlock.getZ(); } else { newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ(); } newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY(); newy2 = clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance; //for top level claims, apply size rules and claim blocks requirement if(playerData.claimResizing.parent == null) { //measure new claim, apply size rules int newWidth = (Math.abs(newx1 - newx2) + 1); int newHeight = (Math.abs(newz1 - newz2) + 1); boolean smaller = newWidth < playerData.claimResizing.getWidth() || newHeight < playerData.claimResizing.getHeight(); if(!player.hasPermission("griefprevention.adminclaims") && !playerData.claimResizing.isAdminClaim() && smaller && (newWidth < GriefPrevention.instance.config_claims_minSize || newHeight < GriefPrevention.instance.config_claims_minSize)) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); return; } //make sure player has enough blocks to make up the difference if(!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) { int newArea = newWidth * newHeight; int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; if(blocksRemainingAfter < 0) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); this.tryAdvertiseAdminAlternatives(player); return; } } } //special rule for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. //rule: in any mode, shrinking a claim removes any surface fluids Claim oldClaim = playerData.claimResizing; boolean smaller = false; if(oldClaim.parent == null) { //temporary claim instance, just for checking contains() Claim newClaim = new Claim( new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), null, new String[]{}, new String[]{}, new String[]{}, new String[]{}, null); //if the new claim is smaller if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) { smaller = true; //remove surface fluids about to be unclaimed oldClaim.removeSurfaceFluids(newClaim); } } //ask the datastore to try and resize the claim, this checks for conflicts with other claims CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim(playerData.claimResizing, newx1, newx2, newy1, newy2, newz1, newz2, player); if(result.succeeded) { //decide how many claim blocks are available for more resizing int claimBlocksRemaining = 0; if(!playerData.claimResizing.isAdminClaim()) { UUID ownerID = playerData.claimResizing.ownerID; if(playerData.claimResizing.parent != null) { ownerID = playerData.claimResizing.parent.ownerID; } if(ownerID == player.getUniqueId()) { claimBlocksRemaining = playerData.getRemainingClaimBlocks(); } else { PlayerData ownerData = this.dataStore.getPlayerData(ownerID); claimBlocksRemaining = ownerData.getRemainingClaimBlocks(); OfflinePlayer owner = GriefPrevention.instance.getServer().getOfflinePlayer(ownerID); if(!owner.isOnline()) { this.dataStore.clearCachedPlayerData(ownerID); } } } //inform about success, visualize, communicate remaining blocks available GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(claimBlocksRemaining)); Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); //if resizing someone else's claim, make a log entry if(!playerID.equals(playerData.claimResizing.ownerID) && playerData.claimResizing.parent == null) { GriefPrevention.AddLogEntry(player.getName() + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); } //if increased to a sufficiently large size and no subdivisions yet, send subdivision instructions if(oldClaim.getArea() < 1000 && result.claim.getArea() >= 1000 && result.claim.children.size() == 0 && !player.hasPermission("griefprevention.adminclaims")) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.BecomeMayor, 200L); GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionVideo2, 201L, DataStore.SUBDIVISION_VIDEO_URL); } //if in a creative mode world and shrinking an existing claim, restore any unclaimed area if(smaller && GriefPrevention.instance.creativeRulesApply(oldClaim.getLesserBoundaryCorner())) { GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); GriefPrevention.instance.restoreClaim(oldClaim, 20L * 60 * 2); //2 minutes GriefPrevention.AddLogEntry(player.getName() + " shrank a claim @ " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.getLesserBoundaryCorner())); } //clean up playerData.claimResizing = null; playerData.lastShovelLocation = null; } else { if(result.claim != null) { //inform player GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); //show the player the conflicting claim Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); Visualization.Apply(player, visualization); } else { GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapRegion); } } 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 String noEditReason = claim.allowEdit(player); 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() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, playerData.lastShovelLocation.getBlockZ(), clickedBlock.getZ(), null, //owner is not used for subdivisions playerData.claimSubdividing, null, player); //if it didn't succeed, tell the player why if(!result.succeeded) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap); Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); Visualization.Apply(player, visualization); return; } //otherwise, advise him on the /trust command and show him his new subdivision else { GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess); Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); playerData.lastShovelLocation = null; playerData.claimSubdividing = null; } } } //otherwise tell him he can't create a claim here, and show him the existing claim //also advise him to consider /abandonclaim or resizing the existing claim else { GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlap); Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); } } //otherwise tell the player he can't claim here because it's someone else's claim, and show him the claim else { GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapOtherPlayer, claim.getOwnerName()); Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); Visualization.Apply(player, visualization); } return; } //otherwise, the player isn't in an existing claim! //if he hasn't already start a claim with a previous shovel action Location lastShovelLocation = playerData.lastShovelLocation; if(lastShovelLocation == null) { //if claims are not enabled in this world and it's not an administrative claim, display an error message and stop if(!GriefPrevention.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(GriefPrevention.instance.config_claims_maxClaimsPerPlayer > 0 && !player.hasPermission("griefprevention.overrideclaimcountlimit") && playerData.getClaims().size() >= GriefPrevention.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 Visualization visualization = Visualization.FromClaim(new Claim(clickedBlock.getLocation(), clickedBlock.getLocation(), null, new String[]{}, new String[]{}, new String[]{}, new String[]{}, null), clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation()); Visualization.Apply(player, visualization); } //otherwise, he's trying to finish creating a claim by setting the other boundary corner else { //if last shovel location was in a different world, assume the player is starting the create-claim workflow over if(!lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) { playerData.lastShovelLocation = null; this.onPlayerInteract(event); return; } //apply 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 && (newClaimWidth < GriefPrevention.instance.config_claims_minSize || newClaimHeight < GriefPrevention.instance.config_claims_minSize)) { //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.NewClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); } 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)); this.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() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, lastShovelLocation.getBlockZ(), clickedBlock.getZ(), playerID, null, null, player); //if it didn't succeed, tell the player why if(!result.succeeded) { if(result.claim != null) { GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); Visualization.Apply(player, visualization); } else { GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion); } return; } //otherwise, advise him on the /trust command and show him his new claim else { GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess); Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); Visualization.Apply(player, visualization); playerData.lastShovelLocation = null; //if it's a big claim, tell the player about subdivisions if(!player.hasPermission("griefprevention.adminclaims") && result.claim.getArea() >= 1000) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.BecomeMayor, 200L); GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionVideo2, 201L, DataStore.SUBDIVISION_VIDEO_URL); } } } } } //educates a player about /adminclaims and /acb, if he can use them private void tryAdvertiseAdminAlternatives(Player player) { if(player.hasPermission("griefprevention.adminclaims") && player.hasPermission("griefprevention.adjustclaimblocks")) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACandACB); } else if(player.hasPermission("griefprevention.adminclaims")) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseAdminClaims); } else if(player.hasPermission("griefprevention.adjustclaimblocks")) { GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACB); } } //determines whether a block type is an inventory holder. uses a caching strategy to save cpu time private ConcurrentHashMap inventoryHolderCache = new ConcurrentHashMap(); private boolean isInventoryHolder(Block clickedBlock) { Integer cacheKey = clickedBlock.getTypeId(); 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 WOOD_BUTTON: case STONE_BUTTON: case LEVER: case DIODE_BLOCK_ON: //redstone repeater case DIODE_BLOCK_OFF: return true; default: return false; } } static Block getTargetBlock(Player player, int maxDistance) throws IllegalStateException { BlockIterator iterator = new BlockIterator(player.getLocation(), player.getEyeHeight(), maxDistance); Block result = player.getLocation().getBlock().getRelative(BlockFace.UP); while (iterator.hasNext()) { result = iterator.next(); if(result.getType() != Material.AIR && result.getType() != Material.STATIONARY_WATER && result.getType() != Material.LONG_GRASS) return result; } return result; } }