AlttdGriefPrevention/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java
2016-05-09 11:10:03 -07:00

2576 lines
101 KiB
Java

/*
GriefPrevention Server Plugin for Minecraft
Copyright (C) 2011 Ryan Hamshire
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
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.Pattern;
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.TravelAgent;
import org.bukkit.World.Environment;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.command.Command;
import org.bukkit.entity.Animals;
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.EquipmentSlot;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.BlockIterator;
class PlayerEventHandler implements Listener
{
private DataStore dataStore;
//list of temporarily banned ip's
private ArrayList<IpBanInfo> tempBannedIps = new ArrayList<IpBanInfo>();
//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<Long> recentLoginLogoutNotifications = new ArrayList<Long>();
//regex pattern for the "how do i claim land?" scanner
private Pattern howToClaimPattern = null;
//matcher for banned words
private WordFinder bannedWordFinder = new WordFinder(GriefPrevention.instance.dataStore.loadBannedWords());
//typical constructor, yawn
PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
{
this.dataStore = dataStore;
}
//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<Player> 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<Player> recipientsToKeep = new HashSet<Player>();
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))
{
//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 = GriefPrevention.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(GriefPrevention.instance.config_trollFilterEnabled)
{
String notificationMessage = "(Auto-Muted " + player.getName() + "): " + message;
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);
GriefPrevention.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<Player> recipientsToRemove = new HashSet<Player>();
PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
for(Player recipient : recipients)
{
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);
}
}
}
//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;
String mutedReason = null;
//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;
mutedReason = "pre-movement chat";
}
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)
{
((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase());
}
}
//always mute an exact match to the last chat message
long now = new Date().getTime();
if(mutedReason != null && message.equals(this.lastChatMessage) && now - this.lastChatMessageTimestamp < 750)
{
playerData.spamCount += ++this.duplicateMessageCount;
spam = true;
mutedReason = "repeat message";
}
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(mutedReason == null && this.stringsAreSimilar(message, playerData.lastMessage) && now - playerData.lastMessageTimestamp.getTime() < 10000)
{
playerData.spamCount++;
spam = true;
mutedReason = "similar message";
}
//filter IP addresses
if(mutedReason == null)
{
if(GriefPrevention.instance.containsBlockedIP(message))
{
//spam notation
playerData.spamCount+=1;
spam = true;
//block message
mutedReason = "IP address";
}
}
//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(mutedReason == null && 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) mutedReason = "gibberish";
playerData.spamCount++;
}
}
//very short messages close together are spam
if(mutedReason == null && message.length() < 5 && millisecondsSinceLastMessage < 3000)
{
spam = true;
playerData.spamCount++;
}
//in any case, record the timestamp of this message and also its content for next time
playerData.lastMessageTimestamp = new Date();
playerData.lastMessage = message;
//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.", CustomLogEntryTypes.AdminActivity);
//kick and ban
PlayerKickBanTask task = new PlayerKickBanTask(player, GriefPrevention.instance.config_spam_banMessage, "GriefPrevention Anti-Spam",true);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.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);
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)
{
if(mutedReason == null)
{
mutedReason = "too-frequent text";
}
if(!playerData.spamWarned)
{
GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L);
GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties.", CustomLogEntryTypes.Debug, true);
playerData.spamWarned = true;
}
}
if(mutedReason != null)
{
//make a log entry
GriefPrevention.AddLogEntry("Muted " + mutedReason + ".");
GriefPrevention.AddLogEntry("Muted " + player.getName() + " " + mutedReason + ":" + message, CustomLogEntryTypes.Debug, true);
//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;
}
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 message = event.getMessage();
String [] args = message.split(" ");
String command = args[0].toLowerCase();
CommandCategory category = this.getCommandCategory(command);
Player player = event.getPlayer();
PlayerData playerData = null;
//if a whisper
if(category == CommandCategory.Whisper && args.length > 1)
{
//determine target player, might be NULL
@SuppressWarnings("deprecation")
Player targetPlayer = GriefPrevention.instance.getServer().getPlayer(args[1]);
//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(GriefPrevention.instance.config_whisperNotifications && !player.hasPermission("griefprevention.eavesdropimmune"))
{
//except for when the recipient has eavesdrop immunity
if(targetPlayer == null || !targetPlayer.hasPermission("griefprevention.eavesdropimmune"))
{
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();
@SuppressWarnings("unchecked")
Collection<Player> players = (Collection<Player>)GriefPrevention.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()))
{
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() || playerData.siegeData != null) && GriefPrevention.instance.config_pvp_blockedCommands.contains(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(GriefPrevention.instance.config_spam_enabled)
{
event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event));
}
if(!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(message))
{
event.setCancelled(true);
}
//unless cancelled, log in abridged logs
if(!event.isCancelled())
{
StringBuilder builder = new StringBuilder();
for(String arg : args)
{
builder.append(arg + " ");
}
makeSocialLogEntry(event.getPlayer().getName(), builder.toString());
}
}
//if requires access trust, check for permission
isMonitoredCommand = false;
String lowerCaseMessage = message.toLowerCase();
for(String monitoredCommand : GriefPrevention.instance.config_claims_commandsRequiringAccessTrust)
{
if(lowerCaseMessage.startsWith(monitoredCommand))
{
isMonitoredCommand = true;
break;
}
}
if(isMonitoredCommand)
{
Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim);
if(claim != null)
{
playerData.lastClaim = claim;
String reason = claim.allowAccess(player);
if(reason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, reason);
event.setCancelled(true);
}
}
}
}
private ConcurrentHashMap<String, CommandCategory> commandCategoryMap = new ConcurrentHashMap<String, CommandCategory>();
private CommandCategory getCommandCategory(String commandName)
{
if(commandName.startsWith("/")) commandName = commandName.substring(1);
//if we've seen this command or alias before, return the category determined previously
CommandCategory category = this.commandCategoryMap.get(commandName);
if(category != null) return category;
//otherwise build a list of all the aliases of this command across all installed plugins
HashSet<String> aliases = new HashSet<String>();
aliases.add(commandName);
aliases.add("minecraft:" + commandName);
for(Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins())
{
JavaPlugin javaPlugin = (JavaPlugin)plugin;
Command command = javaPlugin.getCommand(commandName);
if(command != null)
{
aliases.add(command.getName().toLowerCase());
aliases.add(plugin.getName().toLowerCase() + ":" + command.getName().toLowerCase());
for(String alias : command.getAliases())
{
aliases.add(alias.toLowerCase());
aliases.add(plugin.getName().toLowerCase() + ":" + alias.toLowerCase());
}
}
}
//also consider vanilla commands
Command command = Bukkit.getServer().getPluginCommand(commandName);
if(command != null)
{
aliases.add(command.getName().toLowerCase());
aliases.add("minecraft:" + command.getName().toLowerCase());
for(String alias : command.getAliases())
{
aliases.add(alias.toLowerCase());
aliases.add("minecraft:" + alias.toLowerCase());
}
}
//if any of those aliases are in the chat list or whisper list, then we know the category for that command
category = CommandCategory.None;
for(String alias : aliases)
{
if(GriefPrevention.instance.config_eavesdrop_whisperCommands.contains("/" + alias))
{
category = CommandCategory.Whisper;
}
else if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains("/" + alias))
{
category = CommandCategory.Chat;
}
//remember the categories for later
this.commandCategoryMap.put(alias.toLowerCase(), category);
}
return category;
}
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(": " + message);
longestNameLength = Math.max(longestNameLength, name.length());
GriefPrevention.AddLogEntry(entryBuilder.toString(), CustomLogEntryTypes.SocialActivity, true);
}
private ConcurrentHashMap<UUID, Date> lastLoginThisServerSessionMap = new ConcurrentHashMap<UUID, Date>();
//counts how many players are using each IP address connected to the server right now
private ConcurrentHashMap<String, Integer> ipCountHash = new ConcurrentHashMap<String, Integer>();
//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...
@SuppressWarnings("deprecation")
@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(GriefPrevention.isNewToServer(player))
{
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)
{
WelcomeTask task = new WelcomeTask(player);
Bukkit.getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, GriefPrevention.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(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 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<Player> players = (Collection<Player>)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 Smart Ban - Shared Login:" + info.bannedAccountName, true);
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());
//ensure we're not over the limit for this IP address
InetAddress ipAddress = playerData.ipAddress;
if(ipAddress != null)
{
String ipAddressString = ipAddress.toString();
int ipLimit = GriefPrevention.instance.config_ipLimit;
if(ipLimit > 0 && GriefPrevention.isNewToServer(player))
{
Integer ipCount = this.ipCountHash.get(ipAddressString);
if(ipCount == null) ipCount = 0;
if(ipCount >= ipLimit)
{
//kick player
PlayerKickBanTask task = new PlayerKickBanTask(player, "Sorry, there are too many players logged in with your IP address.", "GriefPrevention IP-sharing limit.", false);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 10L);
//silence join message
event.setJoinMessage(null);
return;
}
else
{
this.ipCountHash.put(ipAddressString, ipCount + 1);
playerData.ipLimited = true;
}
}
}
//create a thread to load ignore information
new IgnoreLoaderThread(playerID, playerData.ignoredPlayers).start();
//is he possibly stuck in a portal frame?
Location returnLocation = PlayerEventHandler.portalReturnMap.get(player.getUniqueId());
if(returnLocation != null)
{
PlayerEventHandler.portalReturnMap.remove(player.getUniqueId());
if(player.getLocation().getBlock().getType() == Material.PORTAL)
{
player.teleport(returnLocation);
}
}
}
//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 = GriefPrevention.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;
}
GriefPrevention.instance.checkPvpProtectionNeeded(player);
}
//when a player dies...
@EventHandler(priority = EventPriority.HIGHEST)
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
}
//reduce count of players with that player's IP address
if(GriefPrevention.instance.config_ipLimit > 0 && playerData.ipLimited)
{
InetAddress ipAddress = playerData.ipAddress;
if(ipAddress != null)
{
String ipAddressString = ipAddress.toString();
Integer count = this.ipCountHash.get(ipAddressString);
if(count == null) count = 1;
this.ipCountHash.put(ipAddressString, count - 1);
}
}
//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 = 5;
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);
}
}
//remember where players teleport from (via portals) in case they're trapped at the destination
static ConcurrentHashMap<UUID, Location> portalReturnMap = new ConcurrentHashMap<UUID, Location>();
//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, 1200L); //after 1 minute
portalReturnMap.put(player.getUniqueId(), event.getFrom());
//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
//this is VERY expensive for the cpu, so this feature is off by default
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
TeleportCause cause = event.getCause();
if(cause == TeleportCause.CHORUS_FRUIT || (cause == 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);
if(cause == TeleportCause.ENDER_PEARL)
player.getInventory().addItem(new ItemStack(Material.ENDER_PEARL));
}
}
}
//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())
{
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(!GriefPrevention.instance.pvpRulesApply(entity.getLocation().getWorld()) || GriefPrevention.instance.config_pvp_protectPets)
{
//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;
}
}
}
else //world repair code for a now-fixed GP bug
{
//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 = GriefPrevention.instance.allowBuild(player, entity.getLocation(), Material.ITEM_FRAME);
if(noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
event.setCancelled(true);
return;
}
}
//limit armor placements when entity count is too high
if(entity.getType() == EntityType.ARMOR_STAND && GriefPrevention.instance.creativeRulesApply(player.getLocation()))
{
if(playerData == null) playerData = this.dataStore.getPlayerData(player.getUniqueId());
Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, playerData.lastClaim);
if(claim == null) return;
String noEntitiesReason = claim.allowMoreEntities(false);
if(noEntitiesReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason);
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;
}
}
}
}
//if the entity is an animal, apply container rules
if((GriefPrevention.instance.config_claims_preventTheft && entity instanceof Animals) || (entity.getType() == EntityType.VILLAGER && GriefPrevention.instance.config_claims_villagerTradingRequiresTrust))
{
//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 && GriefPrevention.instance.getItemInHand(player, event.getHand()).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<MetadataValue> 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;
}
return;
}
}
}
//the rest of this code is specific to pvp worlds
if(!GriefPrevention.instance.pvpRulesApply(player.getWorld())) return;
//if we're preventing spawn camping and the player was previously empty handed...
if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (GriefPrevention.instance.getItemInHand(player, EquipmentSlot.HAND).getType() == Material.AIR))
{
//if that player is currently immune to pvp
PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getUniqueId());
if(playerData.pvpImmune)
{
//if it's been less than 10 seconds since the last time he spawned, don't pick up the item
long now = Calendar.getInstance().getTimeInMillis();
long elapsedSinceLastSpawn = now - playerData.lastSpawn;
if(elapsedSinceLastSpawn < 10000)
{
event.setCancelled(true);
return;
}
//otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot
playerData.pvpImmune = false;
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
}
}
}
//when a player switches in-hand items
@EventHandler(ignoreCancelled = true)
public void onItemHeldChange(PlayerItemHeldEvent event)
{
Player player = event.getPlayer();
//if he's switching to the golden shovel
int newSlot = event.getNewSlot();
ItemStack newItemStack = player.getInventory().getItem(newSlot);
if(newItemStack != null && newItemStack.getType() == GriefPrevention.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(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
private HashSet<Material> commonAdjacentBlocks_water = new HashSet<Material>(Arrays.asList(Material.WATER, Material.STATIONARY_WATER, Material.SOIL, Material.DIRT, Material.STONE));
private HashSet<Material> commonAdjacentBlocks_lava = new HashSet<Material>(Arrays.asList(Material.LAVA, Material.STATIONARY_LAVA, Material.DIRT, Material.STONE));
@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.pvpRulesApply(block.getWorld()) || !GriefPrevention.instance.config_pvp_allowLavaNearPlayers) && !player.hasPermission("griefprevention.lava"))
{
if(bucketEvent.getBucket() == Material.LAVA_BUCKET)
{
List<Player> 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;
}
}
}
}
//log any suspicious placements (check sea level, world type, and adjacent blocks)
if(block.getY() >= GriefPrevention.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
HashSet<Material> exclusionAdjacentTypes;
if(bucketEvent.getBucket() == Material.WATER_BUCKET)
exclusionAdjacentTypes = this.commonAdjacentBlocks_water;
else
exclusionAdjacentTypes = this.commonAdjacentBlocks_lava;
boolean makeLogEntry = true;
BlockFace [] adjacentDirections = new BlockFace[] {BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.DOWN};
for(BlockFace direction : adjacentDirections)
{
Material adjacentBlockType = block.getRelative(direction).getType();
if(exclusionAdjacentTypes.contains(adjacentBlockType))
{
makeLogEntry = false;
break;
}
}
if(makeLogEntry)
{
GriefPrevention.AddLogEntry(player.getName() + " placed suspicious " + bucketEvent.getBucket().name() + " @ " + GriefPrevention.getfriendlyLocationString(block.getLocation()), CustomLogEntryTypes.SuspiciousActivity);
}
}
}
//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
@SuppressWarnings("deprecation")
@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)
{
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;
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;
}
}
}
}
//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)))
{
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 ||
clickedBlockType == Material.CAKE_BLOCK ||
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;
}
}
}
//otherwise apply rule for cake
else if(clickedBlock != null && GriefPrevention.instance.config_claims_preventTheft && clickedBlockType == Material.CAKE_BLOCK)
{
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 noContainerReason = claim.allowAccess(player);
if(noContainerReason != null)
{
event.setCancelled(true);
GriefPrevention.sendMessage(player, TextMode.Err, noContainerReason);
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.DRAGON_EGG ||
clickedBlockType == Material.DAYLIGHT_DETECTOR ||
clickedBlockType == Material.DAYLIGHT_DETECTOR_INVERTED ||
clickedBlockType == Material.REDSTONE_COMPARATOR_ON ||
clickedBlockType == Material.REDSTONE_COMPARATOR_OFF
))
{
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?
EquipmentSlot hand = event.getHand();
ItemStack itemInHand = GriefPrevention.instance.getItemInHand(player, hand);
Material materialInHand = itemInHand.getType();
//if it's bonemeal or armor stand or spawn egg, check for build permission (ink sac == bone meal, must be a Bukkit bug?)
if(clickedBlock != null && (materialInHand == Material.INK_SACK || materialInHand == Material.ARMOR_STAND || materialInHand == Material.MONSTER_EGG))
{
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 ||
materialInHand == Material.BOAT_ACACIA ||
materialInHand == Material.BOAT_BIRCH ||
materialInHand == Material.BOAT_DARK_OAK ||
materialInHand == Material.BOAT_JUNGLE ||
materialInHand == Material.BOAT_SPRUCE))
{
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, Material.BOAT);
if(noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
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.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.EXPLOSIVE_MINECART || materialInHand == Material.HOPPER_MINECART) &&
!GriefPrevention.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)
{
String reason = claim.allowContainers(player);
if(reason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, reason);
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.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.ARMOR_STAND || materialInHand == Material.ITEM_FRAME || materialInHand == Material.MONSTER_EGG || materialInHand == Material.MONSTER_EGGS || materialInHand == Material.EXPLOSIVE_MINECART || materialInHand == Material.HOPPER_MINECART) && 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(false);
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 && hand == EquipmentSlot.HAND)
{
//if claims are disabled in this world, do nothing
if(!GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) return;
//if holding shift (sneaking), show all claims in area
if(player.isSneaking() && player.hasPermission("griefprevention.visualizenearbyclaims"))
{
//find nearby claims
Set<Claim> claims = this.dataStore.getNearbyClaims(player.getLocation());
//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 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;
}
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 || hand != EquipmentSlot.HAND) return;
event.setCancelled(true); //GriefPrevention exclusively reserves this tool (e.g. no grass path creation for golden shovel)
//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<Material> allowedFillBlocks = new ArrayList<Material>();
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;
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
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 ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>(), new ArrayList<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 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 < GriefPrevention.instance.config_claims_minWidth || newClaimHeight < GriefPrevention.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(GriefPrevention.instance.config_claims_minWidth));
}
return;
}
int newArea = newClaimWidth * newClaimHeight;
if(newArea < GriefPrevention.instance.config_claims_minArea)
{
if(newArea != 1)
{
GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimInsufficientArea, String.valueOf(GriefPrevention.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));
GriefPrevention.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() - 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);
}
GriefPrevention.instance.autoExtendClaim(result.claim);
}
}
}
}
//determines whether a block type is an inventory holder. uses a caching strategy to save cpu time
private ConcurrentHashMap<Integer, Boolean> inventoryHolderCache = new ConcurrentHashMap<Integer, Boolean>();
private boolean isInventoryHolder(Block clickedBlock)
{
@SuppressWarnings("deprecation")
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:
case CAKE_BLOCK:
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 || eyeMaterial == Material.STATIONARY_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.STATIONARY_WATER) &&
(!passThroughWater || type != Material.WATER) &&
type != Material.LONG_GRASS &&
type != Material.SNOW) return result;
}
return result;
}
}