diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9103071 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Ignore all differences in line endings +* -crlf \ No newline at end of file diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/DataStore.java b/src/main/java/me/ryanhamshire/GriefPrevention/DataStore.java index d4b518c..387381b 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/DataStore.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/DataStore.java @@ -1,1749 +1,1764 @@ -/* - 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 com.google.common.io.Files; -import me.ryanhamshire.GriefPrevention.alttd.config.Config; -import me.ryanhamshire.GriefPrevention.events.ClaimResizeEvent; -import me.ryanhamshire.GriefPrevention.events.ClaimCreatedEvent; -import me.ryanhamshire.GriefPrevention.events.ClaimDeletedEvent; -import me.ryanhamshire.GriefPrevention.events.ClaimExtendEvent; -import me.ryanhamshire.GriefPrevention.events.ClaimTransferEvent; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.AnimalTamer; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Tameable; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -//singleton class which manages all GriefPrevention data (except for config options) -public abstract class DataStore -{ - - //in-memory cache for player data - protected ConcurrentHashMap playerNameToPlayerDataMap = new ConcurrentHashMap<>(); - - //in-memory cache for group (permission-based) data - protected ConcurrentHashMap permissionToBonusBlocksMap = new ConcurrentHashMap<>(); - - //in-memory cache for claim data - ArrayList claims = new ArrayList<>(); - ConcurrentHashMap> chunksToClaimsMap = new ConcurrentHashMap<>(); - - //in-memory cache for messages - private String[] messages; - - //pattern for unique user identifiers (UUIDs) - protected final static Pattern uuidpattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - - //next claim ID - Long nextClaimID = (long) 0; - - //path information, for where stuff stored on disk is well... stored - public final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; - final static String playerDataFolderPath = dataLayerFolderPath + File.separator + "PlayerData"; - final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; - final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; - final static String softMuteFilePath = dataLayerFolderPath + File.separator + "softMute.txt"; - final static String bannedWordsFilePath = dataLayerFolderPath + File.separator + "bannedWords.txt"; - - //the latest version of the data schema implemented here - protected static final int latestSchemaVersion = 3; - - //reading and writing the schema version to the data store - abstract int getSchemaVersionFromStorage(); - - abstract void updateSchemaVersionInStorage(int versionToSet); - - //current version of the schema of data in secondary storage - private int currentSchemaVersion = -1; //-1 means not determined yet - - //list of UUIDs which are soft-muted - ConcurrentHashMap softMuteMap = new ConcurrentHashMap<>(); - - //world guard reference, if available - private WorldGuardWrapper worldGuard = null; - - protected int getSchemaVersion() - { - if (this.currentSchemaVersion >= 0) - { - return this.currentSchemaVersion; - } - else - { - this.currentSchemaVersion = this.getSchemaVersionFromStorage(); - return this.currentSchemaVersion; - } - } - - protected void setSchemaVersion(int versionToSet) - { - this.currentSchemaVersion = versionToSet; - this.updateSchemaVersionInStorage(versionToSet); - } - - //initialization! - void initialize() throws Exception - { - GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); - - //RoboMWM: ensure the nextClaimID is greater than any other claim ID. If not, data corruption occurred (out of storage space, usually). - for (Claim claim : this.claims) - { - if (claim.id >= nextClaimID) - { - GriefPrevention.instance.getLogger().severe("nextClaimID was lesser or equal to an already-existing claim ID!\n" + - "This usually happens if you ran out of storage space."); - GriefPrevention.AddLogEntry("Changing nextClaimID from " + nextClaimID + " to " + claim.id, CustomLogEntryTypes.Debug, false); - nextClaimID = claim.id + 1; - } - } - - //ensure data folders exist - File playerDataFolder = new File(playerDataFolderPath); - if (!playerDataFolder.exists()) - { - playerDataFolder.mkdirs(); - } - - //load up all the messages from messages.yml - this.loadMessages(); - GriefPrevention.AddLogEntry("Customizable messages loaded."); - - //if converting up from an earlier schema version, write all claims back to storage using the latest format - if (this.getSchemaVersion() < latestSchemaVersion) - { - GriefPrevention.AddLogEntry("Please wait. Updating data format."); - - for (Claim claim : this.claims) - { - this.saveClaim(claim); - - for (Claim subClaim : claim.children) - { - this.saveClaim(subClaim); - } - } - - //clean up any UUID conversion work - if (UUIDFetcher.lookupCache != null) - { - UUIDFetcher.lookupCache.clear(); - UUIDFetcher.correctedNames.clear(); - } - - GriefPrevention.AddLogEntry("Update finished."); - } - - //load list of soft mutes - this.loadSoftMutes(); - - //make a note of the data store schema version - this.setSchemaVersion(latestSchemaVersion); - - //try to hook into world guard - try - { - this.worldGuard = new WorldGuardWrapper(); - GriefPrevention.AddLogEntry("Successfully hooked into WorldGuard."); - } - //if failed, world guard compat features will just be disabled. - catch (IllegalStateException | IllegalArgumentException | ClassCastException | NoClassDefFoundError ignored) { } - } - - private void loadSoftMutes() - { - File softMuteFile = new File(softMuteFilePath); - if (softMuteFile.exists()) - { - BufferedReader inStream = null; - try - { - //open the file - inStream = new BufferedReader(new FileReader(softMuteFile.getAbsolutePath())); - - //while there are lines left - String nextID = inStream.readLine(); - while (nextID != null) - { - //parse line into a UUID - UUID playerID; - try - { - playerID = UUID.fromString(nextID); - } - catch (Exception e) - { - playerID = null; - GriefPrevention.AddLogEntry("Failed to parse soft mute entry as a UUID: " + nextID); - } - - //push it into the map - if (playerID != null) - { - this.softMuteMap.put(playerID, true); - } - - //move to the next - nextID = inStream.readLine(); - } - } - catch (Exception e) - { - GriefPrevention.AddLogEntry("Failed to read from the soft mute data file: " + e.toString()); - e.printStackTrace(); - } - - try - { - if (inStream != null) inStream.close(); - } - catch (IOException exception) {} - } - } - - public List loadBannedWords() - { - try - { - File bannedWordsFile = new File(bannedWordsFilePath); - if (!bannedWordsFile.exists()) - { - Files.touch(bannedWordsFile); - String defaultWords = - "nigger\nniggers\nniger\nnigga\nnigers\nniggas\n" + - "fag\nfags\nfaggot\nfaggots\nfeggit\nfeggits\nfaggit\nfaggits\n" + - "cunt\ncunts\nwhore\nwhores\nslut\nsluts\n"; - Files.append(defaultWords, bannedWordsFile, Charset.forName("UTF-8")); - } - - return Files.readLines(bannedWordsFile, Charset.forName("UTF-8")); - } - catch (Exception e) - { - GriefPrevention.AddLogEntry("Failed to read from the banned words data file: " + e.toString()); - e.printStackTrace(); - return new ArrayList<>(); - } - } - - //updates soft mute map and data file - boolean toggleSoftMute(UUID playerID) - { - boolean newValue = !this.isSoftMuted(playerID); - - this.softMuteMap.put(playerID, newValue); - this.saveSoftMutes(); - - return newValue; - } - - public boolean isSoftMuted(UUID playerID) - { - Boolean mapEntry = this.softMuteMap.get(playerID); - if (mapEntry == null || mapEntry == Boolean.FALSE) - { - return false; - } - - return true; - } - - private void saveSoftMutes() - { - BufferedWriter outStream = null; - - try - { - //open the file and write the new value - File softMuteFile = new File(softMuteFilePath); - softMuteFile.createNewFile(); - outStream = new BufferedWriter(new FileWriter(softMuteFile)); - - for (Map.Entry entry : softMuteMap.entrySet()) - { - if (entry.getValue().booleanValue()) - { - outStream.write(entry.getKey().toString()); - outStream.newLine(); - } - } - - } - - //if any problem, log it - catch (Exception e) - { - GriefPrevention.AddLogEntry("Unexpected exception saving soft mute data: " + e.getMessage()); - e.printStackTrace(); - } - - //close the file - try - { - if (outStream != null) outStream.close(); - } - catch (IOException exception) {} - } - - //removes cached player data from memory - synchronized void clearCachedPlayerData(UUID playerID) - { - this.playerNameToPlayerDataMap.remove(playerID); - } - - //gets the number of bonus blocks a player has from his permissions - //Bukkit doesn't allow for checking permissions of an offline player. - //this will return 0 when he's offline, and the correct number when online. - synchronized public int getGroupBonusBlocks(UUID playerID) - { - Player player = GriefPrevention.instance.getServer().getPlayer(playerID); - - if (player == null) return 0; - - int bonusBlocks = 0; - - for (Map.Entry groupEntry : this.permissionToBonusBlocksMap.entrySet()) - { - if (player.hasPermission(groupEntry.getKey())) - { - bonusBlocks += groupEntry.getValue(); - } - } - - 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); - - public class NoTransferException extends RuntimeException - { - private static final long serialVersionUID = 1L; - - NoTransferException(String message) - { - super(message); - } - } - - synchronized public void changeClaimOwner(Claim claim, UUID newOwnerID) - { - //if it's a subdivision, throw an exception - if (claim.parent != null) - { - throw new NoTransferException("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.ownerID); - } - - //call event - ClaimTransferEvent event = new ClaimTransferEvent(claim, newOwnerID); - Bukkit.getPluginManager().callEvent(event); - - //return if event is cancelled - if (event.isCancelled()) return; - - //determine new owner - PlayerData newOwnerData = null; - - if (event.getNewOwner() != null) - { - newOwnerData = this.getPlayerData(event.getNewOwner()); - } - - //transfer - claim.ownerID = event.getNewOwner(); - this.saveClaim(claim); - - //adjust blocks and other records - if (ownerData != null) - { - ownerData.getClaims().remove(claim); - } - - if (newOwnerData != null) - { - newOwnerData.getClaims().add(claim); - } - } - - //adds a claim to the datastore, making it an effective claim - synchronized void addClaim(Claim newClaim, boolean writeToStorage) - { - //subdivisions are added under their parent, not directly to the hash map for direct search - if (newClaim.parent != null) - { - if (!newClaim.parent.children.contains(newClaim)) - { - newClaim.parent.children.add(newClaim); - } - newClaim.inDataStore = true; - if (writeToStorage) - { - this.saveClaim(newClaim); - } - return; - } - - //add it and mark it as added - this.claims.add(newClaim); - addToChunkClaimMap(newClaim); - - newClaim.inDataStore = true; - - //except for administrative claims (which have no owner), update the owner's playerData with the new claim - if (!newClaim.isAdminClaim() && writeToStorage) - { - PlayerData ownerData = this.getPlayerData(newClaim.ownerID); - ownerData.getClaims().add(newClaim); - } - - //make sure the claim is saved to disk - if (writeToStorage) - { - this.saveClaim(newClaim); - } - } - - private void addToChunkClaimMap(Claim claim) - { - // Subclaims should not be added to chunk claim map. - if (claim.parent != null) return; - - ArrayList chunkHashes = claim.getChunkHashes(); - for (Long chunkHash : chunkHashes) - { - ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkHash); - if (claimsInChunk == null) - { - this.chunksToClaimsMap.put(chunkHash, claimsInChunk = new ArrayList<>()); - } - - claimsInChunk.add(claim); - } - } - - private void removeFromChunkClaimMap(Claim claim) - { - ArrayList chunkHashes = claim.getChunkHashes(); - for (Long chunkHash : chunkHashes) - { - ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkHash); - if (claimsInChunk != null) - { - for (Iterator it = claimsInChunk.iterator(); it.hasNext(); ) - { - Claim c = it.next(); - if (c.id.equals(claim.id)) - { - it.remove(); - break; - } - } - if (claimsInChunk.isEmpty()) - { // if nothing's left, remove this chunk's cache - this.chunksToClaimsMap.remove(chunkHash); - } - } - } - } - - //turns a location into a string, useful in data storage - private final 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, List validWorlds) 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 + "\""); - } - - String worldName = elements[0]; - String xString = elements[1]; - String yString = elements[2]; - String zString = elements[3]; - - //identify world the claim is in - World world = null; - for (World w : validWorlds) - { - if (w.getName().equalsIgnoreCase(worldName)) - { - world = w; - break; - } - } - - 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) - { - assignClaimID(claim); - - this.writeClaimToStorage(claim); - } - - private void assignClaimID(Claim claim) - { - //ensure a unique identifier for the claim which will be used to name the file on disk - if (claim.id == null || claim.id == -1) - { - claim.id = this.nextClaimID; - this.incrementNextClaimID(); - } - } - - 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(UUID playerID) - { - //first, look in memory - PlayerData playerData = this.playerNameToPlayerDataMap.get(playerID); - - //if not there, build a fresh instance with some blanks for what may be in secondary storage - if (playerData == null) - { - playerData = new PlayerData(); - playerData.playerID = playerID; - - //shove that new player data into the hash map cache - this.playerNameToPlayerDataMap.put(playerID, playerData); - } - - return playerData; - } - - abstract PlayerData getPlayerDataFromStorage(UUID playerID); - - //deletes a claim or subdivision - synchronized public void deleteClaim(Claim claim) - { - this.deleteClaim(claim, true, false); - } - - //deletes a claim or subdivision - synchronized public void deleteClaim(Claim claim, boolean releasePets) - { - this.deleteClaim(claim, true, releasePets); - } - - synchronized void deleteClaim(Claim claim, boolean fireEvent, boolean releasePets) - { - //delete any children - for (int j = 1; (j - 1) < claim.children.size(); j++) - { - this.deleteClaim(claim.children.get(j - 1), true); - } - - //subdivisions must also be removed from the parent claim child list - if (claim.parent != null) - { - Claim parentClaim = claim.parent; - parentClaim.children.remove(claim); - } - - //mark as deleted so any references elsewhere can be ignored - claim.inDataStore = false; - - //remove from memory - for (int i = 0; i < this.claims.size(); i++) - { - if (claims.get(i).id.equals(claim.id)) - { - this.claims.remove(i); - break; - } - } - - removeFromChunkClaimMap(claim); - - //remove from secondary storage - this.deleteClaimFromSecondaryStorage(claim); - - //update player data - if (claim.ownerID != null) - { - PlayerData ownerData = this.getPlayerData(claim.ownerID); - for (int i = 0; i < ownerData.getClaims().size(); i++) - { - if (ownerData.getClaims().get(i).id.equals(claim.id)) - { - ownerData.getClaims().remove(i); - break; - } - } - this.savePlayerData(claim.ownerID, ownerData); - } - - if (fireEvent) - { - ClaimDeletedEvent ev = new ClaimDeletedEvent(claim); - Bukkit.getPluginManager().callEvent(ev); - } - - //optionally set any pets free which belong to the claim owner - if (releasePets && claim.ownerID != null && claim.parent == null) - { - for (Chunk chunk : claim.getChunks()) - { - Entity[] entities = chunk.getEntities(); - for (Entity entity : entities) - { - if (entity instanceof Tameable) - { - Tameable pet = (Tameable) entity; - if (pet.isTamed()) - { - AnimalTamer owner = pet.getOwner(); - if (owner != null) - { - UUID ownerID = owner.getUniqueId(); - if (ownerID != null) - { - if (ownerID.equals(claim.ownerID)) - { - pet.setTamed(false); - pet.setOwner(null); - if (pet instanceof InventoryHolder) - { - InventoryHolder holder = (InventoryHolder) pet; - holder.getInventory().clear(); - } - } - } - } - } - } - } - } - } - } - - 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) - { - return getClaimAt(location, ignoreHeight, false, cachedClaim); - } - - /** - * Get the claim at a specific location. - * - *

The cached claim may be null, but will increase performance if you have a reasonable idea - * of which claim is correct. - * - * @param location the location - * @param ignoreHeight whether or not to check containment vertically - * @param ignoreSubclaims whether or not subclaims should be returned over claims - * @param cachedClaim the cached claim, if any - * @return the claim containing the location or null if no claim exists there - */ - synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, boolean ignoreSubclaims, 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, !ignoreSubclaims)) - return cachedClaim; - - //find a top level claim - Long chunkID = getChunkHash(location); - ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkID); - if (claimsInChunk == null) return null; - - for (Claim claim : claimsInChunk) - { - if (claim.inDataStore && claim.contains(location, ignoreHeight, false)) - { - // If ignoring subclaims, claim is a match. - if (ignoreSubclaims) return claim; - - //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.inDataStore && subdivision.contains(location, ignoreHeight, false)) - return subdivision; - } - - return claim; - } - } - - //if no claim found, return null - return null; - } - - //finds a claim by ID - public synchronized Claim getClaim(long id) - { - for (Claim claim : this.claims) - { - if (claim.inDataStore && claim.getID() == id) return claim; - } - - return null; - } - - //returns a read-only access point for the list of all land claims - //if you need to make changes, use provided methods like .deleteClaim() and .createClaim(). - //this will ensure primary memory (RAM) and secondary memory (disk, database) stay in sync - public Collection getClaims() - { - return Collections.unmodifiableCollection(this.claims); - } - - public Collection getClaims(int chunkx, int chunkz) - { - ArrayList chunkClaims = this.chunksToClaimsMap.get(getChunkHash(chunkx, chunkz)); - if (chunkClaims != null) - { - return Collections.unmodifiableCollection(chunkClaims); - } - else - { - return Collections.unmodifiableCollection(new ArrayList<>()); - } - } - - //gets an almost-unique, persistent identifier for a chunk - public static Long getChunkHash(long chunkx, long chunkz) - { - return (chunkz ^ (chunkx << 32)); - } - - //gets an almost-unique, persistent identifier for a chunk - public static Long getChunkHash(Location location) - { - return getChunkHash(location.getBlockX() >> 4, location.getBlockZ() >> 4); - } - - public static ArrayList getChunkHashes(Claim claim) { - return getChunkHashes(claim.getLesserBoundaryCorner(), claim.getGreaterBoundaryCorner()); - } - - public static ArrayList getChunkHashes(Location min, Location max) { - ArrayList hashes = new ArrayList<>(); - int smallX = min.getBlockX() >> 4; - int smallZ = min.getBlockZ() >> 4; - int largeX = max.getBlockX() >> 4; - int largeZ = max.getBlockZ() >> 4; - - for (int x = smallX; x <= largeX; x++) - { - for (int z = smallZ; z <= largeZ; z++) - { - hashes.add(getChunkHash(x, z)); - } - } - - return hashes; - } - - /* - * Creates a claim and flags it as being new....throwing a create claim event; - */ - synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, UUID ownerID, Claim parent, Long id, Player creatingPlayer) - { - return createClaim(world, x1, x2, y1, y2, z1, z2, ownerID, parent, id, creatingPlayer, false); - } - - //creates a claim. - //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim - //if the new claim would overlap a WorldGuard region where the player doesn't have permission to build, returns a failure with NULL for 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 check for world guard regions where the player doesn't have permission - //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, UUID ownerID, Claim parent, Long id, Player creatingPlayer, boolean dryRun) - { - CreateClaimResult result = new CreateClaimResult(); - - int smallx, bigx, smally, bigy, smallz, bigz; - - int worldMinY = world.getMinHeight(); - y1 = Math.max(worldMinY, Math.max(GriefPrevention.instance.config_claims_maxDepth, y1)); - y2 = Math.max(worldMinY, Math.max(GriefPrevention.instance.config_claims_maxDepth, y2)); - - //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; - } - - if (parent != null) - { - Location lesser = parent.getLesserBoundaryCorner(); - Location greater = parent.getGreaterBoundaryCorner(); - if (smallx < lesser.getX() || smallz < lesser.getZ() || bigx > greater.getX() || bigz > greater.getZ()) - { - result.succeeded = false; - result.claim = parent; - return result; - } - smally = sanitizeClaimDepth(parent, smally); - } - - //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), - ownerID, - new ArrayList<>(), - new ArrayList<>(), - new ArrayList<>(), - new ArrayList<>(), - 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 (Claim otherClaim : claimsToCheck) - { - //if we find an existing claim which will be overlapped - if (otherClaim.id != newClaim.id && otherClaim.inDataStore && otherClaim.overlaps(newClaim)) - { - //result = fail, return conflicting claim - result.succeeded = false; - result.claim = otherClaim; - return result; - } - } - - if (creatingPlayer != null && !newClaim.canCleaimNear(creatingPlayer, 100) && newClaim.parent == null) { - result.succeeded = false; - result.claim = null; - return result; - } - - if (creatingPlayer != null && !newClaim.isInsideBorder()) { - result.succeeded = false; - result.claim = null; - creatingPlayer.sendMiniMessage(Config.claimCreatedOutsideBorder, null); - return result; - } - - //if worldguard is installed, also prevent claims from overlapping any worldguard regions - if (GriefPrevention.instance.config_claims_respectWorldGuard && this.worldGuard != null && creatingPlayer != null) - { - if (!this.worldGuard.canBuild(newClaim.lesserBoundaryCorner, newClaim.greaterBoundaryCorner, creatingPlayer)) - { - result.succeeded = false; - result.claim = null; - return result; - } - } - - if (dryRun) - { - // since this is a dry run, just return the unsaved claim as is. - result.succeeded = true; - result.claim = newClaim; - return result; - } - assignClaimID(newClaim); // assign a claim ID before calling event, in case a plugin wants to know the ID. - ClaimCreatedEvent event = new ClaimCreatedEvent(newClaim, creatingPlayer); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled() && creatingPlayer != null) - { - result.succeeded = false; - result.claim = null; - return result; - - } - //otherwise add this new claim to the data store to make it effective - this.addClaim(newClaim, true); - - //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 void savePlayerDataSync(UUID playerID, PlayerData playerData) - { - //ensure player data is already read from file before trying to save - playerData.getAccruedClaimBlocks(); - playerData.getClaims(); - - this.asyncSavePlayerData(playerID, playerData); - } - - //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them - public void savePlayerData(UUID playerID, PlayerData playerData) - { - new SavePlayerDataThread(playerID, playerData).start(); - } - - public void asyncSavePlayerData(UUID playerID, PlayerData playerData) - { - //save everything except the ignore list - this.overrideSavePlayerData(playerID, playerData); - - //save the ignore list - if (playerData.ignoreListChanged) - { - StringBuilder fileContent = new StringBuilder(); - try - { - for (UUID uuidKey : playerData.ignoredPlayers.keySet()) - { - Boolean value = playerData.ignoredPlayers.get(uuidKey); - if (value == null) continue; - - //admin-enforced ignores begin with an asterisk - if (value) - { - fileContent.append("*"); - } - - fileContent.append(uuidKey); - fileContent.append("\n"); - } - - //write data to file - File playerDataFile = new File(playerDataFolderPath + File.separator + playerID + ".ignore"); - Files.write(fileContent.toString().trim().getBytes("UTF-8"), playerDataFile); - } - - //if any problem, log it - catch (Exception e) - { - GriefPrevention.AddLogEntry("GriefPrevention: Unexpected exception saving data for player \"" + playerID.toString() + "\": " + e.getMessage()); - e.printStackTrace(); - } - } - } - - abstract void overrideSavePlayerData(UUID playerID, PlayerData playerData); - - //extends a claim to a new depth - //respects the max depth config variable - synchronized public void extendClaim(Claim claim, int newDepth) - { - if (claim.parent != null) claim = claim.parent; - - newDepth = sanitizeClaimDepth(claim, newDepth); - - //call event and return if event got cancelled - ClaimExtendEvent event = new ClaimExtendEvent(claim, newDepth); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) return; - - //adjust to new depth - setNewDepth(claim, event.getNewDepth()); - } - - /** - * Helper method for sanitizing claim depth to find the minimum expected value. - * - * @param claim the claim - * @param newDepth the new depth - * @return the sanitized new depth - */ - private int sanitizeClaimDepth(Claim claim, int newDepth) { - if (claim.parent != null) claim = claim.parent; - - // Get the old depth including the depth of the lowest subdivision. - int oldDepth = Math.min( - claim.getLesserBoundaryCorner().getBlockY(), - claim.children.stream().mapToInt(child -> child.getLesserBoundaryCorner().getBlockY()) - .min().orElse(Integer.MAX_VALUE)); - - // Use the lowest of the old and new depths. - newDepth = Math.min(newDepth, oldDepth); - // Cap depth to maximum depth allowed by the configuration. - newDepth = Math.max(newDepth, GriefPrevention.instance.config_claims_maxDepth); - // Cap the depth to the world's minimum height. - World world = Objects.requireNonNull(claim.getLesserBoundaryCorner().getWorld()); - newDepth = Math.max(newDepth, world.getMinHeight()); - - return newDepth; - } - - /** - * Helper method for sanitizing and setting claim depth. Saves affected claims. - * - * @param claim the claim - * @param newDepth the new depth - */ - private void setNewDepth(Claim claim, int newDepth) { - if (claim.parent != null) claim = claim.parent; - - final int depth = sanitizeClaimDepth(claim, newDepth); - - Stream.concat(Stream.of(claim), claim.children.stream()).forEach(localClaim -> { - localClaim.lesserBoundaryCorner.setY(depth); - localClaim.greaterBoundaryCorner.setY(Math.max(localClaim.greaterBoundaryCorner.getBlockY(), depth)); - this.saveClaim(localClaim); - }); - } - - //deletes all claims owned by a player - synchronized public void deleteClaimsForPlayer(UUID playerID, boolean releasePets) - { - //make a list of the player's claims - ArrayList claimsToDelete = new ArrayList<>(); - for (Claim claim : this.claims) - { - if ((playerID == claim.ownerID || (playerID != null && playerID.equals(claim.ownerID)))) - claimsToDelete.add(claim); - } - - //delete them one by one - for (Claim claim : claimsToDelete) - { - this.deleteClaim(claim, releasePets); - } - } - - //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, Player resizingPlayer) - { - //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.ownerID, claim.parent, claim.id, resizingPlayer, true); - - //if succeeded - if (result.succeeded) - { - removeFromChunkClaimMap(claim); // remove the old boundary from the chunk cache - // copy the boundary from the claim created in the dry run of createClaim() to our existing claim - claim.lesserBoundaryCorner = result.claim.lesserBoundaryCorner; - claim.greaterBoundaryCorner = result.claim.greaterBoundaryCorner; - // Sanitize claim depth, expanding parent down to the lowest subdivision and subdivisions down to parent. - // Also saves affected claims. - setNewDepth(claim, claim.getLesserBoundaryCorner().getBlockY()); - result.claim = claim; - addToChunkClaimMap(claim); // add the new boundary to the chunk cache - } - - return result; - } - - void resizeClaimWithChecks(Player player, PlayerData playerData, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) - { - //for top level claims, apply size rules and claim blocks requirement - if (playerData.claimResizing.parent == null) - { - //measure new claim, apply size rules - int newWidth = (Math.abs(newx1 - newx2) + 1); - int newHeight = (Math.abs(newz1 - newz2) + 1); - boolean smaller = newWidth < playerData.claimResizing.getWidth() || newHeight < playerData.claimResizing.getHeight(); - - if (!player.hasPermission("griefprevention.adminclaims") && !playerData.claimResizing.isAdminClaim() && smaller) - { - if (newWidth < GriefPrevention.instance.config_claims_minWidth || newHeight < GriefPrevention.instance.config_claims_minWidth) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooNarrow, String.valueOf(GriefPrevention.instance.config_claims_minWidth)); - return; - } - - int newArea = newWidth * newHeight; - if (newArea < GriefPrevention.instance.config_claims_minArea) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimInsufficientArea, String.valueOf(GriefPrevention.instance.config_claims_minArea)); - return; - } - } - - //make sure player has enough blocks to make up the difference - if (!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) - { - int newArea = newWidth * newHeight; - int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; - - if (blocksRemainingAfter < 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); - this.tryAdvertiseAdminAlternatives(player); - return; - } - } - } - - Claim oldClaim = playerData.claimResizing; - Claim newClaim = new Claim(oldClaim); - World world = newClaim.getLesserBoundaryCorner().getWorld(); - newClaim.lesserBoundaryCorner = new Location(world, newx1, newy1, newz1); - newClaim.greaterBoundaryCorner = new Location(world, newx2, newy2, newz2); - - //call event here to check if it has been cancelled - ClaimResizeEvent event = new ClaimResizeEvent(oldClaim, newClaim, player); - Bukkit.getPluginManager().callEvent(event); - - //return here if event is cancelled - if (event.isCancelled()) return; - - //special rule for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. - //rule: in any mode, shrinking a claim removes any surface fluids - boolean smaller = false; - if (oldClaim.parent == null) - { - //if the new claim is smaller - if (!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) - { - smaller = true; - - } - } - - //ask the datastore to try and resize the claim, this checks for conflicts with other claims - CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim( - playerData.claimResizing, - newClaim.getLesserBoundaryCorner().getBlockX(), - newClaim.getGreaterBoundaryCorner().getBlockX(), - newClaim.getLesserBoundaryCorner().getBlockY(), - newClaim.getGreaterBoundaryCorner().getBlockY(), - newClaim.getLesserBoundaryCorner().getBlockZ(), - newClaim.getGreaterBoundaryCorner().getBlockZ(), - player); - - if (result.succeeded) - { - //decide how many claim blocks are available for more resizing - int claimBlocksRemaining = 0; - if (!playerData.claimResizing.isAdminClaim()) - { - UUID ownerID = playerData.claimResizing.ownerID; - if (playerData.claimResizing.parent != null) - { - ownerID = playerData.claimResizing.parent.ownerID; - } - if (ownerID == player.getUniqueId()) - { - claimBlocksRemaining = playerData.getRemainingClaimBlocks(); - } - else - { - PlayerData ownerData = this.getPlayerData(ownerID); - claimBlocksRemaining = ownerData.getRemainingClaimBlocks(); - OfflinePlayer owner = GriefPrevention.instance.getServer().getOfflinePlayer(ownerID); - if (!owner.isOnline()) - { - this.clearCachedPlayerData(ownerID); - } - } - } - - //inform about success, visualize, communicate remaining blocks available - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(claimBlocksRemaining)); - Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - - //if resizing someone else's claim, make a log entry - if (!player.getUniqueId().equals(playerData.claimResizing.ownerID) && playerData.claimResizing.parent == null) - { - GriefPrevention.AddLogEntry(player.getName() + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); - } - - //clean up - playerData.claimResizing = null; - playerData.lastShovelLocation = null; - } - else - { - if (result.claim != null) - { - //inform player - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); - - //show the player the conflicting claim - Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapRegion); - } - } - } - - //educates a player about /adminclaims and /acb, if he can use them - void tryAdvertiseAdminAlternatives(Player player) - { - if (player.hasPermission("griefprevention.adminclaims") && player.hasPermission("griefprevention.adjustclaimblocks")) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACandACB); - } - else if (player.hasPermission("griefprevention.adminclaims")) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseAdminClaims); - } - else if (player.hasPermission("griefprevention.adjustclaimblocks")) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACB); - } - } - - 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.PlayerNotFound2, "No player by that name has logged in recently.", 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.MaxBonusReached, "Can't purchase {0} more claim blocks. The server has a limit of {1} bonus claim blocks.", "0: block count; 1: bonus claims limit"); - 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.SubdivisionVideo2, "Click for Subdivision Help: {0}", "0:video URL"); - 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.AdjustBlocksAllSuccess, "Adjusted all online players' bonus claim blocks by {0}.", "0: adjustment amount"); - this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); - 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.NoSiegeYourself, "You cannot siege yourself, don't be silly", 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.ConfirmAbandonAllClaims, "Are you sure you want to abandon ALL of your claims? Please confirm with /AbandonAllClaims confirm", null); - 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.PlayerTooCloseForFire2, "You can't start a fire this close to another player.", null); - 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.", null); - this.addDefault(defaults, Messages.AutomaticClaimOtherClaimTooClose, "Cannot create a claim for your chest, there is another claim too close!", 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.CreativeBasicsVideo2, "Click for Land Claim Help: {0}", "{0}: video URL"); - this.addDefault(defaults, Messages.SurvivalBasicsVideo2, "Click for Land Claim Help: {0}", "{0}: video URL"); - this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped;stuck", "When mentioned in chat, players get information about the /trapped command (multiple words can be separated with semi-colons)"); - 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.ResizeClaimTooNarrow, "This new size would be too small. Claims must be at least {0} blocks wide.", "0: minimum claim width"); - 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. {0} available claim blocks remaining.", "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.NewClaimTooNarrow, "This claim would be too small. Any claim must be at least {0} blocks wide.", "0: minimum claim width"); - this.addDefault(defaults, Messages.ResizeClaimInsufficientArea, "This claim would be too small. Any claim must use at least {0} total claim blocks.", "0: minimum claim area"); - 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.", 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 lava inside the claim. 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.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 build here, too. Consider creating a land claim to protect your work!", null); - this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin.", 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 and 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); - this.addDefault(defaults, Messages.ExplosivesDisabled, "This claim is now protected from explosions. Use /ClaimExplosions again to disable.", null); - this.addDefault(defaults, Messages.ExplosivesEnabled, "This claim is now vulnerable to explosions. Use /ClaimExplosions again to re-enable protections.", null); - this.addDefault(defaults, Messages.ClaimExplosivesAdvertisement, "To allow explosives to destroy blocks in this land claim, use /ClaimExplosions.", null); - this.addDefault(defaults, Messages.PlayerInPvPSafeZone, "That player is in a PvP safe zone.", null); - this.addDefault(defaults, Messages.NoPistonsOutsideClaims, "Warning: Pistons won't move blocks outside land claims.", null); - this.addDefault(defaults, Messages.SoftMuted, "Soft-muted {0}.", "0: The changed player's name."); - this.addDefault(defaults, Messages.UnSoftMuted, "Un-soft-muted {0}.", "0: The changed player's name."); - this.addDefault(defaults, Messages.DropUnlockAdvertisement, "Other players can't pick up your dropped items unless you /UnlockDrops first.", null); - this.addDefault(defaults, Messages.PickupBlockedExplanation, "You can't pick this up unless {0} uses /UnlockDrops.", "0: The item stack's owner."); - this.addDefault(defaults, Messages.DropUnlockConfirmation, "Unlocked your drops. Other players may now pick them up (until you die again).", null); - this.addDefault(defaults, Messages.DropUnlockOthersConfirmation, "Unlocked {0}'s drops.", "0: The owner of the unlocked drops."); - this.addDefault(defaults, Messages.AdvertiseACandACB, "You may use /ACB to give yourself more claim blocks, or /AdminClaims to create a free administrative claim.", null); - this.addDefault(defaults, Messages.AdvertiseAdminClaims, "You could create an administrative land claim instead using /AdminClaims, which you'd share with other administrators.", null); - this.addDefault(defaults, Messages.AdvertiseACB, "You may use /ACB to give yourself more claim blocks.", null); - this.addDefault(defaults, Messages.NotYourPet, "That belongs to {0} until it's given to you with /GivePet.", "0: owner name"); - this.addDefault(defaults, Messages.PetGiveawayConfirmation, "Pet transferred.", null); - this.addDefault(defaults, Messages.PetTransferCancellation, "Pet giveaway cancelled.", null); - this.addDefault(defaults, Messages.ReadyToTransferPet, "Ready to transfer! Right-click the pet you'd like to give away, or cancel with /GivePet cancel.", null); - this.addDefault(defaults, Messages.AvoidGriefClaimLand, "Prevent grief! If you claim your land, you will be grief-proof.", null); - this.addDefault(defaults, Messages.BecomeMayor, "Subdivide your land claim and become a mayor!", null); - this.addDefault(defaults, Messages.ClaimCreationFailedOverClaimCountLimit, "You've reached your limit on land claims. Use /AbandonClaim to remove one before creating another.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlapRegion, "You can't claim all of this because you're not allowed to build here.", null); - this.addDefault(defaults, Messages.ResizeFailOverlapRegion, "You don't have permission to build there, so you can't claim that area.", null); - this.addDefault(defaults, Messages.ShowNearbyClaims, "Found {0} land claims.", "0: Number of claims found."); - this.addDefault(defaults, Messages.NoChatUntilMove, "Sorry, but you have to move a little more before you can chat. We get lots of spam bots here. :)", null); - this.addDefault(defaults, Messages.SiegeImmune, "That player is immune to /siege.", null); - this.addDefault(defaults, Messages.SetClaimBlocksSuccess, "Updated accrued claim blocks.", null); - this.addDefault(defaults, Messages.IgnoreConfirmation, "You're now ignoring chat messages from that player.", null); - this.addDefault(defaults, Messages.UnIgnoreConfirmation, "You're no longer ignoring chat messages from that player.", null); - this.addDefault(defaults, Messages.NotIgnoringPlayer, "You're not ignoring that player.", null); - this.addDefault(defaults, Messages.SeparateConfirmation, "Those players will now ignore each other in chat.", null); - this.addDefault(defaults, Messages.UnSeparateConfirmation, "Those players will no longer ignore each other in chat.", null); - this.addDefault(defaults, Messages.NotIgnoringAnyone, "You're not ignoring anyone.", null); - this.addDefault(defaults, Messages.TrustListHeader, "Explicit permissions here:", null); - this.addDefault(defaults, Messages.Manage, "Manage", null); - this.addDefault(defaults, Messages.Build, "Build", null); - this.addDefault(defaults, Messages.Containers, "Containers", null); - this.addDefault(defaults, Messages.Access, "Access", null); - this.addDefault(defaults, Messages.HasSubclaimRestriction, "This subclaim does not inherit permissions from the parent", null); - this.addDefault(defaults, Messages.StartBlockMath, "{0} blocks from play + {1} bonus = {2} total.", null); - this.addDefault(defaults, Messages.ClaimsListHeader, "Claims:", null); - this.addDefault(defaults, Messages.ContinueBlockMath, " (-{0} blocks)", null); - this.addDefault(defaults, Messages.EndBlockMath, " = {0} blocks left to spend", null); - this.addDefault(defaults, Messages.NoClaimDuringPvP, "You can't claim lands during PvP combat.", null); - this.addDefault(defaults, Messages.UntrustAllOwnerOnly, "Only the claim owner can clear all its permissions.", null); - this.addDefault(defaults, Messages.ManagersDontUntrustManagers, "Only the claim owner can demote a manager.", null); - this.addDefault(defaults, Messages.PlayerNotIgnorable, "You can't ignore that player.", null); - this.addDefault(defaults, Messages.NoEnoughBlocksForChestClaim, "Because you don't have any claim blocks available, no automatic land claim was created for you. You can use /ClaimsList to monitor your available claim block total.", null); - this.addDefault(defaults, Messages.MustHoldModificationToolForThat, "You must be holding a golden shovel to do that.", null); - this.addDefault(defaults, Messages.StandInClaimToResize, "Stand inside the land claim you want to resize.", null); - this.addDefault(defaults, Messages.ClaimsExtendToSky, "Land claims always extend to max build height.", null); - this.addDefault(defaults, Messages.ClaimsAutoExtendDownward, "Land claims auto-extend deeper into the ground when you place blocks under them.", null); - this.addDefault(defaults, Messages.MinimumRadius, "Minimum radius is {0}.", "0: minimum radius"); - this.addDefault(defaults, Messages.RadiusRequiresGoldenShovel, "You must be holding a golden shovel when specifying a radius.", null); - this.addDefault(defaults, Messages.ClaimTooSmallForActiveBlocks, "This claim isn't big enough to support any active block types (hoppers, spawners, beacons...). Make the claim bigger first.", null); - this.addDefault(defaults, Messages.TooManyActiveBlocksInClaim, "This claim is at its limit for active block types (hoppers, spawners, beacons...). Either make it bigger, or remove other active blocks first.", null); - - this.addDefault(defaults, Messages.BookAuthor, "BigScary", null); - this.addDefault(defaults, Messages.BookTitle, "How to Claim Land", null); - this.addDefault(defaults, Messages.BookLink, "Click: {0}", "{0}: video URL"); - this.addDefault(defaults, Messages.BookIntro, "Claim land to protect your stuff! Click the link above to learn land claims in 3 minutes or less. :)", null); - this.addDefault(defaults, Messages.BookTools, "Our claim tools are {0} and {1}.", "0: claim modification tool name; 1:claim information tool name"); - this.addDefault(defaults, Messages.BookDisabledChestClaims, " On this server, placing a chest will NOT claim land for you.", null); - this.addDefault(defaults, Messages.BookUsefulCommands, "Useful Commands:", null); - this.addDefault(defaults, Messages.NoProfanity, "Please moderate your language.", null); - this.addDefault(defaults, Messages.IsIgnoringYou, "That player is ignoring you.", null); - this.addDefault(defaults, Messages.ConsoleOnlyCommand, "That command may only be executed from the server console.", null); - this.addDefault(defaults, Messages.WorldNotFound, "World not found.", null); - this.addDefault(defaults, Messages.TooMuchIpOverlap, "Sorry, there are too many players logged in with your IP address.", null); - - this.addDefault(defaults, Messages.StandInSubclaim, "You need to be standing in a subclaim to restrict it", null); - this.addDefault(defaults, Messages.SubclaimRestricted, "This subclaim's permissions will no longer inherit from the parent claim", null); - this.addDefault(defaults, Messages.SubclaimUnrestricted, "This subclaim's permissions will now inherit from the parent claim", null); - - this.addDefault(defaults, Messages.NetherPortalTrapDetectionMessage, "It seems you might be stuck inside a nether portal. We will rescue you in a few seconds if that is the case!", "Sent to player on join, if they left while inside a nether portal."); - - //load the config file - FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); - - //for each message ID - for (Messages messageID : messageIDs) - { - //get default for this message - 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()]); - - //support color codes - if (messageID != Messages.HowToClaimRegex) - { - this.messages[messageID.ordinal()] = this.messages[messageID.ordinal()].replace('$', (char) 0x00A7); - } - - 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.options().header("Use a YAML editor like NotepadPlusPlus to edit this file. \nAfter editing, back up your changes before reloading the server in case you made a syntax error. \nUse dollar signs ($) for formatting codes, which are documented here: http://minecraft.gamepedia.com/Formatting_codes"); - 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; - } - - //used in updating the data schema from 0 to 1. - //converts player names in a list to uuids - protected List convertNameListToUUIDList(List names) - { - //doesn't apply after schema has been updated to version 1 - if (this.getSchemaVersion() >= 1) return names; - - //list to build results - List resultNames = new ArrayList<>(); - - for (String name : names) - { - //skip non-player-names (groups and "public"), leave them as-is - if (name.startsWith("[") || name.equals("public")) - { - resultNames.add(name); - continue; - } - - //otherwise try to convert to a UUID - UUID playerID = null; - try - { - playerID = UUIDFetcher.getUUIDOf(name); - } - catch (Exception ex) { } - - //if successful, replace player name with corresponding UUID - if (playerID != null) - { - resultNames.add(playerID.toString()); - } - } - - return resultNames; - } - - abstract void close(); - - private class SavePlayerDataThread extends Thread - { - private final UUID playerID; - private final PlayerData playerData; - - SavePlayerDataThread(UUID playerID, PlayerData playerData) - { - this.playerID = playerID; - this.playerData = playerData; - } - - public void run() - { - //ensure player data is already read from file before trying to save - playerData.getAccruedClaimBlocks(); - playerData.getClaims(); - asyncSavePlayerData(this.playerID, this.playerData); - } - } - - //gets all the claims "near" a location - Set getNearbyClaims(Location location) - { - Set claims = new HashSet<>(); - - Chunk lesserChunk = location.getWorld().getChunkAt(location.subtract(150, 0, 150)); - Chunk greaterChunk = location.getWorld().getChunkAt(location.add(300, 0, 300)); - - for (int chunk_x = lesserChunk.getX(); chunk_x <= greaterChunk.getX(); chunk_x++) - { - for (int chunk_z = lesserChunk.getZ(); chunk_z <= greaterChunk.getZ(); chunk_z++) - { - Chunk chunk = location.getWorld().getChunkAt(chunk_x, chunk_z); - Long chunkID = getChunkHash(chunk.getBlock(0, 0, 0).getLocation()); - ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkID); - if (claimsInChunk != null) - { - for (Claim claim : claimsInChunk) - { - if (claim.inDataStore && claim.getLesserBoundaryCorner().getWorld().equals(location.getWorld())) - { - claims.add(claim); - } - } - } - } - } - - return claims; - } - - //deletes all the land claims in a specified world - void deleteClaimsInWorld(World world, boolean deleteAdminClaims) - { - for (int i = 0; i < claims.size(); i++) - { - Claim claim = claims.get(i); - if (claim.getLesserBoundaryCorner().getWorld().equals(world)) - { - if (!deleteAdminClaims && claim.isAdminClaim()) continue; - this.deleteClaim(claim, false, false); - i--; - } - } - } -} +/* + 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 com.google.common.io.Files; +import me.ryanhamshire.GriefPrevention.alttd.config.Config; +import me.ryanhamshire.GriefPrevention.events.ClaimResizeEvent; +import me.ryanhamshire.GriefPrevention.events.ClaimCreatedEvent; +import me.ryanhamshire.GriefPrevention.events.ClaimDeletedEvent; +import me.ryanhamshire.GriefPrevention.events.ClaimExtendEvent; +import me.ryanhamshire.GriefPrevention.events.ClaimTransferEvent; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.AnimalTamer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Tameable; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +//singleton class which manages all GriefPrevention data (except for config options) +public abstract class DataStore +{ + + //in-memory cache for player data + protected ConcurrentHashMap playerNameToPlayerDataMap = new ConcurrentHashMap<>(); + + //in-memory cache for group (permission-based) data + protected ConcurrentHashMap permissionToBonusBlocksMap = new ConcurrentHashMap<>(); + + //in-memory cache for claim data + ArrayList claims = new ArrayList<>(); + ConcurrentHashMap> chunksToClaimsMap = new ConcurrentHashMap<>(); + + //in-memory cache for messages + private String[] messages; + + //pattern for unique user identifiers (UUIDs) + protected final static Pattern uuidpattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + + //next claim ID + Long nextClaimID = (long) 0; + + //path information, for where stuff stored on disk is well... stored + public final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; + final static String playerDataFolderPath = dataLayerFolderPath + File.separator + "PlayerData"; + final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; + final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; + final static String softMuteFilePath = dataLayerFolderPath + File.separator + "softMute.txt"; + final static String bannedWordsFilePath = dataLayerFolderPath + File.separator + "bannedWords.txt"; + + //the latest version of the data schema implemented here + protected static final int latestSchemaVersion = 3; + + //reading and writing the schema version to the data store + abstract int getSchemaVersionFromStorage(); + + abstract void updateSchemaVersionInStorage(int versionToSet); + + //current version of the schema of data in secondary storage + private int currentSchemaVersion = -1; //-1 means not determined yet + + //list of UUIDs which are soft-muted + ConcurrentHashMap softMuteMap = new ConcurrentHashMap<>(); + + //world guard reference, if available + private WorldGuardWrapper worldGuard = null; + + protected int getSchemaVersion() + { + if (this.currentSchemaVersion >= 0) + { + return this.currentSchemaVersion; + } + else + { + this.currentSchemaVersion = this.getSchemaVersionFromStorage(); + return this.currentSchemaVersion; + } + } + + protected void setSchemaVersion(int versionToSet) + { + this.currentSchemaVersion = versionToSet; + this.updateSchemaVersionInStorage(versionToSet); + } + + //initialization! + void initialize() throws Exception + { + GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); + + //RoboMWM: ensure the nextClaimID is greater than any other claim ID. If not, data corruption occurred (out of storage space, usually). + for (Claim claim : this.claims) + { + if (claim.id >= nextClaimID) + { + GriefPrevention.instance.getLogger().severe("nextClaimID was lesser or equal to an already-existing claim ID!\n" + + "This usually happens if you ran out of storage space."); + GriefPrevention.AddLogEntry("Changing nextClaimID from " + nextClaimID + " to " + claim.id, CustomLogEntryTypes.Debug, false); + nextClaimID = claim.id + 1; + } + } + + //ensure data folders exist + File playerDataFolder = new File(playerDataFolderPath); + if (!playerDataFolder.exists()) + { + playerDataFolder.mkdirs(); + } + + //load up all the messages from messages.yml + this.loadMessages(); + GriefPrevention.AddLogEntry("Customizable messages loaded."); + + //if converting up from an earlier schema version, write all claims back to storage using the latest format + if (this.getSchemaVersion() < latestSchemaVersion) + { + GriefPrevention.AddLogEntry("Please wait. Updating data format."); + + for (Claim claim : this.claims) + { + this.saveClaim(claim); + + for (Claim subClaim : claim.children) + { + this.saveClaim(subClaim); + } + } + + //clean up any UUID conversion work + if (UUIDFetcher.lookupCache != null) + { + UUIDFetcher.lookupCache.clear(); + UUIDFetcher.correctedNames.clear(); + } + + GriefPrevention.AddLogEntry("Update finished."); + } + + //load list of soft mutes + this.loadSoftMutes(); + + //make a note of the data store schema version + this.setSchemaVersion(latestSchemaVersion); + + //try to hook into world guard + try + { + this.worldGuard = new WorldGuardWrapper(); + GriefPrevention.AddLogEntry("Successfully hooked into WorldGuard."); + } + //if failed, world guard compat features will just be disabled. + catch (IllegalStateException | IllegalArgumentException | ClassCastException | NoClassDefFoundError ignored) { } + } + + private void loadSoftMutes() + { + File softMuteFile = new File(softMuteFilePath); + if (softMuteFile.exists()) + { + BufferedReader inStream = null; + try + { + //open the file + inStream = new BufferedReader(new FileReader(softMuteFile.getAbsolutePath())); + + //while there are lines left + String nextID = inStream.readLine(); + while (nextID != null) + { + //parse line into a UUID + UUID playerID; + try + { + playerID = UUID.fromString(nextID); + } + catch (Exception e) + { + playerID = null; + GriefPrevention.AddLogEntry("Failed to parse soft mute entry as a UUID: " + nextID); + } + + //push it into the map + if (playerID != null) + { + this.softMuteMap.put(playerID, true); + } + + //move to the next + nextID = inStream.readLine(); + } + } + catch (Exception e) + { + GriefPrevention.AddLogEntry("Failed to read from the soft mute data file: " + e.toString()); + e.printStackTrace(); + } + + try + { + if (inStream != null) inStream.close(); + } + catch (IOException exception) {} + } + } + + public List loadBannedWords() + { + try + { + File bannedWordsFile = new File(bannedWordsFilePath); + if (!bannedWordsFile.exists()) + { + Files.touch(bannedWordsFile); + String defaultWords = + "nigger\nniggers\nniger\nnigga\nnigers\nniggas\n" + + "fag\nfags\nfaggot\nfaggots\nfeggit\nfeggits\nfaggit\nfaggits\n" + + "cunt\ncunts\nwhore\nwhores\nslut\nsluts\n"; + Files.append(defaultWords, bannedWordsFile, Charset.forName("UTF-8")); + } + + return Files.readLines(bannedWordsFile, Charset.forName("UTF-8")); + } + catch (Exception e) + { + GriefPrevention.AddLogEntry("Failed to read from the banned words data file: " + e.toString()); + e.printStackTrace(); + return new ArrayList<>(); + } + } + + //updates soft mute map and data file + boolean toggleSoftMute(UUID playerID) + { + boolean newValue = !this.isSoftMuted(playerID); + + this.softMuteMap.put(playerID, newValue); + this.saveSoftMutes(); + + return newValue; + } + + public boolean isSoftMuted(UUID playerID) + { + Boolean mapEntry = this.softMuteMap.get(playerID); + if (mapEntry == null || mapEntry == Boolean.FALSE) + { + return false; + } + + return true; + } + + private void saveSoftMutes() + { + BufferedWriter outStream = null; + + try + { + //open the file and write the new value + File softMuteFile = new File(softMuteFilePath); + softMuteFile.createNewFile(); + outStream = new BufferedWriter(new FileWriter(softMuteFile)); + + for (Map.Entry entry : softMuteMap.entrySet()) + { + if (entry.getValue().booleanValue()) + { + outStream.write(entry.getKey().toString()); + outStream.newLine(); + } + } + + } + + //if any problem, log it + catch (Exception e) + { + GriefPrevention.AddLogEntry("Unexpected exception saving soft mute data: " + e.getMessage()); + e.printStackTrace(); + } + + //close the file + try + { + if (outStream != null) outStream.close(); + } + catch (IOException exception) {} + } + + //removes cached player data from memory + synchronized void clearCachedPlayerData(UUID playerID) + { + this.playerNameToPlayerDataMap.remove(playerID); + } + + //gets the number of bonus blocks a player has from his permissions + //Bukkit doesn't allow for checking permissions of an offline player. + //this will return 0 when he's offline, and the correct number when online. + synchronized public int getGroupBonusBlocks(UUID playerID) + { + Player player = GriefPrevention.instance.getServer().getPlayer(playerID); + + if (player == null) return 0; + + int bonusBlocks = 0; + + for (Map.Entry groupEntry : this.permissionToBonusBlocksMap.entrySet()) + { + if (player.hasPermission(groupEntry.getKey())) + { + bonusBlocks += groupEntry.getValue(); + } + } + + 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); + + public class NoTransferException extends RuntimeException + { + private static final long serialVersionUID = 1L; + + NoTransferException(String message) + { + super(message); + } + } + + public class NoClaimblocksTransferException extends RuntimeException + { + private static final long serialVersionUID = 1L; + + NoClaimblocksTransferException(String message) + { + super(message); + } + } + + synchronized public void changeClaimOwner(Claim claim, UUID newOwnerID) + { + //if it's a subdivision, throw an exception + if (claim.parent != null) + { + throw new NoTransferException("Subdivisions can't be transferred. Only top-level claims may change owners."); + } + + //otherwise update information + + //determine current claim owner + PlayerData ownerData = null; + + //determine new owner + PlayerData newOwnerData = null; + if (!claim.isAdminClaim()) + { + ownerData = this.getPlayerData(claim.ownerID); + } + + if (newOwnerID != null) { + newOwnerData = this.getPlayerData(newOwnerID); + if (!(newOwnerData.getRemainingClaimBlocks() >= claim.getArea())) + throw new NoClaimblocksTransferException("Target does not have the claimblocks to claim this area."); + } + //call event + ClaimTransferEvent event = new ClaimTransferEvent(claim, newOwnerID); + Bukkit.getPluginManager().callEvent(event); + + //return if event is cancelled + if (event.isCancelled()) return; + + if (event.getNewOwner() != null) + { + newOwnerData = this.getPlayerData(event.getNewOwner()); + } + + //transfer + claim.ownerID = event.getNewOwner(); + this.saveClaim(claim); + + //adjust blocks and other records + if (ownerData != null) + { + ownerData.getClaims().remove(claim); + } + + if (newOwnerData != null) + { + newOwnerData.getClaims().add(claim); + } + } + + //adds a claim to the datastore, making it an effective claim + synchronized void addClaim(Claim newClaim, boolean writeToStorage) + { + //subdivisions are added under their parent, not directly to the hash map for direct search + if (newClaim.parent != null) + { + if (!newClaim.parent.children.contains(newClaim)) + { + newClaim.parent.children.add(newClaim); + } + newClaim.inDataStore = true; + if (writeToStorage) + { + this.saveClaim(newClaim); + } + return; + } + + //add it and mark it as added + this.claims.add(newClaim); + addToChunkClaimMap(newClaim); + + newClaim.inDataStore = true; + + //except for administrative claims (which have no owner), update the owner's playerData with the new claim + if (!newClaim.isAdminClaim() && writeToStorage) + { + PlayerData ownerData = this.getPlayerData(newClaim.ownerID); + ownerData.getClaims().add(newClaim); + } + + //make sure the claim is saved to disk + if (writeToStorage) + { + this.saveClaim(newClaim); + } + } + + private void addToChunkClaimMap(Claim claim) + { + // Subclaims should not be added to chunk claim map. + if (claim.parent != null) return; + + ArrayList chunkHashes = claim.getChunkHashes(); + for (Long chunkHash : chunkHashes) + { + ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkHash); + if (claimsInChunk == null) + { + this.chunksToClaimsMap.put(chunkHash, claimsInChunk = new ArrayList<>()); + } + + claimsInChunk.add(claim); + } + } + + private void removeFromChunkClaimMap(Claim claim) + { + ArrayList chunkHashes = claim.getChunkHashes(); + for (Long chunkHash : chunkHashes) + { + ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkHash); + if (claimsInChunk != null) + { + for (Iterator it = claimsInChunk.iterator(); it.hasNext(); ) + { + Claim c = it.next(); + if (c.id.equals(claim.id)) + { + it.remove(); + break; + } + } + if (claimsInChunk.isEmpty()) + { // if nothing's left, remove this chunk's cache + this.chunksToClaimsMap.remove(chunkHash); + } + } + } + } + + //turns a location into a string, useful in data storage + private final 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, List validWorlds) 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 + "\""); + } + + String worldName = elements[0]; + String xString = elements[1]; + String yString = elements[2]; + String zString = elements[3]; + + //identify world the claim is in + World world = null; + for (World w : validWorlds) + { + if (w.getName().equalsIgnoreCase(worldName)) + { + world = w; + break; + } + } + + 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) + { + assignClaimID(claim); + + this.writeClaimToStorage(claim); + } + + private void assignClaimID(Claim claim) + { + //ensure a unique identifier for the claim which will be used to name the file on disk + if (claim.id == null || claim.id == -1) + { + claim.id = this.nextClaimID; + this.incrementNextClaimID(); + } + } + + 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(UUID playerID) + { + //first, look in memory + PlayerData playerData = this.playerNameToPlayerDataMap.get(playerID); + + //if not there, build a fresh instance with some blanks for what may be in secondary storage + if (playerData == null) + { + playerData = new PlayerData(); + playerData.playerID = playerID; + + //shove that new player data into the hash map cache + this.playerNameToPlayerDataMap.put(playerID, playerData); + } + + return playerData; + } + + abstract PlayerData getPlayerDataFromStorage(UUID playerID); + + //deletes a claim or subdivision + synchronized public void deleteClaim(Claim claim) + { + this.deleteClaim(claim, true, false); + } + + //deletes a claim or subdivision + synchronized public void deleteClaim(Claim claim, boolean releasePets) + { + this.deleteClaim(claim, true, releasePets); + } + + synchronized void deleteClaim(Claim claim, boolean fireEvent, boolean releasePets) + { + //delete any children + for (int j = 1; (j - 1) < claim.children.size(); j++) + { + this.deleteClaim(claim.children.get(j - 1), true); + } + + //subdivisions must also be removed from the parent claim child list + if (claim.parent != null) + { + Claim parentClaim = claim.parent; + parentClaim.children.remove(claim); + } + + //mark as deleted so any references elsewhere can be ignored + claim.inDataStore = false; + + //remove from memory + for (int i = 0; i < this.claims.size(); i++) + { + if (claims.get(i).id.equals(claim.id)) + { + this.claims.remove(i); + break; + } + } + + removeFromChunkClaimMap(claim); + + //remove from secondary storage + this.deleteClaimFromSecondaryStorage(claim); + + //update player data + if (claim.ownerID != null) + { + PlayerData ownerData = this.getPlayerData(claim.ownerID); + for (int i = 0; i < ownerData.getClaims().size(); i++) + { + if (ownerData.getClaims().get(i).id.equals(claim.id)) + { + ownerData.getClaims().remove(i); + break; + } + } + this.savePlayerData(claim.ownerID, ownerData); + } + + if (fireEvent) + { + ClaimDeletedEvent ev = new ClaimDeletedEvent(claim); + Bukkit.getPluginManager().callEvent(ev); + } + + //optionally set any pets free which belong to the claim owner + if (releasePets && claim.ownerID != null && claim.parent == null) + { + for (Chunk chunk : claim.getChunks()) + { + Entity[] entities = chunk.getEntities(); + for (Entity entity : entities) + { + if (entity instanceof Tameable) + { + Tameable pet = (Tameable) entity; + if (pet.isTamed()) + { + AnimalTamer owner = pet.getOwner(); + if (owner != null) + { + UUID ownerID = owner.getUniqueId(); + if (ownerID != null) + { + if (ownerID.equals(claim.ownerID)) + { + pet.setTamed(false); + pet.setOwner(null); + if (pet instanceof InventoryHolder) + { + InventoryHolder holder = (InventoryHolder) pet; + holder.getInventory().clear(); + } + } + } + } + } + } + } + } + } + } + + 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) + { + return getClaimAt(location, ignoreHeight, false, cachedClaim); + } + + /** + * Get the claim at a specific location. + * + *

