diff --git a/plugin.yml b/plugin.yml index b95abe0..ca55e53 100644 --- a/plugin.yml +++ b/plugin.yml @@ -2,7 +2,7 @@ name: GriefPrevention main: me.ryanhamshire.GriefPrevention.GriefPrevention softdepend: [Vault, Multiverse-Core, My Worlds, MystCraft, Transporter] dev-url: http://dev.bukkit.org/server-mods/grief-prevention -version: 7.1.2 +version: 7.2 commands: abandonclaim: description: Deletes a claim. diff --git a/src/me/ryanhamshire/GriefPrevention/DataStore.java b/src/me/ryanhamshire/GriefPrevention/DataStore.java index 79f7caf..4b4ee9a 100644 --- a/src/me/ryanhamshire/GriefPrevention/DataStore.java +++ b/src/me/ryanhamshire/GriefPrevention/DataStore.java @@ -1,1030 +1,1030 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.io.*; -import java.util.*; - -import org.bukkit.*; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -//singleton class which manages all GriefPrevention data (except for config options) -public abstract class DataStore -{ - //in-memory cache for player data - protected HashMap playerNameToPlayerDataMap = new HashMap(); - - //in-memory cache for group (permission-based) data - protected HashMap permissionToBonusBlocksMap = new HashMap(); - - //in-memory cache for claim data - ArrayList claims = new ArrayList(); - - //in-memory cache for messages - private String [] messages; - - //next claim ID - Long nextClaimID = (long)0; - - //path information, for where stuff stored on disk is well... stored - protected final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; - final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; - final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; - - //initialization! - void initialize() throws Exception - { - GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); - - //make a list of players who own claims - Vector playerNames = new Vector(); - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - - //ignore admin claims - if(claim.isAdminClaim()) continue; - - if(!playerNames.contains(claim.ownerName)) - playerNames.add(claim.ownerName); - } - - GriefPrevention.AddLogEntry(playerNames.size() + " players have staked claims."); - - //load up all the messages from messages.yml - this.loadMessages(); - - //collect garbage, since lots of stuff was loaded into memory and then tossed out - System.gc(); - } - - //removes cached player data from memory - synchronized void clearCachedPlayerData(String playerName) - { - this.playerNameToPlayerDataMap.remove(playerName); - } - - //gets the number of bonus blocks a player has from his permissions - synchronized int getGroupBonusBlocks(String playerName) - { - int bonusBlocks = 0; - Set keys = permissionToBonusBlocksMap.keySet(); - Iterator iterator = keys.iterator(); - while(iterator.hasNext()) - { - String groupName = iterator.next(); - Player player = GriefPrevention.instance.getServer().getPlayer(playerName); - if(player.hasPermission(groupName)) - { - bonusBlocks += this.permissionToBonusBlocksMap.get(groupName); - } - } - - return bonusBlocks; - } - - //grants a group (players with a specific permission) bonus claim blocks as long as they're still members of the group - synchronized public int adjustGroupBonusBlocks(String groupName, int amount) - { - Integer currentValue = this.permissionToBonusBlocksMap.get(groupName); - if(currentValue == null) currentValue = 0; - - currentValue += amount; - this.permissionToBonusBlocksMap.put(groupName, currentValue); - - //write changes to storage to ensure they don't get lost - this.saveGroupBonusBlocks(groupName, currentValue); - - return currentValue; - } - - abstract void saveGroupBonusBlocks(String groupName, int amount); - - synchronized public void changeClaimOwner(Claim claim, String newOwnerName) throws Exception - { - //if it's a subdivision, throw an exception - if(claim.parent != null) - { - throw new Exception("Subdivisions can't be transferred. Only top-level claims may change owners."); - } - - //otherwise update information - - //determine current claim owner - PlayerData ownerData = null; - if(!claim.isAdminClaim()) - { - ownerData = this.getPlayerData(claim.ownerName); - } - - //determine new owner - PlayerData newOwnerData = this.getPlayerData(newOwnerName); - - //transfer - claim.ownerName = newOwnerName; - this.saveClaim(claim); - - //adjust blocks and other records - if(ownerData != null) - { - ownerData.claims.remove(claim); - ownerData.bonusClaimBlocks -= claim.getArea(); - this.savePlayerData(claim.ownerName, ownerData); - } - - newOwnerData.claims.add(claim); - newOwnerData.bonusClaimBlocks += claim.getArea(); - this.savePlayerData(newOwnerName, newOwnerData); - } - - //adds a claim to the datastore, making it an effective claim - synchronized void addClaim(Claim newClaim) - { - //subdivisions are easy - if(newClaim.parent != null) - { - newClaim.parent.children.add(newClaim); - newClaim.inDataStore = true; - this.saveClaim(newClaim); - return; - } - - //add it and mark it as added - int j = 0; - while(j < this.claims.size() && !this.claims.get(j).greaterThan(newClaim)) j++; - if(j < this.claims.size()) - this.claims.add(j, newClaim); - else - this.claims.add(this.claims.size(), newClaim); - newClaim.inDataStore = true; - - //except for administrative claims (which have no owner), update the owner's playerData with the new claim - if(!newClaim.isAdminClaim()) - { - PlayerData ownerData = this.getPlayerData(newClaim.getOwnerName()); - ownerData.claims.add(newClaim); - this.savePlayerData(newClaim.getOwnerName(), ownerData); - } - - //make sure the claim is saved to disk - this.saveClaim(newClaim); - } - - //turns a location into a string, useful in data storage - private String locationStringDelimiter = ";"; - String locationToString(Location location) - { - StringBuilder stringBuilder = new StringBuilder(location.getWorld().getName()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockX()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockY()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockZ()); - - return stringBuilder.toString(); - } - - //turns a location string back into a location - Location locationFromString(String string) throws Exception - { - //split the input string on the space - String [] elements = string.split(locationStringDelimiter); - - //expect four elements - world name, X, Y, and Z, respectively - if(elements.length != 4) - { - throw new Exception("Expected four distinct parts to the location string."); - } - - String worldName = elements[0]; - String xString = elements[1]; - String yString = elements[2]; - String zString = elements[3]; - - //identify world the claim is in - World world = GriefPrevention.instance.getServer().getWorld(worldName); - if(world == null) - { - throw new Exception("World not found: \"" + worldName + "\""); - } - - //convert those numerical strings to integer values - int x = Integer.parseInt(xString); - int y = Integer.parseInt(yString); - int z = Integer.parseInt(zString); - - return new Location(world, x, y, z); - } - - //saves any changes to a claim to secondary storage - synchronized public void saveClaim(Claim claim) - { - //subdivisions don't save to their own files, but instead live in their parent claim's file - //so any attempt to save a subdivision will save its parent (and thus the subdivision) - if(claim.parent != null) - { - this.saveClaim(claim.parent); - return; - } - - //otherwise get a unique identifier for the claim which will be used to name the file on disk - if(claim.id == null) - { - claim.id = this.nextClaimID; - this.incrementNextClaimID(); - } - - this.writeClaimToStorage(claim); - } - - abstract void writeClaimToStorage(Claim claim); - - //increments the claim ID and updates secondary storage to be sure it's saved - abstract void incrementNextClaimID(); - - //retrieves player data from memory or secondary storage, as necessary - //if the player has never been on the server before, this will return a fresh player data with default values - synchronized public PlayerData getPlayerData(String playerName) - { - //first, look in memory - PlayerData playerData = this.playerNameToPlayerDataMap.get(playerName); - - //if not there, look in secondary storage - if(playerData == null) - { - playerData = this.getPlayerDataFromStorage(playerName); - playerData.playerName = playerName; - - //find all the claims belonging to this player and note them for future reference - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - if(claim.ownerName.equals(playerName)) - { - playerData.claims.add(claim); - } - } - - //shove that new player data into the hash map cache - this.playerNameToPlayerDataMap.put(playerName, playerData); - } - - //try the hash map again. if it's STILL not there, we have a bug to fix - return this.playerNameToPlayerDataMap.get(playerName); - } - - abstract PlayerData getPlayerDataFromStorage(String playerName); - - //deletes a claim or subdivision - synchronized public void deleteClaim(Claim claim) - { - //subdivisions are simple - just remove them from their parent claim and save that claim - if(claim.parent != null) - { - Claim parentClaim = claim.parent; - parentClaim.children.remove(claim); - this.saveClaim(parentClaim); - return; - } - - //remove from memory - for(int i = 0; i < this.claims.size(); i++) - { - if(claims.get(i).id.equals(claim.id)) - { - this.claims.remove(i); - claim.inDataStore = false; - for(int j = 0; j < claim.children.size(); j++) - { - claim.children.get(j).inDataStore = false; - } - break; - } - } - - //remove from secondary storage - this.deleteClaimFromSecondaryStorage(claim); - - //update player data, except for administrative claims, which have no owner - if(!claim.isAdminClaim()) - { - PlayerData ownerData = this.getPlayerData(claim.getOwnerName()); - for(int i = 0; i < ownerData.claims.size(); i++) - { - if(ownerData.claims.get(i).id.equals(claim.id)) - { - ownerData.claims.remove(i); - break; - } - } - this.savePlayerData(claim.getOwnerName(), ownerData); - } - } - - abstract void deleteClaimFromSecondaryStorage(Claim claim); - - //gets the claim at a specific location - //ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim - //cachedClaim can be NULL, but will help performance if you have a reasonable guess about which claim the location is in - synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, Claim cachedClaim) - { - //check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done - if(cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) return cachedClaim; - - //the claims list is ordered by greater boundary corner - //create a temporary "fake" claim in memory for comparison purposes - Claim tempClaim = new Claim(); - tempClaim.lesserBoundaryCorner = location; - - //otherwise, search all existing claims until we find the right claim - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - - //if we reach a claim which is greater than the temp claim created above, there's definitely no claim - //in the collection which includes our location - if(claim.greaterThan(tempClaim)) return null; - - //find a top level claim - if(claim.contains(location, ignoreHeight, false)) - { - //when we find a top level claim, if the location is in one of its subdivisions, - //return the SUBDIVISION, not the top level claim - for(int j = 0; j < claim.children.size(); j++) - { - Claim subdivision = claim.children.get(j); - if(subdivision.contains(location, ignoreHeight, false)) return subdivision; - } - - return claim; - } - } - - //if no claim found, return null - return null; - } - - //creates a claim. - //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim - //otherwise, returns a success along with a reference to the new claim - //use ownerName == "" for administrative claims - //for top level claims, pass parent == NULL - //DOES adjust claim blocks available on success (players can go into negative quantity available) - //does NOT check a player has permission to create a claim, or enough claim blocks. - //does NOT check minimum claim size constraints - //does NOT visualize the new claim for any players - synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, String ownerName, Claim parent, Long id) - { - CreateClaimResult result = new CreateClaimResult(); - - int smallx, bigx, smally, bigy, smallz, bigz; - - //determine small versus big inputs - if(x1 < x2) - { - smallx = x1; - bigx = x2; - } - else - { - smallx = x2; - bigx = x1; - } - - if(y1 < y2) - { - smally = y1; - bigy = y2; - } - else - { - smally = y2; - bigy = y1; - } - - if(z1 < z2) - { - smallz = z1; - bigz = z2; - } - else - { - smallz = z2; - bigz = z1; - } - - //creative mode claims always go to bedrock - if(GriefPrevention.instance.config_claims_enabledCreativeWorlds.contains(world)) - { - smally = 2; - } - - //create a new claim instance (but don't save it, yet) - Claim newClaim = new Claim( - new Location(world, smallx, smally, smallz), - new Location(world, bigx, bigy, bigz), - ownerName, - new String [] {}, - new String [] {}, - new String [] {}, - new String [] {}, - id); - - newClaim.parent = parent; - - //ensure this new claim won't overlap any existing claims - ArrayList claimsToCheck; - if(newClaim.parent != null) - { - claimsToCheck = newClaim.parent.children; - } - else - { - claimsToCheck = this.claims; - } - - for(int i = 0; i < claimsToCheck.size(); i++) - { - Claim otherClaim = claimsToCheck.get(i); - - //if we find an existing claim which will be overlapped - if(otherClaim.overlaps(newClaim)) - { - //result = fail, return conflicting claim - result.succeeded = false; - result.claim = otherClaim; - return result; - } - } - - //otherwise add this new claim to the data store to make it effective - this.addClaim(newClaim); - - //then return success along with reference to new claim - result.succeeded = true; - result.claim = newClaim; - return result; - } - - //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them - public abstract void savePlayerData(String playerName, PlayerData playerData); - - //extends a claim to a new depth - //respects the max depth config variable - synchronized public void extendClaim(Claim claim, int newDepth) - { - if(newDepth < GriefPrevention.instance.config_claims_maxDepth) newDepth = GriefPrevention.instance.config_claims_maxDepth; - - if(claim.parent != null) claim = claim.parent; - - //delete the claim - this.deleteClaim(claim); - - //re-create it at the new depth - claim.lesserBoundaryCorner.setY(newDepth); - claim.greaterBoundaryCorner.setY(newDepth); - - //make all subdivisions reach to the same depth - for(int i = 0; i < claim.children.size(); i++) - { - claim.children.get(i).lesserBoundaryCorner.setY(newDepth); - claim.children.get(i).greaterBoundaryCorner.setY(newDepth); - } - - //save changes - this.addClaim(claim); - } - - //starts a siege on a claim - //does NOT check siege cooldowns, see onCooldown() below - synchronized public void startSiege(Player attacker, Player defender, Claim defenderClaim) - { - //fill-in the necessary SiegeData instance - SiegeData siegeData = new SiegeData(attacker, defender, defenderClaim); - PlayerData attackerData = this.getPlayerData(attacker.getName()); - PlayerData defenderData = this.getPlayerData(defender.getName()); - attackerData.siegeData = siegeData; - defenderData.siegeData = siegeData; - defenderClaim.siegeData = siegeData; - - //start a task to monitor the siege - //why isn't this a "repeating" task? - //because depending on the status of the siege at the time the task runs, there may or may not be a reason to run the task again - SiegeCheckupTask task = new SiegeCheckupTask(siegeData); - siegeData.checkupTaskID = GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 30); - } - - //ends a siege - //either winnerName or loserName can be null, but not both - synchronized public void endSiege(SiegeData siegeData, String winnerName, String loserName, boolean death) - { - boolean grantAccess = false; - - //determine winner and loser - if(winnerName == null && loserName != null) - { - if(siegeData.attacker.getName().equals(loserName)) - { - winnerName = siegeData.defender.getName(); - } - else - { - winnerName = siegeData.attacker.getName(); - } - } - else if(winnerName != null && loserName == null) - { - if(siegeData.attacker.getName().equals(winnerName)) - { - loserName = siegeData.defender.getName(); - } - else - { - loserName = siegeData.attacker.getName(); - } - } - - //if the attacker won, plan to open the doors for looting - if(siegeData.attacker.getName().equals(winnerName)) - { - grantAccess = true; - } - - PlayerData attackerData = this.getPlayerData(siegeData.attacker.getName()); - attackerData.siegeData = null; - - PlayerData defenderData = this.getPlayerData(siegeData.defender.getName()); - defenderData.siegeData = null; - - //start a cooldown for this attacker/defender pair - Long now = Calendar.getInstance().getTimeInMillis(); - Long cooldownEnd = now + 1000 * 60 * 60; //one hour from now - this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + siegeData.defender.getName(), cooldownEnd); - - //start cooldowns for every attacker/involved claim pair - for(int i = 0; i < siegeData.claims.size(); i++) - { - Claim claim = siegeData.claims.get(i); - claim.siegeData = null; - this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + claim.ownerName, cooldownEnd); - - //if doors should be opened for looting, do that now - if(grantAccess) - { - claim.doorsOpen = true; - } - } - - //cancel the siege checkup task - GriefPrevention.instance.getServer().getScheduler().cancelTask(siegeData.checkupTaskID); - - //notify everyone who won and lost - if(winnerName != null && loserName != null) - { - GriefPrevention.instance.getServer().broadcastMessage(winnerName + " defeated " + loserName + " in siege warfare!"); - } - - //if the claim should be opened to looting - if(grantAccess) - { - Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); - if(winner != null) - { - //notify the winner - GriefPrevention.sendMessage(winner, TextMode.Success, Messages.SiegeWinDoorsOpen); - - //schedule a task to secure the claims in about 5 minutes - SecureClaimTask task = new SecureClaimTask(siegeData); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 5); - } - } - - //if the siege ended due to death, transfer inventory to winner - if(death) - { - Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); - Player loser = GriefPrevention.instance.getServer().getPlayer(loserName); - if(winner != null && loser != null) - { - //get loser's inventory, then clear it - ItemStack [] loserItems = loser.getInventory().getContents(); - loser.getInventory().clear(); - - //try to add it to the winner's inventory - for(int j = 0; j < loserItems.length; j++) - { - if(loserItems[j] == null || loserItems[j].getType() == Material.AIR || loserItems[j].getAmount() == 0) continue; - - HashMap wontFitItems = winner.getInventory().addItem(loserItems[j]); - - //drop any remainder on the ground at his feet - Object [] keys = wontFitItems.keySet().toArray(); - Location winnerLocation = winner.getLocation(); - for(int i = 0; i < keys.length; i++) - { - Integer key = (Integer)keys[i]; - winnerLocation.getWorld().dropItemNaturally(winnerLocation, wontFitItems.get(key)); - } - } - } - } - } - - //timestamp for each siege cooldown to end - private HashMap siegeCooldownRemaining = new HashMap(); - - //whether or not a sieger can siege a particular victim or claim, considering only cooldowns - synchronized public boolean onCooldown(Player attacker, Player defender, Claim defenderClaim) - { - Long cooldownEnd = null; - - //look for an attacker/defender cooldown - if(this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()) != null) - { - cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()); - - if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) - { - return true; - } - - //if found but expired, remove it - this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defender.getName()); - } - - //look for an attacker/claim cooldown - if(cooldownEnd == null && this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName) != null) - { - cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName); - - if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) - { - return true; - } - - //if found but expired, remove it - this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defenderClaim.ownerName); - } - - return false; - } - - //extend a siege, if it's possible to do so - synchronized void tryExtendSiege(Player player, Claim claim) - { - PlayerData playerData = this.getPlayerData(player.getName()); - - //player must be sieged - if(playerData.siegeData == null) return; - - //claim isn't already under the same siege - if(playerData.siegeData.claims.contains(claim)) return; - - //admin claims can't be sieged - if(claim.isAdminClaim()) return; - - //player must have some level of permission to be sieged in a claim - if(claim.allowAccess(player) != null) return; - - //otherwise extend the siege - playerData.siegeData.claims.add(claim); - claim.siegeData = playerData.siegeData; - } - - //deletes all claims owned by a player - synchronized public void deleteClaimsForPlayer(String playerName, boolean deleteCreativeClaims) - { - //make a list of the player's claims - ArrayList claimsToDelete = new ArrayList(); - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - if(claim.ownerName.equals(playerName) && (deleteCreativeClaims || !GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))) - claimsToDelete.add(claim); - } - - //delete them one by one - for(int i = 0; i < claimsToDelete.size(); i++) - { - Claim claim = claimsToDelete.get(i); - claim.removeSurfaceFluids(null); - - this.deleteClaim(claim); - - //if in a creative mode world, delete the claim - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.instance.restoreClaim(claim, 0); - } - } - } - - //tries to resize a claim - //see CreateClaim() for details on return value - synchronized public CreateClaimResult resizeClaim(Claim claim, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) - { - //remove old claim - this.deleteClaim(claim); - - //try to create this new claim, ignoring the original when checking for overlap - CreateClaimResult result = this.createClaim(claim.getLesserBoundaryCorner().getWorld(), newx1, newx2, newy1, newy2, newz1, newz2, claim.ownerName, claim.parent, claim.id); - - //if succeeded - if(result.succeeded) - { - //copy permissions from old claim - ArrayList builders = new ArrayList(); - ArrayList containers = new ArrayList(); - ArrayList accessors = new ArrayList(); - ArrayList managers = new ArrayList(); - claim.getPermissions(builders, containers, accessors, managers); - - for(int i = 0; i < builders.size(); i++) - result.claim.setPermission(builders.get(i), ClaimPermission.Build); - - for(int i = 0; i < containers.size(); i++) - result.claim.setPermission(containers.get(i), ClaimPermission.Inventory); - - for(int i = 0; i < accessors.size(); i++) - result.claim.setPermission(accessors.get(i), ClaimPermission.Access); - - for(int i = 0; i < managers.size(); i++) - { - result.claim.managers.add(managers.get(i)); - } - - //copy subdivisions from old claim - for(int i = 0; i < claim.children.size(); i++) - { - Claim subdivision = claim.children.get(i); - subdivision.parent = result.claim; - result.claim.children.add(subdivision); - } - - //save those changes - this.saveClaim(result.claim); - } - - else - { - //put original claim back - this.addClaim(claim); - } - - return result; - } - - private void loadMessages() - { - Messages [] messageIDs = Messages.values(); - this.messages = new String[Messages.values().length]; - - HashMap defaults = new HashMap(); - - //initialize defaults - this.addDefault(defaults, Messages.RespectingClaims, "Now respecting claims.", null); - this.addDefault(defaults, Messages.IgnoringClaims, "Now ignoring claims.", null); - this.addDefault(defaults, Messages.NoCreativeUnClaim, "You can't unclaim this land. You can only make this claim larger or create additional claims.", null); - this.addDefault(defaults, Messages.SuccessfulAbandon, "Claims abandoned. You now have {0} available claim blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.RestoreNatureActivate, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop.", null); - this.addDefault(defaults, Messages.RestoreNatureAggressiveActivate, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop.", null); - this.addDefault(defaults, Messages.FillModeActive, "Fill mode activated with radius {0}. Right click an area to fill.", "0: fill radius"); - this.addDefault(defaults, Messages.TransferClaimPermission, "That command requires the administrative claims permission.", null); - this.addDefault(defaults, Messages.TransferClaimMissing, "There's no claim here. Stand in the administrative claim you want to transfer.", null); - this.addDefault(defaults, Messages.TransferClaimAdminOnly, "Only administrative claims may be transferred to a player.", null); - this.addDefault(defaults, Messages.PlayerNotFound, "Player not found.", null); - this.addDefault(defaults, Messages.TransferTopLevel, "Only top level claims (not subdivisions) may be transferred. Stand outside of the subdivision and try again.", null); - this.addDefault(defaults, Messages.TransferSuccess, "Claim transferred.", null); - this.addDefault(defaults, Messages.TrustListNoClaim, "Stand inside the claim you're curious about.", null); - this.addDefault(defaults, Messages.ClearPermsOwnerOnly, "Only the claim owner can clear all permissions.", null); - this.addDefault(defaults, Messages.UntrustIndividualAllClaims, "Revoked {0}'s access to ALL your claims. To set permissions for a single claim, stand inside it.", "0: untrusted player"); - this.addDefault(defaults, Messages.UntrustEveryoneAllClaims, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it.", null); - this.addDefault(defaults, Messages.NoPermissionTrust, "You don't have {0}'s permission to manage permissions here.", "0: claim owner's name"); - this.addDefault(defaults, Messages.ClearPermissionsOneClaim, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them.", null); - this.addDefault(defaults, Messages.UntrustIndividualSingleClaim, "Revoked {0}'s access to this claim. To set permissions for a ALL your claims, stand outside them.", "0: untrusted player"); - this.addDefault(defaults, Messages.OnlySellBlocks, "Claim blocks may only be sold, not purchased.", null); - this.addDefault(defaults, Messages.BlockPurchaseCost, "Each claim block costs {0}. Your balance is {1}.", "0: cost of one block; 1: player's account balance"); - this.addDefault(defaults, Messages.ClaimBlockLimit, "You've reached your claim block limit. You can't purchase more.", null); - this.addDefault(defaults, Messages.InsufficientFunds, "You don't have enough money. You need {0}, but you only have {1}.", "0: total cost; 1: player's account balance"); - this.addDefault(defaults, Messages.PurchaseConfirmation, "Withdrew {0} from your account. You now have {1} available claim blocks.", "0: total cost; 1: remaining blocks"); - this.addDefault(defaults, Messages.OnlyPurchaseBlocks, "Claim blocks may only be purchased, not sold.", null); - this.addDefault(defaults, Messages.BlockSaleValue, "Each claim block is worth {0}. You have {1} available for sale.", "0: block value; 1: available blocks"); - this.addDefault(defaults, Messages.NotEnoughBlocksForSale, "You don't have that many claim blocks available for sale.", null); - this.addDefault(defaults, Messages.BlockSaleConfirmation, "Deposited {0} in your account. You now have {1} available claim blocks.", "0: amount deposited; 1: remaining blocks"); - this.addDefault(defaults, Messages.AdminClaimsMode, "Administrative claims mode active. Any claims created will be free and editable by other administrators.", null); - this.addDefault(defaults, Messages.BasicClaimsMode, "Returned to basic claim creation mode.", null); - this.addDefault(defaults, Messages.SubdivisionMode, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit.", null); - this.addDefault(defaults, Messages.SubdivisionDemo, "Land Claim Help: http://tinyurl.com/7urdtue", null); - this.addDefault(defaults, Messages.DeleteClaimMissing, "There's no claim here.", null); - this.addDefault(defaults, Messages.DeletionSubdivisionWarning, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again.", null); - this.addDefault(defaults, Messages.DeleteSuccess, "Claim deleted.", null); - this.addDefault(defaults, Messages.CantDeleteAdminClaim, "You don't have permission to delete administrative claims.", null); - this.addDefault(defaults, Messages.DeleteAllSuccess, "Deleted all of {0}'s claims.", "0: owner's name"); - this.addDefault(defaults, Messages.NoDeletePermission, "You don't have permission to delete claims.", null); - this.addDefault(defaults, Messages.AllAdminDeleted, "Deleted all administrative claims.", null); - this.addDefault(defaults, Messages.AdjustBlocksSuccess, "Adjusted {0}'s bonus claim blocks by {1}. New total bonus blocks: {2}.", "0: player; 1: adjustment; 2: new total"); - this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); - this.addDefault(defaults, Messages.TrappedOnCooldown, "You used /trapped within the last {0} hours. You have to wait about {1} more minutes before using it again.", "0: default cooldown hours; 1: remaining minutes"); - this.addDefault(defaults, Messages.RescuePending, "If you stay put for 10 seconds, you'll be teleported out. Please wait.", null); - this.addDefault(defaults, Messages.NonSiegeWorld, "Siege is disabled here.", null); - this.addDefault(defaults, Messages.AlreadySieging, "You're already involved in a siege.", null); - this.addDefault(defaults, Messages.AlreadyUnderSiegePlayer, "{0} is already under siege. Join the party!", "0: defending player"); - this.addDefault(defaults, Messages.NotSiegableThere, "{0} isn't protected there.", "0: defending player"); - this.addDefault(defaults, Messages.SiegeTooFarAway, "You're too far away to siege.", null); - this.addDefault(defaults, Messages.NoSiegeDefenseless, "That player is defenseless. Go pick on somebody else.", null); - this.addDefault(defaults, Messages.AlreadyUnderSiegeArea, "That area is already under siege. Join the party!", null); - this.addDefault(defaults, Messages.NoSiegeAdminClaim, "Siege is disabled in this area.", null); - this.addDefault(defaults, Messages.SiegeOnCooldown, "You're still on siege cooldown for this defender or claim. Find another victim.", null); - this.addDefault(defaults, Messages.SiegeAlert, "You're under siege! If you log out now, you will die. You must defeat {0}, wait for him to give up, or escape.", "0: attacker name"); - this.addDefault(defaults, Messages.SiegeConfirmed, "The siege has begun! If you log out now, you will die. You must defeat {0}, chase him away, or admit defeat and walk away.", "0: defender name"); - this.addDefault(defaults, Messages.AbandonClaimMissing, "Stand in the claim you want to delete, or consider /AbandonAllClaims.", null); - this.addDefault(defaults, Messages.NotYourClaim, "This isn't your claim.", null); - this.addDefault(defaults, Messages.DeleteTopLevelClaim, "To delete a subdivision, stand inside it. Otherwise, use /AbandonTopLevelClaim to delete this claim and all subdivisions.", null); - this.addDefault(defaults, Messages.AbandonSuccess, "Claim abandoned. You now have {0} available claim blocks.", "0: remaining claim blocks"); - this.addDefault(defaults, Messages.CantGrantThatPermission, "You can't grant a permission you don't have yourself.", null); - this.addDefault(defaults, Messages.GrantPermissionNoClaim, "Stand inside the claim where you want to grant permission.", null); - this.addDefault(defaults, Messages.GrantPermissionConfirmation, "Granted {0} permission to {1} {2}.", "0: target player; 1: permission description; 2: scope (changed claims)"); - this.addDefault(defaults, Messages.ManageUniversalPermissionsInstruction, "To manage permissions for ALL your claims, stand outside them.", null); - this.addDefault(defaults, Messages.ManageOneClaimPermissionsInstruction, "To manage permissions for a specific claim, stand inside it.", null); - this.addDefault(defaults, Messages.CollectivePublic, "the public", "as in 'granted the public permission to...'"); - this.addDefault(defaults, Messages.BuildPermission, "build", null); - this.addDefault(defaults, Messages.ContainersPermission, "access containers and animals", null); - this.addDefault(defaults, Messages.AccessPermission, "use buttons and levers", null); - this.addDefault(defaults, Messages.PermissionsPermission, "manage permissions", null); - this.addDefault(defaults, Messages.LocationCurrentClaim, "in this claim", null); - this.addDefault(defaults, Messages.LocationAllClaims, "in all your claims", null); - this.addDefault(defaults, Messages.PvPImmunityStart, "You're protected from attack by other players as long as your inventory is empty.", null); - this.addDefault(defaults, Messages.SiegeNoDrop, "You can't give away items while involved in a siege.", null); - this.addDefault(defaults, Messages.DonateItemsInstruction, "To give away the item(s) in your hand, left-click the chest again.", null); - this.addDefault(defaults, Messages.ChestFull, "This chest is full.", null); - this.addDefault(defaults, Messages.DonationSuccess, "Item(s) transferred to chest!", null); - this.addDefault(defaults, Messages.PlayerTooCloseForFire, "You can't start a fire this close to {0}.", "0: other player's name"); - this.addDefault(defaults, Messages.TooDeepToClaim, "This chest can't be protected because it's too deep underground. Consider moving it.", null); - this.addDefault(defaults, Messages.ChestClaimConfirmation, "This chest is protected.", null); - this.addDefault(defaults, Messages.AutomaticClaimNotification, "This chest and nearby blocks are protected from breakage and theft. The temporary gold and glowstone blocks mark the protected area. To toggle them on and off, right-click with a stick.", null); - this.addDefault(defaults, Messages.TrustCommandAdvertisement, "Use the /trust command to grant other players access.", null); - this.addDefault(defaults, Messages.GoldenShovelAdvertisement, "To claim more land, you need a golden shovel. When you equip one, you'll get more information.", null); - this.addDefault(defaults, Messages.UnprotectedChestWarning, "This chest is NOT protected. Consider using a golden shovel to expand an existing claim or to create a new one.", null); - this.addDefault(defaults, Messages.ThatPlayerPvPImmune, "You can't injure defenseless players.", null); - this.addDefault(defaults, Messages.CantFightWhileImmune, "You can't fight someone while you're protected from PvP.", null); - this.addDefault(defaults, Messages.NoDamageClaimedEntity, "That belongs to {0}.", "0: owner name"); - this.addDefault(defaults, Messages.ShovelBasicClaimMode, "Shovel returned to basic claims mode.", null); - this.addDefault(defaults, Messages.RemainingBlocks, "You may claim up to {0} more blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.CreativeBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/c7bajb8", null); - this.addDefault(defaults, Messages.SurvivalBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/6nkwegj", null); - this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped", "When mentioned in chat, players get information about the /trapped command."); - this.addDefault(defaults, Messages.TrappedInstructions, "Are you trapped in someone's land claim? Try the /trapped command.", null); - this.addDefault(defaults, Messages.PvPNoDrop, "You can't drop items while in PvP combat.", null); - this.addDefault(defaults, Messages.SiegeNoTeleport, "You can't teleport out of a besieged area.", null); - this.addDefault(defaults, Messages.BesiegedNoTeleport, "You can't teleport into a besieged area.", null); - this.addDefault(defaults, Messages.SiegeNoContainers, "You can't access containers while involved in a siege.", null); - this.addDefault(defaults, Messages.PvPNoContainers, "You can't access containers during PvP combat.", null); - this.addDefault(defaults, Messages.PvPImmunityEnd, "Now you can fight with other players.", null); - this.addDefault(defaults, Messages.NoBedPermission, "{0} hasn't given you permission to sleep here.", "0: claim owner"); - this.addDefault(defaults, Messages.NoWildernessBuckets, "You may only dump buckets inside your claim(s) or underground.", null); - this.addDefault(defaults, Messages.NoLavaNearOtherPlayer, "You can't place lava this close to {0}.", "0: nearby player"); - this.addDefault(defaults, Messages.TooFarAway, "That's too far away.", null); - this.addDefault(defaults, Messages.BlockNotClaimed, "No one has claimed this block.", null); - this.addDefault(defaults, Messages.BlockClaimed, "That block has been claimed by {0}.", "0: claim owner"); - this.addDefault(defaults, Messages.SiegeNoShovel, "You can't use your shovel tool while involved in a siege.", null); - this.addDefault(defaults, Messages.RestoreNaturePlayerInChunk, "Unable to restore. {0} is in that chunk.", "0: nearby player"); - this.addDefault(defaults, Messages.NoCreateClaimPermission, "You don't have permission to claim land.", null); - this.addDefault(defaults, Messages.ResizeClaimTooSmall, "This new size would be too small. Claims must be at least {0} x {0}.", "0: minimum claim size"); - this.addDefault(defaults, Messages.ResizeNeedMoreBlocks, "You don't have enough blocks for this size. You need {0} more.", "0: how many needed"); - this.addDefault(defaults, Messages.ClaimResizeSuccess, "Claim resized. You now have {0} available claim blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.ResizeFailOverlap, "Can't resize here because it would overlap another nearby claim.", null); - this.addDefault(defaults, Messages.ResizeStart, "Resizing claim. Use your shovel again at the new location for this corner.", null); - this.addDefault(defaults, Messages.ResizeFailOverlapSubdivision, "You can't create a subdivision here because it would overlap another subdivision. Consider /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); - this.addDefault(defaults, Messages.SubdivisionStart, "Subdivision corner set! Use your shovel at the location for the opposite corner of this new subdivision.", null); - this.addDefault(defaults, Messages.CreateSubdivisionOverlap, "Your selected area overlaps another subdivision.", null); - this.addDefault(defaults, Messages.SubdivisionSuccess, "Subdivision created! Use /trust to share it with friends.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlap, "You can't create a claim here because it would overlap your other claim. Use /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlapOtherPlayer, "You can't create a claim here because it would overlap {0}'s claim.", "0: other claim owner"); - this.addDefault(defaults, Messages.ClaimsDisabledWorld, "Land claims are disabled in this world.", null); - this.addDefault(defaults, Messages.ClaimStart, "Claim corner set! Use the shovel again at the opposite corner to claim a rectangle of land. To cancel, put your shovel away.", null); - this.addDefault(defaults, Messages.NewClaimTooSmall, "This claim would be too small. Any claim must be at least {0} x {0}.", "0: minimum claim size"); - this.addDefault(defaults, Messages.CreateClaimInsufficientBlocks, "You don't have enough blocks to claim that entire area. You need {0} more blocks.", "0: additional blocks needed"); - this.addDefault(defaults, Messages.AbandonClaimAdvertisement, "To delete another claim and free up some blocks, use /AbandonClaim.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlapShort, "Your selected area overlaps an existing claim.", null); - this.addDefault(defaults, Messages.CreateClaimSuccess, "Claim created! Use /trust to share it with friends.", null); - this.addDefault(defaults, Messages.SiegeWinDoorsOpen, "Congratulations! Buttons and levers are temporarily unlocked (five minutes).", null); - this.addDefault(defaults, Messages.RescueAbortedMoved, "You moved! Rescue cancelled.", null); - this.addDefault(defaults, Messages.SiegeDoorsLockedEjection, "Looting time is up! Ejected from the claim.", null); - this.addDefault(defaults, Messages.NoModifyDuringSiege, "Claims can't be modified while under siege.", null); - this.addDefault(defaults, Messages.OnlyOwnersModifyClaims, "Only {0} can modify this claim.", "0: owner name"); - this.addDefault(defaults, Messages.NoBuildUnderSiege, "This claim is under siege by {0}. No one can build here.", "0: attacker name"); - this.addDefault(defaults, Messages.NoBuildPvP, "You can't build in claims during PvP combat.", null); - this.addDefault(defaults, Messages.NoBuildPermission, "You don't have {0}'s permission to build here.", "0: owner name"); - this.addDefault(defaults, Messages.NonSiegeMaterial, "That material is too tough to break.", null); - this.addDefault(defaults, Messages.NoOwnerBuildUnderSiege, "You can't make changes while under siege.", null); - this.addDefault(defaults, Messages.NoAccessPermission, "You don't have {0}'s permission to use that.", "0: owner name. access permission controls buttons, levers, and beds"); - this.addDefault(defaults, Messages.NoContainersSiege, "This claim is under siege by {0}. No one can access containers here right now.", "0: attacker name"); - this.addDefault(defaults, Messages.NoContainersPermission, "You don't have {0}'s permission to use that.", "0: owner's name. containers also include crafting blocks"); - this.addDefault(defaults, Messages.OwnerNameForAdminClaims, "an administrator", "as in 'You don't have an administrator's permission to build here.'"); - this.addDefault(defaults, Messages.ClaimTooSmallForEntities, "This claim isn't big enough for that. Try enlarging it.", null); - this.addDefault(defaults, Messages.TooManyEntitiesInClaim, "This claim has too many entities already. Try enlarging the claim or removing some animals, monsters, paintings, or minecarts.", null); - this.addDefault(defaults, Messages.YouHaveNoClaims, "You don't have any land claims.", null); - this.addDefault(defaults, Messages.ConfirmFluidRemoval, "Abandoning this claim will remove all your lava and water. If you're sure, use /AbandonClaim again.", null); - this.addDefault(defaults, Messages.AutoBanNotify, "Auto-banned {0}({1}). See logs for details.", null); - this.addDefault(defaults, Messages.AdjustGroupBlocksSuccess, "Adjusted bonus claim blocks for players with the {0} permission by {1}. New total: {2}.", "0: permission; 1: adjustment amount; 2: new total bonus"); - this.addDefault(defaults, Messages.InvalidPermissionID, "Please specify a player name, or a permission in [brackets].", null); - this.addDefault(defaults, Messages.UntrustOwnerOnly, "Only {0} can revoke permissions here.", "0: claim owner's name"); - this.addDefault(defaults, Messages.HowToClaimRegex, "(^|.*\\W)how\\W.*\\W(claim|protect|lock)(\\W.*|$)", "This is a Java Regular Expression. Look it up before editing! It's used to tell players about the demo video when they ask how to claim land."); - this.addDefault(defaults, Messages.NoBuildOutsideClaims, "You can't build here unless you claim some land first.", null); - this.addDefault(defaults, Messages.PlayerOfflineTime, " Last login: {0} days ago.", "0: number of full days since last login"); - this.addDefault(defaults, Messages.BuildingOutsideClaims, "Other players can undo your work here! Consider using a golden shovel to claim this area so that your work will be protected.", null); - this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin, or consider /kill if you don't want to wait.", null); - this.addDefault(defaults, Messages.CommandBannedInPvP, "You can't use that command while in PvP combat.", null); - this.addDefault(defaults, Messages.UnclaimCleanupWarning, "The land you've unclaimed may be changed by other players or cleaned up by administrators. If you've built something there you want to keep, you should reclaim it.", null); - this.addDefault(defaults, Messages.BuySellNotConfigured, "Sorry, buying anhd selling claim blocks is disabled.", null); - this.addDefault(defaults, Messages.NoTeleportPvPCombat, "You can't teleport while fighting another player.", null); - this.addDefault(defaults, Messages.NoTNTDamageAboveSeaLevel, "Warning: TNT will not destroy blocks above sea level.", null); - this.addDefault(defaults, Messages.NoTNTDamageClaims, "Warning: TNT will not destroy claimed blocks.", null); - this.addDefault(defaults, Messages.IgnoreClaimsAdvertisement, "To override, use /IgnoreClaims.", null); - this.addDefault(defaults, Messages.NoPermissionForCommand, "You don't have permission to do that.", null); - this.addDefault(defaults, Messages.ClaimsListNoPermission, "You don't have permission to get information about another player's land claims.", null); - - //load the config file - FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); - - //for each message ID - for(int i = 0; i < messageIDs.length; i++) - { - //get default for this message - Messages messageID = messageIDs[i]; - CustomizableMessage messageData = defaults.get(messageID.name()); - - //if default is missing, log an error and use some fake data for now so that the plugin can run - if(messageData == null) - { - GriefPrevention.AddLogEntry("Missing message for " + messageID.name() + ". Please contact the developer."); - messageData = new CustomizableMessage(messageID, "Missing message! ID: " + messageID.name() + ". Please contact a server admin.", null); - } - - //read the message from the file, use default if necessary - this.messages[messageID.ordinal()] = config.getString("Messages." + messageID.name() + ".Text", messageData.text); - config.set("Messages." + messageID.name() + ".Text", this.messages[messageID.ordinal()]); - - if(messageData.notes != null) - { - messageData.notes = config.getString("Messages." + messageID.name() + ".Notes", messageData.notes); - config.set("Messages." + messageID.name() + ".Notes", messageData.notes); - } - } - - //save any changes - try - { - config.save(DataStore.messagesFilePath); - } - catch(IOException exception) - { - GriefPrevention.AddLogEntry("Unable to write to the configuration file at \"" + DataStore.messagesFilePath + "\""); - } - - defaults.clear(); - System.gc(); - } - - private void addDefault(HashMap defaults, - Messages id, String text, String notes) - { - CustomizableMessage message = new CustomizableMessage(id, text, notes); - defaults.put(id.name(), message); - } - - synchronized public String getMessage(Messages messageID, String... args) - { - String message = messages[messageID.ordinal()]; - - for(int i = 0; i < args.length; i++) - { - String param = args[i]; - message = message.replace("{" + i + "}", param); - } - - return message; - } - - abstract void close(); -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.io.*; +import java.util.*; + +import org.bukkit.*; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +//singleton class which manages all GriefPrevention data (except for config options) +public abstract class DataStore +{ + //in-memory cache for player data + protected HashMap playerNameToPlayerDataMap = new HashMap(); + + //in-memory cache for group (permission-based) data + protected HashMap permissionToBonusBlocksMap = new HashMap(); + + //in-memory cache for claim data + ArrayList claims = new ArrayList(); + + //in-memory cache for messages + private String [] messages; + + //next claim ID + Long nextClaimID = (long)0; + + //path information, for where stuff stored on disk is well... stored + protected final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; + final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; + final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; + + //initialization! + void initialize() throws Exception + { + GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); + + //make a list of players who own claims + Vector playerNames = new Vector(); + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + + //ignore admin claims + if(claim.isAdminClaim()) continue; + + if(!playerNames.contains(claim.ownerName)) + playerNames.add(claim.ownerName); + } + + GriefPrevention.AddLogEntry(playerNames.size() + " players have staked claims."); + + //load up all the messages from messages.yml + this.loadMessages(); + + //collect garbage, since lots of stuff was loaded into memory and then tossed out + System.gc(); + } + + //removes cached player data from memory + synchronized void clearCachedPlayerData(String playerName) + { + this.playerNameToPlayerDataMap.remove(playerName); + } + + //gets the number of bonus blocks a player has from his permissions + synchronized int getGroupBonusBlocks(String playerName) + { + int bonusBlocks = 0; + Set keys = permissionToBonusBlocksMap.keySet(); + Iterator iterator = keys.iterator(); + while(iterator.hasNext()) + { + String groupName = iterator.next(); + Player player = GriefPrevention.instance.getServer().getPlayer(playerName); + if(player != null && player.hasPermission(groupName)) + { + bonusBlocks += this.permissionToBonusBlocksMap.get(groupName); + } + } + + return bonusBlocks; + } + + //grants a group (players with a specific permission) bonus claim blocks as long as they're still members of the group + synchronized public int adjustGroupBonusBlocks(String groupName, int amount) + { + Integer currentValue = this.permissionToBonusBlocksMap.get(groupName); + if(currentValue == null) currentValue = 0; + + currentValue += amount; + this.permissionToBonusBlocksMap.put(groupName, currentValue); + + //write changes to storage to ensure they don't get lost + this.saveGroupBonusBlocks(groupName, currentValue); + + return currentValue; + } + + abstract void saveGroupBonusBlocks(String groupName, int amount); + + synchronized public void changeClaimOwner(Claim claim, String newOwnerName) throws Exception + { + //if it's a subdivision, throw an exception + if(claim.parent != null) + { + throw new Exception("Subdivisions can't be transferred. Only top-level claims may change owners."); + } + + //otherwise update information + + //determine current claim owner + PlayerData ownerData = null; + if(!claim.isAdminClaim()) + { + ownerData = this.getPlayerData(claim.ownerName); + } + + //determine new owner + PlayerData newOwnerData = this.getPlayerData(newOwnerName); + + //transfer + claim.ownerName = newOwnerName; + this.saveClaim(claim); + + //adjust blocks and other records + if(ownerData != null) + { + ownerData.claims.remove(claim); + ownerData.bonusClaimBlocks -= claim.getArea(); + this.savePlayerData(claim.ownerName, ownerData); + } + + newOwnerData.claims.add(claim); + newOwnerData.bonusClaimBlocks += claim.getArea(); + this.savePlayerData(newOwnerName, newOwnerData); + } + + //adds a claim to the datastore, making it an effective claim + synchronized void addClaim(Claim newClaim) + { + //subdivisions are easy + if(newClaim.parent != null) + { + newClaim.parent.children.add(newClaim); + newClaim.inDataStore = true; + this.saveClaim(newClaim); + return; + } + + //add it and mark it as added + int j = 0; + while(j < this.claims.size() && !this.claims.get(j).greaterThan(newClaim)) j++; + if(j < this.claims.size()) + this.claims.add(j, newClaim); + else + this.claims.add(this.claims.size(), newClaim); + newClaim.inDataStore = true; + + //except for administrative claims (which have no owner), update the owner's playerData with the new claim + if(!newClaim.isAdminClaim()) + { + PlayerData ownerData = this.getPlayerData(newClaim.getOwnerName()); + ownerData.claims.add(newClaim); + this.savePlayerData(newClaim.getOwnerName(), ownerData); + } + + //make sure the claim is saved to disk + this.saveClaim(newClaim); + } + + //turns a location into a string, useful in data storage + private String locationStringDelimiter = ";"; + String locationToString(Location location) + { + StringBuilder stringBuilder = new StringBuilder(location.getWorld().getName()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockX()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockY()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockZ()); + + return stringBuilder.toString(); + } + + //turns a location string back into a location + Location locationFromString(String string) throws Exception + { + //split the input string on the space + String [] elements = string.split(locationStringDelimiter); + + //expect four elements - world name, X, Y, and Z, respectively + if(elements.length != 4) + { + throw new Exception("Expected four distinct parts to the location string."); + } + + String worldName = elements[0]; + String xString = elements[1]; + String yString = elements[2]; + String zString = elements[3]; + + //identify world the claim is in + World world = GriefPrevention.instance.getServer().getWorld(worldName); + if(world == null) + { + throw new Exception("World not found: \"" + worldName + "\""); + } + + //convert those numerical strings to integer values + int x = Integer.parseInt(xString); + int y = Integer.parseInt(yString); + int z = Integer.parseInt(zString); + + return new Location(world, x, y, z); + } + + //saves any changes to a claim to secondary storage + synchronized public void saveClaim(Claim claim) + { + //subdivisions don't save to their own files, but instead live in their parent claim's file + //so any attempt to save a subdivision will save its parent (and thus the subdivision) + if(claim.parent != null) + { + this.saveClaim(claim.parent); + return; + } + + //otherwise get a unique identifier for the claim which will be used to name the file on disk + if(claim.id == null) + { + claim.id = this.nextClaimID; + this.incrementNextClaimID(); + } + + this.writeClaimToStorage(claim); + } + + abstract void writeClaimToStorage(Claim claim); + + //increments the claim ID and updates secondary storage to be sure it's saved + abstract void incrementNextClaimID(); + + //retrieves player data from memory or secondary storage, as necessary + //if the player has never been on the server before, this will return a fresh player data with default values + synchronized public PlayerData getPlayerData(String playerName) + { + //first, look in memory + PlayerData playerData = this.playerNameToPlayerDataMap.get(playerName); + + //if not there, look in secondary storage + if(playerData == null) + { + playerData = this.getPlayerDataFromStorage(playerName); + playerData.playerName = playerName; + + //find all the claims belonging to this player and note them for future reference + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + if(claim.ownerName.equals(playerName)) + { + playerData.claims.add(claim); + } + } + + //shove that new player data into the hash map cache + this.playerNameToPlayerDataMap.put(playerName, playerData); + } + + //try the hash map again. if it's STILL not there, we have a bug to fix + return this.playerNameToPlayerDataMap.get(playerName); + } + + abstract PlayerData getPlayerDataFromStorage(String playerName); + + //deletes a claim or subdivision + synchronized public void deleteClaim(Claim claim) + { + //subdivisions are simple - just remove them from their parent claim and save that claim + if(claim.parent != null) + { + Claim parentClaim = claim.parent; + parentClaim.children.remove(claim); + this.saveClaim(parentClaim); + return; + } + + //remove from memory + for(int i = 0; i < this.claims.size(); i++) + { + if(claims.get(i).id.equals(claim.id)) + { + this.claims.remove(i); + claim.inDataStore = false; + for(int j = 0; j < claim.children.size(); j++) + { + claim.children.get(j).inDataStore = false; + } + break; + } + } + + //remove from secondary storage + this.deleteClaimFromSecondaryStorage(claim); + + //update player data, except for administrative claims, which have no owner + if(!claim.isAdminClaim()) + { + PlayerData ownerData = this.getPlayerData(claim.getOwnerName()); + for(int i = 0; i < ownerData.claims.size(); i++) + { + if(ownerData.claims.get(i).id.equals(claim.id)) + { + ownerData.claims.remove(i); + break; + } + } + this.savePlayerData(claim.getOwnerName(), ownerData); + } + } + + abstract void deleteClaimFromSecondaryStorage(Claim claim); + + //gets the claim at a specific location + //ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim + //cachedClaim can be NULL, but will help performance if you have a reasonable guess about which claim the location is in + synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, Claim cachedClaim) + { + //check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done + if(cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) return cachedClaim; + + //the claims list is ordered by greater boundary corner + //create a temporary "fake" claim in memory for comparison purposes + Claim tempClaim = new Claim(); + tempClaim.lesserBoundaryCorner = location; + + //otherwise, search all existing claims until we find the right claim + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + + //if we reach a claim which is greater than the temp claim created above, there's definitely no claim + //in the collection which includes our location + if(claim.greaterThan(tempClaim)) return null; + + //find a top level claim + if(claim.contains(location, ignoreHeight, false)) + { + //when we find a top level claim, if the location is in one of its subdivisions, + //return the SUBDIVISION, not the top level claim + for(int j = 0; j < claim.children.size(); j++) + { + Claim subdivision = claim.children.get(j); + if(subdivision.contains(location, ignoreHeight, false)) return subdivision; + } + + return claim; + } + } + + //if no claim found, return null + return null; + } + + //creates a claim. + //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim + //otherwise, returns a success along with a reference to the new claim + //use ownerName == "" for administrative claims + //for top level claims, pass parent == NULL + //DOES adjust claim blocks available on success (players can go into negative quantity available) + //does NOT check a player has permission to create a claim, or enough claim blocks. + //does NOT check minimum claim size constraints + //does NOT visualize the new claim for any players + synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, String ownerName, Claim parent, Long id) + { + CreateClaimResult result = new CreateClaimResult(); + + int smallx, bigx, smally, bigy, smallz, bigz; + + //determine small versus big inputs + if(x1 < x2) + { + smallx = x1; + bigx = x2; + } + else + { + smallx = x2; + bigx = x1; + } + + if(y1 < y2) + { + smally = y1; + bigy = y2; + } + else + { + smally = y2; + bigy = y1; + } + + if(z1 < z2) + { + smallz = z1; + bigz = z2; + } + else + { + smallz = z2; + bigz = z1; + } + + //creative mode claims always go to bedrock + if(GriefPrevention.instance.config_claims_enabledCreativeWorlds.contains(world)) + { + smally = 2; + } + + //create a new claim instance (but don't save it, yet) + Claim newClaim = new Claim( + new Location(world, smallx, smally, smallz), + new Location(world, bigx, bigy, bigz), + ownerName, + new String [] {}, + new String [] {}, + new String [] {}, + new String [] {}, + id); + + newClaim.parent = parent; + + //ensure this new claim won't overlap any existing claims + ArrayList claimsToCheck; + if(newClaim.parent != null) + { + claimsToCheck = newClaim.parent.children; + } + else + { + claimsToCheck = this.claims; + } + + for(int i = 0; i < claimsToCheck.size(); i++) + { + Claim otherClaim = claimsToCheck.get(i); + + //if we find an existing claim which will be overlapped + if(otherClaim.overlaps(newClaim)) + { + //result = fail, return conflicting claim + result.succeeded = false; + result.claim = otherClaim; + return result; + } + } + + //otherwise add this new claim to the data store to make it effective + this.addClaim(newClaim); + + //then return success along with reference to new claim + result.succeeded = true; + result.claim = newClaim; + return result; + } + + //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them + public abstract void savePlayerData(String playerName, PlayerData playerData); + + //extends a claim to a new depth + //respects the max depth config variable + synchronized public void extendClaim(Claim claim, int newDepth) + { + if(newDepth < GriefPrevention.instance.config_claims_maxDepth) newDepth = GriefPrevention.instance.config_claims_maxDepth; + + if(claim.parent != null) claim = claim.parent; + + //delete the claim + this.deleteClaim(claim); + + //re-create it at the new depth + claim.lesserBoundaryCorner.setY(newDepth); + claim.greaterBoundaryCorner.setY(newDepth); + + //make all subdivisions reach to the same depth + for(int i = 0; i < claim.children.size(); i++) + { + claim.children.get(i).lesserBoundaryCorner.setY(newDepth); + claim.children.get(i).greaterBoundaryCorner.setY(newDepth); + } + + //save changes + this.addClaim(claim); + } + + //starts a siege on a claim + //does NOT check siege cooldowns, see onCooldown() below + synchronized public void startSiege(Player attacker, Player defender, Claim defenderClaim) + { + //fill-in the necessary SiegeData instance + SiegeData siegeData = new SiegeData(attacker, defender, defenderClaim); + PlayerData attackerData = this.getPlayerData(attacker.getName()); + PlayerData defenderData = this.getPlayerData(defender.getName()); + attackerData.siegeData = siegeData; + defenderData.siegeData = siegeData; + defenderClaim.siegeData = siegeData; + + //start a task to monitor the siege + //why isn't this a "repeating" task? + //because depending on the status of the siege at the time the task runs, there may or may not be a reason to run the task again + SiegeCheckupTask task = new SiegeCheckupTask(siegeData); + siegeData.checkupTaskID = GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 30); + } + + //ends a siege + //either winnerName or loserName can be null, but not both + synchronized public void endSiege(SiegeData siegeData, String winnerName, String loserName, boolean death) + { + boolean grantAccess = false; + + //determine winner and loser + if(winnerName == null && loserName != null) + { + if(siegeData.attacker.getName().equals(loserName)) + { + winnerName = siegeData.defender.getName(); + } + else + { + winnerName = siegeData.attacker.getName(); + } + } + else if(winnerName != null && loserName == null) + { + if(siegeData.attacker.getName().equals(winnerName)) + { + loserName = siegeData.defender.getName(); + } + else + { + loserName = siegeData.attacker.getName(); + } + } + + //if the attacker won, plan to open the doors for looting + if(siegeData.attacker.getName().equals(winnerName)) + { + grantAccess = true; + } + + PlayerData attackerData = this.getPlayerData(siegeData.attacker.getName()); + attackerData.siegeData = null; + + PlayerData defenderData = this.getPlayerData(siegeData.defender.getName()); + defenderData.siegeData = null; + + //start a cooldown for this attacker/defender pair + Long now = Calendar.getInstance().getTimeInMillis(); + Long cooldownEnd = now + 1000 * 60 * 60; //one hour from now + this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + siegeData.defender.getName(), cooldownEnd); + + //start cooldowns for every attacker/involved claim pair + for(int i = 0; i < siegeData.claims.size(); i++) + { + Claim claim = siegeData.claims.get(i); + claim.siegeData = null; + this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + claim.ownerName, cooldownEnd); + + //if doors should be opened for looting, do that now + if(grantAccess) + { + claim.doorsOpen = true; + } + } + + //cancel the siege checkup task + GriefPrevention.instance.getServer().getScheduler().cancelTask(siegeData.checkupTaskID); + + //notify everyone who won and lost + if(winnerName != null && loserName != null) + { + GriefPrevention.instance.getServer().broadcastMessage(winnerName + " defeated " + loserName + " in siege warfare!"); + } + + //if the claim should be opened to looting + if(grantAccess) + { + Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); + if(winner != null) + { + //notify the winner + GriefPrevention.sendMessage(winner, TextMode.Success, Messages.SiegeWinDoorsOpen); + + //schedule a task to secure the claims in about 5 minutes + SecureClaimTask task = new SecureClaimTask(siegeData); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 5); + } + } + + //if the siege ended due to death, transfer inventory to winner + if(death) + { + Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); + Player loser = GriefPrevention.instance.getServer().getPlayer(loserName); + if(winner != null && loser != null) + { + //get loser's inventory, then clear it + ItemStack [] loserItems = loser.getInventory().getContents(); + loser.getInventory().clear(); + + //try to add it to the winner's inventory + for(int j = 0; j < loserItems.length; j++) + { + if(loserItems[j] == null || loserItems[j].getType() == Material.AIR || loserItems[j].getAmount() == 0) continue; + + HashMap wontFitItems = winner.getInventory().addItem(loserItems[j]); + + //drop any remainder on the ground at his feet + Object [] keys = wontFitItems.keySet().toArray(); + Location winnerLocation = winner.getLocation(); + for(int i = 0; i < keys.length; i++) + { + Integer key = (Integer)keys[i]; + winnerLocation.getWorld().dropItemNaturally(winnerLocation, wontFitItems.get(key)); + } + } + } + } + } + + //timestamp for each siege cooldown to end + private HashMap siegeCooldownRemaining = new HashMap(); + + //whether or not a sieger can siege a particular victim or claim, considering only cooldowns + synchronized public boolean onCooldown(Player attacker, Player defender, Claim defenderClaim) + { + Long cooldownEnd = null; + + //look for an attacker/defender cooldown + if(this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()) != null) + { + cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()); + + if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) + { + return true; + } + + //if found but expired, remove it + this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defender.getName()); + } + + //look for an attacker/claim cooldown + if(cooldownEnd == null && this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName) != null) + { + cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName); + + if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) + { + return true; + } + + //if found but expired, remove it + this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defenderClaim.ownerName); + } + + return false; + } + + //extend a siege, if it's possible to do so + synchronized void tryExtendSiege(Player player, Claim claim) + { + PlayerData playerData = this.getPlayerData(player.getName()); + + //player must be sieged + if(playerData.siegeData == null) return; + + //claim isn't already under the same siege + if(playerData.siegeData.claims.contains(claim)) return; + + //admin claims can't be sieged + if(claim.isAdminClaim()) return; + + //player must have some level of permission to be sieged in a claim + if(claim.allowAccess(player) != null) return; + + //otherwise extend the siege + playerData.siegeData.claims.add(claim); + claim.siegeData = playerData.siegeData; + } + + //deletes all claims owned by a player + synchronized public void deleteClaimsForPlayer(String playerName, boolean deleteCreativeClaims) + { + //make a list of the player's claims + ArrayList claimsToDelete = new ArrayList(); + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + if(claim.ownerName.equals(playerName) && (deleteCreativeClaims || !GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))) + claimsToDelete.add(claim); + } + + //delete them one by one + for(int i = 0; i < claimsToDelete.size(); i++) + { + Claim claim = claimsToDelete.get(i); + claim.removeSurfaceFluids(null); + + this.deleteClaim(claim); + + //if in a creative mode world, delete the claim + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + GriefPrevention.instance.restoreClaim(claim, 0); + } + } + } + + //tries to resize a claim + //see CreateClaim() for details on return value + synchronized public CreateClaimResult resizeClaim(Claim claim, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) + { + //remove old claim + this.deleteClaim(claim); + + //try to create this new claim, ignoring the original when checking for overlap + CreateClaimResult result = this.createClaim(claim.getLesserBoundaryCorner().getWorld(), newx1, newx2, newy1, newy2, newz1, newz2, claim.ownerName, claim.parent, claim.id); + + //if succeeded + if(result.succeeded) + { + //copy permissions from old claim + ArrayList builders = new ArrayList(); + ArrayList containers = new ArrayList(); + ArrayList accessors = new ArrayList(); + ArrayList managers = new ArrayList(); + claim.getPermissions(builders, containers, accessors, managers); + + for(int i = 0; i < builders.size(); i++) + result.claim.setPermission(builders.get(i), ClaimPermission.Build); + + for(int i = 0; i < containers.size(); i++) + result.claim.setPermission(containers.get(i), ClaimPermission.Inventory); + + for(int i = 0; i < accessors.size(); i++) + result.claim.setPermission(accessors.get(i), ClaimPermission.Access); + + for(int i = 0; i < managers.size(); i++) + { + result.claim.managers.add(managers.get(i)); + } + + //copy subdivisions from old claim + for(int i = 0; i < claim.children.size(); i++) + { + Claim subdivision = claim.children.get(i); + subdivision.parent = result.claim; + result.claim.children.add(subdivision); + } + + //save those changes + this.saveClaim(result.claim); + } + + else + { + //put original claim back + this.addClaim(claim); + } + + return result; + } + + private void loadMessages() + { + Messages [] messageIDs = Messages.values(); + this.messages = new String[Messages.values().length]; + + HashMap defaults = new HashMap(); + + //initialize defaults + this.addDefault(defaults, Messages.RespectingClaims, "Now respecting claims.", null); + this.addDefault(defaults, Messages.IgnoringClaims, "Now ignoring claims.", null); + this.addDefault(defaults, Messages.NoCreativeUnClaim, "You can't unclaim this land. You can only make this claim larger or create additional claims.", null); + this.addDefault(defaults, Messages.SuccessfulAbandon, "Claims abandoned. You now have {0} available claim blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.RestoreNatureActivate, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop.", null); + this.addDefault(defaults, Messages.RestoreNatureAggressiveActivate, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop.", null); + this.addDefault(defaults, Messages.FillModeActive, "Fill mode activated with radius {0}. Right click an area to fill.", "0: fill radius"); + this.addDefault(defaults, Messages.TransferClaimPermission, "That command requires the administrative claims permission.", null); + this.addDefault(defaults, Messages.TransferClaimMissing, "There's no claim here. Stand in the administrative claim you want to transfer.", null); + this.addDefault(defaults, Messages.TransferClaimAdminOnly, "Only administrative claims may be transferred to a player.", null); + this.addDefault(defaults, Messages.PlayerNotFound, "Player not found.", null); + this.addDefault(defaults, Messages.TransferTopLevel, "Only top level claims (not subdivisions) may be transferred. Stand outside of the subdivision and try again.", null); + this.addDefault(defaults, Messages.TransferSuccess, "Claim transferred.", null); + this.addDefault(defaults, Messages.TrustListNoClaim, "Stand inside the claim you're curious about.", null); + this.addDefault(defaults, Messages.ClearPermsOwnerOnly, "Only the claim owner can clear all permissions.", null); + this.addDefault(defaults, Messages.UntrustIndividualAllClaims, "Revoked {0}'s access to ALL your claims. To set permissions for a single claim, stand inside it.", "0: untrusted player"); + this.addDefault(defaults, Messages.UntrustEveryoneAllClaims, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it.", null); + this.addDefault(defaults, Messages.NoPermissionTrust, "You don't have {0}'s permission to manage permissions here.", "0: claim owner's name"); + this.addDefault(defaults, Messages.ClearPermissionsOneClaim, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them.", null); + this.addDefault(defaults, Messages.UntrustIndividualSingleClaim, "Revoked {0}'s access to this claim. To set permissions for a ALL your claims, stand outside them.", "0: untrusted player"); + this.addDefault(defaults, Messages.OnlySellBlocks, "Claim blocks may only be sold, not purchased.", null); + this.addDefault(defaults, Messages.BlockPurchaseCost, "Each claim block costs {0}. Your balance is {1}.", "0: cost of one block; 1: player's account balance"); + this.addDefault(defaults, Messages.ClaimBlockLimit, "You've reached your claim block limit. You can't purchase more.", null); + this.addDefault(defaults, Messages.InsufficientFunds, "You don't have enough money. You need {0}, but you only have {1}.", "0: total cost; 1: player's account balance"); + this.addDefault(defaults, Messages.PurchaseConfirmation, "Withdrew {0} from your account. You now have {1} available claim blocks.", "0: total cost; 1: remaining blocks"); + this.addDefault(defaults, Messages.OnlyPurchaseBlocks, "Claim blocks may only be purchased, not sold.", null); + this.addDefault(defaults, Messages.BlockSaleValue, "Each claim block is worth {0}. You have {1} available for sale.", "0: block value; 1: available blocks"); + this.addDefault(defaults, Messages.NotEnoughBlocksForSale, "You don't have that many claim blocks available for sale.", null); + this.addDefault(defaults, Messages.BlockSaleConfirmation, "Deposited {0} in your account. You now have {1} available claim blocks.", "0: amount deposited; 1: remaining blocks"); + this.addDefault(defaults, Messages.AdminClaimsMode, "Administrative claims mode active. Any claims created will be free and editable by other administrators.", null); + this.addDefault(defaults, Messages.BasicClaimsMode, "Returned to basic claim creation mode.", null); + this.addDefault(defaults, Messages.SubdivisionMode, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit.", null); + this.addDefault(defaults, Messages.SubdivisionDemo, "Land Claim Help: http://tinyurl.com/7urdtue", null); + this.addDefault(defaults, Messages.DeleteClaimMissing, "There's no claim here.", null); + this.addDefault(defaults, Messages.DeletionSubdivisionWarning, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again.", null); + this.addDefault(defaults, Messages.DeleteSuccess, "Claim deleted.", null); + this.addDefault(defaults, Messages.CantDeleteAdminClaim, "You don't have permission to delete administrative claims.", null); + this.addDefault(defaults, Messages.DeleteAllSuccess, "Deleted all of {0}'s claims.", "0: owner's name"); + this.addDefault(defaults, Messages.NoDeletePermission, "You don't have permission to delete claims.", null); + this.addDefault(defaults, Messages.AllAdminDeleted, "Deleted all administrative claims.", null); + this.addDefault(defaults, Messages.AdjustBlocksSuccess, "Adjusted {0}'s bonus claim blocks by {1}. New total bonus blocks: {2}.", "0: player; 1: adjustment; 2: new total"); + this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); + this.addDefault(defaults, Messages.TrappedOnCooldown, "You used /trapped within the last {0} hours. You have to wait about {1} more minutes before using it again.", "0: default cooldown hours; 1: remaining minutes"); + this.addDefault(defaults, Messages.RescuePending, "If you stay put for 10 seconds, you'll be teleported out. Please wait.", null); + this.addDefault(defaults, Messages.NonSiegeWorld, "Siege is disabled here.", null); + this.addDefault(defaults, Messages.AlreadySieging, "You're already involved in a siege.", null); + this.addDefault(defaults, Messages.AlreadyUnderSiegePlayer, "{0} is already under siege. Join the party!", "0: defending player"); + this.addDefault(defaults, Messages.NotSiegableThere, "{0} isn't protected there.", "0: defending player"); + this.addDefault(defaults, Messages.SiegeTooFarAway, "You're too far away to siege.", null); + this.addDefault(defaults, Messages.NoSiegeDefenseless, "That player is defenseless. Go pick on somebody else.", null); + this.addDefault(defaults, Messages.AlreadyUnderSiegeArea, "That area is already under siege. Join the party!", null); + this.addDefault(defaults, Messages.NoSiegeAdminClaim, "Siege is disabled in this area.", null); + this.addDefault(defaults, Messages.SiegeOnCooldown, "You're still on siege cooldown for this defender or claim. Find another victim.", null); + this.addDefault(defaults, Messages.SiegeAlert, "You're under siege! If you log out now, you will die. You must defeat {0}, wait for him to give up, or escape.", "0: attacker name"); + this.addDefault(defaults, Messages.SiegeConfirmed, "The siege has begun! If you log out now, you will die. You must defeat {0}, chase him away, or admit defeat and walk away.", "0: defender name"); + this.addDefault(defaults, Messages.AbandonClaimMissing, "Stand in the claim you want to delete, or consider /AbandonAllClaims.", null); + this.addDefault(defaults, Messages.NotYourClaim, "This isn't your claim.", null); + this.addDefault(defaults, Messages.DeleteTopLevelClaim, "To delete a subdivision, stand inside it. Otherwise, use /AbandonTopLevelClaim to delete this claim and all subdivisions.", null); + this.addDefault(defaults, Messages.AbandonSuccess, "Claim abandoned. You now have {0} available claim blocks.", "0: remaining claim blocks"); + this.addDefault(defaults, Messages.CantGrantThatPermission, "You can't grant a permission you don't have yourself.", null); + this.addDefault(defaults, Messages.GrantPermissionNoClaim, "Stand inside the claim where you want to grant permission.", null); + this.addDefault(defaults, Messages.GrantPermissionConfirmation, "Granted {0} permission to {1} {2}.", "0: target player; 1: permission description; 2: scope (changed claims)"); + this.addDefault(defaults, Messages.ManageUniversalPermissionsInstruction, "To manage permissions for ALL your claims, stand outside them.", null); + this.addDefault(defaults, Messages.ManageOneClaimPermissionsInstruction, "To manage permissions for a specific claim, stand inside it.", null); + this.addDefault(defaults, Messages.CollectivePublic, "the public", "as in 'granted the public permission to...'"); + this.addDefault(defaults, Messages.BuildPermission, "build", null); + this.addDefault(defaults, Messages.ContainersPermission, "access containers and animals", null); + this.addDefault(defaults, Messages.AccessPermission, "use buttons and levers", null); + this.addDefault(defaults, Messages.PermissionsPermission, "manage permissions", null); + this.addDefault(defaults, Messages.LocationCurrentClaim, "in this claim", null); + this.addDefault(defaults, Messages.LocationAllClaims, "in all your claims", null); + this.addDefault(defaults, Messages.PvPImmunityStart, "You're protected from attack by other players as long as your inventory is empty.", null); + this.addDefault(defaults, Messages.SiegeNoDrop, "You can't give away items while involved in a siege.", null); + this.addDefault(defaults, Messages.DonateItemsInstruction, "To give away the item(s) in your hand, left-click the chest again.", null); + this.addDefault(defaults, Messages.ChestFull, "This chest is full.", null); + this.addDefault(defaults, Messages.DonationSuccess, "Item(s) transferred to chest!", null); + this.addDefault(defaults, Messages.PlayerTooCloseForFire, "You can't start a fire this close to {0}.", "0: other player's name"); + this.addDefault(defaults, Messages.TooDeepToClaim, "This chest can't be protected because it's too deep underground. Consider moving it.", null); + this.addDefault(defaults, Messages.ChestClaimConfirmation, "This chest is protected.", null); + this.addDefault(defaults, Messages.AutomaticClaimNotification, "This chest and nearby blocks are protected from breakage and theft. The temporary gold and glowstone blocks mark the protected area. To toggle them on and off, right-click with a stick.", null); + this.addDefault(defaults, Messages.TrustCommandAdvertisement, "Use the /trust command to grant other players access.", null); + this.addDefault(defaults, Messages.GoldenShovelAdvertisement, "To claim more land, you need a golden shovel. When you equip one, you'll get more information.", null); + this.addDefault(defaults, Messages.UnprotectedChestWarning, "This chest is NOT protected. Consider using a golden shovel to expand an existing claim or to create a new one.", null); + this.addDefault(defaults, Messages.ThatPlayerPvPImmune, "You can't injure defenseless players.", null); + this.addDefault(defaults, Messages.CantFightWhileImmune, "You can't fight someone while you're protected from PvP.", null); + this.addDefault(defaults, Messages.NoDamageClaimedEntity, "That belongs to {0}.", "0: owner name"); + this.addDefault(defaults, Messages.ShovelBasicClaimMode, "Shovel returned to basic claims mode.", null); + this.addDefault(defaults, Messages.RemainingBlocks, "You may claim up to {0} more blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.CreativeBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/c7bajb8", null); + this.addDefault(defaults, Messages.SurvivalBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/6nkwegj", null); + this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped", "When mentioned in chat, players get information about the /trapped command."); + this.addDefault(defaults, Messages.TrappedInstructions, "Are you trapped in someone's land claim? Try the /trapped command.", null); + this.addDefault(defaults, Messages.PvPNoDrop, "You can't drop items while in PvP combat.", null); + this.addDefault(defaults, Messages.SiegeNoTeleport, "You can't teleport out of a besieged area.", null); + this.addDefault(defaults, Messages.BesiegedNoTeleport, "You can't teleport into a besieged area.", null); + this.addDefault(defaults, Messages.SiegeNoContainers, "You can't access containers while involved in a siege.", null); + this.addDefault(defaults, Messages.PvPNoContainers, "You can't access containers during PvP combat.", null); + this.addDefault(defaults, Messages.PvPImmunityEnd, "Now you can fight with other players.", null); + this.addDefault(defaults, Messages.NoBedPermission, "{0} hasn't given you permission to sleep here.", "0: claim owner"); + this.addDefault(defaults, Messages.NoWildernessBuckets, "You may only dump buckets inside your claim(s) or underground.", null); + this.addDefault(defaults, Messages.NoLavaNearOtherPlayer, "You can't place lava this close to {0}.", "0: nearby player"); + this.addDefault(defaults, Messages.TooFarAway, "That's too far away.", null); + this.addDefault(defaults, Messages.BlockNotClaimed, "No one has claimed this block.", null); + this.addDefault(defaults, Messages.BlockClaimed, "That block has been claimed by {0}.", "0: claim owner"); + this.addDefault(defaults, Messages.SiegeNoShovel, "You can't use your shovel tool while involved in a siege.", null); + this.addDefault(defaults, Messages.RestoreNaturePlayerInChunk, "Unable to restore. {0} is in that chunk.", "0: nearby player"); + this.addDefault(defaults, Messages.NoCreateClaimPermission, "You don't have permission to claim land.", null); + this.addDefault(defaults, Messages.ResizeClaimTooSmall, "This new size would be too small. Claims must be at least {0} x {0}.", "0: minimum claim size"); + this.addDefault(defaults, Messages.ResizeNeedMoreBlocks, "You don't have enough blocks for this size. You need {0} more.", "0: how many needed"); + this.addDefault(defaults, Messages.ClaimResizeSuccess, "Claim resized. You now have {0} available claim blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.ResizeFailOverlap, "Can't resize here because it would overlap another nearby claim.", null); + this.addDefault(defaults, Messages.ResizeStart, "Resizing claim. Use your shovel again at the new location for this corner.", null); + this.addDefault(defaults, Messages.ResizeFailOverlapSubdivision, "You can't create a subdivision here because it would overlap another subdivision. Consider /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); + this.addDefault(defaults, Messages.SubdivisionStart, "Subdivision corner set! Use your shovel at the location for the opposite corner of this new subdivision.", null); + this.addDefault(defaults, Messages.CreateSubdivisionOverlap, "Your selected area overlaps another subdivision.", null); + this.addDefault(defaults, Messages.SubdivisionSuccess, "Subdivision created! Use /trust to share it with friends.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlap, "You can't create a claim here because it would overlap your other claim. Use /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlapOtherPlayer, "You can't create a claim here because it would overlap {0}'s claim.", "0: other claim owner"); + this.addDefault(defaults, Messages.ClaimsDisabledWorld, "Land claims are disabled in this world.", null); + this.addDefault(defaults, Messages.ClaimStart, "Claim corner set! Use the shovel again at the opposite corner to claim a rectangle of land. To cancel, put your shovel away.", null); + this.addDefault(defaults, Messages.NewClaimTooSmall, "This claim would be too small. Any claim must be at least {0} x {0}.", "0: minimum claim size"); + this.addDefault(defaults, Messages.CreateClaimInsufficientBlocks, "You don't have enough blocks to claim that entire area. You need {0} more blocks.", "0: additional blocks needed"); + this.addDefault(defaults, Messages.AbandonClaimAdvertisement, "To delete another claim and free up some blocks, use /AbandonClaim.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlapShort, "Your selected area overlaps an existing claim.", null); + this.addDefault(defaults, Messages.CreateClaimSuccess, "Claim created! Use /trust to share it with friends.", null); + this.addDefault(defaults, Messages.SiegeWinDoorsOpen, "Congratulations! Buttons and levers are temporarily unlocked (five minutes).", null); + this.addDefault(defaults, Messages.RescueAbortedMoved, "You moved! Rescue cancelled.", null); + this.addDefault(defaults, Messages.SiegeDoorsLockedEjection, "Looting time is up! Ejected from the claim.", null); + this.addDefault(defaults, Messages.NoModifyDuringSiege, "Claims can't be modified while under siege.", null); + this.addDefault(defaults, Messages.OnlyOwnersModifyClaims, "Only {0} can modify this claim.", "0: owner name"); + this.addDefault(defaults, Messages.NoBuildUnderSiege, "This claim is under siege by {0}. No one can build here.", "0: attacker name"); + this.addDefault(defaults, Messages.NoBuildPvP, "You can't build in claims during PvP combat.", null); + this.addDefault(defaults, Messages.NoBuildPermission, "You don't have {0}'s permission to build here.", "0: owner name"); + this.addDefault(defaults, Messages.NonSiegeMaterial, "That material is too tough to break.", null); + this.addDefault(defaults, Messages.NoOwnerBuildUnderSiege, "You can't make changes while under siege.", null); + this.addDefault(defaults, Messages.NoAccessPermission, "You don't have {0}'s permission to use that.", "0: owner name. access permission controls buttons, levers, and beds"); + this.addDefault(defaults, Messages.NoContainersSiege, "This claim is under siege by {0}. No one can access containers here right now.", "0: attacker name"); + this.addDefault(defaults, Messages.NoContainersPermission, "You don't have {0}'s permission to use that.", "0: owner's name. containers also include crafting blocks"); + this.addDefault(defaults, Messages.OwnerNameForAdminClaims, "an administrator", "as in 'You don't have an administrator's permission to build here.'"); + this.addDefault(defaults, Messages.ClaimTooSmallForEntities, "This claim isn't big enough for that. Try enlarging it.", null); + this.addDefault(defaults, Messages.TooManyEntitiesInClaim, "This claim has too many entities already. Try enlarging the claim or removing some animals, monsters, paintings, or minecarts.", null); + this.addDefault(defaults, Messages.YouHaveNoClaims, "You don't have any land claims.", null); + this.addDefault(defaults, Messages.ConfirmFluidRemoval, "Abandoning this claim will remove all your lava and water. If you're sure, use /AbandonClaim again.", null); + this.addDefault(defaults, Messages.AutoBanNotify, "Auto-banned {0}({1}). See logs for details.", null); + this.addDefault(defaults, Messages.AdjustGroupBlocksSuccess, "Adjusted bonus claim blocks for players with the {0} permission by {1}. New total: {2}.", "0: permission; 1: adjustment amount; 2: new total bonus"); + this.addDefault(defaults, Messages.InvalidPermissionID, "Please specify a player name, or a permission in [brackets].", null); + this.addDefault(defaults, Messages.UntrustOwnerOnly, "Only {0} can revoke permissions here.", "0: claim owner's name"); + this.addDefault(defaults, Messages.HowToClaimRegex, "(^|.*\\W)how\\W.*\\W(claim|protect|lock)(\\W.*|$)", "This is a Java Regular Expression. Look it up before editing! It's used to tell players about the demo video when they ask how to claim land."); + this.addDefault(defaults, Messages.NoBuildOutsideClaims, "You can't build here unless you claim some land first.", null); + this.addDefault(defaults, Messages.PlayerOfflineTime, " Last login: {0} days ago.", "0: number of full days since last login"); + this.addDefault(defaults, Messages.BuildingOutsideClaims, "Other players can undo your work here! Consider using a golden shovel to claim this area so that your work will be protected.", null); + this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin, or consider /kill if you don't want to wait.", null); + this.addDefault(defaults, Messages.CommandBannedInPvP, "You can't use that command while in PvP combat.", null); + this.addDefault(defaults, Messages.UnclaimCleanupWarning, "The land you've unclaimed may be changed by other players or cleaned up by administrators. If you've built something there you want to keep, you should reclaim it.", null); + this.addDefault(defaults, Messages.BuySellNotConfigured, "Sorry, buying anhd selling claim blocks is disabled.", null); + this.addDefault(defaults, Messages.NoTeleportPvPCombat, "You can't teleport while fighting another player.", null); + this.addDefault(defaults, Messages.NoTNTDamageAboveSeaLevel, "Warning: TNT will not destroy blocks above sea level.", null); + this.addDefault(defaults, Messages.NoTNTDamageClaims, "Warning: TNT will not destroy claimed blocks.", null); + this.addDefault(defaults, Messages.IgnoreClaimsAdvertisement, "To override, use /IgnoreClaims.", null); + this.addDefault(defaults, Messages.NoPermissionForCommand, "You don't have permission to do that.", null); + this.addDefault(defaults, Messages.ClaimsListNoPermission, "You don't have permission to get information about another player's land claims.", null); + + //load the config file + FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); + + //for each message ID + for(int i = 0; i < messageIDs.length; i++) + { + //get default for this message + Messages messageID = messageIDs[i]; + CustomizableMessage messageData = defaults.get(messageID.name()); + + //if default is missing, log an error and use some fake data for now so that the plugin can run + if(messageData == null) + { + GriefPrevention.AddLogEntry("Missing message for " + messageID.name() + ". Please contact the developer."); + messageData = new CustomizableMessage(messageID, "Missing message! ID: " + messageID.name() + ". Please contact a server admin.", null); + } + + //read the message from the file, use default if necessary + this.messages[messageID.ordinal()] = config.getString("Messages." + messageID.name() + ".Text", messageData.text); + config.set("Messages." + messageID.name() + ".Text", this.messages[messageID.ordinal()]); + + if(messageData.notes != null) + { + messageData.notes = config.getString("Messages." + messageID.name() + ".Notes", messageData.notes); + config.set("Messages." + messageID.name() + ".Notes", messageData.notes); + } + } + + //save any changes + try + { + config.save(DataStore.messagesFilePath); + } + catch(IOException exception) + { + GriefPrevention.AddLogEntry("Unable to write to the configuration file at \"" + DataStore.messagesFilePath + "\""); + } + + defaults.clear(); + System.gc(); + } + + private void addDefault(HashMap defaults, + Messages id, String text, String notes) + { + CustomizableMessage message = new CustomizableMessage(id, text, notes); + defaults.put(id.name(), message); + } + + synchronized public String getMessage(Messages messageID, String... args) + { + String message = messages[messageID.ordinal()]; + + for(int i = 0; i < args.length; i++) + { + String param = args[i]; + message = message.replace("{" + i + "}", param); + } + + return message; + } + + abstract void close(); +} diff --git a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java index b3336db..7c7d673 100644 --- a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java +++ b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java @@ -105,6 +105,7 @@ public class GriefPrevention extends JavaPlugin public String config_spam_banMessage; //message to show an automatically banned player public String config_spam_warningMessage; //message to show a player who is close to spam level public String config_spam_allowedIpAddresses; //IP addresses which will not be censored + public int config_spam_deathMessageCooldownSeconds; //cooldown period for death messages (per player) in seconds public ArrayList config_pvp_enabledWorlds; //list of worlds where pvp anti-grief rules apply public boolean config_pvp_protectFreshSpawns; //whether to make newly spawned players immune until they pick up an item @@ -324,6 +325,7 @@ public class GriefPrevention extends JavaPlugin this.config_spam_banOffenders = config.getBoolean("GriefPrevention.Spam.BanOffenders", true); this.config_spam_banMessage = config.getString("GriefPrevention.Spam.BanMessage", "Banned for spam."); String slashCommandsToMonitor = config.getString("GriefPrevention.Spam.MonitorSlashCommands", "/me;/tell;/global;/local"); + this.config_spam_deathMessageCooldownSeconds = config.getInt("GriefPrevention.Spam.DeathMessageCooldownSeconds", 60); this.config_pvp_protectFreshSpawns = config.getBoolean("GriefPrevention.PvP.ProtectFreshSpawns", true); this.config_pvp_punishLogout = config.getBoolean("GriefPrevention.PvP.PunishLogout", true); @@ -561,6 +563,7 @@ public class GriefPrevention extends JavaPlugin config.set("GriefPrevention.Spam.BanOffenders", this.config_spam_banOffenders); config.set("GriefPrevention.Spam.BanMessage", this.config_spam_banMessage); config.set("GriefPrevention.Spam.AllowedIpAddresses", this.config_spam_allowedIpAddresses); + config.set("GriefPrevention.Spam.DeathMessageCooldownSeconds", this.config_spam_deathMessageCooldownSeconds); config.set("GriefPrevention.PvP.Worlds", pvpEnabledWorldNames); config.set("GriefPrevention.PvP.ProtectFreshSpawns", this.config_pvp_protectFreshSpawns); @@ -903,11 +906,6 @@ public class GriefPrevention extends JavaPlugin GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TransferClaimMissing); return true; } - else if(!claim.isAdminClaim()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TransferClaimAdminOnly); - return true; - } OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); if(targetPlayer == null) @@ -1493,7 +1491,7 @@ public class GriefPrevention extends JavaPlugin //load the target player's data PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getName()); - GriefPrevention.sendMessage(player, TextMode.Instr, " " + playerData.accruedClaimBlocks + "(+" + playerData.bonusClaimBlocks + this.dataStore.getGroupBonusBlocks(otherPlayer.getName()) + ")=" + (playerData.accruedClaimBlocks + playerData.bonusClaimBlocks + this.dataStore.getGroupBonusBlocks(otherPlayer.getName()))); + GriefPrevention.sendMessage(player, TextMode.Instr, " " + playerData.accruedClaimBlocks + "(+" + (playerData.bonusClaimBlocks + this.dataStore.getGroupBonusBlocks(otherPlayer.getName())) + ")=" + (playerData.accruedClaimBlocks + playerData.bonusClaimBlocks + this.dataStore.getGroupBonusBlocks(otherPlayer.getName()))); for(int i = 0; i < playerData.claims.size(); i++) { Claim claim = playerData.claims.get(i); @@ -2392,7 +2390,7 @@ public class GriefPrevention extends JavaPlugin SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message); if(delayInTicks > 0) { - GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); + GriefPrevention.instance.getServer().getScheduler().runTaskLater(GriefPrevention.instance, task, delayInTicks); } else { @@ -2537,7 +2535,7 @@ public class GriefPrevention extends JavaPlugin //create task //when done processing, this task will create a main thread task to actually update the world with processing results RestoreNatureProcessingTask task = new RestoreNatureProcessingTask(snapshots, miny, chunk.getWorld().getEnvironment(), lesserBoundaryCorner.getBlock().getBiome(), lesserBoundaryCorner, greaterBoundaryCorner, this.getSeaLevel(chunk.getWorld()), aggressiveMode, GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner), playerReceivingVisualization); - GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); + GriefPrevention.instance.getServer().getScheduler().runTaskLaterAsynchronously(GriefPrevention.instance, task, delayInTicks); } private void parseMaterialListFromConfig(List stringsToParse, MaterialCollection materialCollection) diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerData.java b/src/me/ryanhamshire/GriefPrevention/PlayerData.java index 84bb436..8703143 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerData.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerData.java @@ -70,6 +70,9 @@ public class PlayerData //number of blocks placed outside claims before next warning int unclaimedBlockPlacementsUntilWarning = 1; + //timestamp of last death, for use in preventing death message spam + long lastDeathTimeStamp = 0; + //spam public Date lastLogin; //when the player last logged into the server public String lastMessage = ""; //the player's last chat message, or slash command complete with parameters diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java index ecefe7a..c15f7a5 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -1,1729 +1,1745 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.bukkit.ChatColor; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Animals; -import org.bukkit.entity.Boat; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Hanging; -import org.bukkit.entity.PoweredMinecart; -import org.bukkit.entity.StorageMinecart; -import org.bukkit.entity.Player; -import org.bukkit.entity.Vehicle; -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.player.*; -import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; - -class PlayerEventHandler implements Listener -{ - private DataStore dataStore; - - //list of temporarily banned ip's - private ArrayList tempBannedIps = new ArrayList(); - - //number of milliseconds in a day - private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; - - //timestamps of login and logout notifications in the last minute - private ArrayList recentLoginLogoutNotifications = new ArrayList(); - - //regex pattern for the "how do i claim land?" scanner - private Pattern howToClaimPattern = null; - - //typical constructor, yawn - PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) - { - this.dataStore = dataStore; - } - - //when a player chats, monitor for spam - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - void onPlayerChat (AsyncPlayerChatEvent event) - { - Player player = event.getPlayer(); - if(!player.isOnline()) - { - event.setCancelled(true); - return; - } - - String message = event.getMessage(); - - event.setCancelled(this.handlePlayerChat(player, message, event)); - } - - //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.CreativeBasicsDemoAdvertisement, 10L); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsDemoAdvertisement, 10L); - } - } - - //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("stuck") || message.contains(this.dataStore.getMessage(Messages.TrappedChatKeyword))) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrappedInstructions, 10L); - } - - //FEATURE: monitor for chat and command spam - - if(!GriefPrevention.instance.config_spam_enabled) return false; - - //if the player has permission to spam, don't bother even examining the message - if(player.hasPermission("griefprevention.spam")) return false; - - boolean spam = false; - boolean muted = false; - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //remedy any CAPS SPAM, exception for very short messages which could be emoticons like =D or XD - if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message)) - { - //exception for strings containing forward slash to avoid changing a case-sensitive URL - if(event instanceof AsyncPlayerChatEvent && !message.contains("/")) - { - ((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase()); - playerData.spamCount++; - spam = true; - } - } - - //where other types of spam are concerned, casing isn't significant - message = message.toLowerCase(); - - //check message content and timing - long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime(); - - //if the message came too close to the last one - if(millisecondsSinceLastMessage < 2000) - { - //increment the spam counter - playerData.spamCount++; - spam = true; - } - - //if it's very similar to the last message - if(!muted && this.stringsAreSimilar(message, playerData.lastMessage)) - { - playerData.spamCount++; - spam = true; - muted = true; - } - - //filter IP addresses - if(!muted) - { - Pattern ipAddressPattern = Pattern.compile("\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}"); - Matcher matcher = ipAddressPattern.matcher(message); - - //if it looks like an IP address - while(matcher.find()) - { - //and it's not in the list of allowed IP addresses - if(!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) - { - //log entry - GriefPrevention.AddLogEntry("Muted IP address from " + player.getName() + ": " + message); - - //spam notation - playerData.spamCount++; - spam = true; - - //block message - muted = true; - } - } - } - - //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish) - if(!muted && message.length() > 5) - { - int symbolsCount = 0; - int whitespaceCount = 0; - for(int i = 0; i < message.length(); i++) - { - char character = message.charAt(i); - if(!(Character.isLetterOrDigit(character))) - { - symbolsCount++; - } - - if(Character.isWhitespace(character)) - { - whitespaceCount++; - } - } - - if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10)) - { - spam = true; - if(playerData.spamCount > 0) muted = true; - playerData.spamCount++; - } - } - - //very short messages close together are spam - if(!muted && message.length() < 5 && millisecondsSinceLastMessage < 5000) - { - spam = true; - playerData.spamCount++; - } - - //if the message was determined to be a spam, consider taking action - if(!player.hasPermission("griefprevention.spam") && 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."); - - //ban - GriefPrevention.instance.getServer().getOfflinePlayer(player.getName()).setBanned(true); - - //kick - player.kickPlayer(GriefPrevention.instance.config_spam_banMessage); - } - else - { - player.kickPlayer(""); - } - - 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 >= 3) - { - muted = true; - if(!playerData.spamWarned) - { - GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L); - GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); - playerData.spamWarned = true; - } - } - - if(muted) - { - //make a log entry - GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); - - //send a fake message so the player doesn't realize he's muted - //less information for spammers = less effective spam filter dodging - player.sendMessage("<" + player.getName() + "> " + message); - - //cancelling the event guarantees other players don't receive the message - return true; - } - } - - //otherwise if not a spam, reset the spam counter for this player - else - { - playerData.spamCount = 0; - playerData.spamWarned = false; - } - - //in any case, record the timestamp of this message and also its content for next time - playerData.lastMessageTimestamp = new Date(); - playerData.lastMessage = message; - - return false; - } - - //if two strings are 75% identical, they're too close to follow each other in the chat - private boolean stringsAreSimilar(String message, String lastMessage) - { - //determine which is shorter - String shorterString, longerString; - if(lastMessage.length() < message.length()) - { - shorterString = lastMessage; - longerString = message; - } - else - { - shorterString = message; - longerString = lastMessage; - } - - if(shorterString.length() <= 5) return shorterString.equals(longerString); - - //set similarity tolerance - int maxIdenticalCharacters = longerString.length() - longerString.length() / 4; - - //trivial check on length - if(shorterString.length() < maxIdenticalCharacters) return false; - - //compare forward - int identicalCount = 0; - for(int i = 0; i < shorterString.length(); i++) - { - if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++; - if(identicalCount > maxIdenticalCharacters) return true; - } - - //compare backward - for(int i = 0; i < shorterString.length(); i++) - { - if(shorterString.charAt(shorterString.length() - i - 1) == longerString.charAt(longerString.length() - i - 1)) identicalCount++; - if(identicalCount > maxIdenticalCharacters) return true; - } - - return false; - } - - //when a player uses a slash command... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) - { - String [] args = event.getMessage().split(" "); - - //if eavesdrop enabled, eavesdrop - String command = args[0].toLowerCase(); - if(GriefPrevention.instance.config_eavesdrop && GriefPrevention.instance.config_eavesdrop_whisperCommands.contains(command) && !event.getPlayer().hasPermission("griefprevention.eavesdrop") && args.length > 1) - { - StringBuilder logMessageBuilder = new StringBuilder(); - logMessageBuilder.append("[[").append(event.getPlayer().getName()).append("]] "); - - for(int i = 1; i < args.length; i++) - { - logMessageBuilder.append(args[i]).append(" "); - } - - String logMessage = logMessageBuilder.toString(); - - GriefPrevention.AddLogEntry(logMessage.toString()); - - Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); - for(int i = 0; i < players.length; i++) - { - Player player = players[i]; - if(player.hasPermission("griefprevention.eavesdrop") && !player.getName().equalsIgnoreCase(args[1])) - { - player.sendMessage(ChatColor.GRAY + logMessage); - } - } - } - - //if in pvp, block any pvp-banned slash commands - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if((playerData.inPvpCombat() || playerData.siegeData != null) && GriefPrevention.instance.config_pvp_blockedCommands.contains(command)) - { - event.setCancelled(true); - GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); - return; - } - - //if anti spam enabled, check for spam - if(!GriefPrevention.instance.config_spam_enabled) return; - - //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) - if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains(args[0])) - { - event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); - } - } - - //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 - - //if allowed to join and login cooldown enabled - if(GriefPrevention.instance.config_spam_loginCooldownMinutes > 0 && event.getResult() == Result.ALLOWED) - { - //determine how long since last login and cooldown remaining - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - long millisecondsSinceLastLogin = (new Date()).getTime() - playerData.lastLogin.getTime(); - long minutesSinceLastLogin = millisecondsSinceLastLogin / 1000 / 60; - long cooldownRemaining = GriefPrevention.instance.config_spam_loginCooldownMinutes - minutesSinceLastLogin; - - //if cooldown remaining and player doesn't have permission to spam - if(cooldownRemaining > 0 && !player.hasPermission("griefprevention.spam")) - { - //DAS BOOT! - event.setResult(Result.KICK_OTHER); - event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again."); - event.disallow(event.getResult(), event.getKickMessage()); - return; - } - } - } - - //remember the player's ip address - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - playerData.ipAddress = event.getAddress(); - - //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()) - { - //if logging-in account is banned, remember IP address for later - long now = Calendar.getInstance().getTimeInMillis(); - if(event.getResult() == Result.KICK_BANNED) - { - this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); - } - - //otherwise if not banned - else - { - //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 - { - player.setBanned(true); - event.setResult(Result.KICK_BANNED); - event.disallow(event.getResult(), ""); - GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ")."); - - //notify any online ops - Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); - for(int k = 0; k < players.length; k++) - { - if(players[k].isOp()) - { - GriefPrevention.sendMessage(players[k], TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); - } - } - - break; - } - } - } - } - } - } - - //when a player spawns, conditionally apply temporary pvp protection - @EventHandler(ignoreCancelled = true) - void onPlayerRespawn (PlayerRespawnEvent event) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(event.getPlayer().getName()); - playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); - GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); - } - - //when a player successfully joins the server... - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - void onPlayerJoin(PlayerJoinEvent event) - { - String playerName = event.getPlayer().getName(); - - //note login time - PlayerData playerData = this.dataStore.getPlayerData(playerName); - playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); - playerData.lastLogin = new Date(); - this.dataStore.savePlayerData(playerName, playerData); - - //if player has never played on the server before, may need pvp protection - if(!event.getPlayer().hasPlayedBefore()) - { - GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); - } - - //silence notifications when they're coming too fast - if(event.getJoinMessage() != null && this.shouldSilenceNotification()) - { - event.setJoinMessage(null); - } - } - - //when a player quits... - @EventHandler(priority = EventPriority.HIGHEST) - void onPlayerQuit(PlayerQuitEvent event) - { - Player player = event.getPlayer(); - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //if banned, add IP to the temporary IP ban list - if(player.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); - } - - //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately - this.dataStore.savePlayerData(player.getName(), playerData); - - this.onPlayerDisconnect(event.getPlayer(), event.getQuitMessage()); - } - - //helper for above - private void onPlayerDisconnect(Player player, String notificationMessage) - { - String playerName = player.getName(); - PlayerData playerData = this.dataStore.getPlayerData(playerName); - - //FEATURE: players in pvp combat when they log out will die - if(GriefPrevention.instance.config_pvp_punishLogout && playerData.inPvpCombat()) - { - player.setHealth(0); - } - - //FEATURE: during a siege, any player who logs out dies and forfeits the siege - - //if player was involved in a siege, he forfeits - if(playerData.siegeData != null) - { - if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message - } - - //drop data about this player - this.dataStore.clearCachedPlayerData(player.getName()); - } - - //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute - private boolean shouldSilenceNotification() - { - final long ONE_MINUTE = 60000; - final int MAX_ALLOWED = 20; - Long now = Calendar.getInstance().getTimeInMillis(); - - //eliminate any expired entries (longer than a minute ago) - for(int i = 0; i < this.recentLoginLogoutNotifications.size(); i++) - { - Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i); - if(now - notificationTimestamp > ONE_MINUTE) - { - this.recentLoginLogoutNotifications.remove(i--); - } - else - { - break; - } - } - - //add the new entry - this.recentLoginLogoutNotifications.add(now); - - return this.recentLoginLogoutNotifications.size() > MAX_ALLOWED; - } - - //when a player drops an item - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerDropItem(PlayerDropItemEvent event) - { - Player player = event.getPlayer(); - - //in creative worlds, dropping items is blocked - if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) - { - event.setCancelled(true); - return; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide - //them or give them away to other players before they are defeated - - //if in combat, don't let him drop it - if(!GriefPrevention.instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop); - event.setCancelled(true); - } - - //if he's under siege, don't let him drop it - else if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); - event.setCancelled(true); - } - } - - //when a player teleports - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerTeleport(PlayerTeleportEvent event) - { - //FEATURE: prevent teleport abuse to win sieges - - //these rules only apply to non-ender-pearl teleportation - if(event.getCause() == TeleportCause.ENDER_PEARL) return; - - Player player = event.getPlayer(); - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - 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 an entity... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerInteractEntity(PlayerInteractEntityEvent event) - { - Player player = event.getPlayer(); - Entity entity = event.getRightClicked(); - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //don't allow interaction with item frames in claimed areas without build permission - if(entity instanceof Hanging) - { - String noBuildReason = GriefPrevention.instance.allowBuild(player, entity.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - 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 and powered minecarts, apply container rules (this is a potential theft) - if(entity instanceof StorageMinecart || entity instanceof PoweredMinecart) - { - String noContainersReason = claim.allowContainers(player); - if(noContainersReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); - event.setCancelled(true); - } - } - - //for boats, apply access rules - else if(entity instanceof Boat) - { - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - player.sendMessage(noAccessReason); - event.setCancelled(true); - } - } - - //if the entity is an animal, apply container rules - else if(entity instanceof Animals) - { - if(claim.allowContainers(player) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity); - event.setCancelled(true); - } - } - } - } - } - - //when a player picks up an item... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerPickupItem(PlayerPickupItemEvent event) - { - Player player = event.getPlayer(); - - if(!event.getPlayer().getWorld().getPVP()) return; - - //if we're preventing spawn camping and the player was previously empty handed... - if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (player.getItemInHand().getType() == Material.AIR)) - { - //if that player is currently immune to pvp - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if(playerData.pvpImmune) - { - //if it's been less than 10 seconds since the last time he spawned, don't pick up the item - long now = Calendar.getInstance().getTimeInMillis(); - long elapsedSinceLastSpawn = now - playerData.lastSpawn; - if(elapsedSinceLastSpawn < 10000) - { - event.setCancelled(true); - return; - } - - //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot - playerData.pvpImmune = false; - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); - } - } - } - - //when a player switches in-hand items - @EventHandler(ignoreCancelled = true) - public void onItemHeldChange(PlayerItemHeldEvent event) - { - Player player = event.getPlayer(); - - //if he's switching to the golden shovel - ItemStack newItemStack = player.getInventory().getItem(event.getNewSlot()); - if(newItemStack != null && newItemStack.getType() == GriefPrevention.instance.config_claims_modificationTool) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); - - //always reset to basic claims mode - if(playerData.shovelMode != ShovelMode.Basic) - { - playerData.shovelMode = ShovelMode.Basic; - GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShovelBasicClaimMode); - } - - //reset any work he might have been doing - playerData.lastShovelLocation = null; - playerData.claimResizing = null; - - //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam - if(GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) - { - EquipShovelProcessingTask task = new EquipShovelProcessingTask(player); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 15L); //15L is approx. 3/4 of a second - } - } - } - - //block players from entering beds they don't have permission for - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent) - { - if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return; - - Player player = bedEvent.getPlayer(); - Block block = bedEvent.getBed(); - - //if the bed is in a claim - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim != null) - { - //if the player doesn't have access in that claim, tell him so and prevent him from sleeping in the bed - if(claim.allowAccess(player) != null) - { - bedEvent.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoBedPermission, claim.getOwnerName()); - } - } - } - - //block use of buckets within other players' claims - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) - { - 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()); - 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.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - minLavaDistance = 3; - } - - //otherwise no wilderness dumping (unless underground) in worlds where claims are enabled - else if(GriefPrevention.instance.config_claims_enabledWorlds.contains(block.getWorld())) - { - if(block.getY() >= GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava")) - { - if(bucketEvent.getBucket() == Material.LAVA_BUCKET || GriefPrevention.instance.config_blockWildernessWaterBuckets) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); - bucketEvent.setCancelled(true); - return; - } - } - } - - //lava buckets can't be dumped near other players unless pvp is on - if(!GriefPrevention.instance.config_pvp_enabledWorlds.contains(block.getWorld()) && !player.hasPermission("griefprevention.lava")) - { - if(bucketEvent.getBucket() == Material.LAVA_BUCKET) - { - List players = block.getWorld().getPlayers(); - for(int i = 0; i < players.size(); i++) - { - Player otherPlayer = players.get(i); - Location location = otherPlayer.getLocation(); - if(!otherPlayer.equals(player) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, otherPlayer.getName()); - bucketEvent.setCancelled(true); - return; - } - } - } - } - } - - //see above - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) - { - Player player = bucketEvent.getPlayer(); - Block block = bucketEvent.getBlockClicked(); - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - bucketEvent.setCancelled(true); - return; - } - } - - //when a player interacts with the world - @EventHandler(priority = EventPriority.LOWEST) - void onPlayerInteract(PlayerInteractEvent event) - { - Player player = event.getPlayer(); - - //determine target block. FEATURE: shovel and string can be used from a distance away - Block clickedBlock = null; - - try - { - clickedBlock = event.getClickedBlock(); //null returned here means interacting with air - if(clickedBlock == null || clickedBlock.getType() == Material.SNOW) - { - //try to find a far away non-air block along line of sight - HashSet transparentMaterials = new HashSet(); - transparentMaterials.add(Byte.valueOf((byte)Material.AIR.getId())); - transparentMaterials.add(Byte.valueOf((byte)Material.SNOW.getId())); - transparentMaterials.add(Byte.valueOf((byte)Material.LONG_GRASS.getId())); - clickedBlock = player.getTargetBlock(transparentMaterials, 250); - } - } - catch(Exception e) //an exception intermittently comes from getTargetBlock(). when it does, just ignore the event - { - return; - } - - //if no block, stop here - if(clickedBlock == null) - { - return; - } - - Material clickedBlockType = clickedBlock.getType(); - - //apply rules for putting out fires (requires build permission) - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if(event.getClickedBlock() != null && event.getClickedBlock().getRelative(event.getBlockFace()).getType() == Material.FIRE) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noBuildReason = claim.allowBuild(player); - if(noBuildReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - return; - } - } - } - - //apply rules for containers and crafting blocks - if( GriefPrevention.instance.config_claims_preventTheft && ( - event.getAction() == Action.RIGHT_CLICK_BLOCK && ( - clickedBlock.getState() instanceof InventoryHolder || - clickedBlockType == Material.WORKBENCH || - clickedBlockType == Material.ENDER_CHEST || - clickedBlockType == Material.DISPENSER || - clickedBlockType == Material.ANVIL || - clickedBlockType == Material.BREWING_STAND || - clickedBlockType == Material.JUKEBOX || - clickedBlockType == Material.ENCHANTMENT_TABLE || - GriefPrevention.instance.config_mods_containerTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))))) - { - //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, if configured that way - else if((GriefPrevention.instance.config_claims_lockWoodenDoors && clickedBlockType == Material.WOODEN_DOOR) || - (GriefPrevention.instance.config_claims_lockTrapDoors && clickedBlockType == Material.TRAP_DOOR) || - (GriefPrevention.instance.config_claims_lockFenceGates && clickedBlockType == Material.FENCE_GATE)) - { - 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(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)))) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); - return; - } - } - } - - //apply rule for players trampling tilled soil back to dirt (never allow it) - //NOTE: that this event applies only to players. monsters and animals can still trample. - else if(event.getAction() == Action.PHYSICAL && clickedBlockType == Material.SOIL) - { - event.setCancelled(true); - return; - } - - //apply rule for note blocks and repeaters - else if(clickedBlockType == Material.NOTE_BLOCK || clickedBlockType == Material.DIODE_BLOCK_ON || clickedBlockType == Material.DIODE_BLOCK_OFF) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - String noBuildReason = claim.allowBuild(player); - 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 - Action action = event.getAction(); - if(action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; - - //what's the player holding? - Material materialInHand = player.getItemInHand().getType(); - - //if it's bonemeal, check for build permission (ink sac == bone meal, must be a Bukkit bug?) - if(materialInHand == Material.INK_SACK) - { - String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - } - - return; - } - - else if(materialInHand == Material.BOAT) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); - event.setCancelled(true); - } - } - - return; - } - - //if it's a spawn egg, minecart, or boat, and this is a creative world, apply special rules - else if((materialInHand == Material.MONSTER_EGG || materialInHand == Material.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.BOAT) && GriefPrevention.instance.creativeRulesApply(clickedBlock.getLocation())) - { - //player needs build permission at this location - String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - return; - } - - //enforce limit on total number of entities in this claim - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim == null) return; - - String noEntitiesReason = claim.allowMoreEntities(); - if(noEntitiesReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason); - event.setCancelled(true); - return; - } - - return; - } - - //if he's investigating a claim - else if(materialInHand == GriefPrevention.instance.config_claims_investigationTool) - { - //air indicates too far away - if(clickedBlockType == Material.AIR) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); - return; - } - - 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, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - - //if can resize this claim, tell about the boundaries - if(claim.allowEdit(player) == null) - { - GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea()); - } - - //if deleteclaims permission, tell about the player's offline time - if(!claim.isAdminClaim() && player.hasPermission("griefprevention.deleteclaims")) - { - PlayerData otherPlayerData = this.dataStore.getPlayerData(claim.getOwnerName()); - Date lastLogin = otherPlayerData.lastLogin; - 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().getPlayerExact(claim.getOwnerName()) == null) - this.dataStore.clearCachedPlayerData(claim.getOwnerName()); - } - } - - return; - } - - //if it's a golden shovel - else if(materialInHand != GriefPrevention.instance.config_claims_modificationTool) return; - - //disable golden shovel while under siege - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoShovel); - event.setCancelled(true); - 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 - String playerName = player.getName(); - playerData = this.dataStore.getPlayerData(player.getName()); - if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive) - { - //if the clicked block is in a claim, visualize that claim and deliver an error message - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName()); - Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - - return; - } - - //figure out which chunk to repair - Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); - - //start the repair process - - //set boundaries for processing - int miny = clickedBlock.getY(); - - //if not in aggressive mode, extend the selection down to a little below sea level - if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive)) - { - if(miny > GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10) - { - miny = GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10; - } - } - - GriefPrevention.instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player); - - return; - } - - //if in restore nature fill mode - if(playerData.shovelMode == ShovelMode.RestoreNatureFill) - { - ArrayList allowedFillBlocks = new ArrayList(); - Environment environment = clickedBlock.getWorld().getEnvironment(); - if(environment == Environment.NETHER) - { - allowedFillBlocks.add(Material.NETHERRACK); - } - else if(environment == Environment.THE_END) - { - allowedFillBlocks.add(Material.ENDER_STONE); - } - else - { - allowedFillBlocks.add(Material.GRASS); - allowedFillBlocks.add(Material.DIRT); - allowedFillBlocks.add(Material.STONE); - allowedFillBlocks.add(Material.SAND); - allowedFillBlocks.add(Material.SANDSTONE); - allowedFillBlocks.add(Material.ICE); - } - - Block centerBlock = clickedBlock; - - int maxHeight = centerBlock.getY(); - int minx = centerBlock.getX() - playerData.fillRadius; - int maxx = centerBlock.getX() + playerData.fillRadius; - int minz = centerBlock.getZ() - playerData.fillRadius; - int maxz = centerBlock.getZ() + playerData.fillRadius; - int minHeight = maxHeight - 10; - if(minHeight < 0) minHeight = 0; - - Claim cachedClaim = null; - for(int x = minx; x <= maxx; x++) - { - for(int z = minz; z <= maxz; z++) - { - //circular brush - Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z); - if(location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue; - - //default fill block is initially the first from the allowed fill blocks list above - Material defaultFiller = allowedFillBlocks.get(0); - - //prefer to use the block the player clicked on, if it's an acceptable fill block - if(allowedFillBlocks.contains(centerBlock.getType())) - { - defaultFiller = centerBlock.getType(); - } - - //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler - else if(centerBlock.getType() == Material.WATER || centerBlock.getType() == Material.STATIONARY_WATER) - { - Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation()); - while(!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10) - { - block = block.getRelative(BlockFace.DOWN); - } - if(allowedFillBlocks.contains(block.getType())) - { - defaultFiller = block.getType(); - } - } - - //fill bottom to top - for(int y = minHeight; y <= maxHeight; y++) - { - Block block = centerBlock.getWorld().getBlockAt(x, y, z); - - //respect claims - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim); - if(claim != null) - { - cachedClaim = claim; - break; - } - - //only replace air, spilling water, snow, long grass - if(block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.STATIONARY_WATER && block.getData() != 0) || block.getType() == Material.LONG_GRASS) - { - //if the top level, always use the default filler picked above - if(y == maxHeight) - { - block.setType(defaultFiller); - } - - //otherwise look to neighbors for an appropriate fill block - else - { - Block eastBlock = block.getRelative(BlockFace.EAST); - Block westBlock = block.getRelative(BlockFace.WEST); - Block northBlock = block.getRelative(BlockFace.NORTH); - Block southBlock = block.getRelative(BlockFace.SOUTH); - - //first, check lateral neighbors (ideally, want to keep natural layers) - if(allowedFillBlocks.contains(eastBlock.getType())) - { - block.setType(eastBlock.getType()); - } - else if(allowedFillBlocks.contains(westBlock.getType())) - { - block.setType(westBlock.getType()); - } - else if(allowedFillBlocks.contains(northBlock.getType())) - { - block.setType(northBlock.getType()); - } - else if(allowedFillBlocks.contains(southBlock.getType())) - { - block.setType(southBlock.getType()); - } - - //if all else fails, use the default filler selected above - else - { - block.setType(defaultFiller); - } - } - } - } - } - } - - return; - } - - //if the player doesn't have claims permission, don't do anything - if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission); - return; - } - - //if he's resizing a claim and that claim hasn't been deleted since he started resizing it - if(playerData.claimResizing != null && playerData.claimResizing.inDataStore) - { - if(clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return; - - //figure out what the coords of his new claim would be - int newx1, newx2, newz1, newz2, newy1, newy2; - if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX()) - { - newx1 = clickedBlock.getX(); - } - else - { - newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX(); - } - - if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockX()) - { - newx2 = clickedBlock.getX(); - } - else - { - newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX(); - } - - if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ()) - { - newz1 = clickedBlock.getZ(); - } - else - { - newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ(); - } - - if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ()) - { - newz2 = clickedBlock.getZ(); - } - else - { - newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ(); - } - - newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY(); - newy2 = clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance; - - //for top level claims, apply size rules and claim blocks requirement - if(playerData.claimResizing.parent == null) - { - //measure new claim, apply size rules - int newWidth = (Math.abs(newx1 - newx2) + 1); - int newHeight = (Math.abs(newz1 - newz2) + 1); - - if(!playerData.claimResizing.isAdminClaim() && (newWidth < GriefPrevention.instance.config_claims_minSize || newHeight < GriefPrevention.instance.config_claims_minSize)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); - return; - } - - //make sure player has enough blocks to make up the difference - if(!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) - { - int newArea = newWidth * newHeight; - int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; - - if(blocksRemainingAfter < 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); - return; - } - } - } - - //special rules for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. - //rule1: in creative mode, top-level claims can't be moved or resized smaller. - //rule2: in any mode, shrinking a claim removes any surface fluids - Claim oldClaim = playerData.claimResizing; - boolean smaller = false; - if(oldClaim.parent == null) - { - //temporary claim instance, just for checking contains() - Claim newClaim = new Claim( - new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), - new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), - "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null); - - //if the new claim is smaller - if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) - { - smaller = true; - - //enforce creative mode rule - if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && !player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); - return; - } - - //remove surface fluids about to be unclaimed - oldClaim.removeSurfaceFluids(newClaim); - } - } - - //ask the datastore to try and resize the claim, this checks for conflicts with other claims - CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim(playerData.claimResizing, newx1, newx2, newy1, newy2, newz1, newz2); - - if(result.succeeded) - { - //inform and show the player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(playerData.getRemainingClaimBlocks())); - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - - //if resizing someone else's claim, make a log entry - if(!playerData.claimResizing.ownerName.equals(playerName)) - { - GriefPrevention.AddLogEntry(playerName + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); - } - - //if in a creative mode world and shrinking an existing claim, restore any unclaimed area - if(smaller && GriefPrevention.instance.creativeRulesApply(oldClaim.getLesserBoundaryCorner())) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); - GriefPrevention.instance.restoreClaim(oldClaim, 20L * 60 * 2); //2 minutes - GriefPrevention.AddLogEntry(player.getName() + " shrank a claim @ " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.getLesserBoundaryCorner())); - } - - //clean up - playerData.claimResizing = null; - playerData.lastShovelLocation = null; - } - else - { - //inform player - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); - - //show the player the conflicting claim - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - - 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(), - "--subdivision--", //owner name is not used for subdivisions - playerData.claimSubdividing, - null); - - //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()) && playerData.shovelMode != ShovelMode.Admin) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); - 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(), "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null), clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation()); - Visualization.Apply(player, visualization); - } - - //otherwise, he's trying to finish creating a claim by setting the other boundary corner - else - { - //if last shovel location was in a different world, assume the player is starting the create-claim workflow over - if(!lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) - { - playerData.lastShovelLocation = null; - this.onPlayerInteract(event); - return; - } - - //apply minimum claim dimensions rule - int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1; - int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1; - - if(playerData.shovelMode != ShovelMode.Admin && (newClaimWidth < GriefPrevention.instance.config_claims_minSize || newClaimHeight < GriefPrevention.instance.config_claims_minSize)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); - return; - } - - //if not an administrative claim, verify the player has enough claim blocks for this new claim - if(playerData.shovelMode != ShovelMode.Admin) - { - int newClaimArea = newClaimWidth * newClaimHeight; - int remainingBlocks = playerData.getRemainingClaimBlocks(); - if(newClaimArea > remainingBlocks) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks)); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimAdvertisement); - return; - } - } - else - { - playerName = ""; - } - - //try to create a new claim (will return null if this claim overlaps another) - 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(), - playerName, - null, null); - - //if it didn't succeed, tell the player why - if(!result.succeeded) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); - - 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 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; - } - } - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Boat; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Hanging; +import org.bukkit.entity.PoweredMinecart; +import org.bukkit.entity.StorageMinecart; +import org.bukkit.entity.Player; +import org.bukkit.entity.Vehicle; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.*; +import org.bukkit.event.player.PlayerLoginEvent.Result; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +class PlayerEventHandler implements Listener +{ + private DataStore dataStore; + + //list of temporarily banned ip's + private ArrayList tempBannedIps = new ArrayList(); + + //number of milliseconds in a day + private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; + + //timestamps of login and logout notifications in the last minute + private ArrayList recentLoginLogoutNotifications = new ArrayList(); + + //regex pattern for the "how do i claim land?" scanner + private Pattern howToClaimPattern = null; + + //typical constructor, yawn + PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) + { + this.dataStore = dataStore; + } + + //when a player chats, monitor for spam + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + synchronized void onPlayerChat (AsyncPlayerChatEvent event) + { + Player player = event.getPlayer(); + if(!player.isOnline()) + { + event.setCancelled(true); + return; + } + + String message = event.getMessage(); + + event.setCancelled(this.handlePlayerChat(player, message, event)); + } + + //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.CreativeBasicsDemoAdvertisement, 10L); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsDemoAdvertisement, 10L); + } + } + + //FEATURE: automatically educate players about the /trapped command + //check for "trapped" or "stuck" to educate players about the /trapped command + if(!message.contains("/trapped") && (message.contains("trapped") || message.contains("stuck") || message.contains(this.dataStore.getMessage(Messages.TrappedChatKeyword)))) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrappedInstructions, 10L); + } + + //FEATURE: monitor for chat and command spam + + if(!GriefPrevention.instance.config_spam_enabled) return false; + + //if the player has permission to spam, don't bother even examining the message + if(player.hasPermission("griefprevention.spam")) return false; + + boolean spam = false; + boolean muted = false; + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //remedy any CAPS SPAM, exception for very short messages which could be emoticons like =D or XD + if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message)) + { + //exception for strings containing forward slash to avoid changing a case-sensitive URL + if(event instanceof AsyncPlayerChatEvent && !message.contains("/")) + { + ((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase()); + playerData.spamCount++; + spam = true; + } + } + + //where other types of spam are concerned, casing isn't significant + message = message.toLowerCase(); + + //check message content and timing + long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime(); + + //if the message came too close to the last one + if(millisecondsSinceLastMessage < 2000) + { + //increment the spam counter + playerData.spamCount++; + spam = true; + } + + //if it's very similar to the last message + if(!muted && this.stringsAreSimilar(message, playerData.lastMessage)) + { + playerData.spamCount++; + spam = true; + muted = true; + } + + //filter IP addresses + if(!muted) + { + Pattern ipAddressPattern = Pattern.compile("\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}"); + Matcher matcher = ipAddressPattern.matcher(message); + + //if it looks like an IP address + while(matcher.find()) + { + //and it's not in the list of allowed IP addresses + if(!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) + { + //log entry + GriefPrevention.AddLogEntry("Muted IP address from " + player.getName() + ": " + message); + + //spam notation + playerData.spamCount++; + spam = true; + + //block message + muted = true; + } + } + } + + //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish) + if(!muted && message.length() > 5) + { + int symbolsCount = 0; + int whitespaceCount = 0; + for(int i = 0; i < message.length(); i++) + { + char character = message.charAt(i); + if(!(Character.isLetterOrDigit(character))) + { + symbolsCount++; + } + + if(Character.isWhitespace(character)) + { + whitespaceCount++; + } + } + + if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10)) + { + spam = true; + if(playerData.spamCount > 0) muted = true; + playerData.spamCount++; + } + } + + //very short messages close together are spam + if(!muted && message.length() < 5 && millisecondsSinceLastMessage < 5000) + { + spam = true; + playerData.spamCount++; + } + + //if the message was determined to be a spam, consider taking action + if(!player.hasPermission("griefprevention.spam") && 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."); + + //ban + GriefPrevention.instance.getServer().getOfflinePlayer(player.getName()).setBanned(true); + + //kick + player.kickPlayer(GriefPrevention.instance.config_spam_banMessage); + } + else + { + player.kickPlayer(""); + } + + 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 >= 3) + { + muted = true; + if(!playerData.spamWarned) + { + GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L); + GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); + playerData.spamWarned = true; + } + } + + if(muted) + { + //make a log entry + GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); + + //send a fake message so the player doesn't realize he's muted + //less information for spammers = less effective spam filter dodging + player.sendMessage("<" + player.getName() + "> " + message); + + //cancelling the event guarantees other players don't receive the message + return true; + } + } + + //otherwise if not a spam, reset the spam counter for this player + else + { + playerData.spamCount = 0; + playerData.spamWarned = false; + } + + //in any case, record the timestamp of this message and also its content for next time + playerData.lastMessageTimestamp = new Date(); + playerData.lastMessage = message; + + return false; + } + + //if two strings are 75% identical, they're too close to follow each other in the chat + private boolean stringsAreSimilar(String message, String lastMessage) + { + //determine which is shorter + String shorterString, longerString; + if(lastMessage.length() < message.length()) + { + shorterString = lastMessage; + longerString = message; + } + else + { + shorterString = message; + longerString = lastMessage; + } + + if(shorterString.length() <= 5) return shorterString.equals(longerString); + + //set similarity tolerance + int maxIdenticalCharacters = longerString.length() - longerString.length() / 4; + + //trivial check on length + if(shorterString.length() < maxIdenticalCharacters) return false; + + //compare forward + int identicalCount = 0; + for(int i = 0; i < shorterString.length(); i++) + { + if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++; + if(identicalCount > maxIdenticalCharacters) return true; + } + + //compare backward + for(int i = 0; i < shorterString.length(); i++) + { + if(shorterString.charAt(shorterString.length() - i - 1) == longerString.charAt(longerString.length() - i - 1)) identicalCount++; + if(identicalCount > maxIdenticalCharacters) return true; + } + + return false; + } + + //when a player uses a slash command... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + synchronized void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) + { + String [] args = event.getMessage().split(" "); + + //if eavesdrop enabled, eavesdrop + String command = args[0].toLowerCase(); + if(GriefPrevention.instance.config_eavesdrop && GriefPrevention.instance.config_eavesdrop_whisperCommands.contains(command) && !event.getPlayer().hasPermission("griefprevention.eavesdrop") && args.length > 1) + { + StringBuilder logMessageBuilder = new StringBuilder(); + logMessageBuilder.append("[[").append(event.getPlayer().getName()).append("]] "); + + for(int i = 1; i < args.length; i++) + { + logMessageBuilder.append(args[i]).append(" "); + } + + String logMessage = logMessageBuilder.toString(); + + GriefPrevention.AddLogEntry(logMessage.toString()); + + Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); + for(int i = 0; i < players.length; i++) + { + Player player = players[i]; + if(player.hasPermission("griefprevention.eavesdrop") && !player.getName().equalsIgnoreCase(args[1])) + { + player.sendMessage(ChatColor.GRAY + logMessage); + } + } + } + + //if in pvp, block any pvp-banned slash commands + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if((playerData.inPvpCombat() || playerData.siegeData != null) && GriefPrevention.instance.config_pvp_blockedCommands.contains(command)) + { + event.setCancelled(true); + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); + return; + } + + //if anti spam enabled, check for spam + if(!GriefPrevention.instance.config_spam_enabled) return; + + //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) + if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains(args[0])) + { + event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); + } + } + + //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 + + //if allowed to join and login cooldown enabled + if(GriefPrevention.instance.config_spam_loginCooldownMinutes > 0 && event.getResult() == Result.ALLOWED) + { + //determine how long since last login and cooldown remaining + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + long millisecondsSinceLastLogin = (new Date()).getTime() - playerData.lastLogin.getTime(); + long minutesSinceLastLogin = millisecondsSinceLastLogin / 1000 / 60; + long cooldownRemaining = GriefPrevention.instance.config_spam_loginCooldownMinutes - minutesSinceLastLogin; + + //if cooldown remaining and player doesn't have permission to spam + if(cooldownRemaining > 0 && !player.hasPermission("griefprevention.spam")) + { + //DAS BOOT! + event.setResult(Result.KICK_OTHER); + event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again."); + event.disallow(event.getResult(), event.getKickMessage()); + return; + } + } + } + + //remember the player's ip address + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.ipAddress = event.getAddress(); + + //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()) + { + //if logging-in account is banned, remember IP address for later + long now = Calendar.getInstance().getTimeInMillis(); + if(event.getResult() == Result.KICK_BANNED) + { + this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); + } + + //otherwise if not banned + else + { + //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 + { + player.setBanned(true); + event.setResult(Result.KICK_BANNED); + event.disallow(event.getResult(), ""); + GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ")."); + + //notify any online ops + Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); + for(int k = 0; k < players.length; k++) + { + if(players[k].isOp()) + { + GriefPrevention.sendMessage(players[k], TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); + } + } + + break; + } + } + } + } + } + } + + //when a player spawns, conditionally apply temporary pvp protection + @EventHandler(ignoreCancelled = true) + void onPlayerRespawn (PlayerRespawnEvent event) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(event.getPlayer().getName()); + playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); + GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); + } + + //when a player successfully joins the server... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + void onPlayerJoin(PlayerJoinEvent event) + { + String playerName = event.getPlayer().getName(); + + //note login time + PlayerData playerData = this.dataStore.getPlayerData(playerName); + playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); + playerData.lastLogin = new Date(); + this.dataStore.savePlayerData(playerName, playerData); + + //if player has never played on the server before, may need pvp protection + if(!event.getPlayer().hasPlayedBefore()) + { + GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); + } + + //silence notifications when they're coming too fast + if(event.getJoinMessage() != null && this.shouldSilenceNotification()) + { + event.setJoinMessage(null); + } + } + + //when a player dies... + @EventHandler(priority = EventPriority.LOWEST) + void onPlayerDeath(PlayerDeathEvent event) + { + //FEATURE: prevent death message spam by implementing a "cooldown period" for death messages + PlayerData playerData = this.dataStore.getPlayerData(event.getEntity().getName()); + long now = Calendar.getInstance().getTimeInMillis(); + if(now - playerData.lastDeathTimeStamp < GriefPrevention.instance.config_spam_deathMessageCooldownSeconds * 1000) + { + event.setDeathMessage(""); + } + + playerData.lastDeathTimeStamp = now; + } + + //when a player quits... + @EventHandler(priority = EventPriority.HIGHEST) + void onPlayerQuit(PlayerQuitEvent event) + { + Player player = event.getPlayer(); + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //if banned, add IP to the temporary IP ban list + if(player.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); + } + + //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately + this.dataStore.savePlayerData(player.getName(), playerData); + + this.onPlayerDisconnect(event.getPlayer(), event.getQuitMessage()); + } + + //helper for above + private void onPlayerDisconnect(Player player, String notificationMessage) + { + String playerName = player.getName(); + PlayerData playerData = this.dataStore.getPlayerData(playerName); + + //FEATURE: players in pvp combat when they log out will die + if(GriefPrevention.instance.config_pvp_punishLogout && playerData.inPvpCombat()) + { + player.setHealth(0); + } + + //FEATURE: during a siege, any player who logs out dies and forfeits the siege + + //if player was involved in a siege, he forfeits + if(playerData.siegeData != null) + { + if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message + } + + //drop data about this player + this.dataStore.clearCachedPlayerData(player.getName()); + } + + //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute + private boolean shouldSilenceNotification() + { + final long ONE_MINUTE = 60000; + final int MAX_ALLOWED = 20; + Long now = Calendar.getInstance().getTimeInMillis(); + + //eliminate any expired entries (longer than a minute ago) + for(int i = 0; i < this.recentLoginLogoutNotifications.size(); i++) + { + Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i); + if(now - notificationTimestamp > ONE_MINUTE) + { + this.recentLoginLogoutNotifications.remove(i--); + } + else + { + break; + } + } + + //add the new entry + this.recentLoginLogoutNotifications.add(now); + + return this.recentLoginLogoutNotifications.size() > MAX_ALLOWED; + } + + //when a player drops an item + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerDropItem(PlayerDropItemEvent event) + { + Player player = event.getPlayer(); + + //in creative worlds, dropping items is blocked + if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) + { + event.setCancelled(true); + return; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide + //them or give them away to other players before they are defeated + + //if in combat, don't let him drop it + if(!GriefPrevention.instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop); + event.setCancelled(true); + } + + //if he's under siege, don't let him drop it + else if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); + event.setCancelled(true); + } + } + + //when a player teleports + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerTeleport(PlayerTeleportEvent event) + { + //FEATURE: prevent teleport abuse to win sieges + + //these rules only apply to non-ender-pearl teleportation + if(event.getCause() == TeleportCause.ENDER_PEARL) return; + + Player player = event.getPlayer(); + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + 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 an entity... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) + { + Player player = event.getPlayer(); + Entity entity = event.getRightClicked(); + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //don't allow interaction with item frames in claimed areas without build permission + if(entity instanceof Hanging) + { + String noBuildReason = GriefPrevention.instance.allowBuild(player, entity.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + 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 and powered minecarts, apply container rules (this is a potential theft) + if(entity instanceof StorageMinecart || entity instanceof PoweredMinecart) + { + String noContainersReason = claim.allowContainers(player); + if(noContainersReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); + event.setCancelled(true); + } + } + + //for boats, apply access rules + else if(entity instanceof Boat) + { + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + player.sendMessage(noAccessReason); + event.setCancelled(true); + } + } + + //if the entity is an animal, apply container rules + else if(entity instanceof Animals) + { + if(claim.allowContainers(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity); + event.setCancelled(true); + } + } + } + } + } + + //when a player picks up an item... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerPickupItem(PlayerPickupItemEvent event) + { + Player player = event.getPlayer(); + + if(!event.getPlayer().getWorld().getPVP()) return; + + //if we're preventing spawn camping and the player was previously empty handed... + if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (player.getItemInHand().getType() == Material.AIR)) + { + //if that player is currently immune to pvp + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if(playerData.pvpImmune) + { + //if it's been less than 10 seconds since the last time he spawned, don't pick up the item + long now = Calendar.getInstance().getTimeInMillis(); + long elapsedSinceLastSpawn = now - playerData.lastSpawn; + if(elapsedSinceLastSpawn < 10000) + { + event.setCancelled(true); + return; + } + + //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot + playerData.pvpImmune = false; + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); + } + } + } + + //when a player switches in-hand items + @EventHandler(ignoreCancelled = true) + public void onItemHeldChange(PlayerItemHeldEvent event) + { + Player player = event.getPlayer(); + + //if he's switching to the golden shovel + ItemStack newItemStack = player.getInventory().getItem(event.getNewSlot()); + if(newItemStack != null && newItemStack.getType() == GriefPrevention.instance.config_claims_modificationTool) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + + //always reset to basic claims mode + if(playerData.shovelMode != ShovelMode.Basic) + { + playerData.shovelMode = ShovelMode.Basic; + GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShovelBasicClaimMode); + } + + //reset any work he might have been doing + playerData.lastShovelLocation = null; + playerData.claimResizing = null; + + //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam + if(GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) + { + EquipShovelProcessingTask task = new EquipShovelProcessingTask(player); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 15L); //15L is approx. 3/4 of a second + } + } + } + + //block players from entering beds they don't have permission for + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent) + { + if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return; + + Player player = bedEvent.getPlayer(); + Block block = bedEvent.getBed(); + + //if the bed is in a claim + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null) + { + //if the player doesn't have access in that claim, tell him so and prevent him from sleeping in the bed + if(claim.allowAccess(player) != null) + { + bedEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoBedPermission, claim.getOwnerName()); + } + } + } + + //block use of buckets within other players' claims + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) + { + 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()); + 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.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + minLavaDistance = 3; + } + + //otherwise no wilderness dumping (unless underground) in worlds where claims are enabled + else if(GriefPrevention.instance.config_claims_enabledWorlds.contains(block.getWorld())) + { + if(block.getY() >= GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava")) + { + if(bucketEvent.getBucket() == Material.LAVA_BUCKET || GriefPrevention.instance.config_blockWildernessWaterBuckets) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); + bucketEvent.setCancelled(true); + return; + } + } + } + + //lava buckets can't be dumped near other players unless pvp is on + if(!GriefPrevention.instance.config_pvp_enabledWorlds.contains(block.getWorld()) && !player.hasPermission("griefprevention.lava")) + { + if(bucketEvent.getBucket() == Material.LAVA_BUCKET) + { + List players = block.getWorld().getPlayers(); + for(int i = 0; i < players.size(); i++) + { + Player otherPlayer = players.get(i); + Location location = otherPlayer.getLocation(); + if(!otherPlayer.equals(player) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, otherPlayer.getName()); + bucketEvent.setCancelled(true); + return; + } + } + } + } + } + + //see above + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) + { + Player player = bucketEvent.getPlayer(); + Block block = bucketEvent.getBlockClicked(); + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + bucketEvent.setCancelled(true); + return; + } + } + + //when a player interacts with the world + @EventHandler(priority = EventPriority.LOWEST) + void onPlayerInteract(PlayerInteractEvent event) + { + Player player = event.getPlayer(); + + //determine target block. FEATURE: shovel and string can be used from a distance away + Block clickedBlock = null; + + try + { + clickedBlock = event.getClickedBlock(); //null returned here means interacting with air + if(clickedBlock == null || clickedBlock.getType() == Material.SNOW) + { + //try to find a far away non-air block along line of sight + HashSet transparentMaterials = new HashSet(); + transparentMaterials.add(Byte.valueOf((byte)Material.AIR.getId())); + transparentMaterials.add(Byte.valueOf((byte)Material.SNOW.getId())); + transparentMaterials.add(Byte.valueOf((byte)Material.LONG_GRASS.getId())); + clickedBlock = player.getTargetBlock(transparentMaterials, 250); + } + } + catch(Exception e) //an exception intermittently comes from getTargetBlock(). when it does, just ignore the event + { + return; + } + + //if no block, stop here + if(clickedBlock == null) + { + return; + } + + Material clickedBlockType = clickedBlock.getType(); + + //apply rules for putting out fires (requires build permission) + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(event.getClickedBlock() != null && event.getClickedBlock().getRelative(event.getBlockFace()).getType() == Material.FIRE) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noBuildReason = claim.allowBuild(player); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + return; + } + } + } + + //apply rules for containers and crafting blocks + if( GriefPrevention.instance.config_claims_preventTheft && ( + event.getAction() == Action.RIGHT_CLICK_BLOCK && ( + clickedBlock.getState() instanceof InventoryHolder || + clickedBlockType == Material.WORKBENCH || + clickedBlockType == Material.ENDER_CHEST || + clickedBlockType == Material.DISPENSER || + clickedBlockType == Material.ANVIL || + clickedBlockType == Material.BREWING_STAND || + clickedBlockType == Material.JUKEBOX || + clickedBlockType == Material.ENCHANTMENT_TABLE || + GriefPrevention.instance.config_mods_containerTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))))) + { + //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, if configured that way + else if((GriefPrevention.instance.config_claims_lockWoodenDoors && clickedBlockType == Material.WOODEN_DOOR) || + (GriefPrevention.instance.config_claims_lockTrapDoors && clickedBlockType == Material.TRAP_DOOR) || + (GriefPrevention.instance.config_claims_lockFenceGates && clickedBlockType == Material.FENCE_GATE)) + { + 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(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)))) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); + return; + } + } + } + + //apply rule for players trampling tilled soil back to dirt (never allow it) + //NOTE: that this event applies only to players. monsters and animals can still trample. + else if(event.getAction() == Action.PHYSICAL && clickedBlockType == Material.SOIL) + { + event.setCancelled(true); + return; + } + + //apply rule for note blocks and repeaters + else if(clickedBlockType == Material.NOTE_BLOCK || clickedBlockType == Material.DIODE_BLOCK_ON || clickedBlockType == Material.DIODE_BLOCK_OFF) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + String noBuildReason = claim.allowBuild(player); + 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 + Action action = event.getAction(); + if(action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; + + //what's the player holding? + Material materialInHand = player.getItemInHand().getType(); + + //if it's bonemeal, check for build permission (ink sac == bone meal, must be a Bukkit bug?) + if(materialInHand == Material.INK_SACK) + { + String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + } + + return; + } + + else if(materialInHand == Material.BOAT) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); + event.setCancelled(true); + } + } + + return; + } + + //if it's a spawn egg, minecart, or boat, and this is a creative world, apply special rules + else if((materialInHand == Material.MONSTER_EGG || materialInHand == Material.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.BOAT) && GriefPrevention.instance.creativeRulesApply(clickedBlock.getLocation())) + { + //player needs build permission at this location + String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + return; + } + + //enforce limit on total number of entities in this claim + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim == null) return; + + String noEntitiesReason = claim.allowMoreEntities(); + if(noEntitiesReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason); + event.setCancelled(true); + return; + } + + return; + } + + //if he's investigating a claim + else if(materialInHand == GriefPrevention.instance.config_claims_investigationTool) + { + //air indicates too far away + if(clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); + return; + } + + 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, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + + //if can resize this claim, tell about the boundaries + if(claim.allowEdit(player) == null) + { + GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea()); + } + + //if deleteclaims permission, tell about the player's offline time + if(!claim.isAdminClaim() && player.hasPermission("griefprevention.deleteclaims")) + { + PlayerData otherPlayerData = this.dataStore.getPlayerData(claim.getOwnerName()); + Date lastLogin = otherPlayerData.lastLogin; + 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().getPlayerExact(claim.getOwnerName()) == null) + this.dataStore.clearCachedPlayerData(claim.getOwnerName()); + } + } + + return; + } + + //if it's a golden shovel + else if(materialInHand != GriefPrevention.instance.config_claims_modificationTool) return; + + //disable golden shovel while under siege + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoShovel); + event.setCancelled(true); + 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 + String playerName = player.getName(); + playerData = this.dataStore.getPlayerData(player.getName()); + if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive) + { + //if the clicked block is in a claim, visualize that claim and deliver an error message + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName()); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + + return; + } + + //figure out which chunk to repair + Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); + + //start the repair process + + //set boundaries for processing + int miny = clickedBlock.getY(); + + //if not in aggressive mode, extend the selection down to a little below sea level + if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive)) + { + if(miny > GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10) + { + miny = GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10; + } + } + + GriefPrevention.instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player); + + return; + } + + //if in restore nature fill mode + if(playerData.shovelMode == ShovelMode.RestoreNatureFill) + { + ArrayList allowedFillBlocks = new ArrayList(); + Environment environment = clickedBlock.getWorld().getEnvironment(); + if(environment == Environment.NETHER) + { + allowedFillBlocks.add(Material.NETHERRACK); + } + else if(environment == Environment.THE_END) + { + allowedFillBlocks.add(Material.ENDER_STONE); + } + else + { + allowedFillBlocks.add(Material.GRASS); + allowedFillBlocks.add(Material.DIRT); + allowedFillBlocks.add(Material.STONE); + allowedFillBlocks.add(Material.SAND); + allowedFillBlocks.add(Material.SANDSTONE); + allowedFillBlocks.add(Material.ICE); + } + + Block centerBlock = clickedBlock; + + int maxHeight = centerBlock.getY(); + int minx = centerBlock.getX() - playerData.fillRadius; + int maxx = centerBlock.getX() + playerData.fillRadius; + int minz = centerBlock.getZ() - playerData.fillRadius; + int maxz = centerBlock.getZ() + playerData.fillRadius; + int minHeight = maxHeight - 10; + if(minHeight < 0) minHeight = 0; + + Claim cachedClaim = null; + for(int x = minx; x <= maxx; x++) + { + for(int z = minz; z <= maxz; z++) + { + //circular brush + Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z); + if(location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue; + + //default fill block is initially the first from the allowed fill blocks list above + Material defaultFiller = allowedFillBlocks.get(0); + + //prefer to use the block the player clicked on, if it's an acceptable fill block + if(allowedFillBlocks.contains(centerBlock.getType())) + { + defaultFiller = centerBlock.getType(); + } + + //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler + else if(centerBlock.getType() == Material.WATER || centerBlock.getType() == Material.STATIONARY_WATER) + { + Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation()); + while(!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10) + { + block = block.getRelative(BlockFace.DOWN); + } + if(allowedFillBlocks.contains(block.getType())) + { + defaultFiller = block.getType(); + } + } + + //fill bottom to top + for(int y = minHeight; y <= maxHeight; y++) + { + Block block = centerBlock.getWorld().getBlockAt(x, y, z); + + //respect claims + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim); + if(claim != null) + { + cachedClaim = claim; + break; + } + + //only replace air, spilling water, snow, long grass + if(block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.STATIONARY_WATER && block.getData() != 0) || block.getType() == Material.LONG_GRASS) + { + //if the top level, always use the default filler picked above + if(y == maxHeight) + { + block.setType(defaultFiller); + } + + //otherwise look to neighbors for an appropriate fill block + else + { + Block eastBlock = block.getRelative(BlockFace.EAST); + Block westBlock = block.getRelative(BlockFace.WEST); + Block northBlock = block.getRelative(BlockFace.NORTH); + Block southBlock = block.getRelative(BlockFace.SOUTH); + + //first, check lateral neighbors (ideally, want to keep natural layers) + if(allowedFillBlocks.contains(eastBlock.getType())) + { + block.setType(eastBlock.getType()); + } + else if(allowedFillBlocks.contains(westBlock.getType())) + { + block.setType(westBlock.getType()); + } + else if(allowedFillBlocks.contains(northBlock.getType())) + { + block.setType(northBlock.getType()); + } + else if(allowedFillBlocks.contains(southBlock.getType())) + { + block.setType(southBlock.getType()); + } + + //if all else fails, use the default filler selected above + else + { + block.setType(defaultFiller); + } + } + } + } + } + } + + return; + } + + //if the player doesn't have claims permission, don't do anything + if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission); + return; + } + + //if he's resizing a claim and that claim hasn't been deleted since he started resizing it + if(playerData.claimResizing != null && playerData.claimResizing.inDataStore) + { + if(clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return; + + //figure out what the coords of his new claim would be + int newx1, newx2, newz1, newz2, newy1, newy2; + if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX()) + { + newx1 = clickedBlock.getX(); + } + else + { + newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX(); + } + + if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockX()) + { + newx2 = clickedBlock.getX(); + } + else + { + newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX(); + } + + if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ()) + { + newz1 = clickedBlock.getZ(); + } + else + { + newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ(); + } + + if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ()) + { + newz2 = clickedBlock.getZ(); + } + else + { + newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ(); + } + + newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY(); + newy2 = clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance; + + //for top level claims, apply size rules and claim blocks requirement + if(playerData.claimResizing.parent == null) + { + //measure new claim, apply size rules + int newWidth = (Math.abs(newx1 - newx2) + 1); + int newHeight = (Math.abs(newz1 - newz2) + 1); + + if(!playerData.claimResizing.isAdminClaim() && (newWidth < GriefPrevention.instance.config_claims_minSize || newHeight < GriefPrevention.instance.config_claims_minSize)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); + return; + } + + //make sure player has enough blocks to make up the difference + if(!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) + { + int newArea = newWidth * newHeight; + int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; + + if(blocksRemainingAfter < 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); + return; + } + } + } + + //special rules for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. + //rule1: in creative mode, top-level claims can't be moved or resized smaller. + //rule2: in any mode, shrinking a claim removes any surface fluids + Claim oldClaim = playerData.claimResizing; + boolean smaller = false; + if(oldClaim.parent == null) + { + //temporary claim instance, just for checking contains() + Claim newClaim = new Claim( + new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), + new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), + "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null); + + //if the new claim is smaller + if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) + { + smaller = true; + + //enforce creative mode rule + if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && !player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); + return; + } + + //remove surface fluids about to be unclaimed + oldClaim.removeSurfaceFluids(newClaim); + } + } + + //ask the datastore to try and resize the claim, this checks for conflicts with other claims + CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim(playerData.claimResizing, newx1, newx2, newy1, newy2, newz1, newz2); + + if(result.succeeded) + { + //inform and show the player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(playerData.getRemainingClaimBlocks())); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + + //if resizing someone else's claim, make a log entry + if(!playerData.claimResizing.ownerName.equals(playerName)) + { + GriefPrevention.AddLogEntry(playerName + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); + } + + //if in a creative mode world and shrinking an existing claim, restore any unclaimed area + if(smaller && GriefPrevention.instance.creativeRulesApply(oldClaim.getLesserBoundaryCorner())) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); + GriefPrevention.instance.restoreClaim(oldClaim, 20L * 60 * 2); //2 minutes + GriefPrevention.AddLogEntry(player.getName() + " shrank a claim @ " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.getLesserBoundaryCorner())); + } + + //clean up + playerData.claimResizing = null; + playerData.lastShovelLocation = null; + } + else + { + //inform player + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); + + //show the player the conflicting claim + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + + 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(), + "--subdivision--", //owner name is not used for subdivisions + playerData.claimSubdividing, + null); + + //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()) && playerData.shovelMode != ShovelMode.Admin) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); + 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(), "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null), clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation()); + Visualization.Apply(player, visualization); + } + + //otherwise, he's trying to finish creating a claim by setting the other boundary corner + else + { + //if last shovel location was in a different world, assume the player is starting the create-claim workflow over + if(!lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) + { + playerData.lastShovelLocation = null; + this.onPlayerInteract(event); + return; + } + + //apply minimum claim dimensions rule + int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1; + int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1; + + if(playerData.shovelMode != ShovelMode.Admin && (newClaimWidth < GriefPrevention.instance.config_claims_minSize || newClaimHeight < GriefPrevention.instance.config_claims_minSize)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); + return; + } + + //if not an administrative claim, verify the player has enough claim blocks for this new claim + if(playerData.shovelMode != ShovelMode.Admin) + { + int newClaimArea = newClaimWidth * newClaimHeight; + int remainingBlocks = playerData.getRemainingClaimBlocks(); + if(newClaimArea > remainingBlocks) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks)); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimAdvertisement); + return; + } + } + else + { + playerName = ""; + } + + //try to create a new claim (will return null if this claim overlaps another) + 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(), + playerName, + null, null); + + //if it didn't succeed, tell the player why + if(!result.succeeded) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); + + 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 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; + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/TextMode.java b/src/me/ryanhamshire/GriefPrevention/TextMode.java index 984a3b2..2a0daee 100644 --- a/src/me/ryanhamshire/GriefPrevention/TextMode.java +++ b/src/me/ryanhamshire/GriefPrevention/TextMode.java @@ -1,31 +1,31 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import org.bukkit.ChatColor; - -//just a few constants for chat color codes -class TextMode -{ - final static ChatColor Info = ChatColor.BLUE; - final static ChatColor Instr = ChatColor.YELLOW; - final static ChatColor Warn = ChatColor.GOLD; - final static ChatColor Err = ChatColor.RED; - final static ChatColor Success = ChatColor.GREEN; -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import org.bukkit.ChatColor; + +//just a few constants for chat color codes +class TextMode +{ + final static ChatColor Info = ChatColor.AQUA; + final static ChatColor Instr = ChatColor.YELLOW; + final static ChatColor Warn = ChatColor.GOLD; + final static ChatColor Err = ChatColor.RED; + final static ChatColor Success = ChatColor.GREEN; +}