/* GriefPrevention Server Plugin for Minecraft Copyright (C) 2011 Ryan Hamshire This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package me.ryanhamshire.GriefPrevention; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Logger; import net.milkbowl.vault.economy.Economy; import org.bukkit.ChatColor; import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.command.*; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; 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 = Logger.getLogger("Minecraft"); //this handles data storage, like player and region data public DataStore dataStore; //configuration variables, loaded/saved from a config.yml public ArrayList config_claims_enabledWorlds; //list of worlds where players can create GriefPrevention claims public ArrayList config_claims_enabledCreativeWorlds; //list of worlds where additional creative mode anti-grief rules apply public boolean config_claims_preventTheft; //whether containers and crafting blocks are protectable public boolean config_claims_preventButtonsSwitches; //whether buttons and switches are protectable public int config_claims_initialBlocks; //the number of claim blocks a new player starts with public int config_claims_blocksAccruedPerHour; //how many additional blocks players get each hour of play (can be zero) public int config_claims_maxAccruedBlocks; //the limit on accrued blocks (over time). doesn't limit purchased or admin-gifted blocks 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_automaticClaimsForNewPlayersRadius; //how big automatic new player claims (when they place a chest) should be. 0 to disable public boolean config_claims_creationRequiresPermission; //whether creating claims with the shovel requires a permission public int config_claims_claimsExtendIntoGroundDistance; //how far below the shoveled block a new claim will reach public int config_claims_minSize; //minimum width and height for non-admin claims public int config_claims_trappedCooldownHours; //number of hours between uses of the /trapped command public ArrayList config_siege_enabledWorlds; //whether or not /siege is enabled on this server public ArrayList config_siege_blocks; //which blocks will be breakable in siege mode public boolean config_spam_enabled; //whether or not to monitor for spam public int config_spam_loginCooldownMinutes; //how long players must wait between logins. combats login spam. 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 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 boolean config_trees_removeFloatingTreetops; //whether to automatically remove partially cut trees public boolean config_trees_regrowGriefedTrees; //whether to automatically replant partially cut trees 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_blockSurfaceExplosions; //whether creeper/TNT explosions near or above the surface destroy blocks public boolean config_fireSpreads; //whether fire spreads outside of claims public boolean config_fireDestroys; //whether fire destroys blocks outside of claims public boolean config_addItemsToClaimedChests; //whether players may add items to claimed chests by left-clicking them public boolean config_eavesdrop; //whether whispered messages will be visible to administrators public boolean config_smartBan; //whether to ban accounts which very likely owned by a banned player //reference to the economy plugin, if economy integration is enabled public static Economy economy = null; //how far away to search from a tree trunk for its branch blocks public static final int TREE_RADIUS = 5; //how long to wait before deciding a player is staying online or staying offline, for notication messages public static final int NOTIFICATION_SECONDS = 20; //adds a server log entry public static void AddLogEntry(String entry) { log.info("GriefPrevention: " + entry); } //initializes well... everything public void onEnable() { AddLogEntry("Grief Prevention enabled."); instance = this; //load the config if it exists FileConfiguration config = YamlConfiguration.loadConfiguration(new File(DataStore.configFilePath)); //read configuration settings (note defaults) //default for claims worlds list ArrayList defaultClaimsWorldNames = new ArrayList(); List worlds = this.getServer().getWorlds(); for(int i = 0; i < worlds.size(); i++) { defaultClaimsWorldNames.add(worlds.get(i).getName()); } //get claims world names from the config file List claimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.Worlds"); if(claimsEnabledWorldNames == null || claimsEnabledWorldNames.size() == 0) { claimsEnabledWorldNames = defaultClaimsWorldNames; } //validate that list this.config_claims_enabledWorlds = new ArrayList(); for(int i = 0; i < claimsEnabledWorldNames.size(); i++) { String worldName = claimsEnabledWorldNames.get(i); World world = this.getServer().getWorld(worldName); if(world == null) { AddLogEntry("Error: Claims Configuration: There's no world named \"" + worldName + "\". Please update your config.yml."); } else { this.config_claims_enabledWorlds.add(world); } } //default creative claim world names List defaultCreativeWorldNames = new ArrayList(); //if default game mode for the server is creative, creative rules will apply to all worlds unless the config specifies otherwise if(this.getServer().getDefaultGameMode() == GameMode.CREATIVE) { for(int i = 0; i < defaultClaimsWorldNames.size(); i++) { defaultCreativeWorldNames.add(defaultClaimsWorldNames.get(i)); } } //get creative world names from the config file List creativeClaimsEnabledWorldNames = config.getStringList("GriefPrevention.Claims.CreativeRulesWorlds"); if(creativeClaimsEnabledWorldNames == null || creativeClaimsEnabledWorldNames.size() == 0) { creativeClaimsEnabledWorldNames = defaultCreativeWorldNames; } //validate that list this.config_claims_enabledCreativeWorlds = new ArrayList(); for(int i = 0; i < creativeClaimsEnabledWorldNames.size(); i++) { String worldName = creativeClaimsEnabledWorldNames.get(i); World world = this.getServer().getWorld(worldName); if(world == null) { AddLogEntry("Error: Claims Configuration: There's no world named \"" + worldName + "\". Please update your config.yml."); } else { this.config_claims_enabledCreativeWorlds.add(world); } } this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); this.config_claims_preventButtonsSwitches = config.getBoolean("GriefPrevention.Claims.PreventButtonsSwitches", true); this.config_claims_initialBlocks = config.getInt("GriefPrevention.Claims.InitialBlocks", 100); this.config_claims_blocksAccruedPerHour = config.getInt("GriefPrevention.Claims.BlocksAccruedPerHour", 100); this.config_claims_maxAccruedBlocks = config.getInt("GriefPrevention.Claims.MaxAccruedBlocks", 80000); this.config_claims_automaticClaimsForNewPlayersRadius = config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", 4); this.config_claims_claimsExtendIntoGroundDistance = config.getInt("GriefPrevention.Claims.ExtendIntoGroundDistance", 5); this.config_claims_creationRequiresPermission = config.getBoolean("GriefPrevention.Claims.CreationRequiresPermission", false); this.config_claims_minSize = config.getInt("GriefPrevention.Claims.MinimumSize", 10); this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", 0); this.config_claims_expirationDays = config.getInt("GriefPrevention.Claims.IdleLimitDays", 0); this.config_claims_trappedCooldownHours = config.getInt("GriefPrevention.Claims.TrappedCommandCooldownHours", 8); this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true); this.config_spam_loginCooldownMinutes = config.getInt("GriefPrevention.Spam.LoginCooldownMinutes", 2); 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;/tell;/global;/local"); 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); this.config_trees_removeFloatingTreetops = config.getBoolean("GriefPrevention.Trees.RemoveFloatingTreetops", true); this.config_trees_regrowGriefedTrees = config.getBoolean("GriefPrevention.Trees.RegrowGriefedTrees", true); this.config_economy_claimBlocksPurchaseCost = config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0); this.config_economy_claimBlocksSellValue = config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0); this.config_blockSurfaceExplosions = config.getBoolean("GriefPrevention.BlockSurfaceExplosions", true); this.config_fireSpreads = config.getBoolean("GriefPrevention.FireSpreads", false); this.config_fireDestroys = config.getBoolean("GriefPrevention.FireDestroys", false); this.config_addItemsToClaimedChests = config.getBoolean("GriefPrevention.AddItemsToClaimedChests", true); this.config_eavesdrop = config.getBoolean("GriefPrevention.EavesdropEnabled", false); this.config_smartBan = config.getBoolean("GriefPrevention.SmartBan", true); //default for siege worlds list ArrayList defaultSiegeWorldNames = new ArrayList(); //get siege world names from the config file List siegeEnabledWorldNames = config.getStringList("GriefPrevention.Siege.Worlds"); if(siegeEnabledWorldNames == null) { siegeEnabledWorldNames = defaultSiegeWorldNames; } //validate that list this.config_siege_enabledWorlds = new ArrayList(); for(int i = 0; i < siegeEnabledWorldNames.size(); i++) { String worldName = siegeEnabledWorldNames.get(i); World world = this.getServer().getWorld(worldName); if(world == null) { AddLogEntry("Error: Siege Configuration: There's no world named \"" + worldName + "\". Please update your config.yml."); } else { this.config_siege_enabledWorlds.add(world); } } //default siege blocks this.config_siege_blocks = new ArrayList(); this.config_siege_blocks.add(Material.DIRT); this.config_siege_blocks.add(Material.GRASS); this.config_siege_blocks.add(Material.LONG_GRASS); this.config_siege_blocks.add(Material.COBBLESTONE); this.config_siege_blocks.add(Material.GRAVEL); this.config_siege_blocks.add(Material.SAND); this.config_siege_blocks.add(Material.GLASS); this.config_siege_blocks.add(Material.THIN_GLASS); this.config_siege_blocks.add(Material.WOOD); this.config_siege_blocks.add(Material.WOOL); this.config_siege_blocks.add(Material.SNOW); //build a default config entry ArrayList defaultBreakableBlocksList = new ArrayList(); for(int i = 0; i < this.config_siege_blocks.size(); i++) { defaultBreakableBlocksList.add(this.config_siege_blocks.get(i).name()); } //try to load the list from the config file List breakableBlocksList = config.getStringList("GriefPrevention.Siege.BreakableBlocks"); //if it fails, use default list instead if(breakableBlocksList == null || breakableBlocksList.size() == 0) { breakableBlocksList = defaultBreakableBlocksList; } //parse the list of siege-breakable blocks this.config_siege_blocks = new ArrayList(); for(int i = 0; i < breakableBlocksList.size(); i++) { String blockName = breakableBlocksList.get(i); Material material = Material.getMaterial(blockName); if(material == null) { GriefPrevention.AddLogEntry("Siege Configuration: Material not found: " + blockName + "."); } else { this.config_siege_blocks.add(material); } } config.set("GriefPrevention.Claims.Worlds", claimsEnabledWorldNames); config.set("GriefPrevention.Claims.CreativeRulesWorlds", creativeClaimsEnabledWorldNames); config.set("GriefPrevention.Claims.PreventTheft", this.config_claims_preventTheft); config.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); config.set("GriefPrevention.Claims.InitialBlocks", this.config_claims_initialBlocks); config.set("GriefPrevention.Claims.BlocksAccruedPerHour", this.config_claims_blocksAccruedPerHour); config.set("GriefPrevention.Claims.MaxAccruedBlocks", this.config_claims_maxAccruedBlocks); config.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", this.config_claims_automaticClaimsForNewPlayersRadius); config.set("GriefPrevention.Claims.ExtendIntoGroundDistance", this.config_claims_claimsExtendIntoGroundDistance); config.set("GriefPrevention.Claims.CreationRequiresPermission", this.config_claims_creationRequiresPermission); config.set("GriefPrevention.Claims.MinimumSize", this.config_claims_minSize); config.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth); config.set("GriefPrevention.Claims.IdleLimitDays", this.config_claims_expirationDays); config.set("GriefPrevention.Claims.TrappedCommandCooldownHours", this.config_claims_trappedCooldownHours); config.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled); config.set("GriefPrevention.Spam.LoginCooldownMinutes", this.config_spam_loginCooldownMinutes); config.set("GriefPrevention.Spam.MonitorSlashCommands", slashCommandsToMonitor); config.set("GriefPrevention.Spam.WarningMessage", this.config_spam_warningMessage); config.set("GriefPrevention.Spam.BanOffenders", this.config_spam_banOffenders); config.set("GriefPrevention.Spam.BanMessage", this.config_spam_banMessage); config.set("GriefPrevention.Spam.AllowedIpAddresses", this.config_spam_allowedIpAddresses); config.set("GriefPrevention.PvP.ProtectFreshSpawns", this.config_pvp_protectFreshSpawns); config.set("GriefPrevention.PvP.PunishLogout", this.config_pvp_punishLogout); config.set("GriefPrevention.PvP.CombatTimeoutSeconds", this.config_pvp_combatTimeoutSeconds); config.set("GriefPrevention.PvP.AllowCombatItemDrop", this.config_pvp_allowCombatItemDrop); config.set("GriefPrevention.Trees.RemoveFloatingTreetops", this.config_trees_removeFloatingTreetops); config.set("GriefPrevention.Trees.RegrowGriefedTrees", this.config_trees_regrowGriefedTrees); config.set("GriefPrevention.Economy.ClaimBlocksPurchaseCost", this.config_economy_claimBlocksPurchaseCost); config.set("GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue); config.set("GriefPrevention.BlockSurfaceExplosions", this.config_blockSurfaceExplosions); config.set("GriefPrevention.FireSpreads", this.config_fireSpreads); config.set("GriefPrevention.FireDestroys", this.config_fireDestroys); config.set("GriefPrevention.AddItemsToClaimedChests", this.config_addItemsToClaimedChests); config.set("GriefPrevention.EavesdropEnabled", this.config_eavesdrop); config.set("GriefPrevention.SmartBan", this.config_smartBan); config.set("GriefPrevention.Siege.Worlds", siegeEnabledWorldNames); config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList); try { config.save(DataStore.configFilePath); } catch(IOException exception) { AddLogEntry("Unable to write to the configuration file at \"" + DataStore.configFilePath + "\""); } //try to parse the list of commands which should be monitored for spam this.config_spam_monitorSlashCommands = new ArrayList(); String [] commands = slashCommandsToMonitor.split(";"); for(int i = 0; i < commands.length; i++) { this.config_spam_monitorSlashCommands.add(commands[i].trim()); } //when datastore initializes, it loads player and claim data, and posts some stats to the log this.dataStore = new DataStore(); //unless claim block accrual is disabled, start the recurring per 5 minute event to give claim blocks to online players //20L ~ 1 second if(this.config_claims_blocksAccruedPerHour > 0) { DeliverClaimBlocksTask task = new DeliverClaimBlocksTask(); this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task, 20L * 60 * 5, 20L * 60 * 5); } //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 entityEventHandler = new EntityEventHandler(this.dataStore); pluginManager.registerEvents(entityEventHandler, this); //if economy is enabled if(this.config_economy_claimBlocksPurchaseCost > 0 || this.config_economy_claimBlocksSellValue > 0) { //try to load Vault GriefPrevention.AddLogEntry("GriefPrevention requires Vault for economy integration."); GriefPrevention.AddLogEntry("Attempting to load Vault..."); RegisteredServiceProvider economyProvider = getServer().getServicesManager().getRegistration(net.milkbowl.vault.economy.Economy.class); GriefPrevention.AddLogEntry("Vault loaded successfully!"); //ask Vault to hook into an economy plugin GriefPrevention.AddLogEntry("Looking for a Vault-compatible economy plugin..."); if (economyProvider != null) { GriefPrevention.economy = economyProvider.getProvider(); //on success, display success message if(GriefPrevention.economy != null) { GriefPrevention.AddLogEntry("Hooked into economy: " + GriefPrevention.economy.getName() + "."); GriefPrevention.AddLogEntry("Ready to buy/sell claim blocks!"); } //otherwise error message else { GriefPrevention.AddLogEntry("ERROR: Vault was unable to find a supported economy plugin. Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero."); } } //another error case else { GriefPrevention.AddLogEntry("ERROR: Vault was unable to find a supported economy plugin. Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero."); } } } //handles slash commands public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ Player player = null; if (sender instanceof Player) { player = (Player) sender; } //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); } //ignoreclaims if(cmd.getName().equalsIgnoreCase("ignoreclaims") && player != null) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.ignoreClaims = !playerData.ignoreClaims; //toggle ignore claims mode on or off if(!playerData.ignoreClaims) { GriefPrevention.sendMessage(player, TextMode.Success, "Now respecting claims."); } else { GriefPrevention.sendMessage(player, TextMode.Success, "Now ignoring claims."); } return true; } //abandonallclaims else if(cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) { if(args.length != 0) return false; if(creativeRulesApply(player.getLocation())) { GriefPrevention.sendMessage(player, TextMode.Err, "Creative mode claims can't be abandoned."); return true; } //count claims PlayerData playerData = this.dataStore.getPlayerData(player.getName()); int originalClaimCount = playerData.claims.size(); //check count if(originalClaimCount == 0) { GriefPrevention.sendMessage(player, TextMode.Err, "You haven't claimed any land."); return true; } //delete them this.dataStore.deleteClaimsForPlayer(player.getName(), false); //inform the player int remainingBlocks = playerData.getRemainingClaimBlocks(); GriefPrevention.sendMessage(player, TextMode.Success, "Claims abandoned. You now have " + String.valueOf(remainingBlocks) + " available claim blocks."); //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.getName()); playerData.shovelMode = ShovelMode.RestoreNature; GriefPrevention.sendMessage(player, TextMode.Instr, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop."); return true; } //restore nature aggressive mode else if(cmd.getName().equalsIgnoreCase("restorenatureaggressive") && player != null) { //change shovel mode PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.shovelMode = ShovelMode.RestoreNatureAggressive; GriefPrevention.sendMessage(player, TextMode.Warn, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop."); return true; } //restore nature fill mode else if(cmd.getName().equalsIgnoreCase("restorenaturefill") && player != null) { //change shovel mode PlayerData playerData = this.dataStore.getPlayerData(player.getName()); 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, "Fill mode activated with radius " + playerData.fillRadius + ". Right-click an area to fill."); 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) { //requires exactly one parameter, the other player's name if(args.length != 1) return false; //check additional permission if(!player.hasPermission("griefprevention.adminclaims")) { GriefPrevention.sendMessage(player, TextMode.Err, "That command requires the administrative claims permission."); return true; } //which claim is the user in? Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); if(claim == null) { GriefPrevention.sendMessage(player, TextMode.Instr, "There's no claim here. Stand in the administrative claim you want to transfer."); return true; } else if(!claim.isAdminClaim()) { GriefPrevention.sendMessage(player, TextMode.Err, "Only administrative claims may be transferred to a player."); return true; } OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); if(targetPlayer == null) { GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); return true; } //change ownerhsip try { this.dataStore.changeClaimOwner(claim, targetPlayer.getName()); } catch(Exception e) { GriefPrevention.sendMessage(player, TextMode.Instr, "Only top level claims (not subdivisions) may be transferred. Stand outside of the subdivision and try again."); return true; } //confirm GriefPrevention.sendMessage(player, TextMode.Success, "Claim transferred."); GriefPrevention.AddLogEntry(player.getName() + " transferred a claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " to " + targetPlayer.getName() + "."); 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, "Stand inside the claim you're curious about."); return true; } //if no permission to manage permissions, error message String errorMessage = claim.allowGrantPermission(player); if(errorMessage != null) { GriefPrevention.sendMessage(player, TextMode.Err, errorMessage); 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); player.sendMessage("Explicit permissions here:"); StringBuilder permissions = new StringBuilder(); permissions.append(ChatColor.GOLD + "M: "); if(managers.size() > 0) { for(int i = 0; i < managers.size(); i++) permissions.append(managers.get(i) + " "); } player.sendMessage(permissions.toString()); permissions = new StringBuilder(); permissions.append(ChatColor.YELLOW + "B: "); if(builders.size() > 0) { for(int i = 0; i < builders.size(); i++) permissions.append(builders.get(i) + " "); } player.sendMessage(permissions.toString()); permissions = new StringBuilder(); permissions.append(ChatColor.GREEN + "C: "); if(containers.size() > 0) { for(int i = 0; i < containers.size(); i++) permissions.append(containers.get(i) + " "); } player.sendMessage(permissions.toString()); permissions = new StringBuilder(); permissions.append(ChatColor.BLUE + "A :"); if(accessors.size() > 0) { for(int i = 0; i < accessors.size(); i++) permissions.append(accessors.get(i) + " "); } player.sendMessage(permissions.toString()); player.sendMessage("(M-anager, B-builder, C-ontainers, A-ccess)"); return true; } //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.allowEdit(player) == null) { clearPermissions = true; } else { GriefPrevention.sendMessage(player, TextMode.Err, "Only the claim owner can clear all permissions."); return true; } } else { //validate player argument otherPlayer = this.resolvePlayer(args[0]); if(!clearPermissions && otherPlayer == null && !args[0].equals("public")) { GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); 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.getName()); for(int i = 0; i < playerData.claims.size(); i++) { claim = playerData.claims.get(i); //if untrusting "all" drop all permissions if(clearPermissions) { claim.clearPermissions(); } //otherwise drop individual permissions else { claim.dropPermission(args[0]); claim.managers.remove(args[0]); } //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, "Revoked " + args[0] + "'s access to ALL your claims. To set permissions for a single claim, stand inside it."); } else { GriefPrevention.sendMessage(player, TextMode.Success, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it."); } } //otherwise, apply changes to only this claim else if(claim.allowGrantPermission(player) != null) { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have " + claim.getOwnerName() + "'s permission to manage permissions here."); } else { //if clearing all if(clearPermissions) { claim.clearPermissions(); GriefPrevention.sendMessage(player, TextMode.Success, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them."); } //otherwise individual permission drop else { claim.dropPermission(args[0]); if(claim.allowEdit(player) == null) { claim.managers.remove(args[0]); //beautify for output if(args[0].equals("public")) { args[0] = "the public"; } GriefPrevention.sendMessage(player, TextMode.Success, "Revoked " + args[0] + "'s access to this claim. To set permissions for a ALL your claims, stand outside them."); } } //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; } //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; } //buyclaimblocks else if(cmd.getName().equalsIgnoreCase("buyclaimblocks") && player != null) { //if economy is disabled, don't do anything if(GriefPrevention.economy == null) return true; //if purchase disabled, send error message if(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) { GriefPrevention.sendMessage(player, TextMode.Err, "Claim blocks may only be sold, not purchased."); return true; } //if no parameter, just tell player cost per block and balance if(args.length != 1) { GriefPrevention.sendMessage(player, TextMode.Info, "Each claim block costs " + GriefPrevention.instance.config_economy_claimBlocksPurchaseCost + ". Your balance is " + GriefPrevention.economy.getBalance(player.getName()) + "."); return false; } else { //determine max purchasable blocks PlayerData playerData = this.dataStore.getPlayerData(player.getName()); int maxPurchasable = GriefPrevention.instance.config_claims_maxAccruedBlocks - playerData.accruedClaimBlocks; //if the player is at his max, tell him so if(maxPurchasable <= 0) { GriefPrevention.sendMessage(player, TextMode.Err, "You've reached your claim block limit. You can't purchase more."); return true; } //try to parse number of blocks int blockCount; try { blockCount = Integer.parseInt(args[0]); } catch(NumberFormatException numberFormatException) { return false; //causes usage to be displayed } //correct block count to max allowed if(blockCount > maxPurchasable) { blockCount = maxPurchasable; } //if the player can't afford his purchase, send error message double balance = economy.getBalance(player.getName()); double totalCost = blockCount * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost; if(totalCost > balance) { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have enough money. You need " + totalCost + ", but you only have " + balance + "."); } //otherwise carry out transaction else { //withdraw cost economy.withdrawPlayer(player.getName(), totalCost); //add blocks playerData.accruedClaimBlocks += blockCount; this.dataStore.savePlayerData(player.getName(), playerData); //inform player GriefPrevention.sendMessage(player, TextMode.Success, "Withdrew " + totalCost + " from your account. You now have " + playerData.getRemainingClaimBlocks() + " available claim blocks."); } return true; } } //sellclaimblocks else if(cmd.getName().equalsIgnoreCase("sellclaimblocks") && player != null) { //if economy is disabled, don't do anything if(GriefPrevention.economy == null) return true; //if disabled, error message if(GriefPrevention.instance.config_economy_claimBlocksSellValue == 0) { GriefPrevention.sendMessage(player, TextMode.Err, "Claim blocks may only be purchased, not sold."); return true; } //load player data PlayerData playerData = this.dataStore.getPlayerData(player.getName()); 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, "Each claim block is worth " + GriefPrevention.instance.config_economy_claimBlocksSellValue + ". You have " + availableBlocks + " available for sale."); 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 he doesn't have enough blocks, tell him so if(blockCount > availableBlocks) { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have that many claim blocks available for sale."); } //otherwise carry out the transaction else { //compute value and deposit it double totalValue = blockCount * GriefPrevention.instance.config_economy_claimBlocksSellValue; economy.depositPlayer(player.getName(), totalValue); //subtract blocks playerData.accruedClaimBlocks -= blockCount; this.dataStore.savePlayerData(player.getName(), playerData); //inform player GriefPrevention.sendMessage(player, TextMode.Success, "Deposited " + totalValue + " in your account. You now have " + playerData.getRemainingClaimBlocks() + " available claim blocks."); } return true; } //adminclaims else if(cmd.getName().equalsIgnoreCase("adminclaims") && player != null) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.shovelMode = ShovelMode.Admin; GriefPrevention.sendMessage(player, TextMode.Success, "Administrative claims mode active. Any claims created will be free and editable by other administrators."); return true; } //basicclaims else if(cmd.getName().equalsIgnoreCase("basicclaims") && player != null) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.shovelMode = ShovelMode.Basic; playerData.claimSubdividing = null; GriefPrevention.sendMessage(player, TextMode.Success, "Returned to basic claim creation mode."); return true; } //subdivideclaims else if(cmd.getName().equalsIgnoreCase("subdivideclaims") && player != null) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.shovelMode = ShovelMode.Subdivide; playerData.claimSubdividing = null; GriefPrevention.sendMessage(player, TextMode.Instr, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit."); 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, "There's no claim here."); } else { //deleting an admin claim additionally requires the adminclaims permission if(!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims")) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); if(claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion) { GriefPrevention.sendMessage(player, TextMode.Warn, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again."); playerData.warnedAboutMajorDeletion = true; } else { claim.removeSurfaceFluids(null); this.dataStore.deleteClaim(claim); GriefPrevention.sendMessage(player, TextMode.Success, "Claim deleted."); GriefPrevention.AddLogEntry(player.getName() + " deleted " + claim.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); //revert any current visualization Visualization.Revert(player); playerData.warnedAboutMajorDeletion = false; } } else { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have permission to delete administrative claims."); } } return true; } //deleteallclaims else if(cmd.getName().equalsIgnoreCase("deleteallclaims") && player != null) { //requires exactly one parameter, the other player's name if(args.length != 1) return false; //try to find that player OfflinePlayer otherPlayer = this.resolvePlayer(args[0]); if(otherPlayer == null) { GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); return true; } //delete all that player's claims this.dataStore.deleteClaimsForPlayer(otherPlayer.getName(), true); GriefPrevention.sendMessage(player, TextMode.Success, "Deleted all of " + otherPlayer.getName() + "'s claims."); //revert any current visualization Visualization.Revert(player); return true; } //deletealladminclaims else if(cmd.getName().equalsIgnoreCase("deletealladminclaims") && player != null) { if(!player.hasPermission("griefprevention.deleteclaims")) { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have permission to delete claims."); return true; } //delete all admin claims this.dataStore.deleteClaimsForPlayer("", true); //empty string for owner name indicates an administrative claim GriefPrevention.sendMessage(player, TextMode.Success, "Deleted all administrative claims."); //revert any current visualization Visualization.Revert(player); return true; } //adjustbonusclaimblocks else if(cmd.getName().equalsIgnoreCase("adjustbonusclaimblocks") && player != null) { //requires exactly two parameters, the other player's name and the adjustment if(args.length != 2) return false; //find the specified player OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); if(targetPlayer == null) { GriefPrevention.sendMessage(player, TextMode.Err, "Player \"" + args[0] + "\" not found."); return true; } //parse the adjustment amount int adjustment; try { adjustment = Integer.parseInt(args[1]); } catch(NumberFormatException numberFormatException) { return false; //causes usage to be displayed } //give blocks to player PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getName()); playerData.bonusClaimBlocks += adjustment; this.dataStore.savePlayerData(targetPlayer.getName(), playerData); GriefPrevention.sendMessage(player, TextMode.Success, "Adjusted " + targetPlayer.getName() + "'s bonus claim blocks by " + adjustment + ". New total bonus blocks: " + playerData.bonusClaimBlocks + "."); GriefPrevention.AddLogEntry(player.getName() + " adjusted " + targetPlayer.getName() + "'s bonus claim blocks by " + adjustment + "."); 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.getName()); 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.allowBuild(player) == null) { GriefPrevention.sendMessage(player, TextMode.Err, "You can build here. Save yourself."); return true; } //check cooldown long lastTrappedUsage = playerData.lastTrappedUsage.getTime(); long nextTrappedUsage = lastTrappedUsage + 1000 * 60 * 60 * this.config_claims_trappedCooldownHours; long now = Calendar.getInstance().getTimeInMillis(); if(now < nextTrappedUsage) { GriefPrevention.sendMessage(player, TextMode.Err, "You used /trapped within the last " + this.config_claims_trappedCooldownHours + " hours. You have to wait about " + ((nextTrappedUsage - now) / (1000 * 60) + 1) + " more minutes before using it again."); return true; } //send instructions GriefPrevention.sendMessage(player, TextMode.Instr, "If you stay put for 10 seconds, you'll be teleported out. Please wait."); //create a task to rescue this player in a little while PlayerRescueTask task = new PlayerRescueTask(player, player.getLocation()); this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 200L); //20L ~ 1 second return true; } //siege else if(cmd.getName().equalsIgnoreCase("siege") && player != null) { //error message for when siege mode is disabled if(!this.siegeEnabledForWorld(player.getWorld())) { GriefPrevention.sendMessage(player, TextMode.Err, "Siege is disabled here."); return true; } //requires one argument if(args.length > 1) { return false; } //can't start a siege when you're already involved in one Player attacker = player; PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName()); if(attackerData.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, "You're already involved in a siege."); return true; } //if a player name was specified, use that Player defender = null; if(args.length >= 1) { defender = this.getServer().getPlayer(args[0]); if(defender == null) { GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); return true; } } //otherwise use the last player this player was in pvp combat with else if(attackerData.lastPvpPlayer.length() > 0) { defender = this.getServer().getPlayer(attackerData.lastPvpPlayer); if(defender == null) { return false; } } else { return false; } //victim must not be under siege already PlayerData defenderData = this.dataStore.getPlayerData(defender.getName()); if(defenderData.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, defender.getName() + " is already under siege. Join the party!"); return true; } //victim must not be pvp immune if(defenderData.pvpImmune) { GriefPrevention.sendMessage(player, TextMode.Err, defender.getName() + " is defenseless. Go pick on somebody else."); return true; } Claim defenderClaim = this.dataStore.getClaimAt(defender.getLocation(), false, null); //defender must have some level of permission there to be protected if(defenderClaim == null || defenderClaim.allowAccess(defender) != null) { GriefPrevention.sendMessage(player, TextMode.Err, defender.getName() + " isn't protected there."); return true; } //attacker must be close to the claim he wants to siege if(!defenderClaim.isNear(attacker.getLocation(), 25)) { GriefPrevention.sendMessage(player, TextMode.Err, "You're too far away from " + defender.getName() + " to siege."); return true; } //claim can't be under siege already if(defenderClaim.siegeData != null) { GriefPrevention.sendMessage(player, TextMode.Err, "That area is already under siege. Join the party!"); return true; } //can't siege admin claims if(defenderClaim.isAdminClaim()) { GriefPrevention.sendMessage(player, TextMode.Err, "Siege is disabled in this area."); return true; } //can't be on cooldown if(dataStore.onCooldown(attacker, defender, defenderClaim)) { GriefPrevention.sendMessage(player, TextMode.Err, "You're still on siege cooldown for this defender or claim. Find another victim."); return true; } //start the siege dataStore.startSiege(attacker, defender, defenderClaim); //confirmation message for attacker, warning message for defender GriefPrevention.sendMessage(defender, TextMode.Warn, "You're under siege! If you log out now, you will die. You must defeat " + attacker.getName() + ", wait for him to give up, or escape."); GriefPrevention.sendMessage(player, TextMode.Success, "The siege has begun! If you log out now, you will die. You must defeat " + defender.getName() + ", chase him away, or admit defeat and walk away."); } return false; } public static String getfriendlyLocationString(Location location) { return location.getWorld().getName() + "(" + location.getBlockX() + "," + location.getBlockY() + "," + location.getBlockZ() + ")"; } private boolean abandonClaimHandler(Player player, boolean deleteTopLevelClaim) { //which claim is being abandoned? Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); if(claim == null) { GriefPrevention.sendMessage(player, TextMode.Instr, "Stand in the claim you want to delete, or consider /AbandonAllClaims."); } else if(this.creativeRulesApply(player.getLocation())) { GriefPrevention.sendMessage(player, TextMode.Err, "Creative-mode claims can't be abandoned."); } //verify ownership else if(claim.allowEdit(player) != null) { GriefPrevention.sendMessage(player, TextMode.Err, "This isn't your claim."); } //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, "To delete a subdivision, stand inside it. Otherwise, use /AbandonTopLevelClaim to delete this claim and all subdivisions."); return true; } else { //delete it claim.removeSurfaceFluids(null); this.dataStore.deleteClaim(claim); //tell the player how many claim blocks he has left PlayerData playerData = this.dataStore.getPlayerData(player.getName()); int remainingBlocks = playerData.getRemainingClaimBlocks(); GriefPrevention.sendMessage(player, TextMode.Success, "Claim abandoned. You now have " + String.valueOf(remainingBlocks) + " available claim blocks."); //revert any current visualization Visualization.Revert(player); } return true; } //helper method keeps the trust commands consistent and eliminates duplicate code 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 argument OfflinePlayer otherPlayer = this.resolvePlayer(recipientName); if(otherPlayer == null && !recipientName.equals("public")) { GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); return; } if(otherPlayer != null) { recipientName = otherPlayer.getName(); } else { recipientName = "public"; } //determine which claims should be modified ArrayList targetClaims = new ArrayList(); if(claim == null) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); for(int i = 0; i < playerData.claims.size(); i++) { targetClaims.add(playerData.claims.get(i)); } } else { //check permission here if(claim.allowGrantPermission(player) != null) { GriefPrevention.sendMessage(player, TextMode.Err, "You don't have " + claim.getOwnerName() + "'s permission to grant permissions here."); return; } //see if the player has the level of permission he's trying to grant String errorMessage = null; //permission level null indicates granting permission trust if(permissionLevel == null) { errorMessage = claim.allowEdit(player); if(errorMessage != null) { errorMessage = "Only " + claim.getOwnerName() + " can grant /PermissionTrust here."; } } //otherwise just use the ClaimPermission enum values else { switch(permissionLevel) { case Access: errorMessage = claim.allowAccess(player); break; case Inventory: errorMessage = claim.allowContainers(player); break; default: errorMessage = claim.allowBuild(player); } } //error message for trying to grant a permission the player doesn't have if(errorMessage != null) { GriefPrevention.sendMessage(player, TextMode.Err, errorMessage + " You can't grant a permission you don't have yourself."); 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, "Stand inside the claim where you want to grant permission."); return; } //apply changes for(int i = 0; i < targetClaims.size(); i++) { Claim currentClaim = targetClaims.get(i); if(permissionLevel == null) { if(!currentClaim.managers.contains(recipientName)) { currentClaim.managers.add(recipientName); } } else { currentClaim.setPermission(recipientName, permissionLevel); } this.dataStore.saveClaim(currentClaim); } //notify player if(recipientName.equals("public")) recipientName = "the public"; StringBuilder resultString = new StringBuilder(); resultString.append("Granted " + recipientName + " "); if(permissionLevel == null) { resultString.append("manager status"); } else if(permissionLevel == ClaimPermission.Build) { resultString.append("permission to build in"); } else if(permissionLevel == ClaimPermission.Access) { resultString.append("permission to use buttons and levers in"); } else if(permissionLevel == ClaimPermission.Inventory) { resultString.append("permission to access containers in"); } if(claim == null) { resultString.append(" ALL your claims. To modify only one claim, stand inside it."); } else { resultString.append(" this claim. To modify ALL your claims, stand outside them."); } GriefPrevention.sendMessage(player, TextMode.Success, resultString.toString()); } //helper method to resolve a player by name private OfflinePlayer resolvePlayer(String name) { //try online players first Player player = this.getServer().getPlayer(name); if(player != null) return player; //then search offline players OfflinePlayer [] offlinePlayers = this.getServer().getOfflinePlayers(); for(int i = 0; i < offlinePlayers.length; i++) { if(offlinePlayers[i].getName().equalsIgnoreCase(name)) { return offlinePlayers[i]; } } //if none found, return null return null; } public void onDisable() { AddLogEntry("GriefPrevention disabled."); } //called when a player spawns, applies protection for that player if necessary public void checkPvpProtectionNeeded(Player player) { //if pvp is disabled, do nothing if(!player.getWorld().getPVP()) return; //if anti spawn camping feature is not enabled, do nothing if(!this.config_pvp_protectFreshSpawns) return; //check inventory for well, anything PlayerInventory inventory = player.getInventory(); ItemStack [] armorStacks = inventory.getArmorContents(); //check armor slots, stop if any items are found for(int i = 0; i < armorStacks.length; i++) { if(!(armorStacks[i] == null || armorStacks[i].getType() == Material.AIR)) return; } //check other slots, stop if any items are found ItemStack [] generalStacks = inventory.getContents(); for(int i = 0; i < generalStacks.length; i++) { if(!(generalStacks[i] == null || generalStacks[i].getType() == Material.AIR)) return; } //otherwise, apply immunity PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.pvpImmune = true; //inform the player GriefPrevention.sendMessage(player, TextMode.Success, "You're protected from attack by other players as long as your inventory is empty."); } //checks whether players can create claims in a world public boolean claimsEnabledForWorld(World world) { return this.config_claims_enabledWorlds.contains(world); } //checks whether players siege in a world public boolean siegeEnabledForWorld(World world) { return this.config_siege_enabledWorlds.contains(world); } //processes broken log blocks to automatically remove floating treetops void handleLogBroken(Block block) { //find the lowest log in the tree trunk including this log Block rootBlock = this.getRootBlock(block); //null indicates this block isn't part of a tree trunk if(rootBlock == null) return; //next step: scan for other log blocks and leaves in this tree //set boundaries for the scan int min_x = rootBlock.getX() - GriefPrevention.TREE_RADIUS; int max_x = rootBlock.getX() + GriefPrevention.TREE_RADIUS; int min_z = rootBlock.getZ() - GriefPrevention.TREE_RADIUS; int max_z = rootBlock.getZ() + GriefPrevention.TREE_RADIUS; int max_y = rootBlock.getWorld().getMaxHeight() - 1; //keep track of all the examined blocks, and all the log blocks found ArrayList examinedBlocks = new ArrayList(); ArrayList treeBlocks = new ArrayList(); //queue the first block, which is the block immediately above the player-chopped block ConcurrentLinkedQueue blocksToExamine = new ConcurrentLinkedQueue(); blocksToExamine.add(rootBlock); examinedBlocks.add(rootBlock); boolean hasLeaves = false; while(!blocksToExamine.isEmpty()) { //pop a block from the queue Block currentBlock = blocksToExamine.remove(); //if this is a log block, determine whether it should be chopped if(currentBlock.getType() == Material.LOG) { boolean partOfTree = false; //if it's stacked with the original chopped block, the answer is always yes if(currentBlock.getX() == block.getX() && currentBlock.getZ() == block.getZ()) { partOfTree = true; } //otherwise find the block underneath this stack of logs else { Block downBlock = currentBlock.getRelative(BlockFace.DOWN); while(downBlock.getType() == Material.LOG) { downBlock = downBlock.getRelative(BlockFace.DOWN); } //if it's air or leaves, it's okay to chop this block //this avoids accidentally chopping neighboring trees which are close enough to touch their leaves to ours if(downBlock.getType() == Material.AIR || downBlock.getType() == Material.LEAVES) { partOfTree = true; } //otherwise this is a stack of logs which touches a solid surface //if it's close to the original block's stack, don't clean up this tree (just stop here) else { if(Math.abs(downBlock.getX() - block.getX()) <= 1 && Math.abs(downBlock.getZ() - block.getZ()) <= 1) return; } } if(partOfTree) { treeBlocks.add(currentBlock); } } //if this block is a log OR a leaf block, also check its neighbors if(currentBlock.getType() == Material.LOG || currentBlock.getType() == Material.LEAVES) { if(currentBlock.getType() == Material.LEAVES) { hasLeaves = true; } Block [] neighboringBlocks = new Block [] { currentBlock.getRelative(BlockFace.EAST), currentBlock.getRelative(BlockFace.WEST), currentBlock.getRelative(BlockFace.NORTH), currentBlock.getRelative(BlockFace.SOUTH), currentBlock.getRelative(BlockFace.UP), currentBlock.getRelative(BlockFace.DOWN) }; for(int i = 0; i < neighboringBlocks.length; i++) { Block neighboringBlock = neighboringBlocks[i]; //if the neighboringBlock is out of bounds, skip it if(neighboringBlock.getX() < min_x || neighboringBlock.getX() > max_x || neighboringBlock.getZ() < min_z || neighboringBlock.getZ() > max_z || neighboringBlock.getY() > max_y) continue; //if we already saw this block, skip it if(examinedBlocks.contains(neighboringBlock)) continue; //mark the block as examined examinedBlocks.add(neighboringBlock); //if the neighboringBlock is a leaf or log, put it in the queue to be examined later if(neighboringBlock.getType() == Material.LOG || neighboringBlock.getType() == Material.LEAVES) { blocksToExamine.add(neighboringBlock); } //if we encounter any player-placed block type, bail out (don't automatically remove parts of this tree, it might support a treehouse!) else if(this.isPlayerBlock(neighboringBlock)) { return; } } } } //if it doesn't have leaves, it's not a tree, so don't clean it up if(hasLeaves) { //schedule a cleanup task for later, in case the player leaves part of this tree hanging in the air TreeCleanupTask cleanupTask = new TreeCleanupTask(block, rootBlock, treeBlocks); //20L ~ 1 second, so 2 mins = 120 seconds ~ 2400L GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, cleanupTask, 2400L); } } //helper for above, finds the "root" of a stack of logs //will return null if the stack is determined to not be a natural tree private Block getRootBlock(Block logBlock) { if(logBlock.getType() != Material.LOG) return null; //run down through log blocks until finding a non-log block Block underBlock = logBlock.getRelative(BlockFace.DOWN); while(underBlock.getType() == Material.LOG) { underBlock = underBlock.getRelative(BlockFace.DOWN); } //if this is a standard tree, that block MUST be dirt if(underBlock.getType() != Material.DIRT) return null; //run up through log blocks until finding a non-log block Block aboveBlock = logBlock.getRelative(BlockFace.UP); while(aboveBlock.getType() == Material.LOG) { aboveBlock = aboveBlock.getRelative(BlockFace.UP); } //if this is a standard tree, that block MUST be air or leaves if(aboveBlock.getType() != Material.AIR && aboveBlock.getType() != Material.LEAVES) return null; return underBlock.getRelative(BlockFace.UP); } //for sake of identifying trees ONLY, a cheap but not 100% reliable method for identifying player-placed blocks private boolean isPlayerBlock(Block block) { Material material = block.getType(); //list of natural blocks which are OK to have next to a log block in a natural tree setting if( material == Material.AIR || material == Material.LEAVES || material == Material.LOG || material == Material.DIRT || material == Material.GRASS || material == Material.STATIONARY_WATER || material == Material.BROWN_MUSHROOM || material == Material.RED_MUSHROOM || material == Material.RED_ROSE || material == Material.LONG_GRASS || material == Material.SNOW || material == Material.STONE || material == Material.VINE || material == Material.WATER_LILY || material == Material.YELLOW_FLOWER || material == Material.CLAY) { return false; } else { 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 static void sendMessage(Player player, ChatColor color, String message) { player.sendMessage(color + message); } //determines whether creative anti-grief rules apply at a location boolean creativeRulesApply(Location location) { return this.config_claims_enabledCreativeWorlds.contains(location.getWorld()); } public String allowBuild(Player player, Location location) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); //wilderness rules if(claim == null) { //no building in the wilderness in creative mode if(this.creativeRulesApply(location)) { //exception: administrators in ignore claims mode if(playerData.ignoreClaims) return null; return "You can't build here. Use the golden shovel to claim some land first."; } //but it's fine in survival mode else { //cache the claim for later reference playerData.lastClaim = claim; return null; } } //if not in the wilderness, then apply claim rules (permissions, etc) return claim.allowBuild(player); } public String allowBreak(Player player, Location location) { PlayerData playerData = this.dataStore.getPlayerData(player.getName()); Claim claim = this.dataStore.getClaimAt(location, false, playerData.lastClaim); //wilderness rules if(claim == null) { //no building in the wilderness in creative mode if(this.creativeRulesApply(location)) { //exception: administrators in ignore claims mode if(playerData.ignoreClaims) return null; return "You can't build here. Use the golden shovel to claim some land first."; } //but it's fine in survival mode else { //cache the claim for later reference playerData.lastClaim = claim; return null; } } //if not in the wilderness, then apply claim rules (permissions, etc) return claim.allowBreak(player, location.getBlock().getType()); } }