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