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;
+}