The cached claim may be null, but will increase performance if you have a reasonable idea + * of which claim is correct. + * + * @param location the location + * @param ignoreHeight whether or not to check containment vertically + * @param ignoreSubclaims whether or not subclaims should be returned over claims + * @param cachedClaim the cached claim, if any + * @return the claim containing the location or null if no claim exists there + */ + synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, boolean ignoreSubclaims, 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, !ignoreSubclaims)) + return cachedClaim; + + //find a top level claim + Long chunkID = getChunkHash(location); + ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkID); + if (claimsInChunk == null) return null; + + for (Claim claim : claimsInChunk) + { + if (claim.inDataStore && claim.contains(location, ignoreHeight, false)) + { + // If ignoring subclaims, claim is a match. + if (ignoreSubclaims) return claim; + + //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.inDataStore && subdivision.contains(location, ignoreHeight, false)) + return subdivision; + } + + return claim; + } + } + + //if no claim found, return null + return null; + } + + //finds a claim by ID + public synchronized Claim getClaim(long id) + { + for (Claim claim : this.claims) + { + if (claim.inDataStore && claim.getID() == id) return claim; + } + + return null; + } + + //returns a read-only access point for the list of all land claims + //if you need to make changes, use provided methods like .deleteClaim() and .createClaim(). + //this will ensure primary memory (RAM) and secondary memory (disk, database) stay in sync + public Collection getClaims() + { + return Collections.unmodifiableCollection(this.claims); + } + + public Collection getClaims(int chunkx, int chunkz) + { + ArrayList chunkClaims = this.chunksToClaimsMap.get(getChunkHash(chunkx, chunkz)); + if (chunkClaims != null) + { + return Collections.unmodifiableCollection(chunkClaims); + } + else + { + return Collections.unmodifiableCollection(new ArrayList<>()); + } + } + + //gets an almost-unique, persistent identifier for a chunk + public static Long getChunkHash(long chunkx, long chunkz) + { + return (chunkz ^ (chunkx << 32)); + } + + //gets an almost-unique, persistent identifier for a chunk + public static Long getChunkHash(Location location) + { + return getChunkHash(location.getBlockX() >> 4, location.getBlockZ() >> 4); + } + + public static ArrayList getChunkHashes(Claim claim) { + return getChunkHashes(claim.getLesserBoundaryCorner(), claim.getGreaterBoundaryCorner()); + } + + public static ArrayList getChunkHashes(Location min, Location max) { + ArrayList hashes = new ArrayList<>(); + int smallX = min.getBlockX() >> 4; + int smallZ = min.getBlockZ() >> 4; + int largeX = max.getBlockX() >> 4; + int largeZ = max.getBlockZ() >> 4; + + for (int x = smallX; x <= largeX; x++) + { + for (int z = smallZ; z <= largeZ; z++) + { + hashes.add(getChunkHash(x, z)); + } + } + + return hashes; + } + + /* + * Creates a claim and flags it as being new....throwing a create claim event; + */ + synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, UUID ownerID, Claim parent, Long id, Player creatingPlayer) + { + return createClaim(world, x1, x2, y1, y2, z1, z2, ownerID, parent, id, creatingPlayer, false); + } + + //creates a claim. + //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim + //if the new claim would overlap a WorldGuard region where the player doesn't have permission to build, returns a failure with NULL for 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 check for world guard regions where the player doesn't have permission + //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, UUID ownerID, Claim parent, Long id, Player creatingPlayer, boolean dryRun) + { + CreateClaimResult result = new CreateClaimResult(); + + int smallx, bigx, smally, bigy, smallz, bigz; + + int worldMinY = world.getMinHeight(); + y1 = Math.max(worldMinY, Math.max(GriefPrevention.instance.config_claims_maxDepth, y1)); + y2 = Math.max(worldMinY, Math.max(GriefPrevention.instance.config_claims_maxDepth, y2)); + + //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; + } + + if (parent != null) + { + Location lesser = parent.getLesserBoundaryCorner(); + Location greater = parent.getGreaterBoundaryCorner(); + if (smallx < lesser.getX() || smallz < lesser.getZ() || bigx > greater.getX() || bigz > greater.getZ()) + { + result.succeeded = false; + result.claim = parent; + return result; + } + smally = sanitizeClaimDepth(parent, smally); + } + + //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), + ownerID, + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>(), + 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 (Claim otherClaim : claimsToCheck) + { + //if we find an existing claim which will be overlapped + if (otherClaim.id != newClaim.id && otherClaim.inDataStore && otherClaim.overlaps(newClaim)) + { + //result = fail, return conflicting claim + result.succeeded = false; + result.claim = otherClaim; + return result; + } + } + + if (creatingPlayer != null && !newClaim.canCleaimNear(creatingPlayer, 100) && newClaim.parent == null) { + result.succeeded = false; + result.claim = null; + return result; + } + + if (creatingPlayer != null && !newClaim.isInsideBorder()) { + result.succeeded = false; + result.claim = null; + creatingPlayer.sendMiniMessage(Config.claimCreatedOutsideBorder, null); + return result; + } + + //if worldguard is installed, also prevent claims from overlapping any worldguard regions + if (GriefPrevention.instance.config_claims_respectWorldGuard && this.worldGuard != null && creatingPlayer != null) + { + if (!this.worldGuard.canBuild(newClaim.lesserBoundaryCorner, newClaim.greaterBoundaryCorner, creatingPlayer)) + { + result.succeeded = false; + result.claim = null; + return result; + } + } + + if (dryRun) + { + // since this is a dry run, just return the unsaved claim as is. + result.succeeded = true; + result.claim = newClaim; + return result; + } + assignClaimID(newClaim); // assign a claim ID before calling event, in case a plugin wants to know the ID. + ClaimCreatedEvent event = new ClaimCreatedEvent(newClaim, creatingPlayer); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled() && creatingPlayer != null) + { + result.succeeded = false; + result.claim = null; + return result; + + } + //otherwise add this new claim to the data store to make it effective + this.addClaim(newClaim, true); + + //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 void savePlayerDataSync(UUID playerID, PlayerData playerData) + { + //ensure player data is already read from file before trying to save + playerData.getAccruedClaimBlocks(); + playerData.getClaims(); + + this.asyncSavePlayerData(playerID, playerData); + } + + //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them + public void savePlayerData(UUID playerID, PlayerData playerData) + { + new SavePlayerDataThread(playerID, playerData).start(); + } + + public void asyncSavePlayerData(UUID playerID, PlayerData playerData) + { + //save everything except the ignore list + this.overrideSavePlayerData(playerID, playerData); + + //save the ignore list + if (playerData.ignoreListChanged) + { + StringBuilder fileContent = new StringBuilder(); + try + { + for (UUID uuidKey : playerData.ignoredPlayers.keySet()) + { + Boolean value = playerData.ignoredPlayers.get(uuidKey); + if (value == null) continue; + + //admin-enforced ignores begin with an asterisk + if (value) + { + fileContent.append("*"); + } + + fileContent.append(uuidKey); + fileContent.append("\n"); + } + + //write data to file + File playerDataFile = new File(playerDataFolderPath + File.separator + playerID + ".ignore"); + Files.write(fileContent.toString().trim().getBytes("UTF-8"), playerDataFile); + } + + //if any problem, log it + catch (Exception e) + { + GriefPrevention.AddLogEntry("GriefPrevention: Unexpected exception saving data for player \"" + playerID.toString() + "\": " + e.getMessage()); + e.printStackTrace(); + } + } + } + + abstract void overrideSavePlayerData(UUID playerID, PlayerData playerData); + + //extends a claim to a new depth + //respects the max depth config variable + synchronized public void extendClaim(Claim claim, int newDepth) + { + if (claim.parent != null) claim = claim.parent; + + newDepth = sanitizeClaimDepth(claim, newDepth); + + //call event and return if event got cancelled + ClaimExtendEvent event = new ClaimExtendEvent(claim, newDepth); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) return; + + //adjust to new depth + setNewDepth(claim, event.getNewDepth()); + } + + /** + * Helper method for sanitizing claim depth to find the minimum expected value. + * + * @param claim the claim + * @param newDepth the new depth + * @return the sanitized new depth + */ + private int sanitizeClaimDepth(Claim claim, int newDepth) { + if (claim.parent != null) claim = claim.parent; + + // Get the old depth including the depth of the lowest subdivision. + int oldDepth = Math.min( + claim.getLesserBoundaryCorner().getBlockY(), + claim.children.stream().mapToInt(child -> child.getLesserBoundaryCorner().getBlockY()) + .min().orElse(Integer.MAX_VALUE)); + + // Use the lowest of the old and new depths. + newDepth = Math.min(newDepth, oldDepth); + // Cap depth to maximum depth allowed by the configuration. + newDepth = Math.max(newDepth, GriefPrevention.instance.config_claims_maxDepth); + // Cap the depth to the world's minimum height. + World world = Objects.requireNonNull(claim.getLesserBoundaryCorner().getWorld()); + newDepth = Math.max(newDepth, world.getMinHeight()); + + return newDepth; + } + + /** + * Helper method for sanitizing and setting claim depth. Saves affected claims. + * + * @param claim the claim + * @param newDepth the new depth + */ + private void setNewDepth(Claim claim, int newDepth) { + if (claim.parent != null) claim = claim.parent; + + final int depth = sanitizeClaimDepth(claim, newDepth); + + Stream.concat(Stream.of(claim), claim.children.stream()).forEach(localClaim -> { + localClaim.lesserBoundaryCorner.setY(depth); + localClaim.greaterBoundaryCorner.setY(Math.max(localClaim.greaterBoundaryCorner.getBlockY(), depth)); + this.saveClaim(localClaim); + }); + } + + //deletes all claims owned by a player + synchronized public void deleteClaimsForPlayer(UUID playerID, boolean releasePets) + { + //make a list of the player's claims + ArrayList claimsToDelete = new ArrayList<>(); + for (Claim claim : this.claims) + { + if ((playerID == claim.ownerID || (playerID != null && playerID.equals(claim.ownerID)))) + claimsToDelete.add(claim); + } + + //delete them one by one + for (Claim claim : claimsToDelete) + { + this.deleteClaim(claim, releasePets); + } + } + + //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, Player resizingPlayer) + { + //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.ownerID, claim.parent, claim.id, resizingPlayer, true); + + //if succeeded + if (result.succeeded) + { + removeFromChunkClaimMap(claim); // remove the old boundary from the chunk cache + // copy the boundary from the claim created in the dry run of createClaim() to our existing claim + claim.lesserBoundaryCorner = result.claim.lesserBoundaryCorner; + claim.greaterBoundaryCorner = result.claim.greaterBoundaryCorner; + // Sanitize claim depth, expanding parent down to the lowest subdivision and subdivisions down to parent. + // Also saves affected claims. + setNewDepth(claim, claim.getLesserBoundaryCorner().getBlockY()); + result.claim = claim; + addToChunkClaimMap(claim); // add the new boundary to the chunk cache + } + + return result; + } + + void resizeClaimWithChecks(Player player, PlayerData playerData, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) + { + //for top level claims, apply size rules and claim blocks requirement + if (playerData.claimResizing.parent == null) + { + //measure new claim, apply size rules + int newWidth = (Math.abs(newx1 - newx2) + 1); + int newHeight = (Math.abs(newz1 - newz2) + 1); + boolean smaller = newWidth < playerData.claimResizing.getWidth() || newHeight < playerData.claimResizing.getHeight(); + + if (!player.hasPermission("griefprevention.adminclaims") && !playerData.claimResizing.isAdminClaim() && smaller) + { + if (newWidth < GriefPrevention.instance.config_claims_minWidth || newHeight < GriefPrevention.instance.config_claims_minWidth) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooNarrow, String.valueOf(GriefPrevention.instance.config_claims_minWidth)); + return; + } + + int newArea = newWidth * newHeight; + if (newArea < GriefPrevention.instance.config_claims_minArea) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimInsufficientArea, String.valueOf(GriefPrevention.instance.config_claims_minArea)); + return; + } + } + + //make sure player has enough blocks to make up the difference + if (!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) + { + int newArea = newWidth * newHeight; + int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; + + if (blocksRemainingAfter < 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); + this.tryAdvertiseAdminAlternatives(player); + return; + } + } + } + + Claim oldClaim = playerData.claimResizing; + Claim newClaim = new Claim(oldClaim); + World world = newClaim.getLesserBoundaryCorner().getWorld(); + newClaim.lesserBoundaryCorner = new Location(world, newx1, newy1, newz1); + newClaim.greaterBoundaryCorner = new Location(world, newx2, newy2, newz2); + + //call event here to check if it has been cancelled + ClaimResizeEvent event = new ClaimResizeEvent(oldClaim, newClaim, player); + Bukkit.getPluginManager().callEvent(event); + + //return here if event is cancelled + if (event.isCancelled()) return; + + //special rule for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. + //rule: in any mode, shrinking a claim removes any surface fluids + boolean smaller = false; + if (oldClaim.parent == null) + { + //if the new claim is smaller + if (!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) + { + smaller = true; + + } + } + + //ask the datastore to try and resize the claim, this checks for conflicts with other claims + CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim( + playerData.claimResizing, + newClaim.getLesserBoundaryCorner().getBlockX(), + newClaim.getGreaterBoundaryCorner().getBlockX(), + newClaim.getLesserBoundaryCorner().getBlockY(), + newClaim.getGreaterBoundaryCorner().getBlockY(), + newClaim.getLesserBoundaryCorner().getBlockZ(), + newClaim.getGreaterBoundaryCorner().getBlockZ(), + player); + + if (result.succeeded) + { + //decide how many claim blocks are available for more resizing + int claimBlocksRemaining = 0; + if (!playerData.claimResizing.isAdminClaim()) + { + UUID ownerID = playerData.claimResizing.ownerID; + if (playerData.claimResizing.parent != null) + { + ownerID = playerData.claimResizing.parent.ownerID; + } + if (ownerID == player.getUniqueId()) + { + claimBlocksRemaining = playerData.getRemainingClaimBlocks(); + } + else + { + PlayerData ownerData = this.getPlayerData(ownerID); + claimBlocksRemaining = ownerData.getRemainingClaimBlocks(); + OfflinePlayer owner = GriefPrevention.instance.getServer().getOfflinePlayer(ownerID); + if (!owner.isOnline()) + { + this.clearCachedPlayerData(ownerID); + } + } + } + + //inform about success, visualize, communicate remaining blocks available + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(claimBlocksRemaining)); + Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + + //if resizing someone else's claim, make a log entry + if (!player.getUniqueId().equals(playerData.claimResizing.ownerID) && playerData.claimResizing.parent == null) + { + GriefPrevention.AddLogEntry(player.getName() + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); + } + + //clean up + playerData.claimResizing = null; + playerData.lastShovelLocation = null; + } + else + { + if (result.claim != null) + { + //inform player + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); + + //show the player the conflicting claim + Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapRegion); + } + } + } + + //educates a player about /adminclaims and /acb, if he can use them + void tryAdvertiseAdminAlternatives(Player player) + { + if (player.hasPermission("griefprevention.adminclaims") && player.hasPermission("griefprevention.adjustclaimblocks")) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACandACB); + } + else if (player.hasPermission("griefprevention.adminclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseAdminClaims); + } + else if (player.hasPermission("griefprevention.adjustclaimblocks")) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.AdvertiseACB); + } + } + + 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.PlayerNotFound2, "No player by that name has logged in recently.", 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.MaxBonusReached, "Can't purchase {0} more claim blocks. The server has a limit of {1} bonus claim blocks.", "0: block count; 1: bonus claims limit"); + 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.SubdivisionVideo2, "Click for Subdivision Help: {0}", "0:video URL"); + 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.AdjustBlocksAllSuccess, "Adjusted all online players' bonus claim blocks by {0}.", "0: adjustment amount"); + this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); + 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.NoSiegeYourself, "You cannot siege yourself, don't be silly", 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.ConfirmAbandonAllClaims, "Are you sure you want to abandon ALL of your claims? Please confirm with /AbandonAllClaims confirm", null); + 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.PlayerTooCloseForFire2, "You can't start a fire this close to another player.", null); + 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.", null); + this.addDefault(defaults, Messages.AutomaticClaimOtherClaimTooClose, "Cannot create a claim for your chest, there is another claim too close!", 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.CreativeBasicsVideo2, "Click for Land Claim Help: {0}", "{0}: video URL"); + this.addDefault(defaults, Messages.SurvivalBasicsVideo2, "Click for Land Claim Help: {0}", "{0}: video URL"); + this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped;stuck", "When mentioned in chat, players get information about the /trapped command (multiple words can be separated with semi-colons)"); + 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.ResizeClaimTooNarrow, "This new size would be too small. Claims must be at least {0} blocks wide.", "0: minimum claim width"); + 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. {0} available claim blocks remaining.", "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.NewClaimTooNarrow, "This claim would be too small. Any claim must be at least {0} blocks wide.", "0: minimum claim width"); + this.addDefault(defaults, Messages.ResizeClaimInsufficientArea, "This claim would be too small. Any claim must use at least {0} total claim blocks.", "0: minimum claim area"); + 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.", 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 lava inside the claim. 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.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 build here, too. Consider creating a land claim to protect your work!", null); + this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin.", 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 and 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); + this.addDefault(defaults, Messages.ExplosivesDisabled, "This claim is now protected from explosions. Use /ClaimExplosions again to disable.", null); + this.addDefault(defaults, Messages.ExplosivesEnabled, "This claim is now vulnerable to explosions. Use /ClaimExplosions again to re-enable protections.", null); + this.addDefault(defaults, Messages.ClaimExplosivesAdvertisement, "To allow explosives to destroy blocks in this land claim, use /ClaimExplosions.", null); + this.addDefault(defaults, Messages.PlayerInPvPSafeZone, "That player is in a PvP safe zone.", null); + this.addDefault(defaults, Messages.NoPistonsOutsideClaims, "Warning: Pistons won't move blocks outside land claims.", null); + this.addDefault(defaults, Messages.SoftMuted, "Soft-muted {0}.", "0: The changed player's name."); + this.addDefault(defaults, Messages.UnSoftMuted, "Un-soft-muted {0}.", "0: The changed player's name."); + this.addDefault(defaults, Messages.DropUnlockAdvertisement, "Other players can't pick up your dropped items unless you /UnlockDrops first.", null); + this.addDefault(defaults, Messages.PickupBlockedExplanation, "You can't pick this up unless {0} uses /UnlockDrops.", "0: The item stack's owner."); + this.addDefault(defaults, Messages.DropUnlockConfirmation, "Unlocked your drops. Other players may now pick them up (until you die again).", null); + this.addDefault(defaults, Messages.DropUnlockOthersConfirmation, "Unlocked {0}'s drops.", "0: The owner of the unlocked drops."); + this.addDefault(defaults, Messages.AdvertiseACandACB, "You may use /ACB to give yourself more claim blocks, or /AdminClaims to create a free administrative claim.", null); + this.addDefault(defaults, Messages.AdvertiseAdminClaims, "You could create an administrative land claim instead using /AdminClaims, which you'd share with other administrators.", null); + this.addDefault(defaults, Messages.AdvertiseACB, "You may use /ACB to give yourself more claim blocks.", null); + this.addDefault(defaults, Messages.NotYourPet, "That belongs to {0} until it's given to you with /GivePet.", "0: owner name"); + this.addDefault(defaults, Messages.PetGiveawayConfirmation, "Pet transferred.", null); + this.addDefault(defaults, Messages.PetTransferCancellation, "Pet giveaway cancelled.", null); + this.addDefault(defaults, Messages.ReadyToTransferPet, "Ready to transfer! Right-click the pet you'd like to give away, or cancel with /GivePet cancel.", null); + this.addDefault(defaults, Messages.AvoidGriefClaimLand, "Prevent grief! If you claim your land, you will be grief-proof.", null); + this.addDefault(defaults, Messages.BecomeMayor, "Subdivide your land claim and become a mayor!", null); + this.addDefault(defaults, Messages.ClaimCreationFailedOverClaimCountLimit, "You've reached your limit on land claims. Use /AbandonClaim to remove one before creating another.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlapRegion, "You can't claim all of this because you're not allowed to build here.", null); + this.addDefault(defaults, Messages.ResizeFailOverlapRegion, "You don't have permission to build there, so you can't claim that area.", null); + this.addDefault(defaults, Messages.ShowNearbyClaims, "Found {0} land claims.", "0: Number of claims found."); + this.addDefault(defaults, Messages.NoChatUntilMove, "Sorry, but you have to move a little more before you can chat. We get lots of spam bots here. :)", null); + this.addDefault(defaults, Messages.SiegeImmune, "That player is immune to /siege.", null); + this.addDefault(defaults, Messages.SetClaimBlocksSuccess, "Updated accrued claim blocks.", null); + this.addDefault(defaults, Messages.IgnoreConfirmation, "You're now ignoring chat messages from that player.", null); + this.addDefault(defaults, Messages.UnIgnoreConfirmation, "You're no longer ignoring chat messages from that player.", null); + this.addDefault(defaults, Messages.NotIgnoringPlayer, "You're not ignoring that player.", null); + this.addDefault(defaults, Messages.SeparateConfirmation, "Those players will now ignore each other in chat.", null); + this.addDefault(defaults, Messages.UnSeparateConfirmation, "Those players will no longer ignore each other in chat.", null); + this.addDefault(defaults, Messages.NotIgnoringAnyone, "You're not ignoring anyone.", null); + this.addDefault(defaults, Messages.TrustListHeader, "Explicit permissions here:", null); + this.addDefault(defaults, Messages.Manage, "Manage", null); + this.addDefault(defaults, Messages.Build, "Build", null); + this.addDefault(defaults, Messages.Containers, "Containers", null); + this.addDefault(defaults, Messages.Access, "Access", null); + this.addDefault(defaults, Messages.HasSubclaimRestriction, "This subclaim does not inherit permissions from the parent", null); + this.addDefault(defaults, Messages.StartBlockMath, "{0} blocks from play + {1} bonus = {2} total.", null); + this.addDefault(defaults, Messages.ClaimsListHeader, "Claims:", null); + this.addDefault(defaults, Messages.ContinueBlockMath, " (-{0} blocks)", null); + this.addDefault(defaults, Messages.EndBlockMath, " = {0} blocks left to spend", null); + this.addDefault(defaults, Messages.NoClaimDuringPvP, "You can't claim lands during PvP combat.", null); + this.addDefault(defaults, Messages.UntrustAllOwnerOnly, "Only the claim owner can clear all its permissions.", null); + this.addDefault(defaults, Messages.ManagersDontUntrustManagers, "Only the claim owner can demote a manager.", null); + this.addDefault(defaults, Messages.PlayerNotIgnorable, "You can't ignore that player.", null); + this.addDefault(defaults, Messages.NoEnoughBlocksForChestClaim, "Because you don't have any claim blocks available, no automatic land claim was created for you. You can use /ClaimsList to monitor your available claim block total.", null); + this.addDefault(defaults, Messages.MustHoldModificationToolForThat, "You must be holding a golden shovel to do that.", null); + this.addDefault(defaults, Messages.StandInClaimToResize, "Stand inside the land claim you want to resize.", null); + this.addDefault(defaults, Messages.ClaimsExtendToSky, "Land claims always extend to max build height.", null); + this.addDefault(defaults, Messages.ClaimsAutoExtendDownward, "Land claims auto-extend deeper into the ground when you place blocks under them.", null); + this.addDefault(defaults, Messages.MinimumRadius, "Minimum radius is {0}.", "0: minimum radius"); + this.addDefault(defaults, Messages.RadiusRequiresGoldenShovel, "You must be holding a golden shovel when specifying a radius.", null); + this.addDefault(defaults, Messages.ClaimTooSmallForActiveBlocks, "This claim isn't big enough to support any active block types (hoppers, spawners, beacons...). Make the claim bigger first.", null); + this.addDefault(defaults, Messages.TooManyActiveBlocksInClaim, "This claim is at its limit for active block types (hoppers, spawners, beacons...). Either make it bigger, or remove other active blocks first.", null); + + this.addDefault(defaults, Messages.BookAuthor, "BigScary", null); + this.addDefault(defaults, Messages.BookTitle, "How to Claim Land", null); + this.addDefault(defaults, Messages.BookLink, "Click: {0}", "{0}: video URL"); + this.addDefault(defaults, Messages.BookIntro, "Claim land to protect your stuff! Click the link above to learn land claims in 3 minutes or less. :)", null); + this.addDefault(defaults, Messages.BookTools, "Our claim tools are {0} and {1}.", "0: claim modification tool name; 1:claim information tool name"); + this.addDefault(defaults, Messages.BookDisabledChestClaims, " On this server, placing a chest will NOT claim land for you.", null); + this.addDefault(defaults, Messages.BookUsefulCommands, "Useful Commands:", null); + this.addDefault(defaults, Messages.NoProfanity, "Please moderate your language.", null); + this.addDefault(defaults, Messages.IsIgnoringYou, "That player is ignoring you.", null); + this.addDefault(defaults, Messages.ConsoleOnlyCommand, "That command may only be executed from the server console.", null); + this.addDefault(defaults, Messages.WorldNotFound, "World not found.", null); + this.addDefault(defaults, Messages.TooMuchIpOverlap, "Sorry, there are too many players logged in with your IP address.", null); + + this.addDefault(defaults, Messages.StandInSubclaim, "You need to be standing in a subclaim to restrict it", null); + this.addDefault(defaults, Messages.SubclaimRestricted, "This subclaim's permissions will no longer inherit from the parent claim", null); + this.addDefault(defaults, Messages.SubclaimUnrestricted, "This subclaim's permissions will now inherit from the parent claim", null); + + this.addDefault(defaults, Messages.NetherPortalTrapDetectionMessage, "It seems you might be stuck inside a nether portal. We will rescue you in a few seconds if that is the case!", "Sent to player on join, if they left while inside a nether portal."); + + //load the config file + FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); + + //for each message ID + for (Messages messageID : messageIDs) + { + //get default for this message + 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()]); + + //support color codes + if (messageID != Messages.HowToClaimRegex) + { + this.messages[messageID.ordinal()] = this.messages[messageID.ordinal()].replace('$', (char) 0x00A7); + } + + 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.options().header("Use a YAML editor like NotepadPlusPlus to edit this file. \nAfter editing, back up your changes before reloading the server in case you made a syntax error. \nUse dollar signs ($) for formatting codes, which are documented here: http://minecraft.gamepedia.com/Formatting_codes"); + 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; + } + + //used in updating the data schema from 0 to 1. + //converts player names in a list to uuids + protected List convertNameListToUUIDList(List names) + { + //doesn't apply after schema has been updated to version 1 + if (this.getSchemaVersion() >= 1) return names; + + //list to build results + List resultNames = new ArrayList<>(); + + for (String name : names) + { + //skip non-player-names (groups and "public"), leave them as-is + if (name.startsWith("[") || name.equals("public")) + { + resultNames.add(name); + continue; + } + + //otherwise try to convert to a UUID + UUID playerID = null; + try + { + playerID = UUIDFetcher.getUUIDOf(name); + } + catch (Exception ex) { } + + //if successful, replace player name with corresponding UUID + if (playerID != null) + { + resultNames.add(playerID.toString()); + } + } + + return resultNames; + } + + abstract void close(); + + private class SavePlayerDataThread extends Thread + { + private final UUID playerID; + private final PlayerData playerData; + + SavePlayerDataThread(UUID playerID, PlayerData playerData) + { + this.playerID = playerID; + this.playerData = playerData; + } + + public void run() + { + //ensure player data is already read from file before trying to save + playerData.getAccruedClaimBlocks(); + playerData.getClaims(); + asyncSavePlayerData(this.playerID, this.playerData); + } + } + + //gets all the claims "near" a location + Set getNearbyClaims(Location location) + { + Set claims = new HashSet<>(); + + Chunk lesserChunk = location.getWorld().getChunkAt(location.subtract(150, 0, 150)); + Chunk greaterChunk = location.getWorld().getChunkAt(location.add(300, 0, 300)); + + for (int chunk_x = lesserChunk.getX(); chunk_x <= greaterChunk.getX(); chunk_x++) + { + for (int chunk_z = lesserChunk.getZ(); chunk_z <= greaterChunk.getZ(); chunk_z++) + { + Chunk chunk = location.getWorld().getChunkAt(chunk_x, chunk_z); + Long chunkID = getChunkHash(chunk.getBlock(0, 0, 0).getLocation()); + ArrayList claimsInChunk = this.chunksToClaimsMap.get(chunkID); + if (claimsInChunk != null) + { + for (Claim claim : claimsInChunk) + { + if (claim.inDataStore && claim.getLesserBoundaryCorner().getWorld().equals(location.getWorld())) + { + claims.add(claim); + } + } + } + } + } + + return claims; + } + + //deletes all the land claims in a specified world + void deleteClaimsInWorld(World world, boolean deleteAdminClaims) + { + for (int i = 0; i < claims.size(); i++) + { + Claim claim = claims.get(i); + if (claim.getLesserBoundaryCorner().getWorld().equals(world)) + { + if (!deleteAdminClaims && claim.isAdminClaim()) continue; + this.deleteClaim(claim, false, false); + i--; + } + } + } +} diff --git a/src/main/java/me/ryanhamshire/GriefPrevention/GriefPrevention.java b/src/main/java/me/ryanhamshire/GriefPrevention/GriefPrevention.java index 4925a1a..6e44c03 100644 --- a/src/main/java/me/ryanhamshire/GriefPrevention/GriefPrevention.java +++ b/src/main/java/me/ryanhamshire/GriefPrevention/GriefPrevention.java @@ -1,3261 +1,3266 @@ -/* - 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 me.ryanhamshire.GriefPrevention.DataStore.NoTransferException; -import me.ryanhamshire.GriefPrevention.alttd.config.Config; -import me.ryanhamshire.GriefPrevention.alttd.database.DatabaseConnection; -import me.ryanhamshire.GriefPrevention.alttd.hook.Pl3xMapHook; -import me.ryanhamshire.GriefPrevention.alttd.listeners.AltitudeListener; -import me.ryanhamshire.GriefPrevention.alttd.tasks.AdminClaimExpireTask; -import me.ryanhamshire.GriefPrevention.alttd.tasks.IgnoreClaimWarningTask; -import me.ryanhamshire.GriefPrevention.alttd.util.SafeZone; -import me.ryanhamshire.GriefPrevention.alttd.util.Utils; -import me.ryanhamshire.GriefPrevention.events.PreventBlockBreakEvent; -import me.ryanhamshire.GriefPrevention.events.SaveTrappedPlayerEvent; -import me.ryanhamshire.GriefPrevention.events.TrustChangedEvent; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.TextComponent; -import net.milkbowl.vault.economy.Economy; -import org.bukkit.BanList; -import org.bukkit.BanList.Type; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Chunk; -import org.bukkit.ChunkSnapshot; -import org.bukkit.FluidCollisionMode; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.Statistic; -import org.bukkit.World; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; -import java.util.Vector; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class GriefPrevention extends JavaPlugin -{ - //for convenience, a reference to the instance of this plugin - public static GriefPrevention instance; - - //for logging to the console and log file - private static Logger log; - - //this handles data storage, like player and region data - public DataStore dataStore; - - // Event handlers with common functionality - EntityEventHandler entityEventHandler; - - //this tracks item stacks expected to drop which will need protection - ArrayList pendingItemWatchList = new ArrayList<>(); - - //log entry manager for GP's custom log files - CustomLogger customLogger; - - // hashmap to cache offline playernames - public static HashMap playerNameCache = new HashMap<>(); - //configuration variables, loaded/saved from a config.yml - - //claim mode for each world - public ConcurrentHashMap config_claims_worldModes; - - public boolean config_claims_preventGlobalMonsterEggs; //whether monster eggs can be placed regardless of trust. - public boolean config_claims_preventTheft; //whether containers and crafting blocks are protectable - public boolean config_claims_protectCreatures; //whether claimed animals may be injured by players without permission - public boolean config_claims_protectHorses; //whether horses on a claim should be protected by that claim's rules - public boolean config_claims_protectDonkeys; //whether donkeys on a claim should be protected by that claim's rules - public boolean config_claims_protectLlamas; //whether llamas on a claim should be protected by that claim's rules - public boolean config_claims_preventButtonsSwitches; //whether buttons and switches are protectable - public boolean config_claims_lockWoodenDoors; //whether wooden doors should be locked by default (require /accesstrust) - public boolean config_claims_lockTrapDoors; //whether trap doors should be locked by default (require /accesstrust) - public boolean config_claims_lockFenceGates; //whether fence gates should be locked by default (require /accesstrust) - public boolean config_claims_enderPearlsRequireAccessTrust; //whether teleporting into a claim with a pearl requires access trust - public boolean config_claims_raidTriggersRequireBuildTrust; //whether raids are triggered by a player that doesn't have build permission in that claim - public int config_claims_maxClaimsPerPlayer; //maximum number of claims per player - public boolean config_claims_respectWorldGuard; //whether claim creations requires WG build permission in creation area - public boolean config_claims_villagerTradingRequiresTrust; //whether trading with a claimed villager requires permission - - public int config_claims_initialBlocks; //the number of claim blocks a new player starts with - public double config_claims_abandonReturnRatio; //the portion of claim blocks returned to a player when a claim is abandoned - public int config_claims_blocksAccruedPerHour_default; //how many additional blocks players get each hour of play (can be zero) without any special permissions - public int config_claims_maxAccruedBlocks_default; //the limit on accrued blocks (over time) for players without any special permissions. doesn't limit purchased or admin-gifted blocks - public int config_claims_accruedIdleThreshold; //how far (in blocks) a player must move in order to not be considered afk/idle when determining accrued claim blocks - public int config_claims_accruedIdlePercent; //how much percentage of claim block accruals should idle players get - public int config_claims_maxDepth; //limit on how deep claims can go - public int config_claims_expirationDays; //how many days of inactivity before a player loses his claims - public int config_claims_expirationExemptionTotalBlocks; //total claim blocks amount which will exempt a player from claim expiration - public int config_claims_expirationExemptionBonusBlocks; //bonus claim blocks amount which will exempt a player from claim expiration - - public int config_claims_automaticClaimsForNewPlayersRadius; //how big automatic new player claims (when they place a chest) should be. -1 to disable - public int config_claims_automaticClaimsForNewPlayersRadiusMin; //how big automatic new player claims must be. 0 to disable - public int config_claims_claimsExtendIntoGroundDistance; //how far below the shoveled block a new claim will reach - public int config_claims_minWidth; //minimum width for non-admin claims - public int config_claims_minArea; //minimum area for non-admin claims - - public int config_claims_chestClaimExpirationDays; //number of days of inactivity before an automatic chest claim will be deleted - public int config_claims_unusedClaimExpirationDays; //number of days of inactivity before an unused (nothing build) claim will be deleted - public boolean config_claims_survivalAutoNatureRestoration; //whether survival claims will be automatically restored to nature when auto-deleted - public boolean config_claims_allowTrappedInAdminClaims; //whether it should be allowed to use /trapped in adminclaims. - - public Material config_claims_investigationTool; //which material will be used to investigate claims with a right click - public Material config_claims_modificationTool; //which material will be used to create/resize claims with a right click - - public ArrayList config_claims_commandsRequiringAccessTrust; //the list of slash commands requiring access trust when in a claim - public boolean config_claims_supplyPlayerManual; //whether to give new players a book with land claim help in it - public int config_claims_manualDeliveryDelaySeconds; //how long to wait before giving a book to a new player - - public boolean config_claims_firespreads; //whether fire will spread in claims - public boolean config_claims_firedamages; //whether fire will damage in claims - - public boolean config_claims_lecternReadingRequiresAccessTrust; //reading lecterns requires access trust - - public int config_siege_doorsOpenSeconds; // how before claim is re-secured after siege win - public int config_siege_cooldownEndInMinutes; - public boolean config_spam_enabled; //whether or not to monitor for spam - public int config_spam_loginCooldownSeconds; //how long players must wait between logins. combats login spam. - public int config_spam_loginLogoutNotificationsPerMinute; //how many login/logout notifications to show per minute (global, not per player) - public ArrayList config_spam_monitorSlashCommands; //the list of slash commands monitored for spam - public boolean config_spam_banOffenders; //whether or not to ban spammers automatically - 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 int config_spam_logoutMessageDelaySeconds; //delay before a logout message will be shown (only if the player stays offline that long) - - HashMap config_pvp_specifiedWorlds; //list of worlds where pvp anti-grief rules apply, according to the config file - public boolean config_pvp_protectFreshSpawns; //whether to make newly spawned players immune until they pick up an item - public boolean config_pvp_punishLogout; //whether to kill players who log out during PvP combat - public int config_pvp_combatTimeoutSeconds; //how long combat is considered to continue after the most recent damage - public boolean config_pvp_allowCombatItemDrop; //whether a player can drop items during combat to hide them - public ArrayList config_pvp_blockedCommands; //list of commands which may not be used during pvp combat - public boolean config_pvp_noCombatInPlayerLandClaims; //whether players may fight in player-owned land claims - public boolean config_pvp_noCombatInAdminLandClaims; //whether players may fight in admin-owned land claims - public boolean config_pvp_noCombatInAdminSubdivisions; //whether players may fight in subdivisions of admin-owned land claims - public boolean config_pvp_allowLavaNearPlayers; //whether players may dump lava near other players in pvp worlds - public boolean config_pvp_allowLavaNearPlayers_NonPvp; //whather this applies in non-PVP rules worlds - public boolean config_pvp_allowFireNearPlayers; //whether players may start flint/steel fires near other players in pvp worlds - public boolean config_pvp_allowFireNearPlayers_NonPvp; //whether this applies in non-PVP rules worlds - public boolean config_pvp_protectPets; //whether players may damage pets outside of land claims in pvp worlds - - public boolean config_lockDeathDropsInPvpWorlds; //whether players' dropped on death items are protected in pvp worlds - public boolean config_lockDeathDropsInNonPvpWorlds; //whether players' dropped on death items are protected in non-pvp worlds - - private EconomyHandler economyHandler; - public int config_economy_claimBlocksMaxBonus; //max "bonus" blocks a player can buy. set to zero for no limit. - public double config_economy_claimBlocksPurchaseCost; //cost to purchase a claim block. set to zero to disable purchase. - public double config_economy_claimBlocksSellValue; //return on a sold claim block. set to zero to disable sale. - - public boolean config_blockClaimExplosions; //whether explosions may destroy claimed blocks - public boolean config_blockSurfaceCreeperExplosions; //whether creeper explosions near or above the surface destroy blocks - public boolean config_blockSurfaceOtherExplosions; //whether non-creeper explosions near or above the surface destroy blocks - public boolean config_blockSkyTrees; //whether players can build trees on platforms in the sky - - public boolean config_fireSpreads; //whether fire spreads outside of claims - public boolean config_fireDestroys; //whether fire destroys blocks outside of claims - - public boolean config_whisperNotifications; //whether whispered messages will broadcast to administrators in game - public boolean config_signNotifications; //whether sign content will broadcast to administrators in game - public ArrayList config_eavesdrop_whisperCommands; //list of whisper commands to eavesdrop on - - public boolean config_endermenMoveBlocks; //whether or not endermen may move blocks around - public boolean config_claims_ravagersBreakBlocks; //whether or not ravagers may break blocks in claims - public boolean config_silverfishBreakBlocks; //whether silverfish may break blocks - public boolean config_creaturesTrampleCrops; //whether or not non-player entities may trample crops - public boolean config_rabbitsEatCrops; //whether or not rabbits may eat crops - public boolean config_zombiesBreakDoors; //whether or not hard-mode zombies may break down wooden doors - - public int config_ipLimit; //how many players can share an IP address - - public HashMap config_seaLevelOverride; //override for sea level, because bukkit doesn't report the right value for all situations - - public boolean config_limitTreeGrowth; //whether trees should be prevented from growing into a claim from outside - public PistonMode config_pistonMovement; //Setting for piston check options - public boolean config_pistonExplosionSound; //whether pistons make an explosion sound when they get removed - - public boolean config_advanced_fixNegativeClaimblockAmounts; //whether to attempt to fix negative claim block amounts (some addons cause/assume players can go into negative amounts) - public int config_advanced_claim_expiration_check_rate; //How often GP should check for expired claims, amount in seconds - - //custom log settings - public int config_logs_daysToKeep; - public boolean config_logs_socialEnabled; - public boolean config_logs_suspiciousEnabled; - public boolean config_logs_adminEnabled; - public boolean config_logs_debugEnabled; - public boolean config_logs_mutedChatEnabled; - - //ban management plugin interop settings - public boolean config_ban_useCommand; - public String config_ban_commandFormat; - - private String databaseUrl; - private String databaseUserName; - private String databasePassword; - - private Pl3xMapHook pl3xmapHook; - private DatabaseConnection databaseConnection; - - public HashMap ignoreClaimWarningTasks; - - //adds a server log entry - public static synchronized void AddLogEntry(String entry, CustomLogEntryTypes customLogType, boolean excludeFromServerLogs) - { - if (customLogType != null && GriefPrevention.instance.customLogger != null) - { - GriefPrevention.instance.customLogger.AddEntry(entry, customLogType); - } - if (!excludeFromServerLogs) log.info(entry); - } - - public static synchronized void AddLogEntry(String entry, CustomLogEntryTypes customLogType) - { - AddLogEntry(entry, customLogType, false); - } - - public static synchronized void AddLogEntry(String entry) - { - AddLogEntry(entry, CustomLogEntryTypes.Debug); - } - - //initializes well... everything - public void onEnable() - { - instance = this; - log = instance.getLogger(); - - this.loadConfig(); - - this.customLogger = new CustomLogger(); - - AddLogEntry("Finished loading configuration."); - - //when datastore initializes, it loads player and claim data, and posts some stats to the log - if (this.databaseUrl.length() > 0) - { - try - { - DatabaseDataStore databaseStore = new DatabaseDataStore(this.databaseUrl, this.databaseUserName, this.databasePassword); - - if (FlatFileDataStore.hasData()) - { - GriefPrevention.AddLogEntry("There appears to be some data on the hard drive. Migrating those data to the database..."); - FlatFileDataStore flatFileStore = new FlatFileDataStore(); - this.dataStore = flatFileStore; - flatFileStore.migrateData(databaseStore); - GriefPrevention.AddLogEntry("Data migration process complete."); - } - - this.dataStore = databaseStore; - } - catch (Exception e) - { - GriefPrevention.AddLogEntry("Because there was a problem with the database, GriefPrevention will not function properly. Either update the database config settings resolve the issue, or delete those lines from your config.yml so that GriefPrevention can use the file system to store data."); - e.printStackTrace(); - this.getServer().getPluginManager().disablePlugin(this); - return; - } - } - - //if not using the database because it's not configured or because there was a problem, use the file system to store data - //this is the preferred method, as it's simpler than the database scenario - if (this.dataStore == null) - { - File oldclaimdata = new File(getDataFolder(), "ClaimData"); - if (oldclaimdata.exists()) - { - if (!FlatFileDataStore.hasData()) - { - File claimdata = new File("plugins" + File.separator + "GriefPreventionData" + File.separator + "ClaimData"); - oldclaimdata.renameTo(claimdata); - File oldplayerdata = new File(getDataFolder(), "PlayerData"); - File playerdata = new File("plugins" + File.separator + "GriefPreventionData" + File.separator + "PlayerData"); - oldplayerdata.renameTo(playerdata); - } - } - try - { - this.dataStore = new FlatFileDataStore(); - } - catch (Exception e) - { - GriefPrevention.AddLogEntry("Unable to initialize the file system data store. Details:"); - GriefPrevention.AddLogEntry(e.getMessage()); - e.printStackTrace(); - } - } - - String dataMode = (this.dataStore instanceof FlatFileDataStore) ? "(File Mode)" : "(Database Mode)"; - AddLogEntry("Finished loading data " + dataMode + "."); - - //unless claim block accrual is disabled, start the recurring per 10 minute event to give claim blocks to online players - //20L ~ 1 second - if (this.config_claims_blocksAccruedPerHour_default > 0) - { - DeliverClaimBlocksTask task = new DeliverClaimBlocksTask(null, this); - this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task, 20L * 60 * 10, 20L * 60 * 10); - } - - //start the recurring cleanup event for entities in creative worlds - EntityCleanupTask task = new EntityCleanupTask(0); - this.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 2); - - //start recurring cleanup scan for unused claims belonging to inactive players - FindUnusedClaimsTask task2 = new FindUnusedClaimsTask(); - this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task2, 20L * 60, 20L * config_advanced_claim_expiration_check_rate); - - // start task to clean up temporary admin claims - AdminClaimExpireTask claimExpireTask = new AdminClaimExpireTask(this); - claimExpireTask.init(); - - //register for events - PluginManager pluginManager = this.getServer().getPluginManager(); - - //player events - PlayerEventHandler playerEventHandler = new PlayerEventHandler(this.dataStore, this); - pluginManager.registerEvents(playerEventHandler, this); - - //block events - BlockEventHandler blockEventHandler = new BlockEventHandler(this.dataStore); - pluginManager.registerEvents(blockEventHandler, this); - - //entity events - entityEventHandler = new EntityEventHandler(this.dataStore, this); - pluginManager.registerEvents(entityEventHandler, this); - - //vault-based economy integration - economyHandler = new EconomyHandler(this); - pluginManager.registerEvents(economyHandler, this); - - new AltitudeListener(this.dataStore, this); - if (getServer().getPluginManager().isPluginEnabled("Pl3xMap")) { - pl3xmapHook = new Pl3xMapHook(this); - } -// databaseConnection = new DatabaseConnection(); // TODO Set up database to pull data from proxyplaytime - ignoreClaimWarningTasks = new HashMap<>(); - AddLogEntry("Boot finished."); - - } - - private void loadConfig() - { - Config.reload(); - //load the config if it exists - FileConfiguration config = YamlConfiguration.loadConfiguration(new File(DataStore.configFilePath)); - FileConfiguration outConfig = new YamlConfiguration(); - outConfig.options().header("Default values are perfect for most servers. If you want to customize and have a question, look for the answer here first: http://dev.bukkit.org/bukkit-plugins/grief-prevention/pages/setup-and-configuration/"); - - //read configuration settings (note defaults) - - //get (deprecated node) claims world names from the config file - List worlds = this.getServer().getWorlds(); - List deprecated_claimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.Worlds"); - - //validate that list - for (int i = 0; i < deprecated_claimsEnabledWorldNames.size(); i++) - { - String worldName = deprecated_claimsEnabledWorldNames.get(i); - World world = this.getServer().getWorld(worldName); - if (world == null) - { - deprecated_claimsEnabledWorldNames.remove(i--); - } - } - - //get (deprecated node) creative world names from the config file - List deprecated_creativeClaimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.CreativeRulesWorlds"); - - //validate that list - for (int i = 0; i < deprecated_creativeClaimsEnabledWorldNames.size(); i++) - { - String worldName = deprecated_creativeClaimsEnabledWorldNames.get(i); - World world = this.getServer().getWorld(worldName); - if (world == null) - { - deprecated_claimsEnabledWorldNames.remove(i--); - } - } - - //get (deprecated) pvp fire placement proximity note and use it if it exists (in the new config format it will be overwritten later). - config_pvp_allowFireNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers", false); - //get (deprecated) pvp lava dump proximity note and use it if it exists (in the new config format it will be overwritten later). - config_pvp_allowLavaNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers", false); - - //decide claim mode for each world - this.config_claims_worldModes = new ConcurrentHashMap<>(); - for (World world : worlds) - { - //is it specified in the config file? - String configSetting = config.getString("GriefPrevention.Claims.Mode." + world.getName()); - if (configSetting != null) - { - ClaimsMode claimsMode = this.configStringToClaimsMode(configSetting); - if (claimsMode != null) - { - this.config_claims_worldModes.put(world, claimsMode); - continue; - } - else - { - GriefPrevention.AddLogEntry("Error: Invalid claim mode \"" + configSetting + "\". Options are Survival and Disabled."); - this.config_claims_worldModes.put(world, ClaimsMode.Disabled); - } - } - - //was it specified in a deprecated config node? - if (deprecated_claimsEnabledWorldNames.contains(world.getName())) - { - this.config_claims_worldModes.put(world, ClaimsMode.Survival); - } - - //does the world's name indicate its purpose? - else if (world.getName().toLowerCase().contains("survival")) - { - this.config_claims_worldModes.put(world, ClaimsMode.Survival); - } - else if (world.getEnvironment() == Environment.NORMAL) - { - this.config_claims_worldModes.put(world, ClaimsMode.Survival); - } - else - { - this.config_claims_worldModes.put(world, ClaimsMode.Disabled); - } - - //if the setting WOULD be disabled but this is a server upgrading from the old config format, - //then default to survival mode for safety's sake (to protect any admin claims which may - //have been created there) - if (this.config_claims_worldModes.get(world) == ClaimsMode.Disabled && - deprecated_claimsEnabledWorldNames.size() > 0) - { - this.config_claims_worldModes.put(world, ClaimsMode.Survival); - } - } - - //pvp worlds list - this.config_pvp_specifiedWorlds = new HashMap<>(); - for (World world : worlds) - { - boolean pvpWorld = config.getBoolean("GriefPrevention.PvP.RulesEnabledInWorld." + world.getName(), world.getPVP()); - this.config_pvp_specifiedWorlds.put(world, pvpWorld); - } - - //sea level - this.config_seaLevelOverride = new HashMap<>(); - for (World world : worlds) - { - int seaLevelOverride = config.getInt("GriefPrevention.SeaLevelOverrides." + world.getName(), -1); - outConfig.set("GriefPrevention.SeaLevelOverrides." + world.getName(), seaLevelOverride); - this.config_seaLevelOverride.put(world.getName(), seaLevelOverride); - } - - this.config_claims_preventGlobalMonsterEggs = config.getBoolean("GriefPrevention.Claims.PreventGlobalMonsterEggs", true); - this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); - this.config_claims_protectCreatures = config.getBoolean("GriefPrevention.Claims.ProtectCreatures", true); - this.config_claims_protectHorses = config.getBoolean("GriefPrevention.Claims.ProtectHorses", true); - this.config_claims_protectDonkeys = config.getBoolean("GriefPrevention.Claims.ProtectDonkeys", true); - this.config_claims_protectLlamas = config.getBoolean("GriefPrevention.Claims.ProtectLlamas", true); - this.config_claims_preventButtonsSwitches = config.getBoolean("GriefPrevention.Claims.PreventButtonsSwitches", true); - this.config_claims_lockWoodenDoors = config.getBoolean("GriefPrevention.Claims.LockWoodenDoors", false); - this.config_claims_lockTrapDoors = config.getBoolean("GriefPrevention.Claims.LockTrapDoors", false); - this.config_claims_lockFenceGates = config.getBoolean("GriefPrevention.Claims.LockFenceGates", true); - this.config_claims_enderPearlsRequireAccessTrust = config.getBoolean("GriefPrevention.Claims.EnderPearlsRequireAccessTrust", true); - this.config_claims_raidTriggersRequireBuildTrust = config.getBoolean("GriefPrevention.Claims.RaidTriggersRequireBuildTrust", true); - this.config_claims_initialBlocks = config.getInt("GriefPrevention.Claims.InitialBlocks", 100); - this.config_claims_blocksAccruedPerHour_default = config.getInt("GriefPrevention.Claims.BlocksAccruedPerHour", 100); - this.config_claims_blocksAccruedPerHour_default = config.getInt("GriefPrevention.Claims.Claim Blocks Accrued Per Hour.Default", config_claims_blocksAccruedPerHour_default); - this.config_claims_maxAccruedBlocks_default = config.getInt("GriefPrevention.Claims.MaxAccruedBlocks", 80000); - this.config_claims_maxAccruedBlocks_default = config.getInt("GriefPrevention.Claims.Max Accrued Claim Blocks.Default", this.config_claims_maxAccruedBlocks_default); - this.config_claims_accruedIdleThreshold = config.getInt("GriefPrevention.Claims.AccruedIdleThreshold", 0); - this.config_claims_accruedIdleThreshold = config.getInt("GriefPrevention.Claims.Accrued Idle Threshold", this.config_claims_accruedIdleThreshold); - this.config_claims_accruedIdlePercent = config.getInt("GriefPrevention.Claims.AccruedIdlePercent", 0); - this.config_claims_abandonReturnRatio = config.getDouble("GriefPrevention.Claims.AbandonReturnRatio", 1.0D); - this.config_claims_automaticClaimsForNewPlayersRadius = config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", 4); - this.config_claims_automaticClaimsForNewPlayersRadiusMin = Math.max(0, Math.min(this.config_claims_automaticClaimsForNewPlayersRadius, - config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadiusMinimum", 0))); - this.config_claims_claimsExtendIntoGroundDistance = Math.abs(config.getInt("GriefPrevention.Claims.ExtendIntoGroundDistance", 5)); - this.config_claims_minWidth = config.getInt("GriefPrevention.Claims.MinimumWidth", 5); - this.config_claims_minArea = config.getInt("GriefPrevention.Claims.MinimumArea", 100); - this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", Integer.MIN_VALUE); - this.config_claims_chestClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.ChestClaimDays", 7); - this.config_claims_unusedClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.UnusedClaimDays", 14); - this.config_claims_expirationDays = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.DaysInactive", 60); - this.config_claims_expirationExemptionTotalBlocks = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasTotalClaimBlocks", 10000); - this.config_claims_expirationExemptionBonusBlocks = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasBonusClaimBlocks", 5000); - this.config_claims_survivalAutoNatureRestoration = config.getBoolean("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", false); - this.config_claims_allowTrappedInAdminClaims = config.getBoolean("GriefPrevention.Claims.AllowTrappedInAdminClaims", false); - - this.config_claims_maxClaimsPerPlayer = config.getInt("GriefPrevention.Claims.MaximumNumberOfClaimsPerPlayer", 0); - this.config_claims_respectWorldGuard = config.getBoolean("GriefPrevention.Claims.CreationRequiresWorldGuardBuildPermission", true); - this.config_claims_villagerTradingRequiresTrust = config.getBoolean("GriefPrevention.Claims.VillagerTradingRequiresPermission", true); - String accessTrustSlashCommands = config.getString("GriefPrevention.Claims.CommandsRequiringAccessTrust", "/sethome"); - this.config_claims_supplyPlayerManual = config.getBoolean("GriefPrevention.Claims.DeliverManuals", true); - this.config_claims_manualDeliveryDelaySeconds = config.getInt("GriefPrevention.Claims.ManualDeliveryDelaySeconds", 30); - this.config_claims_ravagersBreakBlocks = config.getBoolean("GriefPrevention.Claims.RavagersBreakBlocks", true); - - this.config_claims_firespreads = config.getBoolean("GriefPrevention.Claims.FireSpreadsInClaims", false); - this.config_claims_firedamages = config.getBoolean("GriefPrevention.Claims.FireDamagesInClaims", false); - this.config_claims_lecternReadingRequiresAccessTrust = config.getBoolean("GriefPrevention.Claims.LecternReadingRequiresAccessTrust", true); - - this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true); - this.config_spam_loginCooldownSeconds = config.getInt("GriefPrevention.Spam.LoginCooldownSeconds", 60); - this.config_spam_loginLogoutNotificationsPerMinute = config.getInt("GriefPrevention.Spam.LoginLogoutNotificationsPerMinute", 5); - this.config_spam_warningMessage = config.getString("GriefPrevention.Spam.WarningMessage", "Please reduce your noise level. Spammers will be banned."); - this.config_spam_allowedIpAddresses = config.getString("GriefPrevention.Spam.AllowedIpAddresses", "1.2.3.4; 5.6.7.8"); - 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;/global;/local"); - slashCommandsToMonitor = config.getString("GriefPrevention.Spam.ChatSlashCommands", slashCommandsToMonitor); - this.config_spam_deathMessageCooldownSeconds = config.getInt("GriefPrevention.Spam.DeathMessageCooldownSeconds", 120); - this.config_spam_logoutMessageDelaySeconds = config.getInt("GriefPrevention.Spam.Logout Message Delay In Seconds", 0); - - this.config_pvp_protectFreshSpawns = config.getBoolean("GriefPrevention.PvP.ProtectFreshSpawns", true); - this.config_pvp_punishLogout = config.getBoolean("GriefPrevention.PvP.PunishLogout", true); - this.config_pvp_combatTimeoutSeconds = config.getInt("GriefPrevention.PvP.CombatTimeoutSeconds", 15); - this.config_pvp_allowCombatItemDrop = config.getBoolean("GriefPrevention.PvP.AllowCombatItemDrop", false); - String bannedPvPCommandsList = config.getString("GriefPrevention.PvP.BlockedSlashCommands", "/home;/vanish;/spawn;/tpa"); - - this.config_economy_claimBlocksMaxBonus = config.getInt("GriefPrevention.Economy.ClaimBlocksMaxBonus", 0); - this.config_economy_claimBlocksPurchaseCost = config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0); - this.config_economy_claimBlocksSellValue = config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0); - - this.config_lockDeathDropsInPvpWorlds = config.getBoolean("GriefPrevention.ProtectItemsDroppedOnDeath.PvPWorlds", false); - this.config_lockDeathDropsInNonPvpWorlds = config.getBoolean("GriefPrevention.ProtectItemsDroppedOnDeath.NonPvPWorlds", true); - - this.config_blockClaimExplosions = config.getBoolean("GriefPrevention.BlockLandClaimExplosions", true); - this.config_blockSurfaceCreeperExplosions = config.getBoolean("GriefPrevention.BlockSurfaceCreeperExplosions", true); - this.config_blockSurfaceOtherExplosions = config.getBoolean("GriefPrevention.BlockSurfaceOtherExplosions", true); - this.config_blockSkyTrees = config.getBoolean("GriefPrevention.LimitSkyTrees", true); - this.config_limitTreeGrowth = config.getBoolean("GriefPrevention.LimitTreeGrowth", false); - this.config_pistonExplosionSound = config.getBoolean("GriefPrevention.PistonExplosionSound", true); - this.config_pistonMovement = PistonMode.of(config.getString("GriefPrevention.PistonMovement", "CLAIMS_ONLY")); - if (config.isBoolean("GriefPrevention.LimitPistonsToLandClaims") && !config.getBoolean("GriefPrevention.LimitPistonsToLandClaims")) - this.config_pistonMovement = PistonMode.EVERYWHERE_SIMPLE; - if (config.isBoolean("GriefPrevention.CheckPistonMovement") && !config.getBoolean("GriefPrevention.CheckPistonMovement")) - this.config_pistonMovement = PistonMode.IGNORED; - - this.config_fireSpreads = config.getBoolean("GriefPrevention.FireSpreads", false); - this.config_fireDestroys = config.getBoolean("GriefPrevention.FireDestroys", false); - - this.config_whisperNotifications = config.getBoolean("GriefPrevention.AdminsGetWhispers", true); - this.config_signNotifications = config.getBoolean("GriefPrevention.AdminsGetSignNotifications", true); - String whisperCommandsToMonitor = config.getString("GriefPrevention.WhisperCommands", "/tell;/pm;/r;/whisper;/msg"); - whisperCommandsToMonitor = config.getString("GriefPrevention.Spam.WhisperSlashCommands", whisperCommandsToMonitor); - - this.config_ipLimit = config.getInt("GriefPrevention.MaxPlayersPerIpAddress", 3); - - this.config_endermenMoveBlocks = config.getBoolean("GriefPrevention.EndermenMoveBlocks", false); - this.config_silverfishBreakBlocks = config.getBoolean("GriefPrevention.SilverfishBreakBlocks", false); - this.config_creaturesTrampleCrops = config.getBoolean("GriefPrevention.CreaturesTrampleCrops", false); - this.config_rabbitsEatCrops = config.getBoolean("GriefPrevention.RabbitsEatCrops", true); - this.config_zombiesBreakDoors = config.getBoolean("GriefPrevention.HardModeZombiesBreakDoors", false); - this.config_ban_useCommand = config.getBoolean("GriefPrevention.UseBanCommand", false); - this.config_ban_commandFormat = config.getString("GriefPrevention.BanCommandPattern", "ban %name% %reason%"); - - //default for claim investigation tool - String investigationToolMaterialName = Material.STICK.name(); - - //get investigation tool from config - investigationToolMaterialName = config.getString("GriefPrevention.Claims.InvestigationTool", investigationToolMaterialName); - - //validate investigation tool - this.config_claims_investigationTool = Material.getMaterial(investigationToolMaterialName); - if (this.config_claims_investigationTool == null) - { - GriefPrevention.AddLogEntry("ERROR: Material " + investigationToolMaterialName + " not found. Defaulting to the stick. Please update your config.yml."); - this.config_claims_investigationTool = Material.STICK; - } - - //default for claim creation/modification tool - String modificationToolMaterialName = Material.GOLDEN_SHOVEL.name(); - - //get modification tool from config - modificationToolMaterialName = config.getString("GriefPrevention.Claims.ModificationTool", modificationToolMaterialName); - - //validate modification tool - this.config_claims_modificationTool = Material.getMaterial(modificationToolMaterialName); - if (this.config_claims_modificationTool == null) - { - GriefPrevention.AddLogEntry("ERROR: Material " + modificationToolMaterialName + " not found. Defaulting to the golden shovel. Please update your config.yml."); - this.config_claims_modificationTool = Material.GOLDEN_SHOVEL; - } - - this.config_pvp_noCombatInPlayerLandClaims = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.PlayerOwnedClaims", false); // might break stuff - this.config_pvp_noCombatInAdminLandClaims = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeClaims", false); // might break stuff - this.config_pvp_noCombatInAdminSubdivisions = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeSubdivisions", false); // might break stuff - this.config_pvp_allowLavaNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.PvPWorlds", true); - this.config_pvp_allowLavaNearPlayers_NonPvp = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.NonPvPWorlds", false); - this.config_pvp_allowFireNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.PvPWorlds", true); - this.config_pvp_allowFireNearPlayers_NonPvp = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.NonPvPWorlds", false); - this.config_pvp_protectPets = config.getBoolean("GriefPrevention.PvP.ProtectPetsOutsideLandClaims", false); - - //optional database settings - this.databaseUrl = config.getString("GriefPrevention.Database.URL", ""); - this.databaseUserName = config.getString("GriefPrevention.Database.UserName", ""); - this.databasePassword = config.getString("GriefPrevention.Database.Password", ""); - - this.config_advanced_fixNegativeClaimblockAmounts = config.getBoolean("GriefPrevention.Advanced.fixNegativeClaimblockAmounts", true); - this.config_advanced_claim_expiration_check_rate = config.getInt("GriefPrevention.Advanced.ClaimExpirationCheckRate", 60); - - //custom logger settings - this.config_logs_daysToKeep = config.getInt("GriefPrevention.Abridged Logs.Days To Keep", 7); - this.config_logs_socialEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Social Activity", true); - this.config_logs_suspiciousEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Suspicious Activity", true); - this.config_logs_adminEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Administrative Activity", false); - this.config_logs_debugEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Debug", false); - this.config_logs_mutedChatEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Muted Chat Messages", false); - - //claims mode by world - for (World world : this.config_claims_worldModes.keySet()) - { - outConfig.set( - "GriefPrevention.Claims.Mode." + world.getName(), - this.config_claims_worldModes.get(world).name()); - } - - - outConfig.set("GriefPrevention.Claims.PreventGlobalMonsterEggs", this.config_claims_preventGlobalMonsterEggs); - outConfig.set("GriefPrevention.Claims.PreventTheft", this.config_claims_preventTheft); - outConfig.set("GriefPrevention.Claims.ProtectCreatures", this.config_claims_protectCreatures); - outConfig.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); - outConfig.set("GriefPrevention.Claims.LockWoodenDoors", this.config_claims_lockWoodenDoors); - outConfig.set("GriefPrevention.Claims.LockTrapDoors", this.config_claims_lockTrapDoors); - outConfig.set("GriefPrevention.Claims.LockFenceGates", this.config_claims_lockFenceGates); - outConfig.set("GriefPrevention.Claims.EnderPearlsRequireAccessTrust", this.config_claims_enderPearlsRequireAccessTrust); - outConfig.set("GriefPrevention.Claims.RaidTriggersRequireBuildTrust", this.config_claims_raidTriggersRequireBuildTrust); - outConfig.set("GriefPrevention.Claims.ProtectHorses", this.config_claims_protectHorses); - outConfig.set("GriefPrevention.Claims.ProtectDonkeys", this.config_claims_protectDonkeys); - outConfig.set("GriefPrevention.Claims.ProtectLlamas", this.config_claims_protectLlamas); - outConfig.set("GriefPrevention.Claims.InitialBlocks", this.config_claims_initialBlocks); - outConfig.set("GriefPrevention.Claims.Claim Blocks Accrued Per Hour.Default", this.config_claims_blocksAccruedPerHour_default); - outConfig.set("GriefPrevention.Claims.Max Accrued Claim Blocks.Default", this.config_claims_maxAccruedBlocks_default); - outConfig.set("GriefPrevention.Claims.Accrued Idle Threshold", this.config_claims_accruedIdleThreshold); - outConfig.set("GriefPrevention.Claims.AccruedIdlePercent", this.config_claims_accruedIdlePercent); - outConfig.set("GriefPrevention.Claims.AbandonReturnRatio", this.config_claims_abandonReturnRatio); - outConfig.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", this.config_claims_automaticClaimsForNewPlayersRadius); - outConfig.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadiusMinimum", this.config_claims_automaticClaimsForNewPlayersRadiusMin); - outConfig.set("GriefPrevention.Claims.ExtendIntoGroundDistance", this.config_claims_claimsExtendIntoGroundDistance); - outConfig.set("GriefPrevention.Claims.MinimumWidth", this.config_claims_minWidth); - outConfig.set("GriefPrevention.Claims.MinimumArea", this.config_claims_minArea); - outConfig.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth); - outConfig.set("GriefPrevention.Claims.InvestigationTool", this.config_claims_investigationTool.name()); - outConfig.set("GriefPrevention.Claims.ModificationTool", this.config_claims_modificationTool.name()); - outConfig.set("GriefPrevention.Claims.Expiration.ChestClaimDays", this.config_claims_chestClaimExpirationDays); - outConfig.set("GriefPrevention.Claims.Expiration.UnusedClaimDays", this.config_claims_unusedClaimExpirationDays); - outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.DaysInactive", this.config_claims_expirationDays); - outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasTotalClaimBlocks", this.config_claims_expirationExemptionTotalBlocks); - outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasBonusClaimBlocks", this.config_claims_expirationExemptionBonusBlocks); - outConfig.set("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", this.config_claims_survivalAutoNatureRestoration); - outConfig.set("GriefPrevention.Claims.AllowTrappedInAdminClaims", this.config_claims_allowTrappedInAdminClaims); - outConfig.set("GriefPrevention.Claims.MaximumNumberOfClaimsPerPlayer", this.config_claims_maxClaimsPerPlayer); - outConfig.set("GriefPrevention.Claims.CreationRequiresWorldGuardBuildPermission", this.config_claims_respectWorldGuard); - outConfig.set("GriefPrevention.Claims.VillagerTradingRequiresPermission", this.config_claims_villagerTradingRequiresTrust); - outConfig.set("GriefPrevention.Claims.CommandsRequiringAccessTrust", accessTrustSlashCommands); - outConfig.set("GriefPrevention.Claims.DeliverManuals", config_claims_supplyPlayerManual); - outConfig.set("GriefPrevention.Claims.ManualDeliveryDelaySeconds", config_claims_manualDeliveryDelaySeconds); - outConfig.set("GriefPrevention.Claims.RavagersBreakBlocks", config_claims_ravagersBreakBlocks); - - outConfig.set("GriefPrevention.Claims.FireSpreadsInClaims", config_claims_firespreads); - outConfig.set("GriefPrevention.Claims.FireDamagesInClaims", config_claims_firedamages); - outConfig.set("GriefPrevention.Claims.LecternReadingRequiresAccessTrust", config_claims_lecternReadingRequiresAccessTrust); - - outConfig.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled); - outConfig.set("GriefPrevention.Spam.LoginCooldownSeconds", this.config_spam_loginCooldownSeconds); - outConfig.set("GriefPrevention.Spam.LoginLogoutNotificationsPerMinute", this.config_spam_loginLogoutNotificationsPerMinute); - outConfig.set("GriefPrevention.Spam.ChatSlashCommands", slashCommandsToMonitor); - outConfig.set("GriefPrevention.Spam.WhisperSlashCommands", whisperCommandsToMonitor); - outConfig.set("GriefPrevention.Spam.WarningMessage", this.config_spam_warningMessage); - outConfig.set("GriefPrevention.Spam.BanOffenders", this.config_spam_banOffenders); - outConfig.set("GriefPrevention.Spam.BanMessage", this.config_spam_banMessage); - outConfig.set("GriefPrevention.Spam.AllowedIpAddresses", this.config_spam_allowedIpAddresses); - outConfig.set("GriefPrevention.Spam.DeathMessageCooldownSeconds", this.config_spam_deathMessageCooldownSeconds); - outConfig.set("GriefPrevention.Spam.Logout Message Delay In Seconds", this.config_spam_logoutMessageDelaySeconds); - - for (World world : worlds) - { - outConfig.set("GriefPrevention.PvP.RulesEnabledInWorld." + world.getName(), this.pvpRulesApply(world)); - } - outConfig.set("GriefPrevention.PvP.ProtectFreshSpawns", this.config_pvp_protectFreshSpawns); - outConfig.set("GriefPrevention.PvP.PunishLogout", this.config_pvp_punishLogout); - outConfig.set("GriefPrevention.PvP.CombatTimeoutSeconds", this.config_pvp_combatTimeoutSeconds); - outConfig.set("GriefPrevention.PvP.AllowCombatItemDrop", this.config_pvp_allowCombatItemDrop); - outConfig.set("GriefPrevention.PvP.BlockedSlashCommands", bannedPvPCommandsList); - outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.PlayerOwnedClaims", this.config_pvp_noCombatInPlayerLandClaims); - outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeClaims", this.config_pvp_noCombatInAdminLandClaims); - outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeSubdivisions", this.config_pvp_noCombatInAdminSubdivisions); - outConfig.set("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.PvPWorlds", this.config_pvp_allowLavaNearPlayers); - outConfig.set("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.NonPvPWorlds", this.config_pvp_allowLavaNearPlayers_NonPvp); - outConfig.set("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.PvPWorlds", this.config_pvp_allowFireNearPlayers); - outConfig.set("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.NonPvPWorlds", this.config_pvp_allowFireNearPlayers_NonPvp); - outConfig.set("GriefPrevention.PvP.ProtectPetsOutsideLandClaims", this.config_pvp_protectPets); - - outConfig.set("GriefPrevention.Economy.ClaimBlocksMaxBonus", this.config_economy_claimBlocksMaxBonus); - outConfig.set("GriefPrevention.Economy.ClaimBlocksPurchaseCost", this.config_economy_claimBlocksPurchaseCost); - outConfig.set("GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue); - - outConfig.set("GriefPrevention.ProtectItemsDroppedOnDeath.PvPWorlds", this.config_lockDeathDropsInPvpWorlds); - outConfig.set("GriefPrevention.ProtectItemsDroppedOnDeath.NonPvPWorlds", this.config_lockDeathDropsInNonPvpWorlds); - - outConfig.set("GriefPrevention.BlockLandClaimExplosions", this.config_blockClaimExplosions); - outConfig.set("GriefPrevention.BlockSurfaceCreeperExplosions", this.config_blockSurfaceCreeperExplosions); - outConfig.set("GriefPrevention.BlockSurfaceOtherExplosions", this.config_blockSurfaceOtherExplosions); - outConfig.set("GriefPrevention.LimitSkyTrees", this.config_blockSkyTrees); - outConfig.set("GriefPrevention.LimitTreeGrowth", this.config_limitTreeGrowth); - outConfig.set("GriefPrevention.PistonMovement", this.config_pistonMovement.name()); - outConfig.set("GriefPrevention.CheckPistonMovement", null); - outConfig.set("GriefPrevention.LimitPistonsToLandClaims", null); - outConfig.set("GriefPrevention.PistonExplosionSound", this.config_pistonExplosionSound); - - outConfig.set("GriefPrevention.FireSpreads", this.config_fireSpreads); - outConfig.set("GriefPrevention.FireDestroys", this.config_fireDestroys); - - outConfig.set("GriefPrevention.AdminsGetWhispers", this.config_whisperNotifications); - outConfig.set("GriefPrevention.AdminsGetSignNotifications", this.config_signNotifications); - - outConfig.set("GriefPrevention.MaxPlayersPerIpAddress", this.config_ipLimit); - - outConfig.set("GriefPrevention.Siege.DoorsOpenDelayInSeconds", this.config_siege_doorsOpenSeconds); - outConfig.set("GriefPrevention.Siege.CooldownEndInMinutes", this.config_siege_cooldownEndInMinutes); - outConfig.set("GriefPrevention.EndermenMoveBlocks", this.config_endermenMoveBlocks); - outConfig.set("GriefPrevention.SilverfishBreakBlocks", this.config_silverfishBreakBlocks); - outConfig.set("GriefPrevention.CreaturesTrampleCrops", this.config_creaturesTrampleCrops); - outConfig.set("GriefPrevention.RabbitsEatCrops", this.config_rabbitsEatCrops); - outConfig.set("GriefPrevention.HardModeZombiesBreakDoors", this.config_zombiesBreakDoors); - - outConfig.set("GriefPrevention.Database.URL", this.databaseUrl); - outConfig.set("GriefPrevention.Database.UserName", this.databaseUserName); - outConfig.set("GriefPrevention.Database.Password", this.databasePassword); - - outConfig.set("GriefPrevention.UseBanCommand", this.config_ban_useCommand); - outConfig.set("GriefPrevention.BanCommandPattern", this.config_ban_commandFormat); - - outConfig.set("GriefPrevention.Advanced.fixNegativeClaimblockAmounts", this.config_advanced_fixNegativeClaimblockAmounts); - outConfig.set("GriefPrevention.Advanced.ClaimExpirationCheckRate", this.config_advanced_claim_expiration_check_rate); - - //custom logger settings - outConfig.set("GriefPrevention.Abridged Logs.Days To Keep", this.config_logs_daysToKeep); - outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Social Activity", this.config_logs_socialEnabled); - outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Suspicious Activity", this.config_logs_suspiciousEnabled); - outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Administrative Activity", this.config_logs_adminEnabled); - outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Debug", this.config_logs_debugEnabled); - outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Muted Chat Messages", this.config_logs_mutedChatEnabled); - - try - { - outConfig.save(DataStore.configFilePath); - } - catch (IOException exception) - { - AddLogEntry("Unable to write to the configuration file at \"" + DataStore.configFilePath + "\""); - } - - //try to parse the list of commands requiring access trust in land claims - this.config_claims_commandsRequiringAccessTrust = new ArrayList<>(); - String[] commands = accessTrustSlashCommands.split(";"); - for (String command : commands) - { - if (!command.isEmpty()) - { - this.config_claims_commandsRequiringAccessTrust.add(command.trim().toLowerCase()); - } - } - - //try to parse the list of commands which should be monitored for spam - this.config_spam_monitorSlashCommands = new ArrayList<>(); - commands = slashCommandsToMonitor.split(";"); - for (String command : commands) - { - this.config_spam_monitorSlashCommands.add(command.trim().toLowerCase()); - } - - //try to parse the list of commands which should be included in eavesdropping - this.config_eavesdrop_whisperCommands = new ArrayList<>(); - commands = whisperCommandsToMonitor.split(";"); - for (String command : commands) - { - this.config_eavesdrop_whisperCommands.add(command.trim().toLowerCase()); - } - - //try to parse the list of commands which should be banned during pvp combat - this.config_pvp_blockedCommands = new ArrayList<>(); - commands = bannedPvPCommandsList.split(";"); - for (String command : commands) - { - this.config_pvp_blockedCommands.add(command.trim().toLowerCase()); - } - } - - private ClaimsMode configStringToClaimsMode(String configSetting) - { - if (configSetting.equalsIgnoreCase("Survival")) - { - return ClaimsMode.Survival; - } - else if (configSetting.equalsIgnoreCase("Disabled")) - { - return ClaimsMode.Disabled; - } - else - { - return null; - } - } - - //handles slash commands - public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) - { - - Player player = null; - if (sender instanceof Player) - { - player = (Player) sender; - } - - //claim - if (cmd.getName().equalsIgnoreCase("claim") && player != null) - { - if (!GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); - return true; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - //if he's at the claim count per player limit already and doesn't have permission to bypass, display an error message - if (GriefPrevention.instance.config_claims_maxClaimsPerPlayer > 0 && - !player.hasPermission("griefprevention.overrideclaimcountlimit") && - playerData.getClaims().size() >= GriefPrevention.instance.config_claims_maxClaimsPerPlayer) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimCreationFailedOverClaimCountLimit); - return true; - } - - //default is chest claim radius, unless -1 - int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; - if (radius < 0) radius = (int) Math.ceil(Math.sqrt(GriefPrevention.instance.config_claims_minArea) / 2); - - //if player has any claims, respect claim minimum size setting - if (playerData.getClaims().size() > 0) - { - //if player has exactly one land claim, this requires the claim modification tool to be in hand (or creative mode player) - if (playerData.getClaims().size() == 1 && player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.MustHoldModificationToolForThat); - return true; - } - - radius = (int) Math.ceil(Math.sqrt(GriefPrevention.instance.config_claims_minArea) / 2); - } - - //allow for specifying the radius - if (args.length > 0) - { - if (playerData.getClaims().size() < 2 && player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.RadiusRequiresGoldenShovel); - return true; - } - - int specifiedRadius; - try - { - specifiedRadius = Integer.parseInt(args[0]); - } - catch (NumberFormatException e) - { - return false; - } - - if (specifiedRadius < radius) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.MinimumRadius, String.valueOf(radius)); - return true; - } - else - { - radius = specifiedRadius; - } - } - - if (radius < 0) radius = 0; - - Location lc = player.getLocation().add(-radius, 0, -radius); - Location gc = player.getLocation().add(radius, 0, radius); - - //player must have sufficient unused claim blocks - int area = Math.abs((gc.getBlockX() - lc.getBlockX() + 1) * (gc.getBlockZ() - lc.getBlockZ() + 1)); - int remaining = playerData.getRemainingClaimBlocks(); - if (remaining < area) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(area - remaining)); - GriefPrevention.instance.dataStore.tryAdvertiseAdminAlternatives(player); - return true; - } - - CreateClaimResult result = this.dataStore.createClaim(lc.getWorld(), - lc.getBlockX(), gc.getBlockX(), - lc.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance - 1, - gc.getWorld().getHighestBlockYAt(gc) - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance - 1, - lc.getBlockZ(), gc.getBlockZ(), - player.getUniqueId(), null, null, player); - if (!result.succeeded) - { - if (result.claim != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); - - Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion); - } - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess); - - Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - playerData.claimResizing = null; - playerData.lastShovelLocation = null; - - this.autoExtendClaim(result.claim); - } - - return true; - } - - //extendclaim - if (cmd.getName().equalsIgnoreCase("extendclaim") && player != null) - { - if (args.length < 1) - { - return false; - } - - int amount; - try - { - amount = Integer.parseInt(args[0]); - } - catch (NumberFormatException e) - { - return false; - } - - //requires claim modification tool in hand - if (player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.MustHoldModificationToolForThat); - return true; - } - - //must be standing in a land claim - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, playerData.lastClaim); - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.StandInClaimToResize); - return true; - } - - //must have permission to edit the land claim you're in - Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Edit, null); - if (errorMessage != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); - return true; - } - - //determine new corner coordinates - org.bukkit.util.Vector direction = player.getLocation().getDirection(); - if (direction.getY() > .75) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.ClaimsExtendToSky); - return true; - } - - if (direction.getY() < -.75) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.ClaimsAutoExtendDownward); - return true; - } - - Location lc = claim.getLesserBoundaryCorner(); - Location gc = claim.getGreaterBoundaryCorner(); - int newx1 = lc.getBlockX(); - int newx2 = gc.getBlockX(); - int newy1 = lc.getBlockY(); - int newy2 = gc.getBlockY(); - int newz1 = lc.getBlockZ(); - int newz2 = gc.getBlockZ(); - - //if changing Z only - if (Math.abs(direction.getX()) < .3) - { - if (direction.getZ() > 0) - { - newz2 += amount; //north - } - else - { - newz1 -= amount; //south - } - } - - //if changing X only - else if (Math.abs(direction.getZ()) < .3) - { - if (direction.getX() > 0) - { - newx2 += amount; //east - } - else - { - newx1 -= amount; //west - } - } - - //diagonals - else - { - if (direction.getX() > 0) - { - newx2 += amount; - } - else - { - newx1 -= amount; - } - - if (direction.getZ() > 0) - { - newz2 += amount; - } - else - { - newz1 -= amount; - } - } - - //attempt resize - playerData.claimResizing = claim; - this.dataStore.resizeClaimWithChecks(player, playerData, newx1, newx2, newy1, newy2, newz1, newz2); - playerData.claimResizing = null; - - return true; - } - - //abandonclaim - if (cmd.getName().equalsIgnoreCase("abandonclaim") && player != null) - { - return this.abandonClaimHandler(player, false); - } - - //abandontoplevelclaim - if (cmd.getName().equalsIgnoreCase("abandontoplevelclaim") && player != null) - { - return this.abandonClaimHandler(player, true); - } - - //forceabandonclaim - if (cmd.getName().equalsIgnoreCase("forceabandonclaim") && player != null) - { - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, true, null); - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimMissing); - return true; - } - PlayerData ownerData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(claim.ownerID); - OfflinePlayer ownerInfo = Bukkit.getServer().getOfflinePlayer(claim.ownerID); - Bukkit.getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new CleanupUnusedClaimTask(claim, ownerData, ownerInfo, true), 0); - return true; - } - - //ignoreclaims - if (cmd.getName().equalsIgnoreCase("ignoreclaims") && player != null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - playerData.ignoreClaims = !playerData.ignoreClaims; - - UUID uuid = player.getUniqueId(); - //toggle ignore claims mode on or off - if (!playerData.ignoreClaims) - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.RespectingClaims); - if(ignoreClaimWarningTasks.containsKey(uuid)) - { - ignoreClaimWarningTasks.get(uuid).cancel(); - ignoreClaimWarningTasks.remove(uuid); - } - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.IgnoringClaims); - ignoreClaimWarningTasks.put(uuid, new IgnoreClaimWarningTask(this, uuid)); - } - - return true; - } - - //abandonallclaims - else if (cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) - { - if (args.length > 1) return false; - - if (args.length != 1 || !"confirm".equalsIgnoreCase(args[0])) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConfirmAbandonAllClaims); - return true; - } - - //count claims - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - int originalClaimCount = playerData.getClaims().size(); - - //check count - if (originalClaimCount == 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.YouHaveNoClaims); - return true; - } - - if (this.config_claims_abandonReturnRatio != 1.0D) - { - //adjust claim blocks - for (Claim claim : playerData.getClaims()) - { - playerData.setAccruedClaimBlocks(playerData.getAccruedClaimBlocks() - (int) Math.ceil((claim.getArea() * (1 - this.config_claims_abandonReturnRatio)))); - } - } - - - //delete them - this.dataStore.deleteClaimsForPlayer(player.getUniqueId(), false); - - //inform the player - int remainingBlocks = playerData.getRemainingClaimBlocks(); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SuccessfulAbandon, String.valueOf(remainingBlocks)); - - //revert any current visualization - Visualization.Revert(player); - - return true; - } - - //restore nature - else if (cmd.getName().equalsIgnoreCase("restorenature") && player != null) - { - //change shovel mode - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.RestoreNature; - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.RestoreNatureActivate); - return true; - } - - //restore nature aggressive mode - else if (cmd.getName().equalsIgnoreCase("restorenatureaggressive") && player != null) - { - //change shovel mode - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.RestoreNatureAggressive; - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.RestoreNatureAggressiveActivate); - return true; - } - - //restore nature fill mode - else if (cmd.getName().equalsIgnoreCase("restorenaturefill") && player != null) - { - //change shovel mode - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.RestoreNatureFill; - - //set radius based on arguments - playerData.fillRadius = 2; - if (args.length > 0) - { - try - { - playerData.fillRadius = Integer.parseInt(args[0]); - } - catch (Exception exception) { } - } - - if (playerData.fillRadius < 0) playerData.fillRadius = 2; - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.FillModeActive, String.valueOf(playerData.fillRadius)); - return true; - } - - //trust - else if (cmd.getName().equalsIgnoreCase("trust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - //most trust commands use this helper method, it keeps them consistent - this.handleTrustCommand(player, ClaimPermission.Build, args[0]); - - return true; - } - - //transferclaim - else if (cmd.getName().equalsIgnoreCase("transferclaim") && player != null) - { - //which claim is the user in? - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TransferClaimMissing); - return true; - } - - //check additional permission for admin claims - if (claim.isAdminClaim() && !player.hasPermission("griefprevention.adminclaims")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TransferClaimPermission); - return true; - } - - UUID newOwnerID = null; //no argument = make an admin claim - String ownerName = "admin"; - - if (args.length > 0) - { - OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); - if (targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - newOwnerID = targetPlayer.getUniqueId(); - ownerName = targetPlayer.getName(); - } - - //change ownerhsip - try - { - this.dataStore.changeClaimOwner(claim, newOwnerID); - } - catch (NoTransferException e) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TransferTopLevel); - return true; - } - - //confirm - GriefPrevention.sendMessage(player, TextMode.Success, Messages.TransferSuccess); - GriefPrevention.AddLogEntry(player.getName() + " transferred a claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " to " + ownerName + ".", CustomLogEntryTypes.AdminActivity); - - return true; - } - - //trustlist - else if (cmd.getName().equalsIgnoreCase("trustlist") && player != null) - { - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); - - //if no claim here, error message - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrustListNoClaim); - return true; - } - - //if no permission to manage permissions, error message - Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Manage, null); - if (errorMessage != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, errorMessage.get()); - return true; - } - - //otherwise build a list of explicit permissions by permission level - //and send that to the player - ArrayList builders = new ArrayList<>(); - ArrayList containers = new ArrayList<>(); - ArrayList accessors = new ArrayList<>(); - ArrayList managers = new ArrayList<>(); - claim.getPermissions(builders, containers, accessors, managers); - ArrayList nearbyclaimers = new ArrayList<>(); - claim.getClaimNearbyPermission(nearbyclaimers); - GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrustListHeader); - - StringBuilder permissions = new StringBuilder(); - permissions.append(ChatColor.GOLD).append('>'); - - if (managers.size() > 0) - { - for (String manager : managers) - permissions.append(this.trustEntryToPlayerName(manager)).append(' '); - } - - player.sendMessage(permissions.toString()); - permissions = new StringBuilder(); - permissions.append(ChatColor.YELLOW).append('>'); - - if (builders.size() > 0) - { - for (String builder : builders) - permissions.append(this.trustEntryToPlayerName(builder)).append(' '); - } - - player.sendMessage(permissions.toString()); - permissions = new StringBuilder(); - permissions.append(ChatColor.GREEN).append('>'); - - if (containers.size() > 0) - { - for (String container : containers) - permissions.append(this.trustEntryToPlayerName(container)).append(' '); - } - - player.sendMessage(permissions.toString()); - permissions = new StringBuilder(); - permissions.append(ChatColor.BLUE).append('>'); - - if (accessors.size() > 0) - { - for (String accessor : accessors) - permissions.append(this.trustEntryToPlayerName(accessor)).append(' '); - } - - player.sendMessage(permissions.toString()); - permissions = new StringBuilder(); - permissions.append(ChatColor.RED).append('>'); - - if (nearbyclaimers.size() > 0) - { - for (String accessor : nearbyclaimers) - permissions.append(this.trustEntryToPlayerName(accessor)).append(' '); - } - - player.sendMessage(permissions.toString()); - - player.sendMessage( - ChatColor.GOLD + this.dataStore.getMessage(Messages.Manage) + " " + - ChatColor.YELLOW + this.dataStore.getMessage(Messages.Build) + " " + - ChatColor.GREEN + this.dataStore.getMessage(Messages.Containers) + " " + - ChatColor.BLUE + this.dataStore.getMessage(Messages.Access) + " " + - ChatColor.RED + "Claimnear"); - - if (claim.getSubclaimRestrictions()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.HasSubclaimRestriction); - } - - return true; - } - - //untrust or untrust [] - else if (cmd.getName().equalsIgnoreCase("untrust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - //determine which claim the player is standing in - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); - - //determine whether a single player or clearing permissions entirely - boolean clearPermissions = false; - OfflinePlayer otherPlayer = null; - if (args[0].equals("all")) - { - if (claim == null || claim.checkPermission(player, ClaimPermission.Edit, null) == null) - { - clearPermissions = true; - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClearPermsOwnerOnly); - return true; - } - } - else - { - //validate player argument or group argument - if (!args[0].startsWith("[") || !args[0].endsWith("]")) - { - otherPlayer = this.resolvePlayerByName(args[0]); - if (!clearPermissions && otherPlayer == null && !args[0].equals("public")) - { - //bracket any permissions - at this point it must be a permission without brackets - if (args[0].contains(".")) - { - args[0] = "[" + args[0] + "]"; - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - } - - //correct to proper casing - if (otherPlayer != null) - args[0] = otherPlayer.getName(); - } - } - - //if no claim here, apply changes to all his claims - if (claim == null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - String idToDrop = args[0]; - if (otherPlayer != null) - { - idToDrop = otherPlayer.getUniqueId().toString(); - } - - //calling event - TrustChangedEvent event = new TrustChangedEvent(player, playerData.getClaims(), null, false, idToDrop); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) - { - return true; - } - - //dropping permissions - for (Claim targetClaim : event.getClaims()) { - claim = targetClaim; - - //if untrusting "all" drop all permissions - if (clearPermissions) - { - claim.clearPermissions(); - } - - //otherwise drop individual permissions - else - { - claim.dropPermission(idToDrop); - claim.managers.remove(idToDrop); - } - - //save changes - this.dataStore.saveClaim(claim); - } - - //beautify for output - if (args[0].equals("public")) - { - args[0] = "the public"; - } - - //confirmation message - if (!clearPermissions) - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustIndividualAllClaims, args[0]); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustEveryoneAllClaims); - } - } - - //otherwise, apply changes to only this claim - else if (claim.checkPermission(player, ClaimPermission.Manage, null) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); - return true; - } - else - { - //if clearing all - if (clearPermissions) - { - //requires owner - if (claim.checkPermission(player, ClaimPermission.Edit, null) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.UntrustAllOwnerOnly); - return true; - } - - //calling the event - TrustChangedEvent event = new TrustChangedEvent(player, claim, null, false, args[0]); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) - { - return true; - } - - event.getClaims().forEach(Claim::clearPermissions); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClearPermissionsOneClaim); - } - - //otherwise individual permission drop - else - { - String idToDrop = args[0]; - if (otherPlayer != null) - { - idToDrop = otherPlayer.getUniqueId().toString(); - } - boolean targetIsManager = claim.managers.contains(idToDrop); - if (targetIsManager && claim.checkPermission(player, ClaimPermission.Edit, null) != null) //only claim owners can untrust managers - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ManagersDontUntrustManagers, claim.getOwnerName()); - return true; - } - else - { - //calling the event - TrustChangedEvent event = new TrustChangedEvent(player, claim, null, false, idToDrop); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) - { - return true; - } - - event.getClaims().forEach(targetClaim -> targetClaim.dropPermission(event.getIdentifier())); - - //beautify for output - if (args[0].equals("public")) - { - args[0] = "the public"; - } - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustIndividualSingleClaim, args[0]); - } - } - - //save changes - this.dataStore.saveClaim(claim); - } - - return true; - } - - //accesstrust - else if (cmd.getName().equalsIgnoreCase("accesstrust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - this.handleTrustCommand(player, ClaimPermission.Access, args[0]); - - return true; - } - - //containertrust - else if (cmd.getName().equalsIgnoreCase("containertrust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - this.handleTrustCommand(player, ClaimPermission.Inventory, args[0]); - - return true; - } - - else if (cmd.getName().equalsIgnoreCase("claimnearbytrust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - this.handleTrustCommand(player, ClaimPermission.Claim, args[0]); - - return true; - } - - //permissiontrust - else if (cmd.getName().equalsIgnoreCase("permissiontrust") && player != null) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - this.handleTrustCommand(player, null, args[0]); //null indicates permissiontrust to the helper method - - return true; - } - - //restrictsubclaim - else if (cmd.getName().equalsIgnoreCase("restrictsubclaim") && player != null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, playerData.lastClaim); - if (claim == null || claim.parent == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.StandInSubclaim); - return true; - } - - // If player has /ignoreclaims on, continue - // If admin claim, fail if this user is not an admin - // If not an admin claim, fail if this user is not the owner - if (!playerData.ignoreClaims && (claim.isAdminClaim() ? !player.hasPermission("griefprevention.adminclaims") : !player.getUniqueId().equals(claim.parent.ownerID))) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlyOwnersModifyClaims, claim.getOwnerName()); - return true; - } - - if (claim.getSubclaimRestrictions()) - { - claim.setSubclaimRestrictions(false); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubclaimUnrestricted); - } - else - { - claim.setSubclaimRestrictions(true); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubclaimRestricted); - } - this.dataStore.saveClaim(claim); - return true; - } - - //buyclaimblocks - else if (cmd.getName().equalsIgnoreCase("buyclaimblocks") && player != null) - { - //if economy is disabled, don't do anything - EconomyHandler.EconomyWrapper economyWrapper = economyHandler.getWrapper(); - if (economyWrapper == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); - return true; - } - - if (!player.hasPermission("griefprevention.buysellclaimblocks")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionForCommand); - return true; - } - - //if purchase disabled, send error message - if (GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlySellBlocks); - return true; - } - - Economy economy = economyWrapper.getEconomy(); - - //if no parameter, just tell player cost per block and balance - if (args.length != 1) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockPurchaseCost, String.valueOf(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost), String.valueOf(economy.getBalance(player))); - return false; - } - else - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - //try to parse number of blocks - int blockCount; - try - { - blockCount = Integer.parseInt(args[0]); - } - catch (NumberFormatException numberFormatException) - { - return false; //causes usage to be displayed - } - - if (blockCount <= 0) - { - return false; - } - - //if the player can't afford his purchase, send error message - double balance = economy.getBalance(player); - double totalCost = claimBlockCost(playerData.getBonusClaimBlocks()+playerData.getAccruedClaimBlocks() , blockCount); - if (totalCost > balance) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.InsufficientFunds, String.valueOf(totalCost), String.valueOf(balance)); - } - - //otherwise carry out transaction - else - { - int newBonusClaimBlocks = playerData.getBonusClaimBlocks() + blockCount; - - //if the player is going to reach max bonus limit, send error message - int bonusBlocksLimit = GriefPrevention.instance.config_economy_claimBlocksMaxBonus; - if (bonusBlocksLimit != 0 && newBonusClaimBlocks > bonusBlocksLimit) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.MaxBonusReached, String.valueOf(blockCount), String.valueOf(bonusBlocksLimit)); - return true; - } - - //withdraw cost - economy.withdrawPlayer(player, totalCost); - - //add blocks - playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + blockCount); - this.dataStore.savePlayerData(player.getUniqueId(), playerData); - - //inform player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.PurchaseConfirmation, String.valueOf(totalCost), String.valueOf(playerData.getRemainingClaimBlocks())); - } - - return true; - } - } - - //sellclaimblocks - else if (cmd.getName().equalsIgnoreCase("sellclaimblocks") && player != null) - { - //if economy is disabled, don't do anything - EconomyHandler.EconomyWrapper economyWrapper = economyHandler.getWrapper(); - if (economyWrapper == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); - return true; - } - - if (!player.hasPermission("griefprevention.buysellclaimblocks")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionForCommand); - return true; - } - - //if disabled, error message - if (GriefPrevention.instance.config_economy_claimBlocksSellValue == 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlyPurchaseBlocks); - return true; - } - - //load player data - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - int availableBlocks = playerData.getRemainingClaimBlocks(); - - //if no amount provided, just tell player value per block sold, and how many he can sell - if (args.length != 1) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockSaleValue, String.valueOf(GriefPrevention.instance.config_economy_claimBlocksSellValue), String.valueOf(availableBlocks)); - return false; - } - - //parse number of blocks - int blockCount; - try - { - blockCount = Integer.parseInt(args[0]); - } - catch (NumberFormatException numberFormatException) - { - return false; //causes usage to be displayed - } - - if (blockCount <= 0) - { - return false; - } - - //if he doesn't have enough blocks, tell him so - if (blockCount > availableBlocks) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotEnoughBlocksForSale); - } - - //otherwise carry out the transaction - else - { - //compute value and deposit it - double totalValue = blockCount * GriefPrevention.instance.config_economy_claimBlocksSellValue; - economyWrapper.getEconomy().depositPlayer(player, totalValue); - - //subtract blocks - playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() - blockCount); - this.dataStore.savePlayerData(player.getUniqueId(), playerData); - - //inform player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.BlockSaleConfirmation, String.valueOf(totalValue), String.valueOf(playerData.getRemainingClaimBlocks())); - } - - return true; - } - - //adminclaims - else if (cmd.getName().equalsIgnoreCase("adminclaims") && player != null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.Admin; - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdminClaimsMode); - - return true; - } - - //basicclaims - else if (cmd.getName().equalsIgnoreCase("basicclaims") && player != null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.Basic; - playerData.claimSubdividing = null; - GriefPrevention.sendMessage(player, TextMode.Success, Messages.BasicClaimsMode); - - return true; - } - - //subdivideclaims - else if (cmd.getName().equalsIgnoreCase("subdivideclaims") && player != null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.shovelMode = ShovelMode.Subdivide; - playerData.claimSubdividing = null; - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionMode); - - return true; - } - - //deleteclaim - else if (cmd.getName().equalsIgnoreCase("deleteclaim") && player != null) - { - //determine which claim the player is standing in - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); - - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.DeleteClaimMissing); - } - else - { - //deleting an admin claim additionally requires the adminclaims permission - if (!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims")) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - if (claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.DeletionSubdivisionWarning); - playerData.warnedAboutMajorDeletion = true; - } - else - { - this.dataStore.deleteClaim(claim, true, true); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteSuccess); - GriefPrevention.AddLogEntry(player.getName() + " deleted " + claim.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()), CustomLogEntryTypes.AdminActivity); - - //revert any current visualization - Visualization.Revert(player); - - playerData.warnedAboutMajorDeletion = false; - } - } - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CantDeleteAdminClaim); - } - } - - return true; - } - else if (cmd.getName().equalsIgnoreCase("claimexplosions") && player != null) - { - //determine which claim the player is standing in - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); - - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.DeleteClaimMissing); - } - else - { - Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, null); - if (noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get()); - return true; - } - - if (claim.areExplosivesAllowed) - { - claim.areExplosivesAllowed = false; - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ExplosivesDisabled); - } - else - { - claim.areExplosivesAllowed = true; - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ExplosivesEnabled); - } - } - - return true; - } - - //deleteallclaims - else if (cmd.getName().equalsIgnoreCase("deleteallclaims")) - { - //requires exactly one parameter, the other player's name - if (args.length != 1) return false; - - //try to find that player - OfflinePlayer otherPlayer = this.resolvePlayerByName(args[0]); - if (otherPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - - //delete all that player's claims - this.dataStore.deleteClaimsForPlayer(otherPlayer.getUniqueId(), true); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteAllSuccess, otherPlayer.getName()); - if (player != null) - { - GriefPrevention.AddLogEntry(player.getName() + " deleted all claims belonging to " + otherPlayer.getName() + ".", CustomLogEntryTypes.AdminActivity); - - //revert any current visualization - Visualization.Revert(player); - } - - return true; - } - else if (cmd.getName().equalsIgnoreCase("deleteclaimsinworld")) - { - //must be executed at the console - if (player != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConsoleOnlyCommand); - return true; - } - - //requires exactly one parameter, the world name - if (args.length != 1) return false; - - //try to find the specified world - World world = Bukkit.getServer().getWorld(args[0]); - if (world == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.WorldNotFound); - return true; - } - - //delete all claims in that world - this.dataStore.deleteClaimsInWorld(world, true); - GriefPrevention.AddLogEntry("Deleted all claims in world: " + world.getName() + ".", CustomLogEntryTypes.AdminActivity); - return true; - } - else if (cmd.getName().equalsIgnoreCase("deleteuserclaimsinworld")) - { - //must be executed at the console - if (player != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConsoleOnlyCommand); - return true; - } - - //requires exactly one parameter, the world name - if (args.length != 1) return false; - - //try to find the specified world - World world = Bukkit.getServer().getWorld(args[0]); - if (world == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.WorldNotFound); - return true; - } - - //delete all USER claims in that world - this.dataStore.deleteClaimsInWorld(world, false); - GriefPrevention.AddLogEntry("Deleted all user claims in world: " + world.getName() + ".", CustomLogEntryTypes.AdminActivity); - return true; - } - - //claimbook - else if (cmd.getName().equalsIgnoreCase("claimbook")) - { - //requires one parameter - if (args.length != 1) return false; - - //try to find the specified player - Player otherPlayer = this.getServer().getPlayer(args[0]); - if (otherPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - } - - //claimslist or claimslist - else if (cmd.getName().equalsIgnoreCase("claimslist")) - { - //at most one parameter - if (args.length > 1) return false; - - //player whose claims will be listed - OfflinePlayer otherPlayer; - - //if another player isn't specified, assume current player - if (args.length < 1) - { - if (player != null) - otherPlayer = player; - else - return false; - } - - //otherwise if no permission to delve into another player's claims data - else if (player != null && !player.hasPermission("griefprevention.claimslistother")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsListNoPermission); - return true; - } - - //otherwise try to find the specified player - else - { - otherPlayer = this.resolvePlayerByName(args[0]); - if (otherPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - } - - //load the target player's data - PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getUniqueId()); - Vector claims = playerData.getClaims(); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.StartBlockMath, - String.valueOf(playerData.getAccruedClaimBlocks()), - String.valueOf((playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId()))), - String.valueOf((playerData.getAccruedClaimBlocks() + playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId())))); - if (claims.size() > 0) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); - for (int i = 0; i < playerData.getClaims().size(); i++) - { - Claim claim = playerData.getClaims().get(i); - GriefPrevention.sendMessage(player, TextMode.Instr, getfriendlyLocationString(claim.getLesserBoundaryCorner()) + this.dataStore.getMessage(Messages.ContinueBlockMath, String.valueOf(claim.getArea()))); - } - - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.EndBlockMath, String.valueOf(playerData.getRemainingClaimBlocks())); - } - - //drop the data we just loaded, if the player isn't online - if (!otherPlayer.isOnline()) - this.dataStore.clearCachedPlayerData(otherPlayer.getUniqueId()); - - return true; - } - - //claimslist or claimslist - else if (cmd.getName().equalsIgnoreCase("claimslistextra")) - { - //at most one parameter - if (args.length > 1) return false; - - //player whose claims will be listed - OfflinePlayer otherPlayer; - - //if another player isn't specified, assume current player - if (args.length < 1) - { - if (player != null) - otherPlayer = player; - else - return false; - } - - //otherwise if no permission to delve into another player's claims data - else if (player != null && !player.hasPermission("griefprevention.claimslistother")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsListNoPermission); - return true; - } - - //otherwise try to find the specified player - else - { - otherPlayer = this.resolvePlayerByName(args[0]); - if (otherPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - } - - //load the target player's data - PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getUniqueId()); - Vector claims = playerData.getClaims(); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.StartBlockMath, - String.valueOf(playerData.getAccruedClaimBlocks()), - String.valueOf((playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId()))), - String.valueOf((playerData.getAccruedClaimBlocks() + playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId())))); - if (claims.size() > 0) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); - claims.stream().forEach(claim -> { - TextComponent claimInfo = new TextComponent(getExtraLocationString(claim.getLesserBoundaryCorner(), claim.getGreaterBoundaryCorner()) + this.dataStore.getMessage(Messages.ContinueBlockMath, String.valueOf(claim.getArea()))); - claimInfo.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Click here to visit this claim.").create())); - claimInfo.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/tppos " + claim.getLesserBoundaryCorner().getBlockX() + " " + claim.getLesserBoundaryCorner().getBlockY() + " " + claim.getLesserBoundaryCorner().getBlockZ() + " ")); - sender.sendMessage(claimInfo); - }); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.EndBlockMath, String.valueOf(playerData.getRemainingClaimBlocks())); - } - - //drop the data we just loaded, if the player isn't online - if (!otherPlayer.isOnline()) - this.dataStore.clearCachedPlayerData(otherPlayer.getUniqueId()); - - return true; - } - - //adminclaimslist - else if (cmd.getName().equalsIgnoreCase("adminclaimslist")) - { - //find admin claims - Vector claims = new Vector<>(); - for (Claim claim : this.dataStore.claims) - { - if (claim.ownerID == null) //admin claim - { - claims.add(claim); - } - } - if (claims.size() > 0) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); - for (Claim claim : claims) - { - GriefPrevention.sendMessage(player, TextMode.Instr, getfriendlyLocationString(claim.getLesserBoundaryCorner())); - } - } - - return true; - } - - //deletealladminclaims - else if (player != null && cmd.getName().equalsIgnoreCase("deletealladminclaims")) - { - if (!player.hasPermission("griefprevention.deleteclaims")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDeletePermission); - return true; - } - - //delete all admin claims - this.dataStore.deleteClaimsForPlayer(null, true); //null for owner id indicates an administrative claim - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AllAdminDeleted); - if (player != null) - { - GriefPrevention.AddLogEntry(player.getName() + " deleted all administrative claims.", CustomLogEntryTypes.AdminActivity); - - //revert any current visualization - Visualization.Revert(player); - } - - return true; - } - - //adjustbonusclaimblocks or [] amount - else if (cmd.getName().equalsIgnoreCase("adjustbonusclaimblocks")) - { - //requires exactly two parameters, the other player or group's name and the adjustment - if (args.length != 2) return false; - - //parse the adjustment amount - int adjustment; - try - { - adjustment = Integer.parseInt(args[1]); - } - catch (NumberFormatException numberFormatException) - { - return false; //causes usage to be displayed - } - - //if granting blocks to all players with a specific permission - if (args[0].startsWith("[") && args[0].endsWith("]")) - { - String permissionIdentifier = args[0].substring(1, args[0].length() - 1); - int newTotal = this.dataStore.adjustGroupBonusBlocks(permissionIdentifier, adjustment); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustGroupBlocksSuccess, permissionIdentifier, String.valueOf(adjustment), String.valueOf(newTotal)); - if (player != null) - GriefPrevention.AddLogEntry(player.getName() + " adjusted " + permissionIdentifier + "'s bonus claim blocks by " + adjustment + "."); - - return true; - } - - //otherwise, find the specified player - OfflinePlayer targetPlayer; - try - { - UUID playerID = UUID.fromString(args[0]); - targetPlayer = this.getServer().getOfflinePlayer(playerID); - - } - catch (IllegalArgumentException e) - { - targetPlayer = this.resolvePlayerByName(args[0]); - } - - if (targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - - //give blocks to player - PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); - playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + adjustment); - this.dataStore.savePlayerData(targetPlayer.getUniqueId(), playerData); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustBlocksSuccess, targetPlayer.getName(), String.valueOf(adjustment), String.valueOf(playerData.getBonusClaimBlocks())); - if (player != null) - GriefPrevention.AddLogEntry(player.getName() + " adjusted " + targetPlayer.getName() + "'s bonus claim blocks by " + adjustment + ".", CustomLogEntryTypes.AdminActivity); - - return true; - } - - //adjustbonusclaimblocksall - else if (cmd.getName().equalsIgnoreCase("adjustbonusclaimblocksall")) - { - //requires exactly one parameter, the amount of adjustment - if (args.length != 1) return false; - - //parse the adjustment amount - int adjustment; - try - { - adjustment = Integer.parseInt(args[0]); - } - catch (NumberFormatException numberFormatException) - { - return false; //causes usage to be displayed - } - - //for each online player - @SuppressWarnings("unchecked") - Collection players = (Collection) this.getServer().getOnlinePlayers(); - StringBuilder builder = new StringBuilder(); - for (Player onlinePlayer : players) - { - UUID playerID = onlinePlayer.getUniqueId(); - PlayerData playerData = this.dataStore.getPlayerData(playerID); - playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + adjustment); - this.dataStore.savePlayerData(playerID, playerData); - builder.append(onlinePlayer.getName()).append(' '); - } - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustBlocksAllSuccess, String.valueOf(adjustment)); - GriefPrevention.AddLogEntry("Adjusted all " + players.size() + "players' bonus claim blocks by " + adjustment + ". " + builder.toString(), CustomLogEntryTypes.AdminActivity); - - return true; - } - - //setaccruedclaimblocks - else if (cmd.getName().equalsIgnoreCase("setaccruedclaimblocks")) - { - //requires exactly two parameters, the other player's name and the new amount - if (args.length != 2) return false; - - //parse the adjustment amount - int newAmount; - try - { - newAmount = Integer.parseInt(args[1]); - } - catch (NumberFormatException numberFormatException) - { - return false; //causes usage to be displayed - } - - //find the specified player - OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); - if (targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - - //set player's blocks - PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); - playerData.setAccruedClaimBlocks(newAmount); - this.dataStore.savePlayerData(targetPlayer.getUniqueId(), playerData); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SetClaimBlocksSuccess); - if (player != null) - GriefPrevention.AddLogEntry(player.getName() + " set " + targetPlayer.getName() + "'s accrued claim blocks to " + newAmount + ".", CustomLogEntryTypes.AdminActivity); - - return true; - } - - //trapped - else if (cmd.getName().equalsIgnoreCase("trapped") && player != null) - { - //FEATURE: empower players who get "stuck" in an area where they don't have permission to build to save themselves - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim); - - //if another /trapped is pending, ignore this slash command - if (playerData.pendingTrapped) - { - return true; - } - - //if the player isn't in a claim or has permission to build, tell him to man up - if (claim == null || claim.checkPermission(player, ClaimPermission.Build, null) == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotTrappedHere); - return true; - } - - //rescue destination may be set by GPFlags or other plugin, ask to find out - SaveTrappedPlayerEvent event = new SaveTrappedPlayerEvent(claim); - Bukkit.getPluginManager().callEvent(event); - - //if the player is in the nether or end, he's screwed (there's no way to programmatically find a safe place for him) - if (player.getWorld().getEnvironment() != Environment.NORMAL && event.getDestination() == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); - return true; - } - - //if the player is in an administrative claim and AllowTrappedInAdminClaims is false, he should contact an admin - if (!GriefPrevention.instance.config_claims_allowTrappedInAdminClaims && claim.isAdminClaim() && event.getDestination() == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); - return true; - } - //send instructions - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.RescuePending); - - //create a task to rescue this player in a little while - PlayerRescueTask task = new PlayerRescueTask(player, player.getLocation(), event.getDestination()); - this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 200L); //20L ~ 1 second - - return true; - } - - else if (cmd.getName().equalsIgnoreCase("gpreload")) - { - this.loadConfig(); - if (player != null) - { - GriefPrevention.sendMessage(player, TextMode.Success, "Configuration updated. If you have updated your Grief Prevention JAR, you still need to /reload or reboot your server."); - } - else - { - GriefPrevention.AddLogEntry("Configuration updated. If you have updated your Grief Prevention JAR, you still need to /reload or reboot your server."); - } - - return true; - } - - //givepet - else if (cmd.getName().equalsIgnoreCase("givepet") && player != null) - { - //requires one parameter - if (args.length < 1) return false; - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - //special case: cancellation - if (args[0].equalsIgnoreCase("cancel")) - { - playerData.petGiveawayRecipient = null; - GriefPrevention.sendMessage(player, TextMode.Success, Messages.PetTransferCancellation); - return true; - } - - //find the specified player - OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); - if (targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return true; - } - - //remember the player's ID for later pet transfer - playerData.petGiveawayRecipient = targetPlayer; - - //send instructions - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ReadyToTransferPet); - - return true; - } - - //gpblockinfo - else if (cmd.getName().equalsIgnoreCase("gpblockinfo") && player != null) - { - ItemStack inHand = player.getInventory().getItemInMainHand(); - player.sendMessage("In Hand: " + inHand.getType().name()); - - Block inWorld = player.getTargetBlockExact(300, FluidCollisionMode.ALWAYS); - if (inWorld == null) inWorld = player.getEyeLocation().getBlock(); - player.sendMessage("In World: " + inWorld.getType().name()); - - return true; - } - // kickfromclaim - else if (cmd.getName().equalsIgnoreCase("kickfromclaim") && player != null) - { - if (args.length < 1) { - player.sendMiniMessage(Config.PlayerNotSpecified, null); // todo placeholders. - return true; - } - TagResolver placeholders = TagResolver.resolver( - Placeholder.component("player", player.name()), - Placeholder.parsed("target", args[0]) - ); - Player target = Bukkit.getPlayer(args[0]); - if (target == null) { - player.sendMiniMessage(Config.PlayerOffline, placeholders); // todo placeholders. - return true; - } - if (player.equals(target)) { - player.sendMiniMessage(Config.CannotKickSelf, null); // todo placeholders. - return true; - } - Claim claim = this.dataStore.getClaimAt(target.getLocation(), true, null); - if (claim == null || (claim.checkPermission(player, ClaimPermission.Manage, null) != null)) { - player.sendMiniMessage(Config.TargetNotInClaim, placeholders); // todo placeholders. - return true; - } - placeholders = TagResolver.resolver(placeholders, Placeholder.parsed("claim_owner", claim.getOwnerName())); - SafeZone zone = new SafeZone(claim); - if ((target.hasPermission("griefprevention.adminclaims") && claim.isAdminClaim()) || zone - .hasTrust(target.getUniqueId())) { - player.sendMiniMessage(Config.CannotKickTrustedTarget, placeholders); // todo placeholders. - return true; - } - if (target.hasPermission("griefprevention.kickfromclaimexempt")) { - player.sendMiniMessage(Config.CannotKickExemptTarget, placeholders); // todo placeholders. - return true; - } - zone.testForSafeSpot(); - Location safe = zone.getSafeArea(); - if (safe == null) { - player.sendMiniMessage(Config.NoSafeLocation, null); // todo placeholders. - } else { - if (target.isInsideVehicle()) target.leaveVehicle(); - target.teleport(safe); - Bukkit.getPluginManager().callEvent(new PlayerTeleportEvent(target, safe, safe)); - player.sendMiniMessage(Config.KickSuccess, placeholders); // todo placeholders. - target.sendMiniMessage(Config.KickedFromClaim, placeholders); // todo placeholders. - } - return true; - } - return false; - } - - public enum IgnoreMode - {None, StandardIgnore, AdminIgnore} - - public String trustEntryToPlayerName(String entry) - { - if (entry.startsWith("[") || entry.equals("public")) - { - return entry; - } - else - { - return GriefPrevention.lookupPlayerName(entry); - } - } - - public static String getfriendlyLocationString(Location location) - { - return location.getWorld().getName() + ": x" + location.getBlockX() + ", z" + location.getBlockZ(); - } - - public static String getExtraLocationString(Location location, Location location2) - { - return location.getWorld().getName() + ": x" + location.getBlockX() + ", z" + location.getBlockZ() + " to x" + location2.getBlockX() + ", z" + location2.getBlockZ(); - } - - private boolean abandonClaimHandler(Player player, boolean deleteTopLevelClaim) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - - //which claim is being abandoned? - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); - if (claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimMissing); - } - - //verify ownership - else if (claim.checkPermission(player, ClaimPermission.Edit, null) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); - } - - //warn if has children and we're not explicitly deleting a top level claim - else if (claim.children.size() > 0 && !deleteTopLevelClaim) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DeleteTopLevelClaim); - return true; - } - else - { - //delete it - this.dataStore.deleteClaim(claim, true, false); - - //adjust claim blocks when abandoning a top level claim - if (this.config_claims_abandonReturnRatio != 1.0D && claim.parent == null && claim.ownerID.equals(playerData.playerID)) - { - playerData.setAccruedClaimBlocks(playerData.getAccruedClaimBlocks() - (int) Math.ceil((claim.getArea() * (1 - this.config_claims_abandonReturnRatio)))); - } - - //tell the player how many claim blocks he has left - int remainingBlocks = playerData.getRemainingClaimBlocks(); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AbandonSuccess, String.valueOf(remainingBlocks)); - - //revert any current visualization - Visualization.Revert(player); - - playerData.warnedAboutMajorDeletion = false; - } - - return true; - - } - - //helper method keeps the trust commands consistent and eliminates duplicate code - public void handleTrust(Player player, String recipientName) { - this.handleTrustCommand(player, ClaimPermission.Build, recipientName); - } - private void handleTrustCommand(Player player, ClaimPermission permissionLevel, String recipientName) - { - //determine which claim the player is standing in - Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); - - //validate player or group argument - String permission = null; - OfflinePlayer otherPlayer = null; - UUID recipientID = null; - if (recipientName.startsWith("[") && recipientName.endsWith("]")) - { - permission = recipientName.substring(1, recipientName.length() - 1); - if (permission == null || permission.isEmpty()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.InvalidPermissionID); - return; - } - } - else - { - otherPlayer = this.resolvePlayerByName(recipientName); - boolean isPermissionFormat = recipientName.contains("."); - if (otherPlayer == null && !recipientName.equals("public") && !recipientName.equals("all") && !isPermissionFormat) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); - return; - } - - if (otherPlayer == null && isPermissionFormat) - { - //player does not exist and argument has a period so this is a permission instead - permission = recipientName; - } - else if (otherPlayer != null) - { - recipientName = otherPlayer.getName(); - recipientID = otherPlayer.getUniqueId(); - } - else - { - recipientName = "public"; - } - } - - //determine which claims should be modified - ArrayList targetClaims = new ArrayList<>(); - if (claim == null) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - targetClaims.addAll(playerData.getClaims()); - } - else - { - //check permission here - if (claim.checkPermission(player, ClaimPermission.Manage, null) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); - return; - } - - //see if the player has the level of permission he's trying to grant - Supplier errorMessage; - - //permission level null indicates granting permission trust - if (permissionLevel == null) - { - errorMessage = claim.checkPermission(player, ClaimPermission.Edit, null); - if (errorMessage != null) - { - errorMessage = () -> "Only " + claim.getOwnerName() + " can grant /PermissionTrust here."; - } - } - - //otherwise just use the ClaimPermission enum values - else - { - errorMessage = claim.checkPermission(player, permissionLevel, null); - } - - //error message for trying to grant a permission the player doesn't have - if (errorMessage != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CantGrantThatPermission); - return; - } - - targetClaims.add(claim); - } - - //if we didn't determine which claims to modify, tell the player to be specific - if (targetClaims.size() == 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.GrantPermissionNoClaim); - return; - } - - String identifierToAdd = recipientName; - if (permission != null) - { - identifierToAdd = "[" + permission + "]"; - //replace recipientName as well so the success message clearly signals a permission - recipientName = identifierToAdd; - } - else if (recipientID != null) - { - identifierToAdd = recipientID.toString(); - } - - //calling the event - TrustChangedEvent event = new TrustChangedEvent(player, targetClaims, permissionLevel, true, identifierToAdd); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) - { - return; - } - - //apply changes - for (Claim currentClaim : event.getClaims()) - { - if (permissionLevel == null) - { - if (!currentClaim.managers.contains(identifierToAdd)) - { - currentClaim.managers.add(identifierToAdd); - } - } - else - { - currentClaim.setPermission(identifierToAdd, permissionLevel); - } - this.dataStore.saveClaim(currentClaim); - } - - //notify player - if (recipientName.equals("public")) recipientName = this.dataStore.getMessage(Messages.CollectivePublic); - String permissionDescription; - if (permissionLevel == null) - { - permissionDescription = this.dataStore.getMessage(Messages.PermissionsPermission); - } - else if (permissionLevel == ClaimPermission.Build) - { - permissionDescription = this.dataStore.getMessage(Messages.BuildPermission); - } - else if (permissionLevel == ClaimPermission.Access) - { - permissionDescription = this.dataStore.getMessage(Messages.AccessPermission); - } - else if (permissionLevel == ClaimPermission.Claim) - { - permissionDescription = "Granted this player the ability to claim near your claim, this is not a permanent trust."; // TODO message - } - else //ClaimPermission.Inventory - { - permissionDescription = this.dataStore.getMessage(Messages.ContainersPermission); - } - - String location; - if (claim == null) - { - location = this.dataStore.getMessage(Messages.LocationAllClaims); - } - else - { - location = this.dataStore.getMessage(Messages.LocationCurrentClaim); - } - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.GrantPermissionConfirmation, recipientName, permissionDescription, location); - } - - //helper method to resolve a player by name - ConcurrentHashMap playerNameToIDMap = new ConcurrentHashMap<>(); // TODO REMOVE ME - - - public OfflinePlayer resolvePlayerByName(String name) - { - //try online players first - Player targetPlayer = this.getServer().getPlayerExact(name); - if (targetPlayer != null) return targetPlayer; - - //try exact match first - UUID bestMatchID = this.playerNameToIDMap.get(name); - - //if failed, try ignore case - if (bestMatchID == null) - { - bestMatchID = this.playerNameToIDMap.get(name.toLowerCase()); - return this.getServer().getOfflinePlayer(bestMatchID); - } - - return this.getServer().getOfflinePlayer(name); - } - - //helper method to resolve a player name from the player's UUID - static String lookupPlayerName(UUID playerID) - { - //parameter validation - if (playerID == null) return "somebody"; - - if (playerNameCache.containsKey(playerID)) - return playerNameCache.get(playerID); - //check the cache - OfflinePlayer player = GriefPrevention.instance.getServer().getOfflinePlayer(playerID); - if (player.hasPlayedBefore() || player.isOnline()) - { - String playerName = player.getName(); - playerNameCache.put(playerID, playerName); - return playerName; - } - else - { - return "someone(" + playerID.toString() + ")"; - } - } - - //cache for player name lookups, to save searches of all offline players - static void cacheUUIDNamePair(UUID playerID, String playerName) - { - //store the reverse mapping - playerNameCache.put(playerID, playerName); - GriefPrevention.instance.playerNameToIDMap.put(playerName, playerID); - GriefPrevention.instance.playerNameToIDMap.put(playerName.toLowerCase(), playerID); - } - - //string overload for above helper - static String lookupPlayerName(String playerID) - { - UUID id; - try - { - id = UUID.fromString(playerID); - } - catch (IllegalArgumentException ex) - { - GriefPrevention.AddLogEntry("Error: Tried to look up a local player name for invalid UUID: " + playerID); - return "someone"; - } - - return lookupPlayerName(id); - } - - public void onDisable() - { - //save data for any online players - @SuppressWarnings("unchecked") - Collection players = (Collection) this.getServer().getOnlinePlayers(); - for (Player player : players) - { - UUID playerID = player.getUniqueId(); - PlayerData playerData = this.dataStore.getPlayerData(playerID); - this.dataStore.savePlayerDataSync(playerID, playerData); - } - - this.dataStore.close(); - - //dump any remaining unwritten log entries - this.customLogger.WriteEntries(); - - if (pl3xmapHook != null) { - pl3xmapHook.disable(); - } - - AddLogEntry("GriefPrevention disabled."); - } - - //called when a player spawns, applies protection for that player if necessary - public void checkPvpProtectionNeeded(Player player) - { - //if anti spawn camping feature is not enabled, do nothing - if (!this.config_pvp_protectFreshSpawns) return; - - //if pvp is disabled, do nothing - if (!pvpRulesApply(player.getWorld())) return; - - //if player is in creative mode, do nothing - if (player.getGameMode() == GameMode.CREATIVE) return; - - //if the player has the damage any player permission enabled, do nothing - if (player.hasPermission("griefprevention.nopvpimmunity")) return; - - //check inventory for well, anything - if (GriefPrevention.isInventoryEmpty(player)) - { - //if empty, apply immunity - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - playerData.pvpImmune = true; - - //inform the player after he finishes respawning - GriefPrevention.sendMessage(player, TextMode.Success, Messages.PvPImmunityStart, 5L); - - //start a task to re-check this player's inventory every minute until his immunity is gone - PvPImmunityValidationTask task = new PvPImmunityValidationTask(player); - this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 1200L); - } - } - - static boolean isInventoryEmpty(Player player) - { - PlayerInventory inventory = player.getInventory(); - ItemStack[] armorStacks = inventory.getArmorContents(); - - //check armor slots, stop if any items are found - for (ItemStack armorStack : armorStacks) - { - if (!(armorStack == null || armorStack.getType() == Material.AIR)) return false; - } - - //check other slots, stop if any items are found - ItemStack[] generalStacks = inventory.getContents(); - for (ItemStack generalStack : generalStacks) - { - if (!(generalStack == null || generalStack.getType() == Material.AIR)) return false; - } - - return true; - } - - //moves a player from the claim he's in to a nearby wilderness location - public Location ejectPlayer(Player player) - { - //look for a suitable location - Location candidateLocation = player.getLocation(); - while (true) - { - Claim claim = null; - claim = GriefPrevention.instance.dataStore.getClaimAt(candidateLocation, false, null); - - //if there's a claim here, keep looking - if (claim != null) - { - candidateLocation = new Location(claim.lesserBoundaryCorner.getWorld(), claim.lesserBoundaryCorner.getBlockX() - 1, claim.lesserBoundaryCorner.getBlockY(), claim.lesserBoundaryCorner.getBlockZ() - 1); - continue; - } - - //otherwise find a safe place to teleport the player - else - { - //find a safe height, a couple of blocks above the surface - GuaranteeChunkLoaded(candidateLocation); - Block highestBlock = candidateLocation.getWorld().getHighestBlockAt(candidateLocation.getBlockX(), candidateLocation.getBlockZ()); - Location destination = new Location(highestBlock.getWorld(), highestBlock.getX(), highestBlock.getY() + 2, highestBlock.getZ()); - player.teleport(destination); - return destination; - } - } - } - - //ensures a piece of the managed world is loaded into server memory - //(generates the chunk if necessary) - private static void GuaranteeChunkLoaded(Location location) - { - Chunk chunk = location.getChunk(); - while (!chunk.isLoaded() || !chunk.load(true)) ; - } - - //sends a color-coded message to a player - public static void sendMessage(Player player, ChatColor color, Messages messageID, String... args) - { - sendMessage(player, color, messageID, 0, args); - } - - //sends a color-coded message to a player - public static void sendMessage(Player player, ChatColor color, Messages messageID, long delayInTicks, String... args) - { - String message = GriefPrevention.instance.dataStore.getMessage(messageID, args); - sendMessage(player, color, message, delayInTicks); - } - - //sends a color-coded message to a player - public static void sendMessage(Player player, ChatColor color, String message) - { - if (message == null || message.length() == 0) return; - - if (player == null) - { - GriefPrevention.AddLogEntry(color + message); - } - else - { - player.sendMessage(color + message); - } - } - - public static void sendMessage(Player player, ChatColor color, String message, long delayInTicks) - { - SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message); - - //Only schedule if there should be a delay. Otherwise, send the message right now, else the message will appear out of order. - if (delayInTicks > 0) - { - GriefPrevention.instance.getServer().getScheduler().runTaskLater(GriefPrevention.instance, task, delayInTicks); - } - else - { - task.run(); - } - } - - //checks whether players can create claims in a world - public boolean claimsEnabledForWorld(World world) - { - ClaimsMode mode = this.config_claims_worldModes.get(world); - return mode != null && mode != ClaimsMode.Disabled; - } - - public String allowBuild(Player player, Location location) - { - // TODO check all derivatives and rework API - return this.allowBuild(player, location, location.getBlock().getType()); - } - - public String allowBuild(Player player, Location location, Material material) - { - if (!GriefPrevention.instance.claimsEnabledForWorld(location.getWorld())) return null; - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); - - //exception: administrators in ignore claims mode - if (playerData.ignoreClaims) return null; - - //wilderness rules - if (claim == null) - { - return null; - } - - //if not in the wilderness, then apply claim rules (permissions, etc) - else - { - //cache the claim for later reference - playerData.lastClaim = claim; - Block block = location.getBlock(); - - Supplier supplier = claim.checkPermission(player, ClaimPermission.Build, new BlockPlaceEvent(block, block.getState(), block, new ItemStack(material), player, true, EquipmentSlot.HAND)); - - if (supplier == null) return null; - - return supplier.get(); - } - } - - public String allowBreak(Player player, Block block, Location location) - { - return this.allowBreak(player, block, location, new BlockBreakEvent(block, player)); - } - - public String allowBreak(Player player, Material material, Location location, BlockBreakEvent breakEvent) - { - return this.allowBreak(player, location.getBlock(), location, breakEvent); - } - - public String allowBreak(Player player, Block block, Location location, BlockBreakEvent breakEvent) - { - if (!GriefPrevention.instance.claimsEnabledForWorld(location.getWorld())) return null; - - PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); - Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); - - //exception: administrators in ignore claims mode - if (playerData.ignoreClaims) return null; - - //wilderness rules - if (claim == null) - { - return null; - } - else - { - //cache the claim for later reference - playerData.lastClaim = claim; - - //if not in the wilderness, then apply claim rules (permissions, etc) - Supplier cancel = claim.checkPermission(player, ClaimPermission.Build, breakEvent); - if (cancel != null && breakEvent != null) - { - PreventBlockBreakEvent preventionEvent = new PreventBlockBreakEvent(breakEvent); - Bukkit.getPluginManager().callEvent(preventionEvent); - if (preventionEvent.isCancelled()) - { - cancel = null; - } - } - - if (cancel == null) return null; - - return cancel.get(); - } - } - - public void restoreChunk(Chunk chunk, int miny, boolean aggressiveMode, long delayInTicks, Player playerReceivingVisualization) - { - //build a snapshot of this chunk, including 1 block boundary outside of the chunk all the way around - int maxHeight = chunk.getWorld().getMaxHeight(); - BlockSnapshot[][][] snapshots = new BlockSnapshot[18][maxHeight][18]; - Block startBlock = chunk.getBlock(0, 0, 0); - Location startLocation = new Location(chunk.getWorld(), startBlock.getX() - 1, 0, startBlock.getZ() - 1); - for (int x = 0; x < snapshots.length; x++) - { - for (int z = 0; z < snapshots[0][0].length; z++) - { - for (int y = 0; y < snapshots[0].length; y++) - { - Block block = chunk.getWorld().getBlockAt(startLocation.getBlockX() + x, startLocation.getBlockY() + y, startLocation.getBlockZ() + z); - snapshots[x][y][z] = new BlockSnapshot(block.getLocation(), block.getType(), block.getBlockData()); - } - } - } - - //create task to process those data in another thread - Location lesserBoundaryCorner = chunk.getBlock(0, 0, 0).getLocation(); - Location greaterBoundaryCorner = chunk.getBlock(15, 0, 15).getLocation(); - - //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, false, playerReceivingVisualization); - GriefPrevention.instance.getServer().getScheduler().runTaskLaterAsynchronously(GriefPrevention.instance, task, delayInTicks); - } - - public int getSeaLevel(World world) - { - Integer overrideValue = this.config_seaLevelOverride.get(world.getName()); - if (overrideValue == null || overrideValue == -1) - { - return world.getSeaLevel(); - } - else - { - return overrideValue; - } - } - - public boolean containsBlockedIP(String message) - { - message = message.replace("\r\n", ""); - Pattern ipAddressPattern = Pattern.compile("([0-9]{1,3}\\.){3}[0-9]{1,3}"); - Matcher matcher = ipAddressPattern.matcher(message); - - //if it looks like an IP address - if (matcher.find()) - { - //and it's not in the list of allowed IP addresses - if (!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) - { - return true; - } - } - - return false; - } - - void autoExtendClaim(Claim newClaim) - { - //auto-extend it downward to cover anything already built underground - Location lesserCorner = newClaim.getLesserBoundaryCorner(); - Location greaterCorner = newClaim.getGreaterBoundaryCorner(); - World world = lesserCorner.getWorld(); - ArrayList snapshots = new ArrayList<>(); - for (int chunkx = lesserCorner.getBlockX() / 16; chunkx <= greaterCorner.getBlockX() / 16; chunkx++) - { - for (int chunkz = lesserCorner.getBlockZ() / 16; chunkz <= greaterCorner.getBlockZ() / 16; chunkz++) - { - if (world.isChunkLoaded(chunkx, chunkz)) - { - snapshots.add(world.getChunkAt(chunkx, chunkz).getChunkSnapshot(true, true, false)); - } - } - } - - Bukkit.getScheduler().runTaskAsynchronously(GriefPrevention.instance, new AutoExtendClaimTask(newClaim, snapshots, world.getEnvironment())); - } - - public boolean pvpRulesApply(World world) - { - Boolean configSetting = this.config_pvp_specifiedWorlds.get(world); - if (configSetting != null) return configSetting; - return world.getPVP(); - } - - public static boolean isNewToServer(Player player) - { - if (player.getStatistic(Statistic.PICKUP, Material.OAK_LOG) > 0 || - player.getStatistic(Statistic.PICKUP, Material.SPRUCE_LOG) > 0 || - player.getStatistic(Statistic.PICKUP, Material.BIRCH_LOG) > 0 || - player.getStatistic(Statistic.PICKUP, Material.JUNGLE_LOG) > 0 || - player.getStatistic(Statistic.PICKUP, Material.ACACIA_LOG) > 0 || - player.getStatistic(Statistic.PICKUP, Material.DARK_OAK_LOG) > 0) return false; - - PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); - if (playerData.getClaims().size() > 0) return false; - - return true; - } - - static void banPlayer(Player player, String reason, String source) - { - if (GriefPrevention.instance.config_ban_useCommand) - { - Bukkit.getServer().dispatchCommand( - Bukkit.getConsoleSender(), - GriefPrevention.instance.config_ban_commandFormat.replace("%name%", player.getName()).replace("%reason%", reason)); - } - else - { - BanList bans = Bukkit.getServer().getBanList(Type.NAME); - bans.addBan(player.getName(), reason, null, source); - - //kick - if (player.isOnline()) - { - player.kickPlayer(reason); - } - } - } - - public ItemStack getItemInHand(Player player, EquipmentSlot hand) - { - if (hand == EquipmentSlot.OFF_HAND) return player.getInventory().getItemInOffHand(); - return player.getInventory().getItemInMainHand(); - } - - public boolean claimIsPvPSafeZone(Claim claim) - { - return claim.isAdminClaim() && claim.parent == null && GriefPrevention.instance.config_pvp_noCombatInAdminLandClaims || - claim.isAdminClaim() && claim.parent != null && GriefPrevention.instance.config_pvp_noCombatInAdminSubdivisions || - !claim.isAdminClaim() && GriefPrevention.instance.config_pvp_noCombatInPlayerLandClaims; - } - - //Track scheduled "rescues" so we can cancel them if the player happens to teleport elsewhere so we can cancel it. - ConcurrentHashMap portalReturnTaskMap = new ConcurrentHashMap<>(); - - public void startRescueTask(Player player, Location location) - { - //Schedule task to reset player's portal cooldown after 30 seconds (Maximum timeout time for client, in case their network is slow and taking forever to load chunks) - BukkitTask task = new CheckForPortalTrapTask(player, this, location).runTaskLater(GriefPrevention.instance, 600L); - - //Cancel existing rescue task - if (portalReturnTaskMap.containsKey(player.getUniqueId())) - portalReturnTaskMap.put(player.getUniqueId(), task).cancel(); - else - portalReturnTaskMap.put(player.getUniqueId(), task); - } - - private static final double[] xMult = {0, 10000, 50000, 300000, 1000000, Integer.MAX_VALUE}; - private static final double[] yMultBuy = {0.5, 0.75, 1, 2, 5}; - private double claimBlockCost(int oldPoints, int transPts) { - if (Config.claimBlockPrices.isEmpty()) - return transPts * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost; - - double finalPrice = 0; //Initialize final price - int segment = 1; //Start segment at one - int high = oldPoints + transPts; //Will be the highest point value - - if (oldPoints > high) //If high is not the highest point value, swap it with oldPoints so it is - { - int temp = oldPoints; - oldPoints = high; - high = temp; - } - - while (oldPoints > xMult[segment] && segment < xMult.length - 1) { //Calculate the start segment (first value smaller than lower) - segment++; - } - - for (int i = segment; i < xMult.length && high > xMult[i - 1]; i++) - finalPrice += getPricePerInterval(oldPoints, high, i); - - return finalPrice; - } - - private double getPricePerInterval(int start_points, int end_points, int segment) { - double bottom = xMult[segment - 1]; - double top = xMult[segment]; - double priceMult = yMultBuy[segment - 1]; - double pricePerPoint = 1; - - if (start_points <= bottom && end_points <= top)// +_---+--- - return (end_points - bottom) * pricePerPoint * priceMult; - else if (start_points <= bottom && end_points >= top) // +_---_+ - return (top - bottom) * pricePerPoint * priceMult; - else if (start_points >= bottom && end_points <= top) // _--+--+--_ - return (end_points - start_points) * pricePerPoint * priceMult; - else if (start_points >= bottom && end_points >= top) // _--+--_+ - return (top - start_points) * pricePerPoint * priceMult; - else - return 0; - } - - public DatabaseConnection getDataBase() { - return databaseConnection; - } -} +/* + 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 me.ryanhamshire.GriefPrevention.DataStore.NoTransferException; +import me.ryanhamshire.GriefPrevention.alttd.config.Config; +import me.ryanhamshire.GriefPrevention.alttd.database.DatabaseConnection; +import me.ryanhamshire.GriefPrevention.alttd.hook.Pl3xMapHook; +import me.ryanhamshire.GriefPrevention.alttd.listeners.AltitudeListener; +import me.ryanhamshire.GriefPrevention.alttd.tasks.AdminClaimExpireTask; +import me.ryanhamshire.GriefPrevention.alttd.tasks.IgnoreClaimWarningTask; +import me.ryanhamshire.GriefPrevention.alttd.util.SafeZone; +import me.ryanhamshire.GriefPrevention.alttd.util.Utils; +import me.ryanhamshire.GriefPrevention.events.PreventBlockBreakEvent; +import me.ryanhamshire.GriefPrevention.events.SaveTrappedPlayerEvent; +import me.ryanhamshire.GriefPrevention.events.TrustChangedEvent; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.milkbowl.vault.economy.Economy; +import org.bukkit.BanList; +import org.bukkit.BanList.Type; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.FluidCollisionMode; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GriefPrevention extends JavaPlugin +{ + //for convenience, a reference to the instance of this plugin + public static GriefPrevention instance; + + //for logging to the console and log file + private static Logger log; + + //this handles data storage, like player and region data + public DataStore dataStore; + + // Event handlers with common functionality + EntityEventHandler entityEventHandler; + + //this tracks item stacks expected to drop which will need protection + ArrayList pendingItemWatchList = new ArrayList<>(); + + //log entry manager for GP's custom log files + CustomLogger customLogger; + + // hashmap to cache offline playernames + public static HashMap playerNameCache = new HashMap<>(); + //configuration variables, loaded/saved from a config.yml + + //claim mode for each world + public ConcurrentHashMap config_claims_worldModes; + + public boolean config_claims_preventGlobalMonsterEggs; //whether monster eggs can be placed regardless of trust. + public boolean config_claims_preventTheft; //whether containers and crafting blocks are protectable + public boolean config_claims_protectCreatures; //whether claimed animals may be injured by players without permission + public boolean config_claims_protectHorses; //whether horses on a claim should be protected by that claim's rules + public boolean config_claims_protectDonkeys; //whether donkeys on a claim should be protected by that claim's rules + public boolean config_claims_protectLlamas; //whether llamas on a claim should be protected by that claim's rules + public boolean config_claims_preventButtonsSwitches; //whether buttons and switches are protectable + public boolean config_claims_lockWoodenDoors; //whether wooden doors should be locked by default (require /accesstrust) + public boolean config_claims_lockTrapDoors; //whether trap doors should be locked by default (require /accesstrust) + public boolean config_claims_lockFenceGates; //whether fence gates should be locked by default (require /accesstrust) + public boolean config_claims_enderPearlsRequireAccessTrust; //whether teleporting into a claim with a pearl requires access trust + public boolean config_claims_raidTriggersRequireBuildTrust; //whether raids are triggered by a player that doesn't have build permission in that claim + public int config_claims_maxClaimsPerPlayer; //maximum number of claims per player + public boolean config_claims_respectWorldGuard; //whether claim creations requires WG build permission in creation area + public boolean config_claims_villagerTradingRequiresTrust; //whether trading with a claimed villager requires permission + + public int config_claims_initialBlocks; //the number of claim blocks a new player starts with + public double config_claims_abandonReturnRatio; //the portion of claim blocks returned to a player when a claim is abandoned + public int config_claims_blocksAccruedPerHour_default; //how many additional blocks players get each hour of play (can be zero) without any special permissions + public int config_claims_maxAccruedBlocks_default; //the limit on accrued blocks (over time) for players without any special permissions. doesn't limit purchased or admin-gifted blocks + public int config_claims_accruedIdleThreshold; //how far (in blocks) a player must move in order to not be considered afk/idle when determining accrued claim blocks + public int config_claims_accruedIdlePercent; //how much percentage of claim block accruals should idle players get + public int config_claims_maxDepth; //limit on how deep claims can go + public int config_claims_expirationDays; //how many days of inactivity before a player loses his claims + public int config_claims_expirationExemptionTotalBlocks; //total claim blocks amount which will exempt a player from claim expiration + public int config_claims_expirationExemptionBonusBlocks; //bonus claim blocks amount which will exempt a player from claim expiration + + public int config_claims_automaticClaimsForNewPlayersRadius; //how big automatic new player claims (when they place a chest) should be. -1 to disable + public int config_claims_automaticClaimsForNewPlayersRadiusMin; //how big automatic new player claims must be. 0 to disable + public int config_claims_claimsExtendIntoGroundDistance; //how far below the shoveled block a new claim will reach + public int config_claims_minWidth; //minimum width for non-admin claims + public int config_claims_minArea; //minimum area for non-admin claims + + public int config_claims_chestClaimExpirationDays; //number of days of inactivity before an automatic chest claim will be deleted + public int config_claims_unusedClaimExpirationDays; //number of days of inactivity before an unused (nothing build) claim will be deleted + public boolean config_claims_survivalAutoNatureRestoration; //whether survival claims will be automatically restored to nature when auto-deleted + public boolean config_claims_allowTrappedInAdminClaims; //whether it should be allowed to use /trapped in adminclaims. + + public Material config_claims_investigationTool; //which material will be used to investigate claims with a right click + public Material config_claims_modificationTool; //which material will be used to create/resize claims with a right click + + public ArrayList config_claims_commandsRequiringAccessTrust; //the list of slash commands requiring access trust when in a claim + public boolean config_claims_supplyPlayerManual; //whether to give new players a book with land claim help in it + public int config_claims_manualDeliveryDelaySeconds; //how long to wait before giving a book to a new player + + public boolean config_claims_firespreads; //whether fire will spread in claims + public boolean config_claims_firedamages; //whether fire will damage in claims + + public boolean config_claims_lecternReadingRequiresAccessTrust; //reading lecterns requires access trust + + public int config_siege_doorsOpenSeconds; // how before claim is re-secured after siege win + public int config_siege_cooldownEndInMinutes; + public boolean config_spam_enabled; //whether or not to monitor for spam + public int config_spam_loginCooldownSeconds; //how long players must wait between logins. combats login spam. + public int config_spam_loginLogoutNotificationsPerMinute; //how many login/logout notifications to show per minute (global, not per player) + public ArrayList config_spam_monitorSlashCommands; //the list of slash commands monitored for spam + public boolean config_spam_banOffenders; //whether or not to ban spammers automatically + 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 int config_spam_logoutMessageDelaySeconds; //delay before a logout message will be shown (only if the player stays offline that long) + + HashMap config_pvp_specifiedWorlds; //list of worlds where pvp anti-grief rules apply, according to the config file + public boolean config_pvp_protectFreshSpawns; //whether to make newly spawned players immune until they pick up an item + public boolean config_pvp_punishLogout; //whether to kill players who log out during PvP combat + public int config_pvp_combatTimeoutSeconds; //how long combat is considered to continue after the most recent damage + public boolean config_pvp_allowCombatItemDrop; //whether a player can drop items during combat to hide them + public ArrayList config_pvp_blockedCommands; //list of commands which may not be used during pvp combat + public boolean config_pvp_noCombatInPlayerLandClaims; //whether players may fight in player-owned land claims + public boolean config_pvp_noCombatInAdminLandClaims; //whether players may fight in admin-owned land claims + public boolean config_pvp_noCombatInAdminSubdivisions; //whether players may fight in subdivisions of admin-owned land claims + public boolean config_pvp_allowLavaNearPlayers; //whether players may dump lava near other players in pvp worlds + public boolean config_pvp_allowLavaNearPlayers_NonPvp; //whather this applies in non-PVP rules worlds + public boolean config_pvp_allowFireNearPlayers; //whether players may start flint/steel fires near other players in pvp worlds + public boolean config_pvp_allowFireNearPlayers_NonPvp; //whether this applies in non-PVP rules worlds + public boolean config_pvp_protectPets; //whether players may damage pets outside of land claims in pvp worlds + + public boolean config_lockDeathDropsInPvpWorlds; //whether players' dropped on death items are protected in pvp worlds + public boolean config_lockDeathDropsInNonPvpWorlds; //whether players' dropped on death items are protected in non-pvp worlds + + private EconomyHandler economyHandler; + public int config_economy_claimBlocksMaxBonus; //max "bonus" blocks a player can buy. set to zero for no limit. + public double config_economy_claimBlocksPurchaseCost; //cost to purchase a claim block. set to zero to disable purchase. + public double config_economy_claimBlocksSellValue; //return on a sold claim block. set to zero to disable sale. + + public boolean config_blockClaimExplosions; //whether explosions may destroy claimed blocks + public boolean config_blockSurfaceCreeperExplosions; //whether creeper explosions near or above the surface destroy blocks + public boolean config_blockSurfaceOtherExplosions; //whether non-creeper explosions near or above the surface destroy blocks + public boolean config_blockSkyTrees; //whether players can build trees on platforms in the sky + + public boolean config_fireSpreads; //whether fire spreads outside of claims + public boolean config_fireDestroys; //whether fire destroys blocks outside of claims + + public boolean config_whisperNotifications; //whether whispered messages will broadcast to administrators in game + public boolean config_signNotifications; //whether sign content will broadcast to administrators in game + public ArrayList config_eavesdrop_whisperCommands; //list of whisper commands to eavesdrop on + + public boolean config_endermenMoveBlocks; //whether or not endermen may move blocks around + public boolean config_claims_ravagersBreakBlocks; //whether or not ravagers may break blocks in claims + public boolean config_silverfishBreakBlocks; //whether silverfish may break blocks + public boolean config_creaturesTrampleCrops; //whether or not non-player entities may trample crops + public boolean config_rabbitsEatCrops; //whether or not rabbits may eat crops + public boolean config_zombiesBreakDoors; //whether or not hard-mode zombies may break down wooden doors + + public int config_ipLimit; //how many players can share an IP address + + public HashMap config_seaLevelOverride; //override for sea level, because bukkit doesn't report the right value for all situations + + public boolean config_limitTreeGrowth; //whether trees should be prevented from growing into a claim from outside + public PistonMode config_pistonMovement; //Setting for piston check options + public boolean config_pistonExplosionSound; //whether pistons make an explosion sound when they get removed + + public boolean config_advanced_fixNegativeClaimblockAmounts; //whether to attempt to fix negative claim block amounts (some addons cause/assume players can go into negative amounts) + public int config_advanced_claim_expiration_check_rate; //How often GP should check for expired claims, amount in seconds + + //custom log settings + public int config_logs_daysToKeep; + public boolean config_logs_socialEnabled; + public boolean config_logs_suspiciousEnabled; + public boolean config_logs_adminEnabled; + public boolean config_logs_debugEnabled; + public boolean config_logs_mutedChatEnabled; + + //ban management plugin interop settings + public boolean config_ban_useCommand; + public String config_ban_commandFormat; + + private String databaseUrl; + private String databaseUserName; + private String databasePassword; + + private Pl3xMapHook pl3xmapHook; + private DatabaseConnection databaseConnection; + + public HashMap ignoreClaimWarningTasks; + + //adds a server log entry + public static synchronized void AddLogEntry(String entry, CustomLogEntryTypes customLogType, boolean excludeFromServerLogs) + { + if (customLogType != null && GriefPrevention.instance.customLogger != null) + { + GriefPrevention.instance.customLogger.AddEntry(entry, customLogType); + } + if (!excludeFromServerLogs) log.info(entry); + } + + public static synchronized void AddLogEntry(String entry, CustomLogEntryTypes customLogType) + { + AddLogEntry(entry, customLogType, false); + } + + public static synchronized void AddLogEntry(String entry) + { + AddLogEntry(entry, CustomLogEntryTypes.Debug); + } + + //initializes well... everything + public void onEnable() + { + instance = this; + log = instance.getLogger(); + + this.loadConfig(); + + this.customLogger = new CustomLogger(); + + AddLogEntry("Finished loading configuration."); + + //when datastore initializes, it loads player and claim data, and posts some stats to the log + if (this.databaseUrl.length() > 0) + { + try + { + DatabaseDataStore databaseStore = new DatabaseDataStore(this.databaseUrl, this.databaseUserName, this.databasePassword); + + if (FlatFileDataStore.hasData()) + { + GriefPrevention.AddLogEntry("There appears to be some data on the hard drive. Migrating those data to the database..."); + FlatFileDataStore flatFileStore = new FlatFileDataStore(); + this.dataStore = flatFileStore; + flatFileStore.migrateData(databaseStore); + GriefPrevention.AddLogEntry("Data migration process complete."); + } + + this.dataStore = databaseStore; + } + catch (Exception e) + { + GriefPrevention.AddLogEntry("Because there was a problem with the database, GriefPrevention will not function properly. Either update the database config settings resolve the issue, or delete those lines from your config.yml so that GriefPrevention can use the file system to store data."); + e.printStackTrace(); + this.getServer().getPluginManager().disablePlugin(this); + return; + } + } + + //if not using the database because it's not configured or because there was a problem, use the file system to store data + //this is the preferred method, as it's simpler than the database scenario + if (this.dataStore == null) + { + File oldclaimdata = new File(getDataFolder(), "ClaimData"); + if (oldclaimdata.exists()) + { + if (!FlatFileDataStore.hasData()) + { + File claimdata = new File("plugins" + File.separator + "GriefPreventionData" + File.separator + "ClaimData"); + oldclaimdata.renameTo(claimdata); + File oldplayerdata = new File(getDataFolder(), "PlayerData"); + File playerdata = new File("plugins" + File.separator + "GriefPreventionData" + File.separator + "PlayerData"); + oldplayerdata.renameTo(playerdata); + } + } + try + { + this.dataStore = new FlatFileDataStore(); + } + catch (Exception e) + { + GriefPrevention.AddLogEntry("Unable to initialize the file system data store. Details:"); + GriefPrevention.AddLogEntry(e.getMessage()); + e.printStackTrace(); + } + } + + String dataMode = (this.dataStore instanceof FlatFileDataStore) ? "(File Mode)" : "(Database Mode)"; + AddLogEntry("Finished loading data " + dataMode + "."); + + //unless claim block accrual is disabled, start the recurring per 10 minute event to give claim blocks to online players + //20L ~ 1 second + if (this.config_claims_blocksAccruedPerHour_default > 0) + { + DeliverClaimBlocksTask task = new DeliverClaimBlocksTask(null, this); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task, 20L * 60 * 10, 20L * 60 * 10); + } + + //start the recurring cleanup event for entities in creative worlds + EntityCleanupTask task = new EntityCleanupTask(0); + this.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 2); + + //start recurring cleanup scan for unused claims belonging to inactive players + FindUnusedClaimsTask task2 = new FindUnusedClaimsTask(); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task2, 20L * 60, 20L * config_advanced_claim_expiration_check_rate); + + // start task to clean up temporary admin claims + AdminClaimExpireTask claimExpireTask = new AdminClaimExpireTask(this); + claimExpireTask.init(); + + //register for events + PluginManager pluginManager = this.getServer().getPluginManager(); + + //player events + PlayerEventHandler playerEventHandler = new PlayerEventHandler(this.dataStore, this); + pluginManager.registerEvents(playerEventHandler, this); + + //block events + BlockEventHandler blockEventHandler = new BlockEventHandler(this.dataStore); + pluginManager.registerEvents(blockEventHandler, this); + + //entity events + entityEventHandler = new EntityEventHandler(this.dataStore, this); + pluginManager.registerEvents(entityEventHandler, this); + + //vault-based economy integration + economyHandler = new EconomyHandler(this); + pluginManager.registerEvents(economyHandler, this); + + new AltitudeListener(this.dataStore, this); + if (getServer().getPluginManager().isPluginEnabled("Pl3xMap")) { + pl3xmapHook = new Pl3xMapHook(this); + } +// databaseConnection = new DatabaseConnection(); // TODO Set up database to pull data from proxyplaytime + ignoreClaimWarningTasks = new HashMap<>(); + AddLogEntry("Boot finished."); + + } + + private void loadConfig() + { + Config.reload(); + //load the config if it exists + FileConfiguration config = YamlConfiguration.loadConfiguration(new File(DataStore.configFilePath)); + FileConfiguration outConfig = new YamlConfiguration(); + outConfig.options().header("Default values are perfect for most servers. If you want to customize and have a question, look for the answer here first: http://dev.bukkit.org/bukkit-plugins/grief-prevention/pages/setup-and-configuration/"); + + //read configuration settings (note defaults) + + //get (deprecated node) claims world names from the config file + List worlds = this.getServer().getWorlds(); + List deprecated_claimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.Worlds"); + + //validate that list + for (int i = 0; i < deprecated_claimsEnabledWorldNames.size(); i++) + { + String worldName = deprecated_claimsEnabledWorldNames.get(i); + World world = this.getServer().getWorld(worldName); + if (world == null) + { + deprecated_claimsEnabledWorldNames.remove(i--); + } + } + + //get (deprecated node) creative world names from the config file + List deprecated_creativeClaimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.CreativeRulesWorlds"); + + //validate that list + for (int i = 0; i < deprecated_creativeClaimsEnabledWorldNames.size(); i++) + { + String worldName = deprecated_creativeClaimsEnabledWorldNames.get(i); + World world = this.getServer().getWorld(worldName); + if (world == null) + { + deprecated_claimsEnabledWorldNames.remove(i--); + } + } + + //get (deprecated) pvp fire placement proximity note and use it if it exists (in the new config format it will be overwritten later). + config_pvp_allowFireNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers", false); + //get (deprecated) pvp lava dump proximity note and use it if it exists (in the new config format it will be overwritten later). + config_pvp_allowLavaNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers", false); + + //decide claim mode for each world + this.config_claims_worldModes = new ConcurrentHashMap<>(); + for (World world : worlds) + { + //is it specified in the config file? + String configSetting = config.getString("GriefPrevention.Claims.Mode." + world.getName()); + if (configSetting != null) + { + ClaimsMode claimsMode = this.configStringToClaimsMode(configSetting); + if (claimsMode != null) + { + this.config_claims_worldModes.put(world, claimsMode); + continue; + } + else + { + GriefPrevention.AddLogEntry("Error: Invalid claim mode \"" + configSetting + "\". Options are Survival and Disabled."); + this.config_claims_worldModes.put(world, ClaimsMode.Disabled); + } + } + + //was it specified in a deprecated config node? + if (deprecated_claimsEnabledWorldNames.contains(world.getName())) + { + this.config_claims_worldModes.put(world, ClaimsMode.Survival); + } + + //does the world's name indicate its purpose? + else if (world.getName().toLowerCase().contains("survival")) + { + this.config_claims_worldModes.put(world, ClaimsMode.Survival); + } + else if (world.getEnvironment() == Environment.NORMAL) + { + this.config_claims_worldModes.put(world, ClaimsMode.Survival); + } + else + { + this.config_claims_worldModes.put(world, ClaimsMode.Disabled); + } + + //if the setting WOULD be disabled but this is a server upgrading from the old config format, + //then default to survival mode for safety's sake (to protect any admin claims which may + //have been created there) + if (this.config_claims_worldModes.get(world) == ClaimsMode.Disabled && + deprecated_claimsEnabledWorldNames.size() > 0) + { + this.config_claims_worldModes.put(world, ClaimsMode.Survival); + } + } + + //pvp worlds list + this.config_pvp_specifiedWorlds = new HashMap<>(); + for (World world : worlds) + { + boolean pvpWorld = config.getBoolean("GriefPrevention.PvP.RulesEnabledInWorld." + world.getName(), world.getPVP()); + this.config_pvp_specifiedWorlds.put(world, pvpWorld); + } + + //sea level + this.config_seaLevelOverride = new HashMap<>(); + for (World world : worlds) + { + int seaLevelOverride = config.getInt("GriefPrevention.SeaLevelOverrides." + world.getName(), -1); + outConfig.set("GriefPrevention.SeaLevelOverrides." + world.getName(), seaLevelOverride); + this.config_seaLevelOverride.put(world.getName(), seaLevelOverride); + } + + this.config_claims_preventGlobalMonsterEggs = config.getBoolean("GriefPrevention.Claims.PreventGlobalMonsterEggs", true); + this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); + this.config_claims_protectCreatures = config.getBoolean("GriefPrevention.Claims.ProtectCreatures", true); + this.config_claims_protectHorses = config.getBoolean("GriefPrevention.Claims.ProtectHorses", true); + this.config_claims_protectDonkeys = config.getBoolean("GriefPrevention.Claims.ProtectDonkeys", true); + this.config_claims_protectLlamas = config.getBoolean("GriefPrevention.Claims.ProtectLlamas", true); + this.config_claims_preventButtonsSwitches = config.getBoolean("GriefPrevention.Claims.PreventButtonsSwitches", true); + this.config_claims_lockWoodenDoors = config.getBoolean("GriefPrevention.Claims.LockWoodenDoors", false); + this.config_claims_lockTrapDoors = config.getBoolean("GriefPrevention.Claims.LockTrapDoors", false); + this.config_claims_lockFenceGates = config.getBoolean("GriefPrevention.Claims.LockFenceGates", true); + this.config_claims_enderPearlsRequireAccessTrust = config.getBoolean("GriefPrevention.Claims.EnderPearlsRequireAccessTrust", true); + this.config_claims_raidTriggersRequireBuildTrust = config.getBoolean("GriefPrevention.Claims.RaidTriggersRequireBuildTrust", true); + this.config_claims_initialBlocks = config.getInt("GriefPrevention.Claims.InitialBlocks", 100); + this.config_claims_blocksAccruedPerHour_default = config.getInt("GriefPrevention.Claims.BlocksAccruedPerHour", 100); + this.config_claims_blocksAccruedPerHour_default = config.getInt("GriefPrevention.Claims.Claim Blocks Accrued Per Hour.Default", config_claims_blocksAccruedPerHour_default); + this.config_claims_maxAccruedBlocks_default = config.getInt("GriefPrevention.Claims.MaxAccruedBlocks", 80000); + this.config_claims_maxAccruedBlocks_default = config.getInt("GriefPrevention.Claims.Max Accrued Claim Blocks.Default", this.config_claims_maxAccruedBlocks_default); + this.config_claims_accruedIdleThreshold = config.getInt("GriefPrevention.Claims.AccruedIdleThreshold", 0); + this.config_claims_accruedIdleThreshold = config.getInt("GriefPrevention.Claims.Accrued Idle Threshold", this.config_claims_accruedIdleThreshold); + this.config_claims_accruedIdlePercent = config.getInt("GriefPrevention.Claims.AccruedIdlePercent", 0); + this.config_claims_abandonReturnRatio = config.getDouble("GriefPrevention.Claims.AbandonReturnRatio", 1.0D); + this.config_claims_automaticClaimsForNewPlayersRadius = config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", 4); + this.config_claims_automaticClaimsForNewPlayersRadiusMin = Math.max(0, Math.min(this.config_claims_automaticClaimsForNewPlayersRadius, + config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadiusMinimum", 0))); + this.config_claims_claimsExtendIntoGroundDistance = Math.abs(config.getInt("GriefPrevention.Claims.ExtendIntoGroundDistance", 5)); + this.config_claims_minWidth = config.getInt("GriefPrevention.Claims.MinimumWidth", 5); + this.config_claims_minArea = config.getInt("GriefPrevention.Claims.MinimumArea", 100); + this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", Integer.MIN_VALUE); + this.config_claims_chestClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.ChestClaimDays", 7); + this.config_claims_unusedClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.UnusedClaimDays", 14); + this.config_claims_expirationDays = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.DaysInactive", 60); + this.config_claims_expirationExemptionTotalBlocks = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasTotalClaimBlocks", 10000); + this.config_claims_expirationExemptionBonusBlocks = config.getInt("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasBonusClaimBlocks", 5000); + this.config_claims_survivalAutoNatureRestoration = config.getBoolean("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", false); + this.config_claims_allowTrappedInAdminClaims = config.getBoolean("GriefPrevention.Claims.AllowTrappedInAdminClaims", false); + + this.config_claims_maxClaimsPerPlayer = config.getInt("GriefPrevention.Claims.MaximumNumberOfClaimsPerPlayer", 0); + this.config_claims_respectWorldGuard = config.getBoolean("GriefPrevention.Claims.CreationRequiresWorldGuardBuildPermission", true); + this.config_claims_villagerTradingRequiresTrust = config.getBoolean("GriefPrevention.Claims.VillagerTradingRequiresPermission", true); + String accessTrustSlashCommands = config.getString("GriefPrevention.Claims.CommandsRequiringAccessTrust", "/sethome"); + this.config_claims_supplyPlayerManual = config.getBoolean("GriefPrevention.Claims.DeliverManuals", true); + this.config_claims_manualDeliveryDelaySeconds = config.getInt("GriefPrevention.Claims.ManualDeliveryDelaySeconds", 30); + this.config_claims_ravagersBreakBlocks = config.getBoolean("GriefPrevention.Claims.RavagersBreakBlocks", true); + + this.config_claims_firespreads = config.getBoolean("GriefPrevention.Claims.FireSpreadsInClaims", false); + this.config_claims_firedamages = config.getBoolean("GriefPrevention.Claims.FireDamagesInClaims", false); + this.config_claims_lecternReadingRequiresAccessTrust = config.getBoolean("GriefPrevention.Claims.LecternReadingRequiresAccessTrust", true); + + this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true); + this.config_spam_loginCooldownSeconds = config.getInt("GriefPrevention.Spam.LoginCooldownSeconds", 60); + this.config_spam_loginLogoutNotificationsPerMinute = config.getInt("GriefPrevention.Spam.LoginLogoutNotificationsPerMinute", 5); + this.config_spam_warningMessage = config.getString("GriefPrevention.Spam.WarningMessage", "Please reduce your noise level. Spammers will be banned."); + this.config_spam_allowedIpAddresses = config.getString("GriefPrevention.Spam.AllowedIpAddresses", "1.2.3.4; 5.6.7.8"); + 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;/global;/local"); + slashCommandsToMonitor = config.getString("GriefPrevention.Spam.ChatSlashCommands", slashCommandsToMonitor); + this.config_spam_deathMessageCooldownSeconds = config.getInt("GriefPrevention.Spam.DeathMessageCooldownSeconds", 120); + this.config_spam_logoutMessageDelaySeconds = config.getInt("GriefPrevention.Spam.Logout Message Delay In Seconds", 0); + + this.config_pvp_protectFreshSpawns = config.getBoolean("GriefPrevention.PvP.ProtectFreshSpawns", true); + this.config_pvp_punishLogout = config.getBoolean("GriefPrevention.PvP.PunishLogout", true); + this.config_pvp_combatTimeoutSeconds = config.getInt("GriefPrevention.PvP.CombatTimeoutSeconds", 15); + this.config_pvp_allowCombatItemDrop = config.getBoolean("GriefPrevention.PvP.AllowCombatItemDrop", false); + String bannedPvPCommandsList = config.getString("GriefPrevention.PvP.BlockedSlashCommands", "/home;/vanish;/spawn;/tpa"); + + this.config_economy_claimBlocksMaxBonus = config.getInt("GriefPrevention.Economy.ClaimBlocksMaxBonus", 0); + this.config_economy_claimBlocksPurchaseCost = config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0); + this.config_economy_claimBlocksSellValue = config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0); + + this.config_lockDeathDropsInPvpWorlds = config.getBoolean("GriefPrevention.ProtectItemsDroppedOnDeath.PvPWorlds", false); + this.config_lockDeathDropsInNonPvpWorlds = config.getBoolean("GriefPrevention.ProtectItemsDroppedOnDeath.NonPvPWorlds", true); + + this.config_blockClaimExplosions = config.getBoolean("GriefPrevention.BlockLandClaimExplosions", true); + this.config_blockSurfaceCreeperExplosions = config.getBoolean("GriefPrevention.BlockSurfaceCreeperExplosions", true); + this.config_blockSurfaceOtherExplosions = config.getBoolean("GriefPrevention.BlockSurfaceOtherExplosions", true); + this.config_blockSkyTrees = config.getBoolean("GriefPrevention.LimitSkyTrees", true); + this.config_limitTreeGrowth = config.getBoolean("GriefPrevention.LimitTreeGrowth", false); + this.config_pistonExplosionSound = config.getBoolean("GriefPrevention.PistonExplosionSound", true); + this.config_pistonMovement = PistonMode.of(config.getString("GriefPrevention.PistonMovement", "CLAIMS_ONLY")); + if (config.isBoolean("GriefPrevention.LimitPistonsToLandClaims") && !config.getBoolean("GriefPrevention.LimitPistonsToLandClaims")) + this.config_pistonMovement = PistonMode.EVERYWHERE_SIMPLE; + if (config.isBoolean("GriefPrevention.CheckPistonMovement") && !config.getBoolean("GriefPrevention.CheckPistonMovement")) + this.config_pistonMovement = PistonMode.IGNORED; + + this.config_fireSpreads = config.getBoolean("GriefPrevention.FireSpreads", false); + this.config_fireDestroys = config.getBoolean("GriefPrevention.FireDestroys", false); + + this.config_whisperNotifications = config.getBoolean("GriefPrevention.AdminsGetWhispers", true); + this.config_signNotifications = config.getBoolean("GriefPrevention.AdminsGetSignNotifications", true); + String whisperCommandsToMonitor = config.getString("GriefPrevention.WhisperCommands", "/tell;/pm;/r;/whisper;/msg"); + whisperCommandsToMonitor = config.getString("GriefPrevention.Spam.WhisperSlashCommands", whisperCommandsToMonitor); + + this.config_ipLimit = config.getInt("GriefPrevention.MaxPlayersPerIpAddress", 3); + + this.config_endermenMoveBlocks = config.getBoolean("GriefPrevention.EndermenMoveBlocks", false); + this.config_silverfishBreakBlocks = config.getBoolean("GriefPrevention.SilverfishBreakBlocks", false); + this.config_creaturesTrampleCrops = config.getBoolean("GriefPrevention.CreaturesTrampleCrops", false); + this.config_rabbitsEatCrops = config.getBoolean("GriefPrevention.RabbitsEatCrops", true); + this.config_zombiesBreakDoors = config.getBoolean("GriefPrevention.HardModeZombiesBreakDoors", false); + this.config_ban_useCommand = config.getBoolean("GriefPrevention.UseBanCommand", false); + this.config_ban_commandFormat = config.getString("GriefPrevention.BanCommandPattern", "ban %name% %reason%"); + + //default for claim investigation tool + String investigationToolMaterialName = Material.STICK.name(); + + //get investigation tool from config + investigationToolMaterialName = config.getString("GriefPrevention.Claims.InvestigationTool", investigationToolMaterialName); + + //validate investigation tool + this.config_claims_investigationTool = Material.getMaterial(investigationToolMaterialName); + if (this.config_claims_investigationTool == null) + { + GriefPrevention.AddLogEntry("ERROR: Material " + investigationToolMaterialName + " not found. Defaulting to the stick. Please update your config.yml."); + this.config_claims_investigationTool = Material.STICK; + } + + //default for claim creation/modification tool + String modificationToolMaterialName = Material.GOLDEN_SHOVEL.name(); + + //get modification tool from config + modificationToolMaterialName = config.getString("GriefPrevention.Claims.ModificationTool", modificationToolMaterialName); + + //validate modification tool + this.config_claims_modificationTool = Material.getMaterial(modificationToolMaterialName); + if (this.config_claims_modificationTool == null) + { + GriefPrevention.AddLogEntry("ERROR: Material " + modificationToolMaterialName + " not found. Defaulting to the golden shovel. Please update your config.yml."); + this.config_claims_modificationTool = Material.GOLDEN_SHOVEL; + } + + this.config_pvp_noCombatInPlayerLandClaims = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.PlayerOwnedClaims", false); // might break stuff + this.config_pvp_noCombatInAdminLandClaims = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeClaims", false); // might break stuff + this.config_pvp_noCombatInAdminSubdivisions = config.getBoolean("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeSubdivisions", false); // might break stuff + this.config_pvp_allowLavaNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.PvPWorlds", true); + this.config_pvp_allowLavaNearPlayers_NonPvp = config.getBoolean("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.NonPvPWorlds", false); + this.config_pvp_allowFireNearPlayers = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.PvPWorlds", true); + this.config_pvp_allowFireNearPlayers_NonPvp = config.getBoolean("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.NonPvPWorlds", false); + this.config_pvp_protectPets = config.getBoolean("GriefPrevention.PvP.ProtectPetsOutsideLandClaims", false); + + //optional database settings + this.databaseUrl = config.getString("GriefPrevention.Database.URL", ""); + this.databaseUserName = config.getString("GriefPrevention.Database.UserName", ""); + this.databasePassword = config.getString("GriefPrevention.Database.Password", ""); + + this.config_advanced_fixNegativeClaimblockAmounts = config.getBoolean("GriefPrevention.Advanced.fixNegativeClaimblockAmounts", true); + this.config_advanced_claim_expiration_check_rate = config.getInt("GriefPrevention.Advanced.ClaimExpirationCheckRate", 60); + + //custom logger settings + this.config_logs_daysToKeep = config.getInt("GriefPrevention.Abridged Logs.Days To Keep", 7); + this.config_logs_socialEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Social Activity", true); + this.config_logs_suspiciousEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Suspicious Activity", true); + this.config_logs_adminEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Administrative Activity", false); + this.config_logs_debugEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Debug", false); + this.config_logs_mutedChatEnabled = config.getBoolean("GriefPrevention.Abridged Logs.Included Entry Types.Muted Chat Messages", false); + + //claims mode by world + for (World world : this.config_claims_worldModes.keySet()) + { + outConfig.set( + "GriefPrevention.Claims.Mode." + world.getName(), + this.config_claims_worldModes.get(world).name()); + } + + + outConfig.set("GriefPrevention.Claims.PreventGlobalMonsterEggs", this.config_claims_preventGlobalMonsterEggs); + outConfig.set("GriefPrevention.Claims.PreventTheft", this.config_claims_preventTheft); + outConfig.set("GriefPrevention.Claims.ProtectCreatures", this.config_claims_protectCreatures); + outConfig.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); + outConfig.set("GriefPrevention.Claims.LockWoodenDoors", this.config_claims_lockWoodenDoors); + outConfig.set("GriefPrevention.Claims.LockTrapDoors", this.config_claims_lockTrapDoors); + outConfig.set("GriefPrevention.Claims.LockFenceGates", this.config_claims_lockFenceGates); + outConfig.set("GriefPrevention.Claims.EnderPearlsRequireAccessTrust", this.config_claims_enderPearlsRequireAccessTrust); + outConfig.set("GriefPrevention.Claims.RaidTriggersRequireBuildTrust", this.config_claims_raidTriggersRequireBuildTrust); + outConfig.set("GriefPrevention.Claims.ProtectHorses", this.config_claims_protectHorses); + outConfig.set("GriefPrevention.Claims.ProtectDonkeys", this.config_claims_protectDonkeys); + outConfig.set("GriefPrevention.Claims.ProtectLlamas", this.config_claims_protectLlamas); + outConfig.set("GriefPrevention.Claims.InitialBlocks", this.config_claims_initialBlocks); + outConfig.set("GriefPrevention.Claims.Claim Blocks Accrued Per Hour.Default", this.config_claims_blocksAccruedPerHour_default); + outConfig.set("GriefPrevention.Claims.Max Accrued Claim Blocks.Default", this.config_claims_maxAccruedBlocks_default); + outConfig.set("GriefPrevention.Claims.Accrued Idle Threshold", this.config_claims_accruedIdleThreshold); + outConfig.set("GriefPrevention.Claims.AccruedIdlePercent", this.config_claims_accruedIdlePercent); + outConfig.set("GriefPrevention.Claims.AbandonReturnRatio", this.config_claims_abandonReturnRatio); + outConfig.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", this.config_claims_automaticClaimsForNewPlayersRadius); + outConfig.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadiusMinimum", this.config_claims_automaticClaimsForNewPlayersRadiusMin); + outConfig.set("GriefPrevention.Claims.ExtendIntoGroundDistance", this.config_claims_claimsExtendIntoGroundDistance); + outConfig.set("GriefPrevention.Claims.MinimumWidth", this.config_claims_minWidth); + outConfig.set("GriefPrevention.Claims.MinimumArea", this.config_claims_minArea); + outConfig.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth); + outConfig.set("GriefPrevention.Claims.InvestigationTool", this.config_claims_investigationTool.name()); + outConfig.set("GriefPrevention.Claims.ModificationTool", this.config_claims_modificationTool.name()); + outConfig.set("GriefPrevention.Claims.Expiration.ChestClaimDays", this.config_claims_chestClaimExpirationDays); + outConfig.set("GriefPrevention.Claims.Expiration.UnusedClaimDays", this.config_claims_unusedClaimExpirationDays); + outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.DaysInactive", this.config_claims_expirationDays); + outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasTotalClaimBlocks", this.config_claims_expirationExemptionTotalBlocks); + outConfig.set("GriefPrevention.Claims.Expiration.AllClaims.ExceptWhenOwnerHasBonusClaimBlocks", this.config_claims_expirationExemptionBonusBlocks); + outConfig.set("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", this.config_claims_survivalAutoNatureRestoration); + outConfig.set("GriefPrevention.Claims.AllowTrappedInAdminClaims", this.config_claims_allowTrappedInAdminClaims); + outConfig.set("GriefPrevention.Claims.MaximumNumberOfClaimsPerPlayer", this.config_claims_maxClaimsPerPlayer); + outConfig.set("GriefPrevention.Claims.CreationRequiresWorldGuardBuildPermission", this.config_claims_respectWorldGuard); + outConfig.set("GriefPrevention.Claims.VillagerTradingRequiresPermission", this.config_claims_villagerTradingRequiresTrust); + outConfig.set("GriefPrevention.Claims.CommandsRequiringAccessTrust", accessTrustSlashCommands); + outConfig.set("GriefPrevention.Claims.DeliverManuals", config_claims_supplyPlayerManual); + outConfig.set("GriefPrevention.Claims.ManualDeliveryDelaySeconds", config_claims_manualDeliveryDelaySeconds); + outConfig.set("GriefPrevention.Claims.RavagersBreakBlocks", config_claims_ravagersBreakBlocks); + + outConfig.set("GriefPrevention.Claims.FireSpreadsInClaims", config_claims_firespreads); + outConfig.set("GriefPrevention.Claims.FireDamagesInClaims", config_claims_firedamages); + outConfig.set("GriefPrevention.Claims.LecternReadingRequiresAccessTrust", config_claims_lecternReadingRequiresAccessTrust); + + outConfig.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled); + outConfig.set("GriefPrevention.Spam.LoginCooldownSeconds", this.config_spam_loginCooldownSeconds); + outConfig.set("GriefPrevention.Spam.LoginLogoutNotificationsPerMinute", this.config_spam_loginLogoutNotificationsPerMinute); + outConfig.set("GriefPrevention.Spam.ChatSlashCommands", slashCommandsToMonitor); + outConfig.set("GriefPrevention.Spam.WhisperSlashCommands", whisperCommandsToMonitor); + outConfig.set("GriefPrevention.Spam.WarningMessage", this.config_spam_warningMessage); + outConfig.set("GriefPrevention.Spam.BanOffenders", this.config_spam_banOffenders); + outConfig.set("GriefPrevention.Spam.BanMessage", this.config_spam_banMessage); + outConfig.set("GriefPrevention.Spam.AllowedIpAddresses", this.config_spam_allowedIpAddresses); + outConfig.set("GriefPrevention.Spam.DeathMessageCooldownSeconds", this.config_spam_deathMessageCooldownSeconds); + outConfig.set("GriefPrevention.Spam.Logout Message Delay In Seconds", this.config_spam_logoutMessageDelaySeconds); + + for (World world : worlds) + { + outConfig.set("GriefPrevention.PvP.RulesEnabledInWorld." + world.getName(), this.pvpRulesApply(world)); + } + outConfig.set("GriefPrevention.PvP.ProtectFreshSpawns", this.config_pvp_protectFreshSpawns); + outConfig.set("GriefPrevention.PvP.PunishLogout", this.config_pvp_punishLogout); + outConfig.set("GriefPrevention.PvP.CombatTimeoutSeconds", this.config_pvp_combatTimeoutSeconds); + outConfig.set("GriefPrevention.PvP.AllowCombatItemDrop", this.config_pvp_allowCombatItemDrop); + outConfig.set("GriefPrevention.PvP.BlockedSlashCommands", bannedPvPCommandsList); + outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.PlayerOwnedClaims", this.config_pvp_noCombatInPlayerLandClaims); + outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeClaims", this.config_pvp_noCombatInAdminLandClaims); + outConfig.set("GriefPrevention.PvP.ProtectPlayersInLandClaims.AdministrativeSubdivisions", this.config_pvp_noCombatInAdminSubdivisions); + outConfig.set("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.PvPWorlds", this.config_pvp_allowLavaNearPlayers); + outConfig.set("GriefPrevention.PvP.AllowLavaDumpingNearOtherPlayers.NonPvPWorlds", this.config_pvp_allowLavaNearPlayers_NonPvp); + outConfig.set("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.PvPWorlds", this.config_pvp_allowFireNearPlayers); + outConfig.set("GriefPrevention.PvP.AllowFlintAndSteelNearOtherPlayers.NonPvPWorlds", this.config_pvp_allowFireNearPlayers_NonPvp); + outConfig.set("GriefPrevention.PvP.ProtectPetsOutsideLandClaims", this.config_pvp_protectPets); + + outConfig.set("GriefPrevention.Economy.ClaimBlocksMaxBonus", this.config_economy_claimBlocksMaxBonus); + outConfig.set("GriefPrevention.Economy.ClaimBlocksPurchaseCost", this.config_economy_claimBlocksPurchaseCost); + outConfig.set("GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue); + + outConfig.set("GriefPrevention.ProtectItemsDroppedOnDeath.PvPWorlds", this.config_lockDeathDropsInPvpWorlds); + outConfig.set("GriefPrevention.ProtectItemsDroppedOnDeath.NonPvPWorlds", this.config_lockDeathDropsInNonPvpWorlds); + + outConfig.set("GriefPrevention.BlockLandClaimExplosions", this.config_blockClaimExplosions); + outConfig.set("GriefPrevention.BlockSurfaceCreeperExplosions", this.config_blockSurfaceCreeperExplosions); + outConfig.set("GriefPrevention.BlockSurfaceOtherExplosions", this.config_blockSurfaceOtherExplosions); + outConfig.set("GriefPrevention.LimitSkyTrees", this.config_blockSkyTrees); + outConfig.set("GriefPrevention.LimitTreeGrowth", this.config_limitTreeGrowth); + outConfig.set("GriefPrevention.PistonMovement", this.config_pistonMovement.name()); + outConfig.set("GriefPrevention.CheckPistonMovement", null); + outConfig.set("GriefPrevention.LimitPistonsToLandClaims", null); + outConfig.set("GriefPrevention.PistonExplosionSound", this.config_pistonExplosionSound); + + outConfig.set("GriefPrevention.FireSpreads", this.config_fireSpreads); + outConfig.set("GriefPrevention.FireDestroys", this.config_fireDestroys); + + outConfig.set("GriefPrevention.AdminsGetWhispers", this.config_whisperNotifications); + outConfig.set("GriefPrevention.AdminsGetSignNotifications", this.config_signNotifications); + + outConfig.set("GriefPrevention.MaxPlayersPerIpAddress", this.config_ipLimit); + + outConfig.set("GriefPrevention.Siege.DoorsOpenDelayInSeconds", this.config_siege_doorsOpenSeconds); + outConfig.set("GriefPrevention.Siege.CooldownEndInMinutes", this.config_siege_cooldownEndInMinutes); + outConfig.set("GriefPrevention.EndermenMoveBlocks", this.config_endermenMoveBlocks); + outConfig.set("GriefPrevention.SilverfishBreakBlocks", this.config_silverfishBreakBlocks); + outConfig.set("GriefPrevention.CreaturesTrampleCrops", this.config_creaturesTrampleCrops); + outConfig.set("GriefPrevention.RabbitsEatCrops", this.config_rabbitsEatCrops); + outConfig.set("GriefPrevention.HardModeZombiesBreakDoors", this.config_zombiesBreakDoors); + + outConfig.set("GriefPrevention.Database.URL", this.databaseUrl); + outConfig.set("GriefPrevention.Database.UserName", this.databaseUserName); + outConfig.set("GriefPrevention.Database.Password", this.databasePassword); + + outConfig.set("GriefPrevention.UseBanCommand", this.config_ban_useCommand); + outConfig.set("GriefPrevention.BanCommandPattern", this.config_ban_commandFormat); + + outConfig.set("GriefPrevention.Advanced.fixNegativeClaimblockAmounts", this.config_advanced_fixNegativeClaimblockAmounts); + outConfig.set("GriefPrevention.Advanced.ClaimExpirationCheckRate", this.config_advanced_claim_expiration_check_rate); + + //custom logger settings + outConfig.set("GriefPrevention.Abridged Logs.Days To Keep", this.config_logs_daysToKeep); + outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Social Activity", this.config_logs_socialEnabled); + outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Suspicious Activity", this.config_logs_suspiciousEnabled); + outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Administrative Activity", this.config_logs_adminEnabled); + outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Debug", this.config_logs_debugEnabled); + outConfig.set("GriefPrevention.Abridged Logs.Included Entry Types.Muted Chat Messages", this.config_logs_mutedChatEnabled); + + try + { + outConfig.save(DataStore.configFilePath); + } + catch (IOException exception) + { + AddLogEntry("Unable to write to the configuration file at \"" + DataStore.configFilePath + "\""); + } + + //try to parse the list of commands requiring access trust in land claims + this.config_claims_commandsRequiringAccessTrust = new ArrayList<>(); + String[] commands = accessTrustSlashCommands.split(";"); + for (String command : commands) + { + if (!command.isEmpty()) + { + this.config_claims_commandsRequiringAccessTrust.add(command.trim().toLowerCase()); + } + } + + //try to parse the list of commands which should be monitored for spam + this.config_spam_monitorSlashCommands = new ArrayList<>(); + commands = slashCommandsToMonitor.split(";"); + for (String command : commands) + { + this.config_spam_monitorSlashCommands.add(command.trim().toLowerCase()); + } + + //try to parse the list of commands which should be included in eavesdropping + this.config_eavesdrop_whisperCommands = new ArrayList<>(); + commands = whisperCommandsToMonitor.split(";"); + for (String command : commands) + { + this.config_eavesdrop_whisperCommands.add(command.trim().toLowerCase()); + } + + //try to parse the list of commands which should be banned during pvp combat + this.config_pvp_blockedCommands = new ArrayList<>(); + commands = bannedPvPCommandsList.split(";"); + for (String command : commands) + { + this.config_pvp_blockedCommands.add(command.trim().toLowerCase()); + } + } + + private ClaimsMode configStringToClaimsMode(String configSetting) + { + if (configSetting.equalsIgnoreCase("Survival")) + { + return ClaimsMode.Survival; + } + else if (configSetting.equalsIgnoreCase("Disabled")) + { + return ClaimsMode.Disabled; + } + else + { + return null; + } + } + + //handles slash commands + public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) + { + + Player player = null; + if (sender instanceof Player) + { + player = (Player) sender; + } + + //claim + if (cmd.getName().equalsIgnoreCase("claim") && player != null) + { + if (!GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); + return true; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + //if he's at the claim count per player limit already and doesn't have permission to bypass, display an error message + if (GriefPrevention.instance.config_claims_maxClaimsPerPlayer > 0 && + !player.hasPermission("griefprevention.overrideclaimcountlimit") && + playerData.getClaims().size() >= GriefPrevention.instance.config_claims_maxClaimsPerPlayer) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimCreationFailedOverClaimCountLimit); + return true; + } + + //default is chest claim radius, unless -1 + int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; + if (radius < 0) radius = (int) Math.ceil(Math.sqrt(GriefPrevention.instance.config_claims_minArea) / 2); + + //if player has any claims, respect claim minimum size setting + if (playerData.getClaims().size() > 0) + { + //if player has exactly one land claim, this requires the claim modification tool to be in hand (or creative mode player) + if (playerData.getClaims().size() == 1 && player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.MustHoldModificationToolForThat); + return true; + } + + radius = (int) Math.ceil(Math.sqrt(GriefPrevention.instance.config_claims_minArea) / 2); + } + + //allow for specifying the radius + if (args.length > 0) + { + if (playerData.getClaims().size() < 2 && player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.RadiusRequiresGoldenShovel); + return true; + } + + int specifiedRadius; + try + { + specifiedRadius = Integer.parseInt(args[0]); + } + catch (NumberFormatException e) + { + return false; + } + + if (specifiedRadius < radius) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.MinimumRadius, String.valueOf(radius)); + return true; + } + else + { + radius = specifiedRadius; + } + } + + if (radius < 0) radius = 0; + + Location lc = player.getLocation().add(-radius, 0, -radius); + Location gc = player.getLocation().add(radius, 0, radius); + + //player must have sufficient unused claim blocks + int area = Math.abs((gc.getBlockX() - lc.getBlockX() + 1) * (gc.getBlockZ() - lc.getBlockZ() + 1)); + int remaining = playerData.getRemainingClaimBlocks(); + if (remaining < area) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(area - remaining)); + GriefPrevention.instance.dataStore.tryAdvertiseAdminAlternatives(player); + return true; + } + + CreateClaimResult result = this.dataStore.createClaim(lc.getWorld(), + lc.getBlockX(), gc.getBlockX(), + lc.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance - 1, + gc.getWorld().getHighestBlockYAt(gc) - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance - 1, + lc.getBlockZ(), gc.getBlockZ(), + player.getUniqueId(), null, null, player); + if (!result.succeeded) + { + if (result.claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); + + Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapRegion); + } + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess); + + Visualization visualization = Visualization.FromClaim(result.claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + playerData.claimResizing = null; + playerData.lastShovelLocation = null; + + this.autoExtendClaim(result.claim); + } + + return true; + } + + //extendclaim + if (cmd.getName().equalsIgnoreCase("extendclaim") && player != null) + { + if (args.length < 1) + { + return false; + } + + int amount; + try + { + amount = Integer.parseInt(args[0]); + } + catch (NumberFormatException e) + { + return false; + } + + //requires claim modification tool in hand + if (player.getGameMode() != GameMode.CREATIVE && player.getItemInHand().getType() != GriefPrevention.instance.config_claims_modificationTool) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.MustHoldModificationToolForThat); + return true; + } + + //must be standing in a land claim + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, playerData.lastClaim); + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.StandInClaimToResize); + return true; + } + + //must have permission to edit the land claim you're in + Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Edit, null); + if (errorMessage != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); + return true; + } + + //determine new corner coordinates + org.bukkit.util.Vector direction = player.getLocation().getDirection(); + if (direction.getY() > .75) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.ClaimsExtendToSky); + return true; + } + + if (direction.getY() < -.75) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.ClaimsAutoExtendDownward); + return true; + } + + Location lc = claim.getLesserBoundaryCorner(); + Location gc = claim.getGreaterBoundaryCorner(); + int newx1 = lc.getBlockX(); + int newx2 = gc.getBlockX(); + int newy1 = lc.getBlockY(); + int newy2 = gc.getBlockY(); + int newz1 = lc.getBlockZ(); + int newz2 = gc.getBlockZ(); + + //if changing Z only + if (Math.abs(direction.getX()) < .3) + { + if (direction.getZ() > 0) + { + newz2 += amount; //north + } + else + { + newz1 -= amount; //south + } + } + + //if changing X only + else if (Math.abs(direction.getZ()) < .3) + { + if (direction.getX() > 0) + { + newx2 += amount; //east + } + else + { + newx1 -= amount; //west + } + } + + //diagonals + else + { + if (direction.getX() > 0) + { + newx2 += amount; + } + else + { + newx1 -= amount; + } + + if (direction.getZ() > 0) + { + newz2 += amount; + } + else + { + newz1 -= amount; + } + } + + //attempt resize + playerData.claimResizing = claim; + this.dataStore.resizeClaimWithChecks(player, playerData, newx1, newx2, newy1, newy2, newz1, newz2); + playerData.claimResizing = null; + + return true; + } + + //abandonclaim + if (cmd.getName().equalsIgnoreCase("abandonclaim") && player != null) + { + return this.abandonClaimHandler(player, false); + } + + //abandontoplevelclaim + if (cmd.getName().equalsIgnoreCase("abandontoplevelclaim") && player != null) + { + return this.abandonClaimHandler(player, true); + } + + //forceabandonclaim + if (cmd.getName().equalsIgnoreCase("forceabandonclaim") && player != null) + { + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, true, null); + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimMissing); + return true; + } + PlayerData ownerData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(claim.ownerID); + OfflinePlayer ownerInfo = Bukkit.getServer().getOfflinePlayer(claim.ownerID); + Bukkit.getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new CleanupUnusedClaimTask(claim, ownerData, ownerInfo, true), 0); + return true; + } + + //ignoreclaims + if (cmd.getName().equalsIgnoreCase("ignoreclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + playerData.ignoreClaims = !playerData.ignoreClaims; + + UUID uuid = player.getUniqueId(); + //toggle ignore claims mode on or off + if (!playerData.ignoreClaims) + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.RespectingClaims); + if(ignoreClaimWarningTasks.containsKey(uuid)) + { + ignoreClaimWarningTasks.get(uuid).cancel(); + ignoreClaimWarningTasks.remove(uuid); + } + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.IgnoringClaims); + ignoreClaimWarningTasks.put(uuid, new IgnoreClaimWarningTask(this, uuid)); + } + + return true; + } + + //abandonallclaims + else if (cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) + { + if (args.length > 1) return false; + + if (args.length != 1 || !"confirm".equalsIgnoreCase(args[0])) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConfirmAbandonAllClaims); + return true; + } + + //count claims + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + int originalClaimCount = playerData.getClaims().size(); + + //check count + if (originalClaimCount == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.YouHaveNoClaims); + return true; + } + + if (this.config_claims_abandonReturnRatio != 1.0D) + { + //adjust claim blocks + for (Claim claim : playerData.getClaims()) + { + playerData.setAccruedClaimBlocks(playerData.getAccruedClaimBlocks() - (int) Math.ceil((claim.getArea() * (1 - this.config_claims_abandonReturnRatio)))); + } + } + + + //delete them + this.dataStore.deleteClaimsForPlayer(player.getUniqueId(), false); + + //inform the player + int remainingBlocks = playerData.getRemainingClaimBlocks(); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SuccessfulAbandon, String.valueOf(remainingBlocks)); + + //revert any current visualization + Visualization.Revert(player); + + return true; + } + + //restore nature + else if (cmd.getName().equalsIgnoreCase("restorenature") && player != null) + { + //change shovel mode + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.RestoreNature; + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.RestoreNatureActivate); + return true; + } + + //restore nature aggressive mode + else if (cmd.getName().equalsIgnoreCase("restorenatureaggressive") && player != null) + { + //change shovel mode + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.RestoreNatureAggressive; + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.RestoreNatureAggressiveActivate); + return true; + } + + //restore nature fill mode + else if (cmd.getName().equalsIgnoreCase("restorenaturefill") && player != null) + { + //change shovel mode + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.RestoreNatureFill; + + //set radius based on arguments + playerData.fillRadius = 2; + if (args.length > 0) + { + try + { + playerData.fillRadius = Integer.parseInt(args[0]); + } + catch (Exception exception) { } + } + + if (playerData.fillRadius < 0) playerData.fillRadius = 2; + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.FillModeActive, String.valueOf(playerData.fillRadius)); + return true; + } + + //trust + else if (cmd.getName().equalsIgnoreCase("trust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + //most trust commands use this helper method, it keeps them consistent + this.handleTrustCommand(player, ClaimPermission.Build, args[0]); + + return true; + } + + //transferclaim + else if (cmd.getName().equalsIgnoreCase("transferclaim") && player != null) + { + //which claim is the user in? + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TransferClaimMissing); + return true; + } + + //check additional permission for admin claims + if (claim.isAdminClaim() && !player.hasPermission("griefprevention.adminclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TransferClaimPermission); + return true; + } + + UUID newOwnerID = null; //no argument = make an admin claim + String ownerName = "admin"; + + if (args.length > 0) + { + OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); + if (targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + newOwnerID = targetPlayer.getUniqueId(); + ownerName = targetPlayer.getName(); + } + + //change ownerhsip + try + { + this.dataStore.changeClaimOwner(claim, newOwnerID); + } + catch (NoTransferException e) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TransferTopLevel); + return true; + } + catch (DataStore.NoClaimblocksTransferException e) + { + player.sendMiniMessage("target does have the claimblocks to claim this area.", null); + return true; + } + + //confirm + GriefPrevention.sendMessage(player, TextMode.Success, Messages.TransferSuccess); + GriefPrevention.AddLogEntry(player.getName() + " transferred a claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " to " + ownerName + ".", CustomLogEntryTypes.AdminActivity); + + return true; + } + + //trustlist + else if (cmd.getName().equalsIgnoreCase("trustlist") && player != null) + { + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); + + //if no claim here, error message + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrustListNoClaim); + return true; + } + + //if no permission to manage permissions, error message + Supplier errorMessage = claim.checkPermission(player, ClaimPermission.Manage, null); + if (errorMessage != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, errorMessage.get()); + return true; + } + + //otherwise build a list of explicit permissions by permission level + //and send that to the player + ArrayList builders = new ArrayList<>(); + ArrayList containers = new ArrayList<>(); + ArrayList accessors = new ArrayList<>(); + ArrayList managers = new ArrayList<>(); + claim.getPermissions(builders, containers, accessors, managers); + ArrayList nearbyclaimers = new ArrayList<>(); + claim.getClaimNearbyPermission(nearbyclaimers); + GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrustListHeader); + + StringBuilder permissions = new StringBuilder(); + permissions.append(ChatColor.GOLD).append('>'); + + if (managers.size() > 0) + { + for (String manager : managers) + permissions.append(this.trustEntryToPlayerName(manager)).append(' '); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.YELLOW).append('>'); + + if (builders.size() > 0) + { + for (String builder : builders) + permissions.append(this.trustEntryToPlayerName(builder)).append(' '); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.GREEN).append('>'); + + if (containers.size() > 0) + { + for (String container : containers) + permissions.append(this.trustEntryToPlayerName(container)).append(' '); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.BLUE).append('>'); + + if (accessors.size() > 0) + { + for (String accessor : accessors) + permissions.append(this.trustEntryToPlayerName(accessor)).append(' '); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.RED).append('>'); + + if (nearbyclaimers.size() > 0) + { + for (String accessor : nearbyclaimers) + permissions.append(this.trustEntryToPlayerName(accessor)).append(' '); + } + + player.sendMessage(permissions.toString()); + + player.sendMessage( + ChatColor.GOLD + this.dataStore.getMessage(Messages.Manage) + " " + + ChatColor.YELLOW + this.dataStore.getMessage(Messages.Build) + " " + + ChatColor.GREEN + this.dataStore.getMessage(Messages.Containers) + " " + + ChatColor.BLUE + this.dataStore.getMessage(Messages.Access) + " " + + ChatColor.RED + "Claimnear"); + + if (claim.getSubclaimRestrictions()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.HasSubclaimRestriction); + } + + return true; + } + + //untrust or untrust [] + else if (cmd.getName().equalsIgnoreCase("untrust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + //determine whether a single player or clearing permissions entirely + boolean clearPermissions = false; + OfflinePlayer otherPlayer = null; + if (args[0].equals("all")) + { + if (claim == null || claim.checkPermission(player, ClaimPermission.Edit, null) == null) + { + clearPermissions = true; + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClearPermsOwnerOnly); + return true; + } + } + else + { + //validate player argument or group argument + if (!args[0].startsWith("[") || !args[0].endsWith("]")) + { + otherPlayer = this.resolvePlayerByName(args[0]); + if (!clearPermissions && otherPlayer == null && !args[0].equals("public")) + { + //bracket any permissions - at this point it must be a permission without brackets + if (args[0].contains(".")) + { + args[0] = "[" + args[0] + "]"; + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + } + + //correct to proper casing + if (otherPlayer != null) + args[0] = otherPlayer.getName(); + } + } + + //if no claim here, apply changes to all his claims + if (claim == null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + String idToDrop = args[0]; + if (otherPlayer != null) + { + idToDrop = otherPlayer.getUniqueId().toString(); + } + + //calling event + TrustChangedEvent event = new TrustChangedEvent(player, playerData.getClaims(), null, false, idToDrop); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) + { + return true; + } + + //dropping permissions + for (Claim targetClaim : event.getClaims()) { + claim = targetClaim; + + //if untrusting "all" drop all permissions + if (clearPermissions) + { + claim.clearPermissions(); + } + + //otherwise drop individual permissions + else + { + claim.dropPermission(idToDrop); + claim.managers.remove(idToDrop); + } + + //save changes + this.dataStore.saveClaim(claim); + } + + //beautify for output + if (args[0].equals("public")) + { + args[0] = "the public"; + } + + //confirmation message + if (!clearPermissions) + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustIndividualAllClaims, args[0]); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustEveryoneAllClaims); + } + } + + //otherwise, apply changes to only this claim + else if (claim.checkPermission(player, ClaimPermission.Manage, null) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); + return true; + } + else + { + //if clearing all + if (clearPermissions) + { + //requires owner + if (claim.checkPermission(player, ClaimPermission.Edit, null) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.UntrustAllOwnerOnly); + return true; + } + + //calling the event + TrustChangedEvent event = new TrustChangedEvent(player, claim, null, false, args[0]); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) + { + return true; + } + + event.getClaims().forEach(Claim::clearPermissions); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClearPermissionsOneClaim); + } + + //otherwise individual permission drop + else + { + String idToDrop = args[0]; + if (otherPlayer != null) + { + idToDrop = otherPlayer.getUniqueId().toString(); + } + boolean targetIsManager = claim.managers.contains(idToDrop); + if (targetIsManager && claim.checkPermission(player, ClaimPermission.Edit, null) != null) //only claim owners can untrust managers + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ManagersDontUntrustManagers, claim.getOwnerName()); + return true; + } + else + { + //calling the event + TrustChangedEvent event = new TrustChangedEvent(player, claim, null, false, idToDrop); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) + { + return true; + } + + event.getClaims().forEach(targetClaim -> targetClaim.dropPermission(event.getIdentifier())); + + //beautify for output + if (args[0].equals("public")) + { + args[0] = "the public"; + } + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustIndividualSingleClaim, args[0]); + } + } + + //save changes + this.dataStore.saveClaim(claim); + } + + return true; + } + + //accesstrust + else if (cmd.getName().equalsIgnoreCase("accesstrust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + this.handleTrustCommand(player, ClaimPermission.Access, args[0]); + + return true; + } + + //containertrust + else if (cmd.getName().equalsIgnoreCase("containertrust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + this.handleTrustCommand(player, ClaimPermission.Inventory, args[0]); + + return true; + } + + else if (cmd.getName().equalsIgnoreCase("claimnearbytrust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + this.handleTrustCommand(player, ClaimPermission.Claim, args[0]); + + return true; + } + + //permissiontrust + else if (cmd.getName().equalsIgnoreCase("permissiontrust") && player != null) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + this.handleTrustCommand(player, null, args[0]); //null indicates permissiontrust to the helper method + + return true; + } + + //restrictsubclaim + else if (cmd.getName().equalsIgnoreCase("restrictsubclaim") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, playerData.lastClaim); + if (claim == null || claim.parent == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.StandInSubclaim); + return true; + } + + // If player has /ignoreclaims on, continue + // If admin claim, fail if this user is not an admin + // If not an admin claim, fail if this user is not the owner + if (!playerData.ignoreClaims && (claim.isAdminClaim() ? !player.hasPermission("griefprevention.adminclaims") : !player.getUniqueId().equals(claim.parent.ownerID))) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlyOwnersModifyClaims, claim.getOwnerName()); + return true; + } + + if (claim.getSubclaimRestrictions()) + { + claim.setSubclaimRestrictions(false); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubclaimUnrestricted); + } + else + { + claim.setSubclaimRestrictions(true); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubclaimRestricted); + } + this.dataStore.saveClaim(claim); + return true; + } + + //buyclaimblocks + else if (cmd.getName().equalsIgnoreCase("buyclaimblocks") && player != null) + { + //if economy is disabled, don't do anything + EconomyHandler.EconomyWrapper economyWrapper = economyHandler.getWrapper(); + if (economyWrapper == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); + return true; + } + + if (!player.hasPermission("griefprevention.buysellclaimblocks")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionForCommand); + return true; + } + + //if purchase disabled, send error message + if (GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlySellBlocks); + return true; + } + + Economy economy = economyWrapper.getEconomy(); + + //if no parameter, just tell player cost per block and balance + if (args.length != 1) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockPurchaseCost, String.valueOf(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost), String.valueOf(economy.getBalance(player))); + return false; + } + else + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + //try to parse number of blocks + int blockCount; + try + { + blockCount = Integer.parseInt(args[0]); + } + catch (NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + if (blockCount <= 0) + { + return false; + } + + //if the player can't afford his purchase, send error message + double balance = economy.getBalance(player); + double totalCost = claimBlockCost(playerData.getBonusClaimBlocks()+playerData.getAccruedClaimBlocks() , blockCount); + if (totalCost > balance) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.InsufficientFunds, String.valueOf(totalCost), String.valueOf(balance)); + } + + //otherwise carry out transaction + else + { + int newBonusClaimBlocks = playerData.getBonusClaimBlocks() + blockCount; + + //if the player is going to reach max bonus limit, send error message + int bonusBlocksLimit = GriefPrevention.instance.config_economy_claimBlocksMaxBonus; + if (bonusBlocksLimit != 0 && newBonusClaimBlocks > bonusBlocksLimit) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.MaxBonusReached, String.valueOf(blockCount), String.valueOf(bonusBlocksLimit)); + return true; + } + + //withdraw cost + economy.withdrawPlayer(player, totalCost); + + //add blocks + playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + blockCount); + this.dataStore.savePlayerData(player.getUniqueId(), playerData); + + //inform player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.PurchaseConfirmation, String.valueOf(totalCost), String.valueOf(playerData.getRemainingClaimBlocks())); + } + + return true; + } + } + + //sellclaimblocks + else if (cmd.getName().equalsIgnoreCase("sellclaimblocks") && player != null) + { + //if economy is disabled, don't do anything + EconomyHandler.EconomyWrapper economyWrapper = economyHandler.getWrapper(); + if (economyWrapper == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); + return true; + } + + if (!player.hasPermission("griefprevention.buysellclaimblocks")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionForCommand); + return true; + } + + //if disabled, error message + if (GriefPrevention.instance.config_economy_claimBlocksSellValue == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlyPurchaseBlocks); + return true; + } + + //load player data + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + int availableBlocks = playerData.getRemainingClaimBlocks(); + + //if no amount provided, just tell player value per block sold, and how many he can sell + if (args.length != 1) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockSaleValue, String.valueOf(GriefPrevention.instance.config_economy_claimBlocksSellValue), String.valueOf(availableBlocks)); + return false; + } + + //parse number of blocks + int blockCount; + try + { + blockCount = Integer.parseInt(args[0]); + } + catch (NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + if (blockCount <= 0) + { + return false; + } + + //if he doesn't have enough blocks, tell him so + if (blockCount > availableBlocks) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotEnoughBlocksForSale); + } + + //otherwise carry out the transaction + else + { + //compute value and deposit it + double totalValue = blockCount * GriefPrevention.instance.config_economy_claimBlocksSellValue; + economyWrapper.getEconomy().depositPlayer(player, totalValue); + + //subtract blocks + playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() - blockCount); + this.dataStore.savePlayerData(player.getUniqueId(), playerData); + + //inform player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.BlockSaleConfirmation, String.valueOf(totalValue), String.valueOf(playerData.getRemainingClaimBlocks())); + } + + return true; + } + + //adminclaims + else if (cmd.getName().equalsIgnoreCase("adminclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.Admin; + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdminClaimsMode); + + return true; + } + + //basicclaims + else if (cmd.getName().equalsIgnoreCase("basicclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.Basic; + playerData.claimSubdividing = null; + GriefPrevention.sendMessage(player, TextMode.Success, Messages.BasicClaimsMode); + + return true; + } + + //subdivideclaims + else if (cmd.getName().equalsIgnoreCase("subdivideclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.shovelMode = ShovelMode.Subdivide; + playerData.claimSubdividing = null; + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionMode); + + return true; + } + + //deleteclaim + else if (cmd.getName().equalsIgnoreCase("deleteclaim") && player != null) + { + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.DeleteClaimMissing); + } + else + { + //deleting an admin claim additionally requires the adminclaims permission + if (!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims")) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + if (claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.DeletionSubdivisionWarning); + playerData.warnedAboutMajorDeletion = true; + } + else + { + this.dataStore.deleteClaim(claim, true, true); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteSuccess); + GriefPrevention.AddLogEntry(player.getName() + " deleted " + claim.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()), CustomLogEntryTypes.AdminActivity); + + //revert any current visualization + Visualization.Revert(player); + + playerData.warnedAboutMajorDeletion = false; + } + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CantDeleteAdminClaim); + } + } + + return true; + } + else if (cmd.getName().equalsIgnoreCase("claimexplosions") && player != null) + { + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.DeleteClaimMissing); + } + else + { + Supplier noBuildReason = claim.checkPermission(player, ClaimPermission.Build, null); + if (noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason.get()); + return true; + } + + if (claim.areExplosivesAllowed) + { + claim.areExplosivesAllowed = false; + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ExplosivesDisabled); + } + else + { + claim.areExplosivesAllowed = true; + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ExplosivesEnabled); + } + } + + return true; + } + + //deleteallclaims + else if (cmd.getName().equalsIgnoreCase("deleteallclaims")) + { + //requires exactly one parameter, the other player's name + if (args.length != 1) return false; + + //try to find that player + OfflinePlayer otherPlayer = this.resolvePlayerByName(args[0]); + if (otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + + //delete all that player's claims + this.dataStore.deleteClaimsForPlayer(otherPlayer.getUniqueId(), true); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteAllSuccess, otherPlayer.getName()); + if (player != null) + { + GriefPrevention.AddLogEntry(player.getName() + " deleted all claims belonging to " + otherPlayer.getName() + ".", CustomLogEntryTypes.AdminActivity); + + //revert any current visualization + Visualization.Revert(player); + } + + return true; + } + else if (cmd.getName().equalsIgnoreCase("deleteclaimsinworld")) + { + //must be executed at the console + if (player != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConsoleOnlyCommand); + return true; + } + + //requires exactly one parameter, the world name + if (args.length != 1) return false; + + //try to find the specified world + World world = Bukkit.getServer().getWorld(args[0]); + if (world == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.WorldNotFound); + return true; + } + + //delete all claims in that world + this.dataStore.deleteClaimsInWorld(world, true); + GriefPrevention.AddLogEntry("Deleted all claims in world: " + world.getName() + ".", CustomLogEntryTypes.AdminActivity); + return true; + } + else if (cmd.getName().equalsIgnoreCase("deleteuserclaimsinworld")) + { + //must be executed at the console + if (player != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ConsoleOnlyCommand); + return true; + } + + //requires exactly one parameter, the world name + if (args.length != 1) return false; + + //try to find the specified world + World world = Bukkit.getServer().getWorld(args[0]); + if (world == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.WorldNotFound); + return true; + } + + //delete all USER claims in that world + this.dataStore.deleteClaimsInWorld(world, false); + GriefPrevention.AddLogEntry("Deleted all user claims in world: " + world.getName() + ".", CustomLogEntryTypes.AdminActivity); + return true; + } + + //claimbook + else if (cmd.getName().equalsIgnoreCase("claimbook")) + { + //requires one parameter + if (args.length != 1) return false; + + //try to find the specified player + Player otherPlayer = this.getServer().getPlayer(args[0]); + if (otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + } + + //claimslist or claimslist + else if (cmd.getName().equalsIgnoreCase("claimslist")) + { + //at most one parameter + if (args.length > 1) return false; + + //player whose claims will be listed + OfflinePlayer otherPlayer; + + //if another player isn't specified, assume current player + if (args.length < 1) + { + if (player != null) + otherPlayer = player; + else + return false; + } + + //otherwise if no permission to delve into another player's claims data + else if (player != null && !player.hasPermission("griefprevention.claimslistother")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsListNoPermission); + return true; + } + + //otherwise try to find the specified player + else + { + otherPlayer = this.resolvePlayerByName(args[0]); + if (otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + } + + //load the target player's data + PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getUniqueId()); + Vector claims = playerData.getClaims(); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.StartBlockMath, + String.valueOf(playerData.getAccruedClaimBlocks()), + String.valueOf((playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId()))), + String.valueOf((playerData.getAccruedClaimBlocks() + playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId())))); + if (claims.size() > 0) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); + for (int i = 0; i < playerData.getClaims().size(); i++) + { + Claim claim = playerData.getClaims().get(i); + GriefPrevention.sendMessage(player, TextMode.Instr, getfriendlyLocationString(claim.getLesserBoundaryCorner()) + this.dataStore.getMessage(Messages.ContinueBlockMath, String.valueOf(claim.getArea()))); + } + + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.EndBlockMath, String.valueOf(playerData.getRemainingClaimBlocks())); + } + + //drop the data we just loaded, if the player isn't online + if (!otherPlayer.isOnline()) + this.dataStore.clearCachedPlayerData(otherPlayer.getUniqueId()); + + return true; + } + + //claimslist or claimslist + else if (cmd.getName().equalsIgnoreCase("claimslistextra")) + { + //at most one parameter + if (args.length > 1) return false; + + //player whose claims will be listed + OfflinePlayer otherPlayer; + + //if another player isn't specified, assume current player + if (args.length < 1) + { + if (player != null) + otherPlayer = player; + else + return false; + } + + //otherwise if no permission to delve into another player's claims data + else if (player != null && !player.hasPermission("griefprevention.claimslistother")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsListNoPermission); + return true; + } + + //otherwise try to find the specified player + else + { + otherPlayer = this.resolvePlayerByName(args[0]); + if (otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + } + + //load the target player's data + PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getUniqueId()); + Vector claims = playerData.getClaims(); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.StartBlockMath, + String.valueOf(playerData.getAccruedClaimBlocks()), + String.valueOf((playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId()))), + String.valueOf((playerData.getAccruedClaimBlocks() + playerData.getBonusClaimBlocks() + this.dataStore.getGroupBonusBlocks(otherPlayer.getUniqueId())))); + if (claims.size() > 0) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); + claims.stream().forEach(claim -> { + TextComponent claimInfo = new TextComponent(getExtraLocationString(claim.getLesserBoundaryCorner(), claim.getGreaterBoundaryCorner()) + this.dataStore.getMessage(Messages.ContinueBlockMath, String.valueOf(claim.getArea()))); + claimInfo.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Click here to visit this claim.").create())); + claimInfo.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/tppos " + claim.getLesserBoundaryCorner().getBlockX() + " " + claim.getLesserBoundaryCorner().getBlockY() + " " + claim.getLesserBoundaryCorner().getBlockZ() + " ")); + sender.sendMessage(claimInfo); + }); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.EndBlockMath, String.valueOf(playerData.getRemainingClaimBlocks())); + } + + //drop the data we just loaded, if the player isn't online + if (!otherPlayer.isOnline()) + this.dataStore.clearCachedPlayerData(otherPlayer.getUniqueId()); + + return true; + } + + //adminclaimslist + else if (cmd.getName().equalsIgnoreCase("adminclaimslist")) + { + //find admin claims + Vector claims = new Vector<>(); + for (Claim claim : this.dataStore.claims) + { + if (claim.ownerID == null) //admin claim + { + claims.add(claim); + } + } + if (claims.size() > 0) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimsListHeader); + for (Claim claim : claims) + { + GriefPrevention.sendMessage(player, TextMode.Instr, getfriendlyLocationString(claim.getLesserBoundaryCorner())); + } + } + + return true; + } + + //deletealladminclaims + else if (player != null && cmd.getName().equalsIgnoreCase("deletealladminclaims")) + { + if (!player.hasPermission("griefprevention.deleteclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDeletePermission); + return true; + } + + //delete all admin claims + this.dataStore.deleteClaimsForPlayer(null, true); //null for owner id indicates an administrative claim + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AllAdminDeleted); + if (player != null) + { + GriefPrevention.AddLogEntry(player.getName() + " deleted all administrative claims.", CustomLogEntryTypes.AdminActivity); + + //revert any current visualization + Visualization.Revert(player); + } + + return true; + } + + //adjustbonusclaimblocks or [] amount + else if (cmd.getName().equalsIgnoreCase("adjustbonusclaimblocks")) + { + //requires exactly two parameters, the other player or group's name and the adjustment + if (args.length != 2) return false; + + //parse the adjustment amount + int adjustment; + try + { + adjustment = Integer.parseInt(args[1]); + } + catch (NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + //if granting blocks to all players with a specific permission + if (args[0].startsWith("[") && args[0].endsWith("]")) + { + String permissionIdentifier = args[0].substring(1, args[0].length() - 1); + int newTotal = this.dataStore.adjustGroupBonusBlocks(permissionIdentifier, adjustment); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustGroupBlocksSuccess, permissionIdentifier, String.valueOf(adjustment), String.valueOf(newTotal)); + if (player != null) + GriefPrevention.AddLogEntry(player.getName() + " adjusted " + permissionIdentifier + "'s bonus claim blocks by " + adjustment + "."); + + return true; + } + + //otherwise, find the specified player + OfflinePlayer targetPlayer; + try + { + UUID playerID = UUID.fromString(args[0]); + targetPlayer = this.getServer().getOfflinePlayer(playerID); + + } + catch (IllegalArgumentException e) + { + targetPlayer = this.resolvePlayerByName(args[0]); + } + + if (targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + + //give blocks to player + PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); + playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + adjustment); + this.dataStore.savePlayerData(targetPlayer.getUniqueId(), playerData); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustBlocksSuccess, targetPlayer.getName(), String.valueOf(adjustment), String.valueOf(playerData.getBonusClaimBlocks())); + if (player != null) + GriefPrevention.AddLogEntry(player.getName() + " adjusted " + targetPlayer.getName() + "'s bonus claim blocks by " + adjustment + ".", CustomLogEntryTypes.AdminActivity); + + return true; + } + + //adjustbonusclaimblocksall + else if (cmd.getName().equalsIgnoreCase("adjustbonusclaimblocksall")) + { + //requires exactly one parameter, the amount of adjustment + if (args.length != 1) return false; + + //parse the adjustment amount + int adjustment; + try + { + adjustment = Integer.parseInt(args[0]); + } + catch (NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + //for each online player + @SuppressWarnings("unchecked") + Collection players = (Collection) this.getServer().getOnlinePlayers(); + StringBuilder builder = new StringBuilder(); + for (Player onlinePlayer : players) + { + UUID playerID = onlinePlayer.getUniqueId(); + PlayerData playerData = this.dataStore.getPlayerData(playerID); + playerData.setBonusClaimBlocks(playerData.getBonusClaimBlocks() + adjustment); + this.dataStore.savePlayerData(playerID, playerData); + builder.append(onlinePlayer.getName()).append(' '); + } + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AdjustBlocksAllSuccess, String.valueOf(adjustment)); + GriefPrevention.AddLogEntry("Adjusted all " + players.size() + "players' bonus claim blocks by " + adjustment + ". " + builder.toString(), CustomLogEntryTypes.AdminActivity); + + return true; + } + + //setaccruedclaimblocks + else if (cmd.getName().equalsIgnoreCase("setaccruedclaimblocks")) + { + //requires exactly two parameters, the other player's name and the new amount + if (args.length != 2) return false; + + //parse the adjustment amount + int newAmount; + try + { + newAmount = Integer.parseInt(args[1]); + } + catch (NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + //find the specified player + OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); + if (targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + + //set player's blocks + PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getUniqueId()); + playerData.setAccruedClaimBlocks(newAmount); + this.dataStore.savePlayerData(targetPlayer.getUniqueId(), playerData); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SetClaimBlocksSuccess); + if (player != null) + GriefPrevention.AddLogEntry(player.getName() + " set " + targetPlayer.getName() + "'s accrued claim blocks to " + newAmount + ".", CustomLogEntryTypes.AdminActivity); + + return true; + } + + //trapped + else if (cmd.getName().equalsIgnoreCase("trapped") && player != null) + { + //FEATURE: empower players who get "stuck" in an area where they don't have permission to build to save themselves + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim); + + //if another /trapped is pending, ignore this slash command + if (playerData.pendingTrapped) + { + return true; + } + + //if the player isn't in a claim or has permission to build, tell him to man up + if (claim == null || claim.checkPermission(player, ClaimPermission.Build, null) == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotTrappedHere); + return true; + } + + //rescue destination may be set by GPFlags or other plugin, ask to find out + SaveTrappedPlayerEvent event = new SaveTrappedPlayerEvent(claim); + Bukkit.getPluginManager().callEvent(event); + + //if the player is in the nether or end, he's screwed (there's no way to programmatically find a safe place for him) + if (player.getWorld().getEnvironment() != Environment.NORMAL && event.getDestination() == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); + return true; + } + + //if the player is in an administrative claim and AllowTrappedInAdminClaims is false, he should contact an admin + if (!GriefPrevention.instance.config_claims_allowTrappedInAdminClaims && claim.isAdminClaim() && event.getDestination() == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); + return true; + } + //send instructions + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.RescuePending); + + //create a task to rescue this player in a little while + PlayerRescueTask task = new PlayerRescueTask(player, player.getLocation(), event.getDestination()); + this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 200L); //20L ~ 1 second + + return true; + } + + else if (cmd.getName().equalsIgnoreCase("gpreload")) + { + this.loadConfig(); + if (player != null) + { + GriefPrevention.sendMessage(player, TextMode.Success, "Configuration updated. If you have updated your Grief Prevention JAR, you still need to /reload or reboot your server."); + } + else + { + GriefPrevention.AddLogEntry("Configuration updated. If you have updated your Grief Prevention JAR, you still need to /reload or reboot your server."); + } + + return true; + } + + //givepet + else if (cmd.getName().equalsIgnoreCase("givepet") && player != null) + { + //requires one parameter + if (args.length < 1) return false; + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + //special case: cancellation + if (args[0].equalsIgnoreCase("cancel")) + { + playerData.petGiveawayRecipient = null; + GriefPrevention.sendMessage(player, TextMode.Success, Messages.PetTransferCancellation); + return true; + } + + //find the specified player + OfflinePlayer targetPlayer = this.resolvePlayerByName(args[0]); + if (targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return true; + } + + //remember the player's ID for later pet transfer + playerData.petGiveawayRecipient = targetPlayer; + + //send instructions + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ReadyToTransferPet); + + return true; + } + + //gpblockinfo + else if (cmd.getName().equalsIgnoreCase("gpblockinfo") && player != null) + { + ItemStack inHand = player.getInventory().getItemInMainHand(); + player.sendMessage("In Hand: " + inHand.getType().name()); + + Block inWorld = player.getTargetBlockExact(300, FluidCollisionMode.ALWAYS); + if (inWorld == null) inWorld = player.getEyeLocation().getBlock(); + player.sendMessage("In World: " + inWorld.getType().name()); + + return true; + } + // kickfromclaim + else if (cmd.getName().equalsIgnoreCase("kickfromclaim") && player != null) + { + if (args.length < 1) { + player.sendMiniMessage(Config.PlayerNotSpecified, null); // todo placeholders. + return true; + } + TagResolver placeholders = TagResolver.resolver( + Placeholder.component("player", player.name()), + Placeholder.parsed("target", args[0]) + ); + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + player.sendMiniMessage(Config.PlayerOffline, placeholders); // todo placeholders. + return true; + } + if (player.equals(target)) { + player.sendMiniMessage(Config.CannotKickSelf, null); // todo placeholders. + return true; + } + Claim claim = this.dataStore.getClaimAt(target.getLocation(), true, null); + if (claim == null || (claim.checkPermission(player, ClaimPermission.Manage, null) != null)) { + player.sendMiniMessage(Config.TargetNotInClaim, placeholders); // todo placeholders. + return true; + } + placeholders = TagResolver.resolver(placeholders, Placeholder.parsed("claim_owner", claim.getOwnerName())); + SafeZone zone = new SafeZone(claim); + if ((target.hasPermission("griefprevention.adminclaims") && claim.isAdminClaim()) || zone + .hasTrust(target.getUniqueId())) { + player.sendMiniMessage(Config.CannotKickTrustedTarget, placeholders); // todo placeholders. + return true; + } + if (target.hasPermission("griefprevention.kickfromclaimexempt")) { + player.sendMiniMessage(Config.CannotKickExemptTarget, placeholders); // todo placeholders. + return true; + } + zone.testForSafeSpot(); + Location safe = zone.getSafeArea(); + if (safe == null) { + player.sendMiniMessage(Config.NoSafeLocation, null); // todo placeholders. + } else { + if (target.isInsideVehicle()) target.leaveVehicle(); + target.teleport(safe); + Bukkit.getPluginManager().callEvent(new PlayerTeleportEvent(target, safe, safe)); + player.sendMiniMessage(Config.KickSuccess, placeholders); // todo placeholders. + target.sendMiniMessage(Config.KickedFromClaim, placeholders); // todo placeholders. + } + return true; + } + return false; + } + + public enum IgnoreMode + {None, StandardIgnore, AdminIgnore} + + public String trustEntryToPlayerName(String entry) + { + if (entry.startsWith("[") || entry.equals("public")) + { + return entry; + } + else + { + return GriefPrevention.lookupPlayerName(entry); + } + } + + public static String getfriendlyLocationString(Location location) + { + return location.getWorld().getName() + ": x" + location.getBlockX() + ", z" + location.getBlockZ(); + } + + public static String getExtraLocationString(Location location, Location location2) + { + return location.getWorld().getName() + ": x" + location.getBlockX() + ", z" + location.getBlockZ() + " to x" + location2.getBlockX() + ", z" + location2.getBlockZ(); + } + + private boolean abandonClaimHandler(Player player, boolean deleteTopLevelClaim) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + + //which claim is being abandoned? + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + if (claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimMissing); + } + + //verify ownership + else if (claim.checkPermission(player, ClaimPermission.Edit, null) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); + } + + //warn if has children and we're not explicitly deleting a top level claim + else if (claim.children.size() > 0 && !deleteTopLevelClaim) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DeleteTopLevelClaim); + return true; + } + else + { + //delete it + this.dataStore.deleteClaim(claim, true, false); + + //adjust claim blocks when abandoning a top level claim + if (this.config_claims_abandonReturnRatio != 1.0D && claim.parent == null && claim.ownerID.equals(playerData.playerID)) + { + playerData.setAccruedClaimBlocks(playerData.getAccruedClaimBlocks() - (int) Math.ceil((claim.getArea() * (1 - this.config_claims_abandonReturnRatio)))); + } + + //tell the player how many claim blocks he has left + int remainingBlocks = playerData.getRemainingClaimBlocks(); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AbandonSuccess, String.valueOf(remainingBlocks)); + + //revert any current visualization + Visualization.Revert(player); + + playerData.warnedAboutMajorDeletion = false; + } + + return true; + + } + + //helper method keeps the trust commands consistent and eliminates duplicate code + public void handleTrust(Player player, String recipientName) { + this.handleTrustCommand(player, ClaimPermission.Build, recipientName); + } + private void handleTrustCommand(Player player, ClaimPermission permissionLevel, String recipientName) + { + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + //validate player or group argument + String permission = null; + OfflinePlayer otherPlayer = null; + UUID recipientID = null; + if (recipientName.startsWith("[") && recipientName.endsWith("]")) + { + permission = recipientName.substring(1, recipientName.length() - 1); + if (permission == null || permission.isEmpty()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.InvalidPermissionID); + return; + } + } + else + { + otherPlayer = this.resolvePlayerByName(recipientName); + boolean isPermissionFormat = recipientName.contains("."); + if (otherPlayer == null && !recipientName.equals("public") && !recipientName.equals("all") && !isPermissionFormat) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound2); + return; + } + + if (otherPlayer == null && isPermissionFormat) + { + //player does not exist and argument has a period so this is a permission instead + permission = recipientName; + } + else if (otherPlayer != null) + { + recipientName = otherPlayer.getName(); + recipientID = otherPlayer.getUniqueId(); + } + else + { + recipientName = "public"; + } + } + + //determine which claims should be modified + ArrayList targetClaims = new ArrayList<>(); + if (claim == null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + targetClaims.addAll(playerData.getClaims()); + } + else + { + //check permission here + if (claim.checkPermission(player, ClaimPermission.Manage, null) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); + return; + } + + //see if the player has the level of permission he's trying to grant + Supplier errorMessage; + + //permission level null indicates granting permission trust + if (permissionLevel == null) + { + errorMessage = claim.checkPermission(player, ClaimPermission.Edit, null); + if (errorMessage != null) + { + errorMessage = () -> "Only " + claim.getOwnerName() + " can grant /PermissionTrust here."; + } + } + + //otherwise just use the ClaimPermission enum values + else + { + errorMessage = claim.checkPermission(player, permissionLevel, null); + } + + //error message for trying to grant a permission the player doesn't have + if (errorMessage != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CantGrantThatPermission); + return; + } + + targetClaims.add(claim); + } + + //if we didn't determine which claims to modify, tell the player to be specific + if (targetClaims.size() == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.GrantPermissionNoClaim); + return; + } + + String identifierToAdd = recipientName; + if (permission != null) + { + identifierToAdd = "[" + permission + "]"; + //replace recipientName as well so the success message clearly signals a permission + recipientName = identifierToAdd; + } + else if (recipientID != null) + { + identifierToAdd = recipientID.toString(); + } + + //calling the event + TrustChangedEvent event = new TrustChangedEvent(player, targetClaims, permissionLevel, true, identifierToAdd); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) + { + return; + } + + //apply changes + for (Claim currentClaim : event.getClaims()) + { + if (permissionLevel == null) + { + if (!currentClaim.managers.contains(identifierToAdd)) + { + currentClaim.managers.add(identifierToAdd); + } + } + else + { + currentClaim.setPermission(identifierToAdd, permissionLevel); + } + this.dataStore.saveClaim(currentClaim); + } + + //notify player + if (recipientName.equals("public")) recipientName = this.dataStore.getMessage(Messages.CollectivePublic); + String permissionDescription; + if (permissionLevel == null) + { + permissionDescription = this.dataStore.getMessage(Messages.PermissionsPermission); + } + else if (permissionLevel == ClaimPermission.Build) + { + permissionDescription = this.dataStore.getMessage(Messages.BuildPermission); + } + else if (permissionLevel == ClaimPermission.Access) + { + permissionDescription = this.dataStore.getMessage(Messages.AccessPermission); + } + else if (permissionLevel == ClaimPermission.Claim) + { + permissionDescription = "Granted this player the ability to claim near your claim, this is not a permanent trust."; // TODO message + } + else //ClaimPermission.Inventory + { + permissionDescription = this.dataStore.getMessage(Messages.ContainersPermission); + } + + String location; + if (claim == null) + { + location = this.dataStore.getMessage(Messages.LocationAllClaims); + } + else + { + location = this.dataStore.getMessage(Messages.LocationCurrentClaim); + } + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.GrantPermissionConfirmation, recipientName, permissionDescription, location); + } + + //helper method to resolve a player by name + ConcurrentHashMap playerNameToIDMap = new ConcurrentHashMap<>(); // TODO REMOVE ME + + + public OfflinePlayer resolvePlayerByName(String name) + { + //try online players first + Player targetPlayer = this.getServer().getPlayerExact(name); + if (targetPlayer != null) return targetPlayer; + + //try exact match first + UUID bestMatchID = this.playerNameToIDMap.get(name); + + //if failed, try ignore case + if (bestMatchID == null) + { + bestMatchID = this.playerNameToIDMap.get(name.toLowerCase()); + return this.getServer().getOfflinePlayer(bestMatchID); + } + + return this.getServer().getOfflinePlayer(name); + } + + //helper method to resolve a player name from the player's UUID + static String lookupPlayerName(UUID playerID) + { + //parameter validation + if (playerID == null) return "somebody"; + + if (playerNameCache.containsKey(playerID)) + return playerNameCache.get(playerID); + //check the cache + OfflinePlayer player = GriefPrevention.instance.getServer().getOfflinePlayer(playerID); + if (player.hasPlayedBefore() || player.isOnline()) + { + String playerName = player.getName(); + playerNameCache.put(playerID, playerName); + return playerName; + } + else + { + return "someone(" + playerID.toString() + ")"; + } + } + + //cache for player name lookups, to save searches of all offline players + static void cacheUUIDNamePair(UUID playerID, String playerName) + { + //store the reverse mapping + playerNameCache.put(playerID, playerName); + GriefPrevention.instance.playerNameToIDMap.put(playerName, playerID); + GriefPrevention.instance.playerNameToIDMap.put(playerName.toLowerCase(), playerID); + } + + //string overload for above helper + static String lookupPlayerName(String playerID) + { + UUID id; + try + { + id = UUID.fromString(playerID); + } + catch (IllegalArgumentException ex) + { + GriefPrevention.AddLogEntry("Error: Tried to look up a local player name for invalid UUID: " + playerID); + return "someone"; + } + + return lookupPlayerName(id); + } + + public void onDisable() + { + //save data for any online players + @SuppressWarnings("unchecked") + Collection players = (Collection) this.getServer().getOnlinePlayers(); + for (Player player : players) + { + UUID playerID = player.getUniqueId(); + PlayerData playerData = this.dataStore.getPlayerData(playerID); + this.dataStore.savePlayerDataSync(playerID, playerData); + } + + this.dataStore.close(); + + //dump any remaining unwritten log entries + this.customLogger.WriteEntries(); + + if (pl3xmapHook != null) { + pl3xmapHook.disable(); + } + + AddLogEntry("GriefPrevention disabled."); + } + + //called when a player spawns, applies protection for that player if necessary + public void checkPvpProtectionNeeded(Player player) + { + //if anti spawn camping feature is not enabled, do nothing + if (!this.config_pvp_protectFreshSpawns) return; + + //if pvp is disabled, do nothing + if (!pvpRulesApply(player.getWorld())) return; + + //if player is in creative mode, do nothing + if (player.getGameMode() == GameMode.CREATIVE) return; + + //if the player has the damage any player permission enabled, do nothing + if (player.hasPermission("griefprevention.nopvpimmunity")) return; + + //check inventory for well, anything + if (GriefPrevention.isInventoryEmpty(player)) + { + //if empty, apply immunity + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + playerData.pvpImmune = true; + + //inform the player after he finishes respawning + GriefPrevention.sendMessage(player, TextMode.Success, Messages.PvPImmunityStart, 5L); + + //start a task to re-check this player's inventory every minute until his immunity is gone + PvPImmunityValidationTask task = new PvPImmunityValidationTask(player); + this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 1200L); + } + } + + static boolean isInventoryEmpty(Player player) + { + PlayerInventory inventory = player.getInventory(); + ItemStack[] armorStacks = inventory.getArmorContents(); + + //check armor slots, stop if any items are found + for (ItemStack armorStack : armorStacks) + { + if (!(armorStack == null || armorStack.getType() == Material.AIR)) return false; + } + + //check other slots, stop if any items are found + ItemStack[] generalStacks = inventory.getContents(); + for (ItemStack generalStack : generalStacks) + { + if (!(generalStack == null || generalStack.getType() == Material.AIR)) return false; + } + + return true; + } + + //moves a player from the claim he's in to a nearby wilderness location + public Location ejectPlayer(Player player) + { + //look for a suitable location + Location candidateLocation = player.getLocation(); + while (true) + { + Claim claim = null; + claim = GriefPrevention.instance.dataStore.getClaimAt(candidateLocation, false, null); + + //if there's a claim here, keep looking + if (claim != null) + { + candidateLocation = new Location(claim.lesserBoundaryCorner.getWorld(), claim.lesserBoundaryCorner.getBlockX() - 1, claim.lesserBoundaryCorner.getBlockY(), claim.lesserBoundaryCorner.getBlockZ() - 1); + continue; + } + + //otherwise find a safe place to teleport the player + else + { + //find a safe height, a couple of blocks above the surface + GuaranteeChunkLoaded(candidateLocation); + Block highestBlock = candidateLocation.getWorld().getHighestBlockAt(candidateLocation.getBlockX(), candidateLocation.getBlockZ()); + Location destination = new Location(highestBlock.getWorld(), highestBlock.getX(), highestBlock.getY() + 2, highestBlock.getZ()); + player.teleport(destination); + return destination; + } + } + } + + //ensures a piece of the managed world is loaded into server memory + //(generates the chunk if necessary) + private static void GuaranteeChunkLoaded(Location location) + { + Chunk chunk = location.getChunk(); + while (!chunk.isLoaded() || !chunk.load(true)) ; + } + + //sends a color-coded message to a player + public static void sendMessage(Player player, ChatColor color, Messages messageID, String... args) + { + sendMessage(player, color, messageID, 0, args); + } + + //sends a color-coded message to a player + public static void sendMessage(Player player, ChatColor color, Messages messageID, long delayInTicks, String... args) + { + String message = GriefPrevention.instance.dataStore.getMessage(messageID, args); + sendMessage(player, color, message, delayInTicks); + } + + //sends a color-coded message to a player + public static void sendMessage(Player player, ChatColor color, String message) + { + if (message == null || message.length() == 0) return; + + if (player == null) + { + GriefPrevention.AddLogEntry(color + message); + } + else + { + player.sendMessage(color + message); + } + } + + public static void sendMessage(Player player, ChatColor color, String message, long delayInTicks) + { + SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message); + + //Only schedule if there should be a delay. Otherwise, send the message right now, else the message will appear out of order. + if (delayInTicks > 0) + { + GriefPrevention.instance.getServer().getScheduler().runTaskLater(GriefPrevention.instance, task, delayInTicks); + } + else + { + task.run(); + } + } + + //checks whether players can create claims in a world + public boolean claimsEnabledForWorld(World world) + { + ClaimsMode mode = this.config_claims_worldModes.get(world); + return mode != null && mode != ClaimsMode.Disabled; + } + + public String allowBuild(Player player, Location location) + { + // TODO check all derivatives and rework API + return this.allowBuild(player, location, location.getBlock().getType()); + } + + public String allowBuild(Player player, Location location, Material material) + { + if (!GriefPrevention.instance.claimsEnabledForWorld(location.getWorld())) return null; + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); + + //exception: administrators in ignore claims mode + if (playerData.ignoreClaims) return null; + + //wilderness rules + if (claim == null) + { + return null; + } + + //if not in the wilderness, then apply claim rules (permissions, etc) + else + { + //cache the claim for later reference + playerData.lastClaim = claim; + Block block = location.getBlock(); + + Supplier supplier = claim.checkPermission(player, ClaimPermission.Build, new BlockPlaceEvent(block, block.getState(), block, new ItemStack(material), player, true, EquipmentSlot.HAND)); + + if (supplier == null) return null; + + return supplier.get(); + } + } + + public String allowBreak(Player player, Block block, Location location) + { + return this.allowBreak(player, block, location, new BlockBreakEvent(block, player)); + } + + public String allowBreak(Player player, Material material, Location location, BlockBreakEvent breakEvent) + { + return this.allowBreak(player, location.getBlock(), location, breakEvent); + } + + public String allowBreak(Player player, Block block, Location location, BlockBreakEvent breakEvent) + { + if (!GriefPrevention.instance.claimsEnabledForWorld(location.getWorld())) return null; + + PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId()); + Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); + + //exception: administrators in ignore claims mode + if (playerData.ignoreClaims) return null; + + //wilderness rules + if (claim == null) + { + return null; + } + else + { + //cache the claim for later reference + playerData.lastClaim = claim; + + //if not in the wilderness, then apply claim rules (permissions, etc) + Supplier cancel = claim.checkPermission(player, ClaimPermission.Build, breakEvent); + if (cancel != null && breakEvent != null) + { + PreventBlockBreakEvent preventionEvent = new PreventBlockBreakEvent(breakEvent); + Bukkit.getPluginManager().callEvent(preventionEvent); + if (preventionEvent.isCancelled()) + { + cancel = null; + } + } + + if (cancel == null) return null; + + return cancel.get(); + } + } + + public void restoreChunk(Chunk chunk, int miny, boolean aggressiveMode, long delayInTicks, Player playerReceivingVisualization) + { + //build a snapshot of this chunk, including 1 block boundary outside of the chunk all the way around + int maxHeight = chunk.getWorld().getMaxHeight(); + BlockSnapshot[][][] snapshots = new BlockSnapshot[18][maxHeight][18]; + Block startBlock = chunk.getBlock(0, 0, 0); + Location startLocation = new Location(chunk.getWorld(), startBlock.getX() - 1, 0, startBlock.getZ() - 1); + for (int x = 0; x < snapshots.length; x++) + { + for (int z = 0; z < snapshots[0][0].length; z++) + { + for (int y = 0; y < snapshots[0].length; y++) + { + Block block = chunk.getWorld().getBlockAt(startLocation.getBlockX() + x, startLocation.getBlockY() + y, startLocation.getBlockZ() + z); + snapshots[x][y][z] = new BlockSnapshot(block.getLocation(), block.getType(), block.getBlockData()); + } + } + } + + //create task to process those data in another thread + Location lesserBoundaryCorner = chunk.getBlock(0, 0, 0).getLocation(); + Location greaterBoundaryCorner = chunk.getBlock(15, 0, 15).getLocation(); + + //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, false, playerReceivingVisualization); + GriefPrevention.instance.getServer().getScheduler().runTaskLaterAsynchronously(GriefPrevention.instance, task, delayInTicks); + } + + public int getSeaLevel(World world) + { + Integer overrideValue = this.config_seaLevelOverride.get(world.getName()); + if (overrideValue == null || overrideValue == -1) + { + return world.getSeaLevel(); + } + else + { + return overrideValue; + } + } + + public boolean containsBlockedIP(String message) + { + message = message.replace("\r\n", ""); + Pattern ipAddressPattern = Pattern.compile("([0-9]{1,3}\\.){3}[0-9]{1,3}"); + Matcher matcher = ipAddressPattern.matcher(message); + + //if it looks like an IP address + if (matcher.find()) + { + //and it's not in the list of allowed IP addresses + if (!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) + { + return true; + } + } + + return false; + } + + void autoExtendClaim(Claim newClaim) + { + //auto-extend it downward to cover anything already built underground + Location lesserCorner = newClaim.getLesserBoundaryCorner(); + Location greaterCorner = newClaim.getGreaterBoundaryCorner(); + World world = lesserCorner.getWorld(); + ArrayList snapshots = new ArrayList<>(); + for (int chunkx = lesserCorner.getBlockX() / 16; chunkx <= greaterCorner.getBlockX() / 16; chunkx++) + { + for (int chunkz = lesserCorner.getBlockZ() / 16; chunkz <= greaterCorner.getBlockZ() / 16; chunkz++) + { + if (world.isChunkLoaded(chunkx, chunkz)) + { + snapshots.add(world.getChunkAt(chunkx, chunkz).getChunkSnapshot(true, true, false)); + } + } + } + + Bukkit.getScheduler().runTaskAsynchronously(GriefPrevention.instance, new AutoExtendClaimTask(newClaim, snapshots, world.getEnvironment())); + } + + public boolean pvpRulesApply(World world) + { + Boolean configSetting = this.config_pvp_specifiedWorlds.get(world); + if (configSetting != null) return configSetting; + return world.getPVP(); + } + + public static boolean isNewToServer(Player player) + { + if (player.getStatistic(Statistic.PICKUP, Material.OAK_LOG) > 0 || + player.getStatistic(Statistic.PICKUP, Material.SPRUCE_LOG) > 0 || + player.getStatistic(Statistic.PICKUP, Material.BIRCH_LOG) > 0 || + player.getStatistic(Statistic.PICKUP, Material.JUNGLE_LOG) > 0 || + player.getStatistic(Statistic.PICKUP, Material.ACACIA_LOG) > 0 || + player.getStatistic(Statistic.PICKUP, Material.DARK_OAK_LOG) > 0) return false; + + PlayerData playerData = instance.dataStore.getPlayerData(player.getUniqueId()); + if (playerData.getClaims().size() > 0) return false; + + return true; + } + + static void banPlayer(Player player, String reason, String source) + { + if (GriefPrevention.instance.config_ban_useCommand) + { + Bukkit.getServer().dispatchCommand( + Bukkit.getConsoleSender(), + GriefPrevention.instance.config_ban_commandFormat.replace("%name%", player.getName()).replace("%reason%", reason)); + } + else + { + BanList bans = Bukkit.getServer().getBanList(Type.NAME); + bans.addBan(player.getName(), reason, null, source); + + //kick + if (player.isOnline()) + { + player.kickPlayer(reason); + } + } + } + + public ItemStack getItemInHand(Player player, EquipmentSlot hand) + { + if (hand == EquipmentSlot.OFF_HAND) return player.getInventory().getItemInOffHand(); + return player.getInventory().getItemInMainHand(); + } + + public boolean claimIsPvPSafeZone(Claim claim) + { + return claim.isAdminClaim() && claim.parent == null && GriefPrevention.instance.config_pvp_noCombatInAdminLandClaims || + claim.isAdminClaim() && claim.parent != null && GriefPrevention.instance.config_pvp_noCombatInAdminSubdivisions || + !claim.isAdminClaim() && GriefPrevention.instance.config_pvp_noCombatInPlayerLandClaims; + } + + //Track scheduled "rescues" so we can cancel them if the player happens to teleport elsewhere so we can cancel it. + ConcurrentHashMap portalReturnTaskMap = new ConcurrentHashMap<>(); + + public void startRescueTask(Player player, Location location) + { + //Schedule task to reset player's portal cooldown after 30 seconds (Maximum timeout time for client, in case their network is slow and taking forever to load chunks) + BukkitTask task = new CheckForPortalTrapTask(player, this, location).runTaskLater(GriefPrevention.instance, 600L); + + //Cancel existing rescue task + if (portalReturnTaskMap.containsKey(player.getUniqueId())) + portalReturnTaskMap.put(player.getUniqueId(), task).cancel(); + else + portalReturnTaskMap.put(player.getUniqueId(), task); + } + + private static final double[] xMult = {0, 10000, 50000, 300000, 1000000, Integer.MAX_VALUE}; + private static final double[] yMultBuy = {0.5, 0.75, 1, 2, 5}; + private double claimBlockCost(int oldPoints, int transPts) { + if (Config.claimBlockPrices.isEmpty()) + return transPts * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost; + + double finalPrice = 0; //Initialize final price + int segment = 1; //Start segment at one + int high = oldPoints + transPts; //Will be the highest point value + + if (oldPoints > high) //If high is not the highest point value, swap it with oldPoints so it is + { + int temp = oldPoints; + oldPoints = high; + high = temp; + } + + while (oldPoints > xMult[segment] && segment < xMult.length - 1) { //Calculate the start segment (first value smaller than lower) + segment++; + } + + for (int i = segment; i < xMult.length && high > xMult[i - 1]; i++) + finalPrice += getPricePerInterval(oldPoints, high, i); + + return finalPrice; + } + + private double getPricePerInterval(int start_points, int end_points, int segment) { + double bottom = xMult[segment - 1]; + double top = xMult[segment]; + double priceMult = yMultBuy[segment - 1]; + double pricePerPoint = 1; + + if (start_points <= bottom && end_points <= top)// +_---+--- + return (end_points - bottom) * pricePerPoint * priceMult; + else if (start_points <= bottom && end_points >= top) // +_---_+ + return (top - bottom) * pricePerPoint * priceMult; + else if (start_points >= bottom && end_points <= top) // _--+--+--_ + return (end_points - start_points) * pricePerPoint * priceMult; + else if (start_points >= bottom && end_points >= top) // _--+--_+ + return (top - start_points) * pricePerPoint * priceMult; + else + return 0; + } + + public DatabaseConnection getDataBase() { + return databaseConnection; + } +}