From e09a6732bae23bac343be2964d5b0e0a3b2ae503 Mon Sep 17 00:00:00 2001 From: Ryan Hamshire Date: Wed, 7 Nov 2012 17:36:25 -0800 Subject: [PATCH] 6.9 --- plugin.yml | 354 +- .../GriefPrevention/BlockEventHandler.java | 1436 ++--- .../ryanhamshire/GriefPrevention/Claim.java | 1654 +++--- .../CleanupUnusedClaimsTask.java | 404 +- .../GriefPrevention/DataStore.java | 2055 +++---- .../GriefPrevention/EntityEventHandler.java | 1052 ++-- .../GriefPrevention/GriefPrevention.java | 4975 +++++++++-------- .../GriefPrevention/Messages.java | 12 +- .../GriefPrevention/PlayerEventHandler.java | 3420 +++++------ .../RestoreNatureExecutionTask.java | 232 +- .../RestoreNatureProcessingTask.java | 1489 ++--- .../GriefPrevention/Visualization.java | 452 +- 12 files changed, 8839 insertions(+), 8696 deletions(-) diff --git a/plugin.yml b/plugin.yml index 835e070..2111048 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,178 +1,178 @@ -name: GriefPrevention -main: me.ryanhamshire.GriefPrevention.GriefPrevention -softdepend: [Vault, Multiverse-Core, My Worlds, MystCraft, Transporter] -dev-url: http://dev.bukkit.org/server-mods/grief-prevention -version: 6.7 -commands: - abandonclaim: - description: Deletes a claim. - usage: /AbandonClaim - permission: griefprevention.claims - abandontoplevelclaim: - description: Deletes a claim and all its subdivisions. - usage: /AbandonTopLevelClaim - permission: griefprevention.claims - abandonallclaims: - description: Deletes ALL your claims. - usage: /AbandonAllClaims - permission: griefprevention.claims - trust: - description: Grants a player full access to your claim(s). - usage: /Trust Graants a player permission to build. See also /UnTrust, /ContainerTrust, /AccessTrust, and /PermissionTrust. - aliases: tr - permission: griefprevention.claims - untrust: - description: Revokes a player's access to your claim(s). - usage: /UnTrust - aliases: ut - permission: griefprevention.claims - containertrust: - description: Grants a player access to your containers. - usage: /ContainerTrust . Grants a player access to your inventory, bed, and buttons/levers. - aliases: ct - permission: griefprevention.claims - accesstrust: - description: Grants a player entry to your claim(s) and use of your bed. - usage: /AccessTrust . Grants a player access to your bed, buttons, and levers. - aliases: at - permission: griefprevention.claims - permissiontrust: - description: Grants a player permission to grant his level of permission to others. - usage: /PermissionTrust . Permits a player to share his permission level with others. - aliases: pt - permission: griefprevention.claims - subdivideclaims: - description: Switches the shovel tool to subdivision mode, used to subdivide your claims. - usage: /SubdivideClaims - aliases: sc - permission: griefprevention.claims - adjustbonusclaimblocks: - description: Adds or subtracts bonus claim blocks for a player. - usage: /AdjustBonusClaimBlocks - permission: griefprevention.adjustclaimblocks - aliases: acb - deleteclaim: - description: Deletes the claim you're standing in, even if it's not your claim. - usage: /DeleteClaim - permission: griefprevention.deleteclaims - aliases: dc - deleteallclaims: - description: Deletes all of another player's claims. - usage: /DeleteAllClaims - permission: griefprevention.deleteclaims - adminclaims: - description: Switches the shovel tool to administrative claims mode. - usage: /AdminClaims - permission: griefprevention.adminclaims - aliases: ac - restorenature: - description: Switches the shovel tool to restoration mode. - usage: /RestoreNature - permission: griefprevention.restorenature - aliases: rn - restorenatureaggressive: - description: Switches the shovel tool to aggressive restoration mode. - usage: /RestoreNatureAggressive - permission: griefprevention.restorenatureaggressive - aliases: rna - restorenaturefill: - description: Switches the shovel tool to fill mode. - usage: /RestoreNatureFill - permission: griefprevention.restorenatureaggressive - aliases: rnf - basicclaims: - description: Switches the shovel tool back to basic claims mode. - usage: /BasicClaims - aliases: bc - permission: griefprevention.claims - buyclaimblocks: - description: Purchases additional claim blocks with server money. Doesn't work on servers without a Vault-compatible economy plugin. - usage: /BuyClaimBlocks - aliases: buyclaim - permission: griefprevention.claims - sellclaimblocks: - description: Sells your claim blocks for server money. Doesn't work on servers without a Vault-compatible economy plugin. - usage: /SellClaimBlocks - aliases: sellclaim - permission: griefprevention.claims - trapped: - description: Ejects you to nearby unclaimed land. Has a substantial cooldown period. - usage: /Trapped - trustlist: - description: Lists permissions for the claim you're standing in. - usage: /TrustList - permission: griefprevention.claims - siege: - description: Initiates a siege versus another player. - usage: /Siege - ignoreclaims: - description: Toggles ignore claims mode. - usage: /IgnoreClaims - permission: griefprevention.ignoreclaims - aliases: ic - deletealladminclaims: - description: Deletes all administrative claims. - usage: /DeleteAllAdminClaims - permission: griefprevention.adminclaims - transferclaim: - description: Converts an administrative claim to a private claim. - usage: /TransferClaim - permission: griefprevention.adjustclaimblocks - deathblow: - description: Kills a player, optionally giving his inventory to another player. - usage: /DeathBlow [recipientPlayer] - permission: griefprevention.deathblow - claimslist: - description: Lists information about a player's claim blocks and claims. - usage: /ClaimsList - permission: griefprevention.adjustclaimblocks -permissions: - griefprevention.createclaims: - description: Grants permission to create claims. - default: op - griefprevention.admin.*: - description: Grants all administrative functionality. - children: - griefprevention.restorenature: true - griefprevention.restorenatureaggressive: true - griefprevention.ignoreclaims: true - griefprevention.adminclaims: true - griefprevention.adjustclaimblocks: true - griefprevention.deleteclaims: true - griefprevention.spam: true - griefprevention.lava: true - griefprevention.eavesdrop: true - griefprevention.deathblow: true - griefprevention.restorenature: - description: Grants permission to use /RestoreNature. - default: op - griefprevention.ignoreclaims: - description: Grants permission to use /IgnoreClaims. - default: op - griefprevention.adminclaims: - description: Grants permission to create administrative claims. - default: op - griefprevention.deleteclaims: - description: Grants permission to delete other players' claims. - default: op - griefprevention.adjustclaimblocks: - description: Grants permission to add or remove bonus blocks from a player's account. - default: op - griefprevention.spam: - description: Grants permission to log in, send messages, and send commands rapidly. - default: op - griefprevention.lava: - description: Grants permission to place lava near the surface and outside of claims. - default: op - griefprevention.eavesdrop: - description: Allows a player to see whispered chat messages (/tell). - default: op - griefprevention.restorenatureaggressive: - description: Grants access to /RestoreNatureAggressive and /RestoreNatureFill. - default: op - griefprevention.deathblow: - description: Grants access to /DeathBlow. - default: op - griefprevention.claims: - description: Grants access to claim-related slash commands. +name: GriefPrevention +main: me.ryanhamshire.GriefPrevention.GriefPrevention +softdepend: [Vault, Multiverse-Core, My Worlds, MystCraft, Transporter] +dev-url: http://dev.bukkit.org/server-mods/grief-prevention +version: 6.9 +commands: + abandonclaim: + description: Deletes a claim. + usage: /AbandonClaim + permission: griefprevention.claims + abandontoplevelclaim: + description: Deletes a claim and all its subdivisions. + usage: /AbandonTopLevelClaim + permission: griefprevention.claims + abandonallclaims: + description: Deletes ALL your claims. + usage: /AbandonAllClaims + permission: griefprevention.claims + trust: + description: Grants a player full access to your claim(s). + usage: /Trust Graants a player permission to build. See also /UnTrust, /ContainerTrust, /AccessTrust, and /PermissionTrust. + aliases: tr + permission: griefprevention.claims + untrust: + description: Revokes a player's access to your claim(s). + usage: /UnTrust + aliases: ut + permission: griefprevention.claims + containertrust: + description: Grants a player access to your containers. + usage: /ContainerTrust . Grants a player access to your inventory, bed, and buttons/levers. + aliases: ct + permission: griefprevention.claims + accesstrust: + description: Grants a player entry to your claim(s) and use of your bed. + usage: /AccessTrust . Grants a player access to your bed, buttons, and levers. + aliases: at + permission: griefprevention.claims + permissiontrust: + description: Grants a player permission to grant his level of permission to others. + usage: /PermissionTrust . Permits a player to share his permission level with others. + aliases: pt + permission: griefprevention.claims + subdivideclaims: + description: Switches the shovel tool to subdivision mode, used to subdivide your claims. + usage: /SubdivideClaims + aliases: sc + permission: griefprevention.claims + adjustbonusclaimblocks: + description: Adds or subtracts bonus claim blocks for a player. + usage: /AdjustBonusClaimBlocks + permission: griefprevention.adjustclaimblocks + aliases: acb + deleteclaim: + description: Deletes the claim you're standing in, even if it's not your claim. + usage: /DeleteClaim + permission: griefprevention.deleteclaims + aliases: dc + deleteallclaims: + description: Deletes all of another player's claims. + usage: /DeleteAllClaims + permission: griefprevention.deleteclaims + adminclaims: + description: Switches the shovel tool to administrative claims mode. + usage: /AdminClaims + permission: griefprevention.adminclaims + aliases: ac + restorenature: + description: Switches the shovel tool to restoration mode. + usage: /RestoreNature + permission: griefprevention.restorenature + aliases: rn + restorenatureaggressive: + description: Switches the shovel tool to aggressive restoration mode. + usage: /RestoreNatureAggressive + permission: griefprevention.restorenatureaggressive + aliases: rna + restorenaturefill: + description: Switches the shovel tool to fill mode. + usage: /RestoreNatureFill + permission: griefprevention.restorenatureaggressive + aliases: rnf + basicclaims: + description: Switches the shovel tool back to basic claims mode. + usage: /BasicClaims + aliases: bc + permission: griefprevention.claims + buyclaimblocks: + description: Purchases additional claim blocks with server money. Doesn't work on servers without a Vault-compatible economy plugin. + usage: /BuyClaimBlocks + aliases: buyclaim + permission: griefprevention.claims + sellclaimblocks: + description: Sells your claim blocks for server money. Doesn't work on servers without a Vault-compatible economy plugin. + usage: /SellClaimBlocks + aliases: sellclaim + permission: griefprevention.claims + trapped: + description: Ejects you to nearby unclaimed land. Has a substantial cooldown period. + usage: /Trapped + trustlist: + description: Lists permissions for the claim you're standing in. + usage: /TrustList + permission: griefprevention.claims + siege: + description: Initiates a siege versus another player. + usage: /Siege + ignoreclaims: + description: Toggles ignore claims mode. + usage: /IgnoreClaims + permission: griefprevention.ignoreclaims + aliases: ic + deletealladminclaims: + description: Deletes all administrative claims. + usage: /DeleteAllAdminClaims + permission: griefprevention.adminclaims + transferclaim: + description: Converts an administrative claim to a private claim. + usage: /TransferClaim + permission: griefprevention.adjustclaimblocks + deathblow: + description: Kills a player, optionally giving his inventory to another player. + usage: /DeathBlow [recipientPlayer] + permission: griefprevention.deathblow + claimslist: + description: Lists information about a player's claim blocks and claims. + usage: /ClaimsList + permission: griefprevention.adjustclaimblocks +permissions: + griefprevention.createclaims: + description: Grants permission to create claims. + default: op + griefprevention.admin.*: + description: Grants all administrative functionality. + children: + griefprevention.restorenature: true + griefprevention.restorenatureaggressive: true + griefprevention.ignoreclaims: true + griefprevention.adminclaims: true + griefprevention.adjustclaimblocks: true + griefprevention.deleteclaims: true + griefprevention.spam: true + griefprevention.lava: true + griefprevention.eavesdrop: true + griefprevention.deathblow: true + griefprevention.restorenature: + description: Grants permission to use /RestoreNature. + default: op + griefprevention.ignoreclaims: + description: Grants permission to use /IgnoreClaims. + default: op + griefprevention.adminclaims: + description: Grants permission to create administrative claims. + default: op + griefprevention.deleteclaims: + description: Grants permission to delete other players' claims. + default: op + griefprevention.adjustclaimblocks: + description: Grants permission to add or remove bonus blocks from a player's account. + default: op + griefprevention.spam: + description: Grants permission to log in, send messages, and send commands rapidly. + default: op + griefprevention.lava: + description: Grants permission to place lava near the surface and outside of claims. + default: op + griefprevention.eavesdrop: + description: Allows a player to see whispered chat messages (/tell). + default: op + griefprevention.restorenatureaggressive: + description: Grants access to /RestoreNatureAggressive and /RestoreNatureFill. + default: op + griefprevention.deathblow: + description: Grants access to /DeathBlow. + default: op + griefprevention.claims: + description: Grants access to claim-related slash commands. default: true \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java index bab6be2..d06bfe9 100644 --- a/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java @@ -1,703 +1,733 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; -import java.util.List; - -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.Chest; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockDamageEvent; -import org.bukkit.event.block.BlockDispenseEvent; -import org.bukkit.event.block.BlockFromToEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; -import org.bukkit.event.block.BlockPistonExtendEvent; -import org.bukkit.event.block.BlockPistonRetractEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.block.BlockSpreadEvent; -import org.bukkit.event.block.SignChangeEvent; -import org.bukkit.event.world.StructureGrowEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.util.Vector; - -//event handlers related to blocks -public class BlockEventHandler implements Listener -{ - //convenience reference to singleton datastore - private DataStore dataStore; - - private ArrayList trashBlocks; - - //constructor - public BlockEventHandler(DataStore dataStore) - { - this.dataStore = dataStore; - - //create the list of blocks which will not trigger a warning when they're placed outside of land claims - this.trashBlocks = new ArrayList(); - this.trashBlocks.add(Material.COBBLESTONE); - this.trashBlocks.add(Material.TORCH); - this.trashBlocks.add(Material.DIRT); - this.trashBlocks.add(Material.SAPLING); - this.trashBlocks.add(Material.GRAVEL); - this.trashBlocks.add(Material.SAND); - this.trashBlocks.add(Material.TNT); - this.trashBlocks.add(Material.WORKBENCH); - } - - //when a block is damaged... - @EventHandler(ignoreCancelled = true) - public void onBlockDamaged(BlockDamageEvent event) - { - //if placing items in protected chests isn't enabled, none of this code needs to run - if(!GriefPrevention.instance.config_addItemsToClaimedChests) return; - - Block block = event.getBlock(); - Player player = event.getPlayer(); - - //only care about player-damaged blocks - if(player == null) return; - - //FEATURE: players may add items to a chest they don't have permission for by hitting it - - //if it's a chest - if(block.getType() == Material.CHEST) - { - //only care about non-creative mode players, since those would outright break the box in one hit - if(player.getGameMode() == GameMode.CREATIVE) return; - - //only care if the player has an itemstack in hand - PlayerInventory playerInventory = player.getInventory(); - ItemStack stackInHand = playerInventory.getItemInHand(); - if(stackInHand == null || stackInHand.getType() == Material.AIR) return; - - //only care if the chest is in a claim, and the player does not have access to the chest - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim == null || claim.allowContainers(player) == null) return; - - //if the player is under siege, he can't give away items - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); - event.setCancelled(true); - return; - } - - //NOTE: to eliminate accidental give-aways, first hit on a chest displays a confirmation message - //subsequent hits donate item to the chest - - //if first time damaging this chest, show confirmation message - if(playerData.lastChestDamageLocation == null || !block.getLocation().equals(playerData.lastChestDamageLocation)) - { - //remember this location - playerData.lastChestDamageLocation = block.getLocation(); - - //give the player instructions - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DonateItemsInstruction); - } - - //otherwise, try to donate the item stack in hand - else - { - //look for empty slot in chest - Chest chest = (Chest)block.getState(); - Inventory chestInventory = chest.getInventory(); - int availableSlot = chestInventory.firstEmpty(); - - //if there isn't one - if(availableSlot < 0) - { - //tell the player and stop here - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ChestFull); - - return; - } - - //otherwise, transfer item stack from player to chest - //NOTE: Inventory.addItem() is smart enough to add items to existing stacks, making filling a chest with garbage as a grief very difficult - chestInventory.addItem(stackInHand); - playerInventory.setItemInHand(new ItemStack(Material.AIR)); - - //and confirm for the player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DonationSuccess); - } - } - } - - //when a player breaks a block... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockBreak(BlockBreakEvent breakEvent) - { - Player player = breakEvent.getPlayer(); - Block block = breakEvent.getBlock(); - - //make sure the player is allowed to break at the location - String noBuildReason = GriefPrevention.instance.allowBreak(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - breakEvent.setCancelled(true); - return; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - - //if there's a claim here - if(claim != null) - { - //if breaking UNDER the claim and the player has permission to build in the claim - if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) - { - //extend the claim downward beyond the breakage point - this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); - } - } - - //FEATURE: automatically clean up hanging treetops - //if it's a log - if(block.getType() == Material.LOG && GriefPrevention.instance.config_trees_removeFloatingTreetops) - { - //run the specialized code for treetop removal (see below) - GriefPrevention.instance.handleLogBroken(block); - } - } - - //when a player places a sign... - @EventHandler(ignoreCancelled = true) - public void onSignChanged(SignChangeEvent event) - { - Player player = event.getPlayer(); - if(player == null) return; - - StringBuilder lines = new StringBuilder(); - boolean notEmpty = false; - for(int i = 0; i < event.getLines().length; i++) - { - if(event.getLine(i).length() != 0) notEmpty = true; - lines.append(event.getLine(i) + ";"); - } - - String signMessage = lines.toString(); - - //if not empty and wasn't the same as the last sign, log it and remember it for later - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if(notEmpty && playerData.lastMessage != null && !playerData.lastMessage.equals(signMessage)) - { - GriefPrevention.AddLogEntry("[Sign Placement] <" + player.getName() + "> " + lines.toString() + " @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); - playerData.lastMessage = signMessage; - } - } - - //when a player places a block... - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) - public void onBlockPlace(BlockPlaceEvent placeEvent) - { - Player player = placeEvent.getPlayer(); - Block block = placeEvent.getBlock(); - - //FEATURE: limit fire placement, to prevent PvP-by-fire - - //if placed block is fire and pvp is off, apply rules for proximity to other players - if(block.getType() == Material.FIRE && !block.getWorld().getPVP() && !player.hasPermission("griefprevention.lava")) - { - List players = block.getWorld().getPlayers(); - for(int i = 0; i < players.size(); i++) - { - Player otherPlayer = players.get(i); - Location location = otherPlayer.getLocation(); - if(!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire, otherPlayer.getName()); - placeEvent.setCancelled(true); - return; - } - } - } - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - placeEvent.setCancelled(true); - return; - } - - //if the block is being placed within an existing claim - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); - if(claim != null) - { - //warn about TNT not destroying claimed blocks - if(block.getType() == Material.TNT) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); - } - - //if the player has permission for the claim and he's placing UNDER the claim - if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) - { - //extend the claim downward - this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); - } - - //reset the counter for warning the player when he places outside his claims - playerData.unclaimedBlockPlacementsUntilWarning = 1; - } - - //FEATURE: automatically create a claim when a player who has no claims places a chest - - //otherwise if there's no claim, the player is placing a chest, and new player automatic claims are enabled - else if(block.getType() == Material.CHEST && GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) - { - //if the chest is too deep underground, don't create the claim and explain why - if(GriefPrevention.instance.config_claims_preventTheft && block.getY() < GriefPrevention.instance.config_claims_maxDepth) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.TooDeepToClaim); - return; - } - - int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; - - //if the player doesn't have any claims yet, automatically create a claim centered at the chest - if(playerData.claims.size() == 0) - { - //radius == 0 means protect ONLY the chest - if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius == 0) - { - this.dataStore.createClaim(block.getWorld(), block.getX(), block.getX(), block.getY(), block.getY(), block.getZ(), block.getZ(), player.getName(), null, null); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); - } - - //otherwise, create a claim in the area around the chest - else - { - //as long as the automatic claim overlaps another existing claim, shrink it - //note that since the player had permission to place the chest, at the very least, the automatic claim will include the chest - while(radius >= 0 && !this.dataStore.createClaim(block.getWorld(), - block.getX() - radius, block.getX() + radius, - block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, block.getY(), - block.getZ() - radius, block.getZ() + radius, - player.getName(), - null, null).succeeded) - { - radius--; - } - - //notify and explain to player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); - - //show the player the protected area - Claim newClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); - Visualization visualization = Visualization.FromClaim(newClaim, block.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - - //instructions for using /trust - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TrustCommandAdvertisement); - - //unless special permission is required to create a claim with the shovel, educate the player about the shovel - if(!GriefPrevention.instance.config_claims_creationRequiresPermission) - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.GoldenShovelAdvertisement); - } - } - - //check to see if this chest is in a claim, and warn when it isn't - if(GriefPrevention.instance.config_claims_preventTheft && this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim) == null) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnprotectedChestWarning); - } - } - - //FEATURE: limit wilderness tree planting to grass, or dirt with more blocks beneath it - else if(block.getType() == Material.SAPLING && GriefPrevention.instance.config_blockSkyTrees && GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) - { - Block earthBlock = placeEvent.getBlockAgainst(); - if(earthBlock.getType() != Material.GRASS) - { - if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR || - earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR) - { - placeEvent.setCancelled(true); - } - } - } - - //FEATURE: warn players when they're placing non-trash blocks outside of their claimed areas - else if(GriefPrevention.instance.config_claims_warnOnBuildOutside && !this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld()) && playerData.claims.size() > 0) - { - if(--playerData.unclaimedBlockPlacementsUntilWarning <= 0) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); - playerData.unclaimedBlockPlacementsUntilWarning = 15; - - if(playerData.lastClaim != null && playerData.lastClaim.allowBuild(player) == null) - { - Visualization visualization = Visualization.FromClaim(playerData.lastClaim, block.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - } - } - - //warn players when they place TNT above sea level, since it doesn't destroy blocks there - if( GriefPrevention.instance.config_blockSurfaceOtherExplosions && block.getType() == Material.TNT && - block.getWorld().getEnvironment() != Environment.NETHER && - block.getY() > block.getWorld().getSeaLevel() - 5) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); - } - } - - //blocks "pushing" other players' blocks around (pistons) - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonExtend (BlockPistonExtendEvent event) - { - List blocks = event.getBlocks(); - - //if no blocks moving, then only check to make sure we're not pushing into a claim from outside - //this avoids pistons breaking non-solids just inside a claim, like torches, doors, and touchplates - if(blocks.size() == 0) - { - Block pistonBlock = event.getBlock(); - Block invadedBlock = pistonBlock.getRelative(event.getDirection()); - - if( this.dataStore.getClaimAt(pistonBlock.getLocation(), false, null) == null && - this.dataStore.getClaimAt(invadedBlock.getLocation(), false, null) != null) - { - event.setCancelled(true); - } - - return; - } - - //who owns the piston, if anyone? - String pistonClaimOwnerName = "_"; - Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null); - if(claim != null) pistonClaimOwnerName = claim.getOwnerName(); - - //which blocks are being pushed? - for(int i = 0; i < blocks.size(); i++) - { - //if ANY of the pushed blocks are owned by someone other than the piston owner, cancel the event - Block block = blocks.get(i); - claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim != null && !claim.getOwnerName().equals(pistonClaimOwnerName)) - { - event.setCancelled(true); - event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); - event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); - event.getBlock().setType(Material.AIR); - return; - } - } - - //which direction? note we're ignoring vertical push - int xchange = 0; - int zchange = 0; - - Block piston = event.getBlock(); - Block firstBlock = blocks.get(0); - - if(firstBlock.getX() > piston.getX()) - { - xchange = 1; - } - else if(firstBlock.getX() < piston.getX()) - { - xchange = -1; - } - else if(firstBlock.getZ() > piston.getZ()) - { - zchange = 1; - } - else if(firstBlock.getZ() < piston.getZ()) - { - zchange = -1; - } - - //if horizontal movement - if(xchange != 0 || zchange != 0) - { - for(int i = 0; i < blocks.size(); i++) - { - Block block = blocks.get(i); - Claim originalClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); - String originalOwnerName = ""; - if(originalClaim != null) - { - originalOwnerName = originalClaim.getOwnerName(); - } - - Claim newClaim = this.dataStore.getClaimAt(block.getLocation().add(xchange, 0, zchange), false, null); - String newOwnerName = ""; - if(newClaim != null) - { - newOwnerName = newClaim.getOwnerName(); - } - - //if pushing this block will change ownership, cancel the event and take away the piston (for performance reasons) - if(!newOwnerName.equals(originalOwnerName)) - { - event.setCancelled(true); - event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); - event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); - event.getBlock().setType(Material.AIR); - return; - } - - } - } - } - - //blocks theft by pulling blocks out of a claim (again pistons) - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockPistonRetract (BlockPistonRetractEvent event) - { - //we only care about sticky pistons - if(!event.isSticky()) return; - - //who owns the moving block, if anyone? - String movingBlockOwnerName = "_"; - Claim movingBlockClaim = this.dataStore.getClaimAt(event.getRetractLocation(), false, null); - if(movingBlockClaim != null) movingBlockOwnerName = movingBlockClaim.getOwnerName(); - - //who owns the piston, if anyone? - String pistonOwnerName = "_"; - Location pistonLocation = event.getBlock().getLocation(); - Claim pistonClaim = this.dataStore.getClaimAt(pistonLocation, false, null); - if(pistonClaim != null) pistonOwnerName = pistonClaim.getOwnerName(); - - //if there are owners for the blocks, they must be the same player - //otherwise cancel the event - if(!pistonOwnerName.equals(movingBlockOwnerName)) - { - event.setCancelled(true); - } - } - - //blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockIgnite (BlockIgniteEvent igniteEvent) - { - if(!GriefPrevention.instance.config_fireSpreads && - igniteEvent.getBlock().getWorld().getEnvironment() == Environment.NORMAL && - igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && - igniteEvent.getCause() != IgniteCause.LIGHTNING && - igniteEvent.getCause() != IgniteCause.LAVA) - { - igniteEvent.setCancelled(true); - } - } - - //fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example) - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockSpread (BlockSpreadEvent spreadEvent) - { - if(spreadEvent.getSource().getType() != Material.FIRE) return; - - if(!GriefPrevention.instance.config_fireSpreads) - { - spreadEvent.setCancelled(true); - return; - } - - //never spread into a claimed area, regardless of settings - if(this.dataStore.getClaimAt(spreadEvent.getBlock().getLocation(), false, null) != null) - { - spreadEvent.setCancelled(true); - - //if the source of the spread is not fire on netherrack, put out that source fire to save cpu cycles - Block source = spreadEvent.getSource(); - if(source.getType() == Material.FIRE && source.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK) - { - source.setType(Material.AIR); - } - } - } - - //blocks are not destroyed by fire, unless configured to do so - @EventHandler(priority = EventPriority.LOWEST) - public void onBlockBurn (BlockBurnEvent burnEvent) - { - if(!GriefPrevention.instance.config_fireDestroys) - { - burnEvent.setCancelled(true); - return; - } - - //never burn claimed blocks, regardless of settings - if(this.dataStore.getClaimAt(burnEvent.getBlock().getLocation(), false, null) != null) - { - burnEvent.setCancelled(true); - } - } - - //ensures fluids don't flow out of claims, unless into another claim where the owner is trusted to build - private Claim lastSpreadClaim = null; - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onBlockFromTo (BlockFromToEvent spreadEvent) - { - //don't track fluid movement in worlds where claims are not enabled - if(!GriefPrevention.instance.config_claims_enabledWorlds.contains(spreadEvent.getBlock().getWorld())) return; - - //always allow fluids to flow straight down - if(spreadEvent.getFace() == BlockFace.DOWN) return; - - //from where? - Block fromBlock = spreadEvent.getBlock(); - Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, this.lastSpreadClaim); - if(fromClaim != null) - { - this.lastSpreadClaim = fromClaim; - } - - //where to? - Block toBlock = spreadEvent.getToBlock(); - Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); - - //if it's within the same claim or wilderness to wilderness, allow it - if(fromClaim == toClaim) return; - - //block any spread into the wilderness from a claim - if(fromClaim != null && toClaim == null) - { - spreadEvent.setCancelled(true); - return; - } - - //if spreading into a claim - else if(toClaim != null) - { - //who owns the spreading block, if anyone? - OfflinePlayer fromOwner = null; - if(fromClaim != null) - { - fromOwner = GriefPrevention.instance.getServer().getOfflinePlayer(fromClaim.ownerName); - } - - //cancel unless the owner of the spreading block is allowed to build in the receiving claim - if(fromOwner == null || fromOwner.getPlayer() == null || toClaim.allowBuild(fromOwner.getPlayer()) != null) - { - spreadEvent.setCancelled(true); - } - } - } - - //ensures dispensers can't be used to dispense a block(like water or lava) or item across a claim boundary - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onDispense(BlockDispenseEvent dispenseEvent) - { - //from where? - Block fromBlock = dispenseEvent.getBlock(); - - //to where? - Vector velocity = dispenseEvent.getVelocity(); - int xChange = 0; - int zChange = 0; - if(Math.abs(velocity.getX()) > Math.abs(velocity.getZ())) - { - if(velocity.getX() > 0) xChange = 1; - else xChange = -1; - } - else - { - if(velocity.getZ() > 0) zChange = 1; - else zChange = -1; - } - - Block toBlock = fromBlock.getRelative(xChange, 0, zChange); - - Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); - Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); - - //into wilderness is NOT OK when surface buckets are limited - Material materialDispensed = dispenseEvent.getItem().getType(); - if((materialDispensed == Material.WATER_BUCKET || materialDispensed == Material.LAVA_BUCKET) && GriefPrevention.instance.config_blockWildernessWaterBuckets && GriefPrevention.instance.claimsEnabledForWorld(fromBlock.getWorld()) && toClaim == null) - { - dispenseEvent.setCancelled(true); - return; - } - - //wilderness to wilderness is OK - if(fromClaim == null && toClaim == null) return; - - //within claim is OK - if(fromClaim == toClaim) return; - - //everything else is NOT OK - dispenseEvent.setCancelled(true); - } - - @EventHandler(ignoreCancelled = true) - public void onTreeGrow (StructureGrowEvent growEvent) - { - Location rootLocation = growEvent.getLocation(); - Claim rootClaim = this.dataStore.getClaimAt(rootLocation, false, null); - String rootOwnerName = null; - - //who owns the spreading block, if anyone? - if(rootClaim != null) - { - //tree growth in subdivisions is dependent on who owns the top level claim - if(rootClaim.parent != null) rootClaim = rootClaim.parent; - - //if an administrative claim, just let the tree grow where it wants - if(rootClaim.isAdminClaim()) return; - - //otherwise, note the owner of the claim - rootOwnerName = rootClaim.getOwnerName(); - } - - //for each block growing - for(int i = 0; i < growEvent.getBlocks().size(); i++) - { - BlockState block = growEvent.getBlocks().get(i); - Claim blockClaim = this.dataStore.getClaimAt(block.getLocation(), false, rootClaim); - - //if it's growing into a claim - if(blockClaim != null) - { - //if there's no owner for the new tree, or the owner for the new tree is different from the owner of the claim - if(rootOwnerName == null || !rootOwnerName.equals(blockClaim.getOwnerName())) - { - growEvent.getBlocks().remove(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 java.util.ArrayList; +import java.util.List; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Chest; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockDamageEvent; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockIgniteEvent.IgniteCause; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.entity.ProjectileHitEvent; +import org.bukkit.event.world.StructureGrowEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.util.Vector; + +//event handlers related to blocks +public class BlockEventHandler implements Listener +{ + //convenience reference to singleton datastore + private DataStore dataStore; + + private ArrayList trashBlocks; + + //constructor + public BlockEventHandler(DataStore dataStore) + { + this.dataStore = dataStore; + + //create the list of blocks which will not trigger a warning when they're placed outside of land claims + this.trashBlocks = new ArrayList(); + this.trashBlocks.add(Material.COBBLESTONE); + this.trashBlocks.add(Material.TORCH); + this.trashBlocks.add(Material.DIRT); + this.trashBlocks.add(Material.SAPLING); + this.trashBlocks.add(Material.GRAVEL); + this.trashBlocks.add(Material.SAND); + this.trashBlocks.add(Material.TNT); + this.trashBlocks.add(Material.WORKBENCH); + } + + //when a wooden button is triggered by an arrow... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onProjectileHit(ProjectileHitEvent event) + { + Projectile projectile = event.getEntity(); + Location location = projectile.getLocation(); + Block block = location.getBlock(); + + //only care about wooden buttons + if(block.getType() != Material.WOOD_BUTTON) return; + + //only care about arrows + if(projectile instanceof Arrow) + { + Arrow arrow = (Arrow)projectile; + LivingEntity shooterEntity = arrow.getShooter(); + + //player arrows only trigger buttons when they have permission + if(shooterEntity instanceof Player) + { + //Player player = (Player)shooterEntity; + } + + //other arrows don't trigger buttons, could be used as a workaround to get access to areas without permission + else + { + } + } + } + + //when a block is damaged... + @EventHandler(ignoreCancelled = true) + public void onBlockDamaged(BlockDamageEvent event) + { + //if placing items in protected chests isn't enabled, none of this code needs to run + if(!GriefPrevention.instance.config_addItemsToClaimedChests) return; + + Block block = event.getBlock(); + Player player = event.getPlayer(); + + //only care about player-damaged blocks + if(player == null) return; + + //FEATURE: players may add items to a chest they don't have permission for by hitting it + + //if it's a chest + if(block.getType() == Material.CHEST) + { + //only care about non-creative mode players, since those would outright break the box in one hit + if(player.getGameMode() == GameMode.CREATIVE) return; + + //only care if the player has an itemstack in hand + PlayerInventory playerInventory = player.getInventory(); + ItemStack stackInHand = playerInventory.getItemInHand(); + if(stackInHand == null || stackInHand.getType() == Material.AIR) return; + + //only care if the chest is in a claim, and the player does not have access to the chest + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim == null || claim.allowContainers(player) == null) return; + + //if the player is under siege, he can't give away items + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); + event.setCancelled(true); + return; + } + + //NOTE: to eliminate accidental give-aways, first hit on a chest displays a confirmation message + //subsequent hits donate item to the chest + + //if first time damaging this chest, show confirmation message + if(playerData.lastChestDamageLocation == null || !block.getLocation().equals(playerData.lastChestDamageLocation)) + { + //remember this location + playerData.lastChestDamageLocation = block.getLocation(); + + //give the player instructions + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.DonateItemsInstruction); + } + + //otherwise, try to donate the item stack in hand + else + { + //look for empty slot in chest + Chest chest = (Chest)block.getState(); + Inventory chestInventory = chest.getInventory(); + int availableSlot = chestInventory.firstEmpty(); + + //if there isn't one + if(availableSlot < 0) + { + //tell the player and stop here + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ChestFull); + + return; + } + + //otherwise, transfer item stack from player to chest + //NOTE: Inventory.addItem() is smart enough to add items to existing stacks, making filling a chest with garbage as a grief very difficult + chestInventory.addItem(stackInHand); + playerInventory.setItemInHand(new ItemStack(Material.AIR)); + + //and confirm for the player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DonationSuccess); + } + } + } + + //when a player breaks a block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockBreak(BlockBreakEvent breakEvent) + { + Player player = breakEvent.getPlayer(); + Block block = breakEvent.getBlock(); + + //make sure the player is allowed to break at the location + String noBuildReason = GriefPrevention.instance.allowBreak(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + breakEvent.setCancelled(true); + return; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + + //if there's a claim here + if(claim != null) + { + //if breaking UNDER the claim and the player has permission to build in the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) + { + //extend the claim downward beyond the breakage point + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + } + + //FEATURE: automatically clean up hanging treetops + //if it's a log + if(block.getType() == Material.LOG && GriefPrevention.instance.config_trees_removeFloatingTreetops) + { + //run the specialized code for treetop removal (see below) + GriefPrevention.instance.handleLogBroken(block); + } + } + + //when a player places a sign... + @EventHandler(ignoreCancelled = true) + public void onSignChanged(SignChangeEvent event) + { + Player player = event.getPlayer(); + if(player == null) return; + + StringBuilder lines = new StringBuilder(); + boolean notEmpty = false; + for(int i = 0; i < event.getLines().length; i++) + { + if(event.getLine(i).length() != 0) notEmpty = true; + lines.append(event.getLine(i) + ";"); + } + + String signMessage = lines.toString(); + + //if not empty and wasn't the same as the last sign, log it and remember it for later + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(notEmpty && playerData.lastMessage != null && !playerData.lastMessage.equals(signMessage)) + { + GriefPrevention.AddLogEntry("[Sign Placement] <" + player.getName() + "> " + lines.toString() + " @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation())); + playerData.lastMessage = signMessage; + } + } + + //when a player places a block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + public void onBlockPlace(BlockPlaceEvent placeEvent) + { + Player player = placeEvent.getPlayer(); + Block block = placeEvent.getBlock(); + + //FEATURE: limit fire placement, to prevent PvP-by-fire + + //if placed block is fire and pvp is off, apply rules for proximity to other players + if(block.getType() == Material.FIRE && !block.getWorld().getPVP() && !player.hasPermission("griefprevention.lava")) + { + List players = block.getWorld().getPlayers(); + for(int i = 0; i < players.size(); i++) + { + Player otherPlayer = players.get(i); + Location location = otherPlayer.getLocation(); + if(!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire, otherPlayer.getName()); + placeEvent.setCancelled(true); + return; + } + } + } + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + placeEvent.setCancelled(true); + return; + } + + //if the block is being placed within an existing claim + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim); + if(claim != null) + { + //warn about TNT not destroying claimed blocks + if(block.getType() == Material.TNT) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims); + } + + //if the player has permission for the claim and he's placing UNDER the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player) == null) + { + //extend the claim downward + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + + //reset the counter for warning the player when he places outside his claims + playerData.unclaimedBlockPlacementsUntilWarning = 1; + } + + //FEATURE: automatically create a claim when a player who has no claims places a chest + + //otherwise if there's no claim, the player is placing a chest, and new player automatic claims are enabled + else if(block.getType() == Material.CHEST && GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld())) + { + //if the chest is too deep underground, don't create the claim and explain why + if(GriefPrevention.instance.config_claims_preventTheft && block.getY() < GriefPrevention.instance.config_claims_maxDepth) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.TooDeepToClaim); + return; + } + + int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius; + + //if the player doesn't have any claims yet, automatically create a claim centered at the chest + if(playerData.claims.size() == 0) + { + //radius == 0 means protect ONLY the chest + if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius == 0) + { + this.dataStore.createClaim(block.getWorld(), block.getX(), block.getX(), block.getY(), block.getY(), block.getZ(), block.getZ(), player.getName(), null, null); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation); + } + + //otherwise, create a claim in the area around the chest + else + { + //as long as the automatic claim overlaps another existing claim, shrink it + //note that since the player had permission to place the chest, at the very least, the automatic claim will include the chest + while(radius >= 0 && !this.dataStore.createClaim(block.getWorld(), + block.getX() - radius, block.getX() + radius, + block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, block.getY(), + block.getZ() - radius, block.getZ() + radius, + player.getName(), + null, null).succeeded) + { + radius--; + } + + //notify and explain to player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification); + + //show the player the protected area + Claim newClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); + Visualization visualization = Visualization.FromClaim(newClaim, block.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + + //instructions for using /trust + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.TrustCommandAdvertisement); + + //unless special permission is required to create a claim with the shovel, educate the player about the shovel + if(!GriefPrevention.instance.config_claims_creationRequiresPermission) + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.GoldenShovelAdvertisement); + } + } + + //check to see if this chest is in a claim, and warn when it isn't + if(GriefPrevention.instance.config_claims_preventTheft && this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim) == null) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnprotectedChestWarning); + } + } + + //FEATURE: limit wilderness tree planting to grass, or dirt with more blocks beneath it + else if(block.getType() == Material.SAPLING && GriefPrevention.instance.config_blockSkyTrees && GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) + { + Block earthBlock = placeEvent.getBlockAgainst(); + if(earthBlock.getType() != Material.GRASS) + { + if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR || + earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR) + { + placeEvent.setCancelled(true); + } + } + } + + //FEATURE: warn players when they're placing non-trash blocks outside of their claimed areas + else if(GriefPrevention.instance.config_claims_warnOnBuildOutside && !this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld()) && playerData.claims.size() > 0) + { + if(--playerData.unclaimedBlockPlacementsUntilWarning <= 0) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims); + playerData.unclaimedBlockPlacementsUntilWarning = 15; + + if(playerData.lastClaim != null && playerData.lastClaim.allowBuild(player) == null) + { + Visualization visualization = Visualization.FromClaim(playerData.lastClaim, block.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + } + } + + //warn players when they place TNT above sea level, since it doesn't destroy blocks there + if( GriefPrevention.instance.config_blockSurfaceOtherExplosions && block.getType() == Material.TNT && + block.getWorld().getEnvironment() != Environment.NETHER && + block.getY() > GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel); + } + } + + //blocks "pushing" other players' blocks around (pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonExtend (BlockPistonExtendEvent event) + { + List blocks = event.getBlocks(); + + //if no blocks moving, then only check to make sure we're not pushing into a claim from outside + //this avoids pistons breaking non-solids just inside a claim, like torches, doors, and touchplates + if(blocks.size() == 0) + { + Block pistonBlock = event.getBlock(); + Block invadedBlock = pistonBlock.getRelative(event.getDirection()); + + if( this.dataStore.getClaimAt(pistonBlock.getLocation(), false, null) == null && + this.dataStore.getClaimAt(invadedBlock.getLocation(), false, null) != null) + { + event.setCancelled(true); + } + + return; + } + + //who owns the piston, if anyone? + String pistonClaimOwnerName = "_"; + Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null); + if(claim != null) pistonClaimOwnerName = claim.getOwnerName(); + + //which blocks are being pushed? + for(int i = 0; i < blocks.size(); i++) + { + //if ANY of the pushed blocks are owned by someone other than the piston owner, cancel the event + Block block = blocks.get(i); + claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null && !claim.getOwnerName().equals(pistonClaimOwnerName)) + { + event.setCancelled(true); + event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); + event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); + event.getBlock().setType(Material.AIR); + return; + } + } + + //which direction? note we're ignoring vertical push + int xchange = 0; + int zchange = 0; + + Block piston = event.getBlock(); + Block firstBlock = blocks.get(0); + + if(firstBlock.getX() > piston.getX()) + { + xchange = 1; + } + else if(firstBlock.getX() < piston.getX()) + { + xchange = -1; + } + else if(firstBlock.getZ() > piston.getZ()) + { + zchange = 1; + } + else if(firstBlock.getZ() < piston.getZ()) + { + zchange = -1; + } + + //if horizontal movement + if(xchange != 0 || zchange != 0) + { + for(int i = 0; i < blocks.size(); i++) + { + Block block = blocks.get(i); + Claim originalClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); + String originalOwnerName = ""; + if(originalClaim != null) + { + originalOwnerName = originalClaim.getOwnerName(); + } + + Claim newClaim = this.dataStore.getClaimAt(block.getLocation().add(xchange, 0, zchange), false, null); + String newOwnerName = ""; + if(newClaim != null) + { + newOwnerName = newClaim.getOwnerName(); + } + + //if pushing this block will change ownership, cancel the event and take away the piston (for performance reasons) + if(!newOwnerName.equals(originalOwnerName)) + { + event.setCancelled(true); + event.getBlock().getWorld().createExplosion(event.getBlock().getLocation(), 0); + event.getBlock().getWorld().dropItem(event.getBlock().getLocation(), new ItemStack(event.getBlock().getType())); + event.getBlock().setType(Material.AIR); + return; + } + + } + } + } + + //blocks theft by pulling blocks out of a claim (again pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockPistonRetract (BlockPistonRetractEvent event) + { + //we only care about sticky pistons + if(!event.isSticky()) return; + + //who owns the moving block, if anyone? + String movingBlockOwnerName = "_"; + Claim movingBlockClaim = this.dataStore.getClaimAt(event.getRetractLocation(), false, null); + if(movingBlockClaim != null) movingBlockOwnerName = movingBlockClaim.getOwnerName(); + + //who owns the piston, if anyone? + String pistonOwnerName = "_"; + Location pistonLocation = event.getBlock().getLocation(); + Claim pistonClaim = this.dataStore.getClaimAt(pistonLocation, false, null); + if(pistonClaim != null) pistonOwnerName = pistonClaim.getOwnerName(); + + //if there are owners for the blocks, they must be the same player + //otherwise cancel the event + if(!pistonOwnerName.equals(movingBlockOwnerName)) + { + event.setCancelled(true); + } + } + + //blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockIgnite (BlockIgniteEvent igniteEvent) + { + if(!GriefPrevention.instance.config_fireSpreads && igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && igniteEvent.getCause() != IgniteCause.LIGHTNING) + { + igniteEvent.setCancelled(true); + } + } + + //fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example) + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockSpread (BlockSpreadEvent spreadEvent) + { + if(spreadEvent.getSource().getType() != Material.FIRE) return; + + if(!GriefPrevention.instance.config_fireSpreads) + { + spreadEvent.setCancelled(true); + return; + } + + //never spread into a claimed area, regardless of settings + if(this.dataStore.getClaimAt(spreadEvent.getBlock().getLocation(), false, null) != null) + { + spreadEvent.setCancelled(true); + + //if the source of the spread is not fire on netherrack, put out that source fire to save cpu cycles + Block source = spreadEvent.getSource(); + if(source.getType() == Material.FIRE && source.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK) + { + source.setType(Material.AIR); + } + } + } + + //blocks are not destroyed by fire, unless configured to do so + @EventHandler(priority = EventPriority.LOWEST) + public void onBlockBurn (BlockBurnEvent burnEvent) + { + if(!GriefPrevention.instance.config_fireDestroys) + { + burnEvent.setCancelled(true); + return; + } + + //never burn claimed blocks, regardless of settings + if(this.dataStore.getClaimAt(burnEvent.getBlock().getLocation(), false, null) != null) + { + burnEvent.setCancelled(true); + } + } + + //ensures fluids don't flow out of claims, unless into another claim where the owner is trusted to build + private Claim lastSpreadClaim = null; + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onBlockFromTo (BlockFromToEvent spreadEvent) + { + //don't track fluid movement in worlds where claims are not enabled + if(!GriefPrevention.instance.config_claims_enabledWorlds.contains(spreadEvent.getBlock().getWorld())) return; + + //always allow fluids to flow straight down + if(spreadEvent.getFace() == BlockFace.DOWN) return; + + //from where? + Block fromBlock = spreadEvent.getBlock(); + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, this.lastSpreadClaim); + if(fromClaim != null) + { + this.lastSpreadClaim = fromClaim; + } + + //where to? + Block toBlock = spreadEvent.getToBlock(); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); + + //if it's within the same claim or wilderness to wilderness, allow it + if(fromClaim == toClaim) return; + + //block any spread into the wilderness from a claim + if(fromClaim != null && toClaim == null) + { + spreadEvent.setCancelled(true); + return; + } + + //if spreading into a claim + else if(toClaim != null) + { + //who owns the spreading block, if anyone? + OfflinePlayer fromOwner = null; + if(fromClaim != null) + { + fromOwner = GriefPrevention.instance.getServer().getOfflinePlayer(fromClaim.ownerName); + } + + //cancel unless the owner of the spreading block is allowed to build in the receiving claim + if(fromOwner == null || fromOwner.getPlayer() == null || toClaim.allowBuild(fromOwner.getPlayer()) != null) + { + spreadEvent.setCancelled(true); + } + } + } + + //ensures dispensers can't be used to dispense a block(like water or lava) or item across a claim boundary + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onDispense(BlockDispenseEvent dispenseEvent) + { + //from where? + Block fromBlock = dispenseEvent.getBlock(); + + //to where? + Vector velocity = dispenseEvent.getVelocity(); + int xChange = 0; + int zChange = 0; + if(Math.abs(velocity.getX()) > Math.abs(velocity.getZ())) + { + if(velocity.getX() > 0) xChange = 1; + else xChange = -1; + } + else + { + if(velocity.getZ() > 0) zChange = 1; + else zChange = -1; + } + + Block toBlock = fromBlock.getRelative(xChange, 0, zChange); + + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim); + + //into wilderness is NOT OK when surface buckets are limited + Material materialDispensed = dispenseEvent.getItem().getType(); + if((materialDispensed == Material.WATER_BUCKET || materialDispensed == Material.LAVA_BUCKET) && GriefPrevention.instance.config_blockWildernessWaterBuckets && GriefPrevention.instance.claimsEnabledForWorld(fromBlock.getWorld()) && toClaim == null) + { + dispenseEvent.setCancelled(true); + return; + } + + //wilderness to wilderness is OK + if(fromClaim == null && toClaim == null) return; + + //within claim is OK + if(fromClaim == toClaim) return; + + //everything else is NOT OK + dispenseEvent.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onTreeGrow (StructureGrowEvent growEvent) + { + Location rootLocation = growEvent.getLocation(); + Claim rootClaim = this.dataStore.getClaimAt(rootLocation, false, null); + String rootOwnerName = null; + + //who owns the spreading block, if anyone? + if(rootClaim != null) + { + //tree growth in subdivisions is dependent on who owns the top level claim + if(rootClaim.parent != null) rootClaim = rootClaim.parent; + + //if an administrative claim, just let the tree grow where it wants + if(rootClaim.isAdminClaim()) return; + + //otherwise, note the owner of the claim + rootOwnerName = rootClaim.getOwnerName(); + } + + //for each block growing + for(int i = 0; i < growEvent.getBlocks().size(); i++) + { + BlockState block = growEvent.getBlocks().get(i); + Claim blockClaim = this.dataStore.getClaimAt(block.getLocation(), false, rootClaim); + + //if it's growing into a claim + if(blockClaim != null) + { + //if there's no owner for the new tree, or the owner for the new tree is different from the owner of the claim + if(rootOwnerName == null || !rootOwnerName.equals(blockClaim.getOwnerName())) + { + growEvent.getBlocks().remove(i--); + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/Claim.java b/src/me/ryanhamshire/GriefPrevention/Claim.java index 97baf4d..101c9a9 100644 --- a/src/me/ryanhamshire/GriefPrevention/Claim.java +++ b/src/me/ryanhamshire/GriefPrevention/Claim.java @@ -1,821 +1,833 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -import org.bukkit.*; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; - -//represents a player claim -//creating an instance doesn't make an effective claim -//only claims which have been added to the datastore have any effect -public class Claim -{ - //two locations, which together define the boundaries of the claim - //note that the upper Y value is always ignored, because claims ALWAYS extend up to the sky - Location lesserBoundaryCorner; - Location greaterBoundaryCorner; - - //modification date. this comes from the file timestamp during load, and is updated with runtime changes - public Date modifiedDate; - - //id number. unique to this claim, never changes. - Long id = null; - - //ownername. for admin claims, this is the empty string - //use getOwnerName() to get a friendly name (will be "an administrator" for admin claims) - public String ownerName; - - //list of players who (beyond the claim owner) have permission to grant permissions in this claim - public ArrayList managers = new ArrayList(); - - //permissions for this claim, see ClaimPermission class - private HashMap playerNameToClaimPermissionMap = new HashMap(); - - //whether or not this claim is in the data store - //if a claim instance isn't in the data store, it isn't "active" - players can't interract with it - //why keep this? so that claims which have been removed from the data store can be correctly - //ignored even though they may have references floating around - public boolean inDataStore = false; - - //parent claim - //only used for claim subdivisions. top level claims have null here - public Claim parent = null; - - //children (subdivisions) - //note subdivisions themselves never have children - public ArrayList children = new ArrayList(); - - //information about a siege involving this claim. null means no siege is impacting this claim - public SiegeData siegeData = null; - - //following a siege, buttons/levers are unlocked temporarily. this represents that state - public boolean doorsOpen = false; - - //whether or not this is an administrative claim - //administrative claims are created and maintained by players with the griefprevention.adminclaims permission. - public boolean isAdminClaim() - { - return this.ownerName.isEmpty(); - } - - //accessor for ID - public Long getID() - { - return new Long(this.id); - } - - //basic constructor, just notes the creation time - //see above declarations for other defaults - Claim() - { - this.modifiedDate = Calendar.getInstance().getTime(); - } - - //players may only siege someone when he's not in an admin claim - //and when he has some level of permission in the claim - public boolean canSiege(Player defender) - { - if(this.isAdminClaim()) return false; - - if(this.allowAccess(defender) != null) return false; - - return true; - } - - //removes any fluids above sea level in a claim - //exclusionClaim is another claim indicating an sub-area to be excluded from this operation - //it may be null - public void removeSurfaceFluids(Claim exclusionClaim) - { - //don't do this for administrative claims - if(this.isAdminClaim()) return; - - //don't do it for very large claims - if(this.getArea() > 10000) return; - - //don't do it when surface fluids are allowed to be dumped - if(!GriefPrevention.instance.config_blockWildernessWaterBuckets) return; - - Location lesser = this.getLesserBoundaryCorner(); - Location greater = this.getGreaterBoundaryCorner(); - - if(lesser.getWorld().getEnvironment() == Environment.NETHER) return; //don't clean up lava in the nether - - int seaLevel = 0; //clean up all fluids in the end - - //respect sea level in normal worlds - if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = lesser.getWorld().getSeaLevel(); - - for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++) - { - for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++) - { - for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++) - { - //dodge the exclusion claim - Block block = lesser.getWorld().getBlockAt(x, y, z); - if(exclusionClaim != null && exclusionClaim.contains(block.getLocation(), true, false)) continue; - - if(block.getType() == Material.STATIONARY_WATER || block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.LAVA || block.getType() == Material.WATER) - { - block.setType(Material.AIR); - } - } - } - } - } - - //determines whether or not a claim has surface fluids (lots of water blocks, or any lava blocks) - //used to warn players when they abandon their claims about automatic fluid cleanup - boolean hasSurfaceFluids() - { - Location lesser = this.getLesserBoundaryCorner(); - Location greater = this.getGreaterBoundaryCorner(); - - //don't bother for very large claims, too expensive - if(this.getArea() > 10000) return false; - - int seaLevel = 0; //clean up all fluids in the end - - //respect sea level in normal worlds - if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = lesser.getWorld().getSeaLevel(); - - int waterCount = 0; - for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++) - { - for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++) - { - for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++) - { - //dodge the exclusion claim - Block block = lesser.getWorld().getBlockAt(x, y, z); - - if(block.getType() == Material.STATIONARY_WATER || block.getType() == Material.WATER) - { - waterCount++; - if(waterCount > 10) return true; - } - - else if(block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.LAVA) - { - return true; - } - } - } - } - - return false; - } - - //main constructor. note that only creating a claim instance does nothing - a claim must be added to the data store to be effective - Claim(Location lesserBoundaryCorner, Location greaterBoundaryCorner, String ownerName, String [] builderNames, String [] containerNames, String [] accessorNames, String [] managerNames, Long id) - { - //modification date - this.modifiedDate = Calendar.getInstance().getTime(); - - //id - this.id = id; - - //store corners - this.lesserBoundaryCorner = lesserBoundaryCorner; - this.greaterBoundaryCorner = greaterBoundaryCorner; - - //owner - this.ownerName = ownerName; - - //other permissions - for(int i = 0; i < builderNames.length; i++) - { - String name = builderNames[i]; - if(name != null && !name.isEmpty()) - { - this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Build); - } - } - - for(int i = 0; i < containerNames.length; i++) - { - String name = containerNames[i]; - if(name != null && !name.isEmpty()) - { - this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Inventory); - } - } - - for(int i = 0; i < accessorNames.length; i++) - { - String name = accessorNames[i]; - if(name != null && !name.isEmpty()) - { - this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Access); - } - } - - for(int i = 0; i < managerNames.length; i++) - { - String name = managerNames[i]; - if(name != null && !name.isEmpty()) - { - this.managers.add(name); - } - } - } - - //measurements. all measurements are in blocks - public int getArea() - { - int claimWidth = this.greaterBoundaryCorner.getBlockX() - this.lesserBoundaryCorner.getBlockX() + 1; - int claimHeight = this.greaterBoundaryCorner.getBlockZ() - this.lesserBoundaryCorner.getBlockZ() + 1; - - return claimWidth * claimHeight; - } - - public int getWidth() - { - return this.greaterBoundaryCorner.getBlockX() - this.lesserBoundaryCorner.getBlockX() + 1; - } - - public int getHeight() - { - return this.greaterBoundaryCorner.getBlockZ() - this.lesserBoundaryCorner.getBlockZ() + 1; - } - - //distance check for claims, distance in this case is a band around the outside of the claim rather then euclidean distance - public boolean isNear(Location location, int howNear) - { - Claim claim = new Claim - (new Location(this.lesserBoundaryCorner.getWorld(), this.lesserBoundaryCorner.getBlockX() - howNear, this.lesserBoundaryCorner.getBlockY(), this.lesserBoundaryCorner.getBlockZ() - howNear), - new Location(this.greaterBoundaryCorner.getWorld(), this.greaterBoundaryCorner.getBlockX() + howNear, this.greaterBoundaryCorner.getBlockY(), this.greaterBoundaryCorner.getBlockZ() + howNear), - "", new String[] {}, new String[] {}, new String[] {}, new String[] {}, null); - - return claim.contains(location, false, true); - } - - //permissions. note administrative "public" claims have different rules than other claims - //all of these return NULL when a player has permission, or a String error message when the player doesn't have permission - public String allowEdit(Player player) - { - //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) - if(player == null) return ""; - - //special cases... - - //admin claims need adminclaims permission only. - if(this.isAdminClaim()) - { - if(player.hasPermission("griefprevention.adminclaims")) return null; - } - - //anyone with deleteclaims permission can modify non-admin claims at any time - else - { - if(player.hasPermission("griefprevention.deleteclaims")) return null; - } - - //no resizing, deleting, and so forth while under siege - if(this.ownerName.equals(player.getName())) - { - if(this.siegeData != null) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NoModifyDuringSiege); - } - - //otherwise, owners can do whatever - return null; - } - - //permission inheritance for subdivisions - if(this.parent != null) - return this.parent.allowBuild(player); - - //error message if all else fails - return GriefPrevention.instance.dataStore.getMessage(Messages.OnlyOwnersModifyClaims, this.getOwnerName()); - } - - //build permission check - public String allowBuild(Player player) - { - //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) - if(player == null) return ""; - - //when a player tries to build in a claim, if he's under siege, the siege may extend to include the new claim - GriefPrevention.instance.dataStore.tryExtendSiege(player, this); - - //admin claims can always be modified by admins, no exceptions - if(this.isAdminClaim()) - { - if(player.hasPermission("griefprevention.adminclaims")) return null; - } - - //no building while under siege - if(this.siegeData != null) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildUnderSiege, this.siegeData.attacker.getName()); - } - - //no building while in pvp combat - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); - if(playerData.inPvpCombat()) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPvP); - } - - //owners can make changes, or admins with ignore claims mode enabled - if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; - - //anyone with explicit build permission can make changes - if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; - - //also everyone is a member of the "public", so check for public permission - ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); - if(ClaimPermission.Build == permissionLevel) return null; - - //subdivision permission inheritance - if(this.parent != null) - return this.parent.allowBuild(player); - - //failure message for all other cases - return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPermission, this.getOwnerName()); - } - - private boolean hasExplicitPermission(Player player, ClaimPermission level) - { - String playerName = player.getName(); - Set keys = this.playerNameToClaimPermissionMap.keySet(); - Iterator iterator = keys.iterator(); - while(iterator.hasNext()) - { - String identifier = iterator.next(); - if(playerName.equalsIgnoreCase(identifier) && this.playerNameToClaimPermissionMap.get(identifier) == level) return true; - - else if(identifier.startsWith("[") && identifier.endsWith("]")) - { - //drop the brackets - String permissionIdentifier = identifier.substring(1, identifier.length() - 1); - - //defensive coding - if(permissionIdentifier == null || permissionIdentifier.isEmpty()) continue; - - //check permission - if(player.hasPermission(permissionIdentifier) && this.playerNameToClaimPermissionMap.get(identifier) == level) return true; - } - } - - return false; - } - - //break permission check - public String allowBreak(Player player, Material material) - { - //if under siege, some blocks will be breakable - if(this.siegeData != null) - { - boolean breakable = false; - - //search for block type in list of breakable blocks - for(int i = 0; i < GriefPrevention.instance.config_siege_blocks.size(); i++) - { - Material breakableMaterial = GriefPrevention.instance.config_siege_blocks.get(i); - if(breakableMaterial.getId() == material.getId()) - { - breakable = true; - break; - } - } - - //custom error messages for siege mode - if(!breakable) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NonSiegeMaterial); - } - else if(this.ownerName.equals(player.getName())) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NoOwnerBuildUnderSiege); - } - else - { - return null; - } - } - - //if not under siege, build rules apply - return this.allowBuild(player); - } - - //access permission check - public String allowAccess(Player player) - { - //following a siege where the defender lost, the claim will allow everyone access for a time - if(this.doorsOpen) return null; - - //admin claims need adminclaims permission only. - if(this.isAdminClaim()) - { - if(player.hasPermission("griefprevention.adminclaims")) return null; - } - - //claim owner and admins in ignoreclaims mode have access - if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; - - //look for explicit individual access, inventory, or build permission - if(this.hasExplicitPermission(player, ClaimPermission.Access)) return null; - if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null; - if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; - - //also check for public permission - ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); - if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel || ClaimPermission.Access == permissionLevel) return null; - - //permission inheritance for subdivisions - if(this.parent != null) - return this.parent.allowAccess(player); - - //catch-all error message for all other cases - return GriefPrevention.instance.dataStore.getMessage(Messages.NoAccessPermission, this.getOwnerName()); - } - - //inventory permission check - public String allowContainers(Player player) - { - //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) - if(player == null) return ""; - - //trying to access inventory in a claim may extend an existing siege to include this claim - GriefPrevention.instance.dataStore.tryExtendSiege(player, this); - - //if under siege, nobody accesses containers - if(this.siegeData != null) - { - return GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersSiege, siegeData.attacker.getName()); - } - - //owner and administrators in ignoreclaims mode have access - if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; - - //admin claims need adminclaims permission only. - if(this.isAdminClaim()) - { - if(player.hasPermission("griefprevention.adminclaims")) return null; - } - - //check for explicit individual container or build permission - if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null; - if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; - - //check for public container or build permission - ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); - if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel) return null; - - //permission inheritance for subdivisions - if(this.parent != null) - return this.parent.allowContainers(player); - - //error message for all other cases - return GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersPermission, this.getOwnerName()); - } - - //grant permission check, relatively simple - public String allowGrantPermission(Player player) - { - //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) - if(player == null) return ""; - - //anyone who can modify the claim can do this - if(this.allowEdit(player) == null) return null; - - //anyone who's in the managers (/PermissionTrust) list can do this - for(int i = 0; i < this.managers.size(); i++) - { - String managerID = this.managers.get(i); - if(player.getName().equalsIgnoreCase(managerID)) return null; - - else if(managerID.startsWith("[") && managerID.endsWith("]")) - { - managerID = managerID.substring(1, managerID.length() - 1); - if(managerID == null || managerID.isEmpty()) continue; - if(player.hasPermission(managerID)) return null; - } - } - - //permission inheritance for subdivisions - if(this.parent != null) - return this.parent.allowGrantPermission(player); - - //generic error message - return GriefPrevention.instance.dataStore.getMessage(Messages.NoPermissionTrust, this.getOwnerName()); - } - - //grants a permission for a player or the public - public void setPermission(String playerName, ClaimPermission permissionLevel) - { - this.playerNameToClaimPermissionMap.put(playerName.toLowerCase(), permissionLevel); - } - - //revokes a permission for a player or the public - public void dropPermission(String playerName) - { - this.playerNameToClaimPermissionMap.remove(playerName.toLowerCase()); - } - - //clears all permissions (except owner of course) - public void clearPermissions() - { - this.playerNameToClaimPermissionMap.clear(); - } - - //gets ALL permissions - //useful for making copies of permissions during a claim resize and listing all permissions in a claim - public void getPermissions(ArrayList builders, ArrayList containers, ArrayList accessors, ArrayList managers) - { - //loop through all the entries in the hash map - Iterator> mappingsIterator = this.playerNameToClaimPermissionMap.entrySet().iterator(); - while(mappingsIterator.hasNext()) - { - Map.Entry entry = mappingsIterator.next(); - - //build up a list for each permission level - if(entry.getValue() == ClaimPermission.Build) - { - builders.add(entry.getKey()); - } - else if(entry.getValue() == ClaimPermission.Inventory) - { - containers.add(entry.getKey()); - } - else - { - accessors.add(entry.getKey()); - } - } - - //managers are handled a little differently - for(int i = 0; i < this.managers.size(); i++) - { - managers.add(this.managers.get(i)); - } - } - - //returns a copy of the location representing lower x, y, z limits - public Location getLesserBoundaryCorner() - { - return this.lesserBoundaryCorner.clone(); - } - - //returns a copy of the location representing upper x, y, z limits - //NOTE: remember upper Y will always be ignored, all claims always extend to the sky - public Location getGreaterBoundaryCorner() - { - return this.greaterBoundaryCorner.clone(); - } - - //returns a friendly owner name (for admin claims, returns "an administrator" as the owner) - public String getOwnerName() - { - if(this.parent != null) - return this.parent.getOwnerName(); - - if(this.ownerName.length() == 0) - return GriefPrevention.instance.dataStore.getMessage(Messages.OwnerNameForAdminClaims); - - return this.ownerName; - } - - //whether or not a location is in a claim - //ignoreHeight = true means location UNDER the claim will return TRUE - //excludeSubdivisions = true means that locations inside subdivisions of the claim will return FALSE - public boolean contains(Location location, boolean ignoreHeight, boolean excludeSubdivisions) - { - //not in the same world implies false - if(!location.getWorld().equals(this.lesserBoundaryCorner.getWorld())) return false; - - int x = location.getBlockX(); - int y = location.getBlockY(); - int z = location.getBlockZ(); - - //main check - boolean inClaim = (ignoreHeight || y >= this.lesserBoundaryCorner.getBlockY()) && - x >= this.lesserBoundaryCorner.getBlockX() && - x <= this.greaterBoundaryCorner.getBlockX() && - z >= this.lesserBoundaryCorner.getBlockZ() && - z <= this.greaterBoundaryCorner.getBlockZ(); - - if(!inClaim) return false; - - //additional check for subdivisions - //you're only in a subdivision when you're also in its parent claim - //NOTE: if a player creates subdivions then resizes the parent claim, it's possible that - //a subdivision can reach outside of its parent's boundaries. so this check is important! - if(this.parent != null) - { - return this.parent.contains(location, ignoreHeight, false); - } - - //code to exclude subdivisions in this check - else if(excludeSubdivisions) - { - //search all subdivisions to see if the location is in any of them - for(int i = 0; i < this.children.size(); i++) - { - //if we find such a subdivision, return false - if(this.children.get(i).contains(location, ignoreHeight, true)) - { - return false; - } - } - } - - //otherwise yes - return true; - } - - //whether or not two claims overlap - //used internally to prevent overlaps when creating claims - boolean overlaps(Claim otherClaim) - { - //NOTE: if trying to understand this makes your head hurt, don't feel bad - it hurts mine too. - //try drawing pictures to visualize test cases. - - if(!this.lesserBoundaryCorner.getWorld().equals(otherClaim.getLesserBoundaryCorner().getWorld())) return false; - - //first, check the corners of this claim aren't inside any existing claims - if(otherClaim.contains(this.lesserBoundaryCorner, true, false)) return true; - if(otherClaim.contains(this.greaterBoundaryCorner, true, false)) return true; - if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.lesserBoundaryCorner.getBlockX(), 0, this.greaterBoundaryCorner.getBlockZ()), true, false)) return true; - if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.greaterBoundaryCorner.getBlockX(), 0, this.lesserBoundaryCorner.getBlockZ()), true, false)) return true; - - //verify that no claim's lesser boundary point is inside this new claim, to cover the "existing claim is entirely inside new claim" case - if(this.contains(otherClaim.getLesserBoundaryCorner(), true, false)) return true; - - //verify this claim doesn't band across an existing claim, either horizontally or vertically - if( this.getLesserBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() && - this.getLesserBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() && - this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() && - this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() ) - return true; - - if( this.getGreaterBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() && - this.getGreaterBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() && - this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() && - this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() ) - return true; - - if( this.getLesserBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() && - this.getLesserBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() && - this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() && - this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() ) - return true; - - if( this.getGreaterBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() && - this.getGreaterBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() && - this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() && - this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() ) - return true; - - return false; - } - - //whether more entities may be added to a claim - public String allowMoreEntities() - { - if(this.parent != null) return this.parent.allowMoreEntities(); - - //this rule only applies to creative mode worlds - if(!GriefPrevention.instance.creativeRulesApply(this.getLesserBoundaryCorner())) return null; - - //admin claims aren't restricted - if(this.isAdminClaim()) return null; - - //don't apply this rule to very large claims - if(this.getArea() > 10000) return null; - - //determine maximum allowable entity count, based on claim size - int maxEntities = this.getArea() / 50; - if(maxEntities == 0) return GriefPrevention.instance.dataStore.getMessage(Messages.ClaimTooSmallForEntities); - - //count current entities (ignoring players) - Chunk lesserChunk = this.getLesserBoundaryCorner().getChunk(); - Chunk greaterChunk = this.getGreaterBoundaryCorner().getChunk(); - - int totalEntities = 0; - for(int x = lesserChunk.getX(); x <= greaterChunk.getX(); x++) - for(int z = lesserChunk.getZ(); z <= greaterChunk.getZ(); z++) - { - Chunk chunk = lesserChunk.getWorld().getChunkAt(x, z); - Entity [] entities = chunk.getEntities(); - for(int i = 0; i < entities.length; i++) - { - Entity entity = entities[i]; - if(!(entity instanceof Player) && this.contains(entity.getLocation(), false, false)) - { - totalEntities++; - if(totalEntities > maxEntities) entity.remove(); - } - } - } - - if(totalEntities > maxEntities) return GriefPrevention.instance.dataStore.getMessage(Messages.TooManyEntitiesInClaim); - - return null; - } - - //implements a strict ordering of claims, used to keep the claims collection sorted for faster searching - boolean greaterThan(Claim otherClaim) - { - Location thisCorner = this.getLesserBoundaryCorner(); - Location otherCorner = otherClaim.getLesserBoundaryCorner(); - - if(thisCorner.getBlockX() > otherCorner.getBlockX()) return true; - - if(thisCorner.getBlockX() < otherCorner.getBlockX()) return false; - - if(thisCorner.getBlockZ() > otherCorner.getBlockZ()) return true; - - if(thisCorner.getBlockZ() < otherCorner.getBlockZ()) return false; - - return thisCorner.getWorld().getName().compareTo(otherCorner.getWorld().getName()) < 0; - } - - long getPlayerInvestmentScore() - { - //decide which blocks will be considered player placed - Location lesserBoundaryCorner = this.getLesserBoundaryCorner(); - ArrayList playerBlocks = RestoreNatureProcessingTask.getPlayerBlocks(lesserBoundaryCorner.getWorld().getEnvironment(), lesserBoundaryCorner.getBlock().getBiome()); - - //scan the claim for player placed blocks - double score = 0; - - boolean creativeMode = GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner); - - for(int x = this.lesserBoundaryCorner.getBlockX(); x <= this.greaterBoundaryCorner.getBlockX(); x++) - { - for(int z = this.lesserBoundaryCorner.getBlockZ(); z <= this.greaterBoundaryCorner.getBlockZ(); z++) - { - int y = this.lesserBoundaryCorner.getBlockY(); - for(; y < this.lesserBoundaryCorner.getWorld().getSeaLevel(); y++) - { - Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z); - if(playerBlocks.contains(block.getTypeId())) - { - if(block.getType() == Material.CHEST && !creativeMode) - { - score += 10; - } - else - { - score += .2; - } - } - } - - for(; y < this.lesserBoundaryCorner.getWorld().getMaxHeight(); y++) - { - Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z); - if(playerBlocks.contains(block.getTypeId())) - { - if(block.getType() == Material.CHEST && !creativeMode) - { - score += 10; - } - else if(creativeMode && (block.getType() == Material.LAVA || block.getType() == Material.STATIONARY_LAVA)) - { - score -= 10; - } - else - { - score += 1; - } - } - } - } - } - - return (long)score; - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.bukkit.*; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +//represents a player claim +//creating an instance doesn't make an effective claim +//only claims which have been added to the datastore have any effect +public class Claim +{ + //two locations, which together define the boundaries of the claim + //note that the upper Y value is always ignored, because claims ALWAYS extend up to the sky + Location lesserBoundaryCorner; + Location greaterBoundaryCorner; + + //modification date. this comes from the file timestamp during load, and is updated with runtime changes + public Date modifiedDate; + + //id number. unique to this claim, never changes. + Long id = null; + + //ownername. for admin claims, this is the empty string + //use getOwnerName() to get a friendly name (will be "an administrator" for admin claims) + public String ownerName; + + //list of players who (beyond the claim owner) have permission to grant permissions in this claim + public ArrayList managers = new ArrayList(); + + //permissions for this claim, see ClaimPermission class + private HashMap playerNameToClaimPermissionMap = new HashMap(); + + //whether or not this claim is in the data store + //if a claim instance isn't in the data store, it isn't "active" - players can't interract with it + //why keep this? so that claims which have been removed from the data store can be correctly + //ignored even though they may have references floating around + public boolean inDataStore = false; + + //parent claim + //only used for claim subdivisions. top level claims have null here + public Claim parent = null; + + //children (subdivisions) + //note subdivisions themselves never have children + public ArrayList children = new ArrayList(); + + //information about a siege involving this claim. null means no siege is impacting this claim + public SiegeData siegeData = null; + + //following a siege, buttons/levers are unlocked temporarily. this represents that state + public boolean doorsOpen = false; + + //whether or not this is an administrative claim + //administrative claims are created and maintained by players with the griefprevention.adminclaims permission. + public boolean isAdminClaim() + { + return this.ownerName.isEmpty(); + } + + //accessor for ID + public Long getID() + { + return new Long(this.id); + } + + //basic constructor, just notes the creation time + //see above declarations for other defaults + Claim() + { + this.modifiedDate = Calendar.getInstance().getTime(); + } + + //players may only siege someone when he's not in an admin claim + //and when he has some level of permission in the claim + public boolean canSiege(Player defender) + { + if(this.isAdminClaim()) return false; + + if(this.allowAccess(defender) != null) return false; + + return true; + } + + //removes any fluids above sea level in a claim + //exclusionClaim is another claim indicating an sub-area to be excluded from this operation + //it may be null + public void removeSurfaceFluids(Claim exclusionClaim) + { + //don't do this for administrative claims + if(this.isAdminClaim()) return; + + //don't do it for very large claims + if(this.getArea() > 10000) return; + + //don't do it when surface fluids are allowed to be dumped + if(!GriefPrevention.instance.config_blockWildernessWaterBuckets) return; + + Location lesser = this.getLesserBoundaryCorner(); + Location greater = this.getGreaterBoundaryCorner(); + + if(lesser.getWorld().getEnvironment() == Environment.NETHER) return; //don't clean up lava in the nether + + int seaLevel = 0; //clean up all fluids in the end + + //respect sea level in normal worlds + if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = GriefPrevention.instance.getSeaLevel(lesser.getWorld()); + + for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++) + { + for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++) + { + for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++) + { + //dodge the exclusion claim + Block block = lesser.getWorld().getBlockAt(x, y, z); + if(exclusionClaim != null && exclusionClaim.contains(block.getLocation(), true, false)) continue; + + if(block.getType() == Material.STATIONARY_WATER || block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.LAVA || block.getType() == Material.WATER) + { + block.setType(Material.AIR); + } + } + } + } + } + + //determines whether or not a claim has surface fluids (lots of water blocks, or any lava blocks) + //used to warn players when they abandon their claims about automatic fluid cleanup + boolean hasSurfaceFluids() + { + Location lesser = this.getLesserBoundaryCorner(); + Location greater = this.getGreaterBoundaryCorner(); + + //don't bother for very large claims, too expensive + if(this.getArea() > 10000) return false; + + int seaLevel = 0; //clean up all fluids in the end + + //respect sea level in normal worlds + if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = GriefPrevention.instance.getSeaLevel(lesser.getWorld()); + + int waterCount = 0; + for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++) + { + for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++) + { + for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++) + { + //dodge the exclusion claim + Block block = lesser.getWorld().getBlockAt(x, y, z); + + if(block.getType() == Material.STATIONARY_WATER || block.getType() == Material.WATER) + { + waterCount++; + if(waterCount > 10) return true; + } + + else if(block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.LAVA) + { + return true; + } + } + } + } + + return false; + } + + //main constructor. note that only creating a claim instance does nothing - a claim must be added to the data store to be effective + Claim(Location lesserBoundaryCorner, Location greaterBoundaryCorner, String ownerName, String [] builderNames, String [] containerNames, String [] accessorNames, String [] managerNames, Long id) + { + //modification date + this.modifiedDate = Calendar.getInstance().getTime(); + + //id + this.id = id; + + //store corners + this.lesserBoundaryCorner = lesserBoundaryCorner; + this.greaterBoundaryCorner = greaterBoundaryCorner; + + //owner + this.ownerName = ownerName; + + //other permissions + for(int i = 0; i < builderNames.length; i++) + { + String name = builderNames[i]; + if(name != null && !name.isEmpty()) + { + this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Build); + } + } + + for(int i = 0; i < containerNames.length; i++) + { + String name = containerNames[i]; + if(name != null && !name.isEmpty()) + { + this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Inventory); + } + } + + for(int i = 0; i < accessorNames.length; i++) + { + String name = accessorNames[i]; + if(name != null && !name.isEmpty()) + { + this.playerNameToClaimPermissionMap.put(name, ClaimPermission.Access); + } + } + + for(int i = 0; i < managerNames.length; i++) + { + String name = managerNames[i]; + if(name != null && !name.isEmpty()) + { + this.managers.add(name); + } + } + } + + //measurements. all measurements are in blocks + public int getArea() + { + int claimWidth = this.greaterBoundaryCorner.getBlockX() - this.lesserBoundaryCorner.getBlockX() + 1; + int claimHeight = this.greaterBoundaryCorner.getBlockZ() - this.lesserBoundaryCorner.getBlockZ() + 1; + + return claimWidth * claimHeight; + } + + public int getWidth() + { + return this.greaterBoundaryCorner.getBlockX() - this.lesserBoundaryCorner.getBlockX() + 1; + } + + public int getHeight() + { + return this.greaterBoundaryCorner.getBlockZ() - this.lesserBoundaryCorner.getBlockZ() + 1; + } + + //distance check for claims, distance in this case is a band around the outside of the claim rather then euclidean distance + public boolean isNear(Location location, int howNear) + { + Claim claim = new Claim + (new Location(this.lesserBoundaryCorner.getWorld(), this.lesserBoundaryCorner.getBlockX() - howNear, this.lesserBoundaryCorner.getBlockY(), this.lesserBoundaryCorner.getBlockZ() - howNear), + new Location(this.greaterBoundaryCorner.getWorld(), this.greaterBoundaryCorner.getBlockX() + howNear, this.greaterBoundaryCorner.getBlockY(), this.greaterBoundaryCorner.getBlockZ() + howNear), + "", new String[] {}, new String[] {}, new String[] {}, new String[] {}, null); + + return claim.contains(location, false, true); + } + + //permissions. note administrative "public" claims have different rules than other claims + //all of these return NULL when a player has permission, or a String error message when the player doesn't have permission + public String allowEdit(Player player) + { + //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) + if(player == null) return ""; + + //special cases... + + //admin claims need adminclaims permission only. + if(this.isAdminClaim()) + { + if(player.hasPermission("griefprevention.adminclaims")) return null; + } + + //anyone with deleteclaims permission can modify non-admin claims at any time + else + { + if(player.hasPermission("griefprevention.deleteclaims")) return null; + } + + //no resizing, deleting, and so forth while under siege + if(this.ownerName.equals(player.getName())) + { + if(this.siegeData != null) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NoModifyDuringSiege); + } + + //otherwise, owners can do whatever + return null; + } + + //permission inheritance for subdivisions + if(this.parent != null) + return this.parent.allowBuild(player); + + //error message if all else fails + return GriefPrevention.instance.dataStore.getMessage(Messages.OnlyOwnersModifyClaims, this.getOwnerName()); + } + + //build permission check + public String allowBuild(Player player) + { + //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) + if(player == null) return ""; + + //when a player tries to build in a claim, if he's under siege, the siege may extend to include the new claim + GriefPrevention.instance.dataStore.tryExtendSiege(player, this); + + //admin claims can always be modified by admins, no exceptions + if(this.isAdminClaim()) + { + if(player.hasPermission("griefprevention.adminclaims")) return null; + } + + //no building while under siege + if(this.siegeData != null) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildUnderSiege, this.siegeData.attacker.getName()); + } + + //no building while in pvp combat + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + if(playerData.inPvpCombat()) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPvP); + } + + //owners can make changes, or admins with ignore claims mode enabled + if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; + + //anyone with explicit build permission can make changes + if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; + + //also everyone is a member of the "public", so check for public permission + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); + if(ClaimPermission.Build == permissionLevel) return null; + + //subdivision permission inheritance + if(this.parent != null) + return this.parent.allowBuild(player); + + //failure message for all other cases + String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPermission, this.getOwnerName()); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + private boolean hasExplicitPermission(Player player, ClaimPermission level) + { + String playerName = player.getName(); + Set keys = this.playerNameToClaimPermissionMap.keySet(); + Iterator iterator = keys.iterator(); + while(iterator.hasNext()) + { + String identifier = iterator.next(); + if(playerName.equalsIgnoreCase(identifier) && this.playerNameToClaimPermissionMap.get(identifier) == level) return true; + + else if(identifier.startsWith("[") && identifier.endsWith("]")) + { + //drop the brackets + String permissionIdentifier = identifier.substring(1, identifier.length() - 1); + + //defensive coding + if(permissionIdentifier == null || permissionIdentifier.isEmpty()) continue; + + //check permission + if(player.hasPermission(permissionIdentifier) && this.playerNameToClaimPermissionMap.get(identifier) == level) return true; + } + } + + return false; + } + + //break permission check + public String allowBreak(Player player, Material material) + { + //if under siege, some blocks will be breakable + if(this.siegeData != null) + { + boolean breakable = false; + + //search for block type in list of breakable blocks + for(int i = 0; i < GriefPrevention.instance.config_siege_blocks.size(); i++) + { + Material breakableMaterial = GriefPrevention.instance.config_siege_blocks.get(i); + if(breakableMaterial.getId() == material.getId()) + { + breakable = true; + break; + } + } + + //custom error messages for siege mode + if(!breakable) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NonSiegeMaterial); + } + else if(this.ownerName.equals(player.getName())) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NoOwnerBuildUnderSiege); + } + else + { + return null; + } + } + + //if not under siege, build rules apply + return this.allowBuild(player); + } + + //access permission check + public String allowAccess(Player player) + { + //following a siege where the defender lost, the claim will allow everyone access for a time + if(this.doorsOpen) return null; + + //admin claims need adminclaims permission only. + if(this.isAdminClaim()) + { + if(player.hasPermission("griefprevention.adminclaims")) return null; + } + + //claim owner and admins in ignoreclaims mode have access + if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; + + //look for explicit individual access, inventory, or build permission + if(this.hasExplicitPermission(player, ClaimPermission.Access)) return null; + if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null; + if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; + + //also check for public permission + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); + if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel || ClaimPermission.Access == permissionLevel) return null; + + //permission inheritance for subdivisions + if(this.parent != null) + return this.parent.allowAccess(player); + + //catch-all error message for all other cases + String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoAccessPermission, this.getOwnerName()); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + //inventory permission check + public String allowContainers(Player player) + { + //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) + if(player == null) return ""; + + //trying to access inventory in a claim may extend an existing siege to include this claim + GriefPrevention.instance.dataStore.tryExtendSiege(player, this); + + //if under siege, nobody accesses containers + if(this.siegeData != null) + { + return GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersSiege, siegeData.attacker.getName()); + } + + //owner and administrators in ignoreclaims mode have access + if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; + + //admin claims need adminclaims permission only. + if(this.isAdminClaim()) + { + if(player.hasPermission("griefprevention.adminclaims")) return null; + } + + //check for explicit individual container or build permission + if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null; + if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null; + + //check for public container or build permission + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get("public"); + if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel) return null; + + //permission inheritance for subdivisions + if(this.parent != null) + return this.parent.allowContainers(player); + + //error message for all other cases + String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersPermission, this.getOwnerName()); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + //grant permission check, relatively simple + public String allowGrantPermission(Player player) + { + //if we don't know who's asking, always say no (i've been told some mods can make this happen somehow) + if(player == null) return ""; + + //anyone who can modify the claim can do this + if(this.allowEdit(player) == null) return null; + + //anyone who's in the managers (/PermissionTrust) list can do this + for(int i = 0; i < this.managers.size(); i++) + { + String managerID = this.managers.get(i); + if(player.getName().equalsIgnoreCase(managerID)) return null; + + else if(managerID.startsWith("[") && managerID.endsWith("]")) + { + managerID = managerID.substring(1, managerID.length() - 1); + if(managerID == null || managerID.isEmpty()) continue; + if(player.hasPermission(managerID)) return null; + } + } + + //permission inheritance for subdivisions + if(this.parent != null) + return this.parent.allowGrantPermission(player); + + //generic error message + String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoPermissionTrust, this.getOwnerName()); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + //grants a permission for a player or the public + public void setPermission(String playerName, ClaimPermission permissionLevel) + { + this.playerNameToClaimPermissionMap.put(playerName.toLowerCase(), permissionLevel); + } + + //revokes a permission for a player or the public + public void dropPermission(String playerName) + { + this.playerNameToClaimPermissionMap.remove(playerName.toLowerCase()); + } + + //clears all permissions (except owner of course) + public void clearPermissions() + { + this.playerNameToClaimPermissionMap.clear(); + } + + //gets ALL permissions + //useful for making copies of permissions during a claim resize and listing all permissions in a claim + public void getPermissions(ArrayList builders, ArrayList containers, ArrayList accessors, ArrayList managers) + { + //loop through all the entries in the hash map + Iterator> mappingsIterator = this.playerNameToClaimPermissionMap.entrySet().iterator(); + while(mappingsIterator.hasNext()) + { + Map.Entry entry = mappingsIterator.next(); + + //build up a list for each permission level + if(entry.getValue() == ClaimPermission.Build) + { + builders.add(entry.getKey()); + } + else if(entry.getValue() == ClaimPermission.Inventory) + { + containers.add(entry.getKey()); + } + else + { + accessors.add(entry.getKey()); + } + } + + //managers are handled a little differently + for(int i = 0; i < this.managers.size(); i++) + { + managers.add(this.managers.get(i)); + } + } + + //returns a copy of the location representing lower x, y, z limits + public Location getLesserBoundaryCorner() + { + return this.lesserBoundaryCorner.clone(); + } + + //returns a copy of the location representing upper x, y, z limits + //NOTE: remember upper Y will always be ignored, all claims always extend to the sky + public Location getGreaterBoundaryCorner() + { + return this.greaterBoundaryCorner.clone(); + } + + //returns a friendly owner name (for admin claims, returns "an administrator" as the owner) + public String getOwnerName() + { + if(this.parent != null) + return this.parent.getOwnerName(); + + if(this.ownerName.length() == 0) + return GriefPrevention.instance.dataStore.getMessage(Messages.OwnerNameForAdminClaims); + + return this.ownerName; + } + + //whether or not a location is in a claim + //ignoreHeight = true means location UNDER the claim will return TRUE + //excludeSubdivisions = true means that locations inside subdivisions of the claim will return FALSE + public boolean contains(Location location, boolean ignoreHeight, boolean excludeSubdivisions) + { + //not in the same world implies false + if(!location.getWorld().equals(this.lesserBoundaryCorner.getWorld())) return false; + + int x = location.getBlockX(); + int y = location.getBlockY(); + int z = location.getBlockZ(); + + //main check + boolean inClaim = (ignoreHeight || y >= this.lesserBoundaryCorner.getBlockY()) && + x >= this.lesserBoundaryCorner.getBlockX() && + x <= this.greaterBoundaryCorner.getBlockX() && + z >= this.lesserBoundaryCorner.getBlockZ() && + z <= this.greaterBoundaryCorner.getBlockZ(); + + if(!inClaim) return false; + + //additional check for subdivisions + //you're only in a subdivision when you're also in its parent claim + //NOTE: if a player creates subdivions then resizes the parent claim, it's possible that + //a subdivision can reach outside of its parent's boundaries. so this check is important! + if(this.parent != null) + { + return this.parent.contains(location, ignoreHeight, false); + } + + //code to exclude subdivisions in this check + else if(excludeSubdivisions) + { + //search all subdivisions to see if the location is in any of them + for(int i = 0; i < this.children.size(); i++) + { + //if we find such a subdivision, return false + if(this.children.get(i).contains(location, ignoreHeight, true)) + { + return false; + } + } + } + + //otherwise yes + return true; + } + + //whether or not two claims overlap + //used internally to prevent overlaps when creating claims + boolean overlaps(Claim otherClaim) + { + //NOTE: if trying to understand this makes your head hurt, don't feel bad - it hurts mine too. + //try drawing pictures to visualize test cases. + + if(!this.lesserBoundaryCorner.getWorld().equals(otherClaim.getLesserBoundaryCorner().getWorld())) return false; + + //first, check the corners of this claim aren't inside any existing claims + if(otherClaim.contains(this.lesserBoundaryCorner, true, false)) return true; + if(otherClaim.contains(this.greaterBoundaryCorner, true, false)) return true; + if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.lesserBoundaryCorner.getBlockX(), 0, this.greaterBoundaryCorner.getBlockZ()), true, false)) return true; + if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.greaterBoundaryCorner.getBlockX(), 0, this.lesserBoundaryCorner.getBlockZ()), true, false)) return true; + + //verify that no claim's lesser boundary point is inside this new claim, to cover the "existing claim is entirely inside new claim" case + if(this.contains(otherClaim.getLesserBoundaryCorner(), true, false)) return true; + + //verify this claim doesn't band across an existing claim, either horizontally or vertically + if( this.getLesserBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() && + this.getLesserBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() && + this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() && + this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() ) + return true; + + if( this.getGreaterBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() && + this.getGreaterBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() && + this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() && + this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() ) + return true; + + if( this.getLesserBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() && + this.getLesserBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() && + this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() && + this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() ) + return true; + + if( this.getGreaterBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() && + this.getGreaterBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() && + this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() && + this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() ) + return true; + + return false; + } + + //whether more entities may be added to a claim + public String allowMoreEntities() + { + if(this.parent != null) return this.parent.allowMoreEntities(); + + //this rule only applies to creative mode worlds + if(!GriefPrevention.instance.creativeRulesApply(this.getLesserBoundaryCorner())) return null; + + //admin claims aren't restricted + if(this.isAdminClaim()) return null; + + //don't apply this rule to very large claims + if(this.getArea() > 10000) return null; + + //determine maximum allowable entity count, based on claim size + int maxEntities = this.getArea() / 50; + if(maxEntities == 0) return GriefPrevention.instance.dataStore.getMessage(Messages.ClaimTooSmallForEntities); + + //count current entities (ignoring players) + Chunk lesserChunk = this.getLesserBoundaryCorner().getChunk(); + Chunk greaterChunk = this.getGreaterBoundaryCorner().getChunk(); + + int totalEntities = 0; + for(int x = lesserChunk.getX(); x <= greaterChunk.getX(); x++) + for(int z = lesserChunk.getZ(); z <= greaterChunk.getZ(); z++) + { + Chunk chunk = lesserChunk.getWorld().getChunkAt(x, z); + Entity [] entities = chunk.getEntities(); + for(int i = 0; i < entities.length; i++) + { + Entity entity = entities[i]; + if(!(entity instanceof Player) && this.contains(entity.getLocation(), false, false)) + { + totalEntities++; + if(totalEntities > maxEntities) entity.remove(); + } + } + } + + if(totalEntities > maxEntities) return GriefPrevention.instance.dataStore.getMessage(Messages.TooManyEntitiesInClaim); + + return null; + } + + //implements a strict ordering of claims, used to keep the claims collection sorted for faster searching + boolean greaterThan(Claim otherClaim) + { + Location thisCorner = this.getLesserBoundaryCorner(); + Location otherCorner = otherClaim.getLesserBoundaryCorner(); + + if(thisCorner.getBlockX() > otherCorner.getBlockX()) return true; + + if(thisCorner.getBlockX() < otherCorner.getBlockX()) return false; + + if(thisCorner.getBlockZ() > otherCorner.getBlockZ()) return true; + + if(thisCorner.getBlockZ() < otherCorner.getBlockZ()) return false; + + return thisCorner.getWorld().getName().compareTo(otherCorner.getWorld().getName()) < 0; + } + + long getPlayerInvestmentScore() + { + //decide which blocks will be considered player placed + Location lesserBoundaryCorner = this.getLesserBoundaryCorner(); + ArrayList playerBlocks = RestoreNatureProcessingTask.getPlayerBlocks(lesserBoundaryCorner.getWorld().getEnvironment(), lesserBoundaryCorner.getBlock().getBiome()); + + //scan the claim for player placed blocks + double score = 0; + + boolean creativeMode = GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner); + + for(int x = this.lesserBoundaryCorner.getBlockX(); x <= this.greaterBoundaryCorner.getBlockX(); x++) + { + for(int z = this.lesserBoundaryCorner.getBlockZ(); z <= this.greaterBoundaryCorner.getBlockZ(); z++) + { + int y = this.lesserBoundaryCorner.getBlockY(); + for(; y < GriefPrevention.instance.getSeaLevel(this.lesserBoundaryCorner.getWorld()) - 5; y++) + { + Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z); + if(playerBlocks.contains(block.getTypeId())) + { + if(block.getType() == Material.CHEST && !creativeMode) + { + score += 10; + } + else + { + score += .5; + } + } + } + + for(; y < this.lesserBoundaryCorner.getWorld().getMaxHeight(); y++) + { + Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z); + if(playerBlocks.contains(block.getTypeId())) + { + if(block.getType() == Material.CHEST && !creativeMode) + { + score += 10; + } + else if(creativeMode && (block.getType() == Material.LAVA || block.getType() == Material.STATIONARY_LAVA)) + { + score -= 10; + } + else + { + score += 1; + } + } + } + } + } + + return (long)score; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/CleanupUnusedClaimsTask.java b/src/me/ryanhamshire/GriefPrevention/CleanupUnusedClaimsTask.java index 5b35872..189508c 100644 --- a/src/me/ryanhamshire/GriefPrevention/CleanupUnusedClaimsTask.java +++ b/src/me/ryanhamshire/GriefPrevention/CleanupUnusedClaimsTask.java @@ -1,189 +1,215 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - - package me.ryanhamshire.GriefPrevention; - -import java.util.Calendar; -import java.util.Random; - -import org.bukkit.Chunk; -import org.bukkit.World; - -//FEATURE: automatically remove claims owned by inactive players which: -//...aren't protecting much OR -//...are a free new player claim (and the player has no other claims) OR -//...because the player has been gone a REALLY long time, and that expiration has been configured in config.yml - -//runs every 1 minute in the main thread -class CleanupUnusedClaimsTask implements Runnable -{ - int nextClaimIndex; - - CleanupUnusedClaimsTask() - { - //start scanning in a random spot - if(GriefPrevention.instance.dataStore.claims.size() == 0) - { - this.nextClaimIndex = 0; - } - else - { - Random randomNumberGenerator = new Random(); - this.nextClaimIndex = randomNumberGenerator.nextInt(GriefPrevention.instance.dataStore.claims.size()); - } - } - - @Override - public void run() - { - //don't do anything when there are no claims - if(GriefPrevention.instance.dataStore.claims.size() == 0) return; - - //wrap search around to beginning - if(this.nextClaimIndex >= GriefPrevention.instance.dataStore.claims.size()) this.nextClaimIndex = 0; - - //decide which claim to check next - Claim claim = GriefPrevention.instance.dataStore.claims.get(this.nextClaimIndex++); - - //skip administrative claims - if(claim.isAdminClaim()) return; - - //get data for the player, especially last login timestamp - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(claim.ownerName); - - //determine area of the default chest claim - int areaOfDefaultClaim = 0; - if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius >= 0) - { - areaOfDefaultClaim = (int)Math.pow(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius * 2 + 1, 2); - } - - //if he's been gone at least a week, if he has ONLY the new player claim, it will be removed - Calendar sevenDaysAgo = Calendar.getInstance(); - sevenDaysAgo.add(Calendar.DATE, -7); - boolean newPlayerClaimsExpired = sevenDaysAgo.getTime().after(playerData.lastLogin); - - //if only one claim, and the player hasn't played in a week - if(newPlayerClaimsExpired && playerData.claims.size() == 1) - { - //if that's a chest claim, delete it - if(claim.getArea() <= areaOfDefaultClaim) - { - claim.removeSurfaceFluids(null); - GriefPrevention.instance.dataStore.deleteClaim(claim); - - //if in a creative mode world, delete the claim - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.instance.restoreClaim(claim, 0); - } - - GriefPrevention.AddLogEntry(" " + claim.getOwnerName() + "'s new player claim expired."); - } - } - - //if configured to always remove claims after some inactivity period without exceptions... - else if(GriefPrevention.instance.config_claims_expirationDays > 0) - { - Calendar earliestPermissibleLastLogin = Calendar.getInstance(); - earliestPermissibleLastLogin.add(Calendar.DATE, -GriefPrevention.instance.config_claims_expirationDays); - - if(earliestPermissibleLastLogin.getTime().after(playerData.lastLogin)) - { - GriefPrevention.instance.dataStore.deleteClaimsForPlayer(claim.getOwnerName(), true); - GriefPrevention.AddLogEntry(" All of " + claim.getOwnerName() + "'s claims have expired."); - } - } - - else - { - - //if the player has been gone two weeks, scan claim content to assess player investment - Calendar fourteenDaysAgo = Calendar.getInstance(); - fourteenDaysAgo.add(Calendar.DATE, -14); - boolean needsInvestmentScan = fourteenDaysAgo.getTime().after(playerData.lastLogin); - - //avoid scanning large claims and administrative claims - if(claim.isAdminClaim() || claim.getWidth() > 25 || claim.getHeight() > 25) return; - - //if creative mode or the claim owner has been away a long enough time, scan the claim content - if(needsInvestmentScan || GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - int minInvestment; - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - minInvestment = 400; - } - else - { - minInvestment = 200; - } - - long investmentScore = claim.getPlayerInvestmentScore(); - boolean removeClaim = false; - - //in creative mode, a build which is almost entirely lava above sea level will be automatically removed, even if the owner is an active player - //lava above the surface deducts 10 points per block from the investment score - //so 500 blocks of lava without anything built to offset all that potential mess would be cleaned up automatically - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()) && investmentScore < -5000) - { - removeClaim = true; - } - - //otherwise, the only way to get a claim automatically removed based on build investment is to be away for two weeks AND not build much of anything - else if(needsInvestmentScan && investmentScore < minInvestment) - { - removeClaim = true; - } - - if(removeClaim) - { - GriefPrevention.instance.dataStore.deleteClaim(claim); - GriefPrevention.AddLogEntry("Removed " + claim.getOwnerName() + "'s unused claim @ " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); - - //if in a creative mode world, restore the claim area - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.instance.restoreClaim(claim, 0); - } - } - } - } - - //toss that player data out of the cache, it's probably not needed in memory right now - if(!GriefPrevention.instance.getServer().getOfflinePlayer(claim.ownerName).isOnline()) - { - GriefPrevention.instance.dataStore.clearCachedPlayerData(claim.ownerName); - } - - //since we're potentially loading a lot of chunks to scan parts of the world where there are no players currently playing, be mindful of memory usage - //unfortunately, java/minecraft don't do a good job of clearing unused memory, leading to out of memory errors from this type of world scanning - if(this.nextClaimIndex % 20 == 0) - { - World world = claim.getLesserBoundaryCorner().getWorld(); - Chunk [] chunks = world.getLoadedChunks(); - for(int i = 0; i < chunks.length; i++) - { - Chunk chunk = chunks[i]; - chunk.unload(true, true); - } - - System.gc(); - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + + package me.ryanhamshire.GriefPrevention; + +import java.util.Calendar; +import java.util.Random; +import java.util.Vector; + +import org.bukkit.Chunk; +import org.bukkit.World; + +//FEATURE: automatically remove claims owned by inactive players which: +//...aren't protecting much OR +//...are a free new player claim (and the player has no other claims) OR +//...because the player has been gone a REALLY long time, and that expiration has been configured in config.yml + +//runs every 1 minute in the main thread +class CleanupUnusedClaimsTask implements Runnable +{ + int nextClaimIndex; + + CleanupUnusedClaimsTask() + { + //start scanning in a random spot + if(GriefPrevention.instance.dataStore.claims.size() == 0) + { + this.nextClaimIndex = 0; + } + else + { + Random randomNumberGenerator = new Random(); + this.nextClaimIndex = randomNumberGenerator.nextInt(GriefPrevention.instance.dataStore.claims.size()); + } + } + + @Override + public void run() + { + //don't do anything when there are no claims + if(GriefPrevention.instance.dataStore.claims.size() == 0) return; + + //wrap search around to beginning + if(this.nextClaimIndex >= GriefPrevention.instance.dataStore.claims.size()) this.nextClaimIndex = 0; + + //decide which claim to check next + Claim claim = GriefPrevention.instance.dataStore.claims.get(this.nextClaimIndex++); + + //skip administrative claims + if(claim.isAdminClaim()) return; + + //track whether we do any important work which would require cleanup afterward + boolean cleanupChunks = false; + + //get data for the player, especially last login timestamp + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(claim.ownerName); + + //determine area of the default chest claim + int areaOfDefaultClaim = 0; + if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius >= 0) + { + areaOfDefaultClaim = (int)Math.pow(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius * 2 + 1, 2); + } + + //if he's been gone at least a week, if he has ONLY the new player claim, it will be removed + Calendar sevenDaysAgo = Calendar.getInstance(); + sevenDaysAgo.add(Calendar.DATE, -GriefPrevention.instance.config_claims_chestClaimExpirationDays); + boolean newPlayerClaimsExpired = sevenDaysAgo.getTime().after(playerData.lastLogin); + + //if only one claim, and the player hasn't played in a week + if(newPlayerClaimsExpired && playerData.claims.size() == 1) + { + //if that's a chest claim and those are set to expire + if(claim.getArea() <= areaOfDefaultClaim && GriefPrevention.instance.config_claims_chestClaimExpirationDays > 0) + { + claim.removeSurfaceFluids(null); + GriefPrevention.instance.dataStore.deleteClaim(claim); + cleanupChunks = true; + + //if configured to do so, restore the land to natural + if((GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()) && GriefPrevention.instance.config_claims_creativeAutoNatureRestoration) || GriefPrevention.instance.config_claims_survivalAutoNatureRestoration) + { + GriefPrevention.instance.restoreClaim(claim, 0); + } + + GriefPrevention.AddLogEntry(" " + claim.getOwnerName() + "'s new player claim expired."); + } + } + + //if configured to always remove claims after some inactivity period without exceptions... + else if(GriefPrevention.instance.config_claims_expirationDays > 0) + { + Calendar earliestPermissibleLastLogin = Calendar.getInstance(); + earliestPermissibleLastLogin.add(Calendar.DATE, -GriefPrevention.instance.config_claims_expirationDays); + + if(earliestPermissibleLastLogin.getTime().after(playerData.lastLogin)) + { + //make a copy of this player's claim list + Vector claims = new Vector(); + for(int i = 0; i < playerData.claims.size(); i++) + { + claims.add(playerData.claims.get(i)); + } + + //delete them + GriefPrevention.instance.dataStore.deleteClaimsForPlayer(claim.getOwnerName(), true); + GriefPrevention.AddLogEntry(" All of " + claim.getOwnerName() + "'s claims have expired."); + + for(int i = 0; i < claims.size(); i++) + { + //if configured to do so, restore the land to natural + if((GriefPrevention.instance.creativeRulesApply(claims.get(i).getLesserBoundaryCorner()) && GriefPrevention.instance.config_claims_creativeAutoNatureRestoration) || GriefPrevention.instance.config_claims_survivalAutoNatureRestoration) + { + GriefPrevention.instance.restoreClaim(claims.get(i), 0); + cleanupChunks = true; + } + } + } + } + + else if(GriefPrevention.instance.config_claims_unusedClaimExpirationDays > 0) + { + //if the player has been gone two weeks, scan claim content to assess player investment + Calendar earliestAllowedLoginDate = Calendar.getInstance(); + earliestAllowedLoginDate.add(Calendar.DATE, -GriefPrevention.instance.config_claims_unusedClaimExpirationDays); + boolean needsInvestmentScan = earliestAllowedLoginDate.getTime().after(playerData.lastLogin); + + //avoid scanning large claims and administrative claims + if(claim.isAdminClaim() || claim.getWidth() > 25 || claim.getHeight() > 25) return; + + //if creative mode or the claim owner has been away a long enough time, scan the claim content + if(needsInvestmentScan || GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + int minInvestment; + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + minInvestment = 400; + } + else + { + minInvestment = 100; + } + + long investmentScore = claim.getPlayerInvestmentScore(); + cleanupChunks = true; + boolean removeClaim = false; + + //in creative mode, a build which is almost entirely lava above sea level will be automatically removed, even if the owner is an active player + //lava above the surface deducts 10 points per block from the investment score + //so 500 blocks of lava without anything built to offset all that potential mess would be cleaned up automatically + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()) && investmentScore < -5000) + { + removeClaim = true; + } + + //otherwise, the only way to get a claim automatically removed based on build investment is to be away for two weeks AND not build much of anything + else if(needsInvestmentScan && investmentScore < minInvestment) + { + removeClaim = true; + } + + if(removeClaim) + { + GriefPrevention.instance.dataStore.deleteClaim(claim); + GriefPrevention.AddLogEntry("Removed " + claim.getOwnerName() + "'s unused claim @ " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); + + //if configured to do so, restore the claim area to natural state + if((GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()) && GriefPrevention.instance.config_claims_creativeAutoNatureRestoration) || GriefPrevention.instance.config_claims_survivalAutoNatureRestoration) + { + GriefPrevention.instance.restoreClaim(claim, 0); + } + } + } + } + + //toss that player data out of the cache, it's probably not needed in memory right now + if(!GriefPrevention.instance.getServer().getOfflinePlayer(claim.ownerName).isOnline()) + { + GriefPrevention.instance.dataStore.clearCachedPlayerData(claim.ownerName); + } + + //since we're potentially loading a lot of chunks to scan parts of the world where there are no players currently playing, be mindful of memory usage + if(cleanupChunks) + { + World world = claim.getLesserBoundaryCorner().getWorld(); + Chunk [] chunks = world.getLoadedChunks(); + for(int i = 0; i < chunks.length; i++) + { + Chunk chunk = chunks[i]; + chunk.unload(true, true); + } + } + + //unfortunately, java/minecraft don't do a good job of clearing unused memory, leading to out of memory errors from this type of world scanning + if(this.nextClaimIndex % 10 == 0) + { + System.gc(); + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/DataStore.java b/src/me/ryanhamshire/GriefPrevention/DataStore.java index 8ca3e30..ce894d5 100644 --- a/src/me/ryanhamshire/GriefPrevention/DataStore.java +++ b/src/me/ryanhamshire/GriefPrevention/DataStore.java @@ -1,1027 +1,1028 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.io.*; -import java.util.*; - -import org.bukkit.*; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -//singleton class which manages all GriefPrevention data (except for config options) -public abstract class DataStore -{ - //in-memory cache for player data - protected HashMap playerNameToPlayerDataMap = new HashMap(); - - //in-memory cache for group (permission-based) data - protected HashMap permissionToBonusBlocksMap = new HashMap(); - - //in-memory cache for claim data - ArrayList claims = new ArrayList(); - - //in-memory cache for messages - private String [] messages; - - //next claim ID - Long nextClaimID = (long)0; - - //path information, for where stuff stored on disk is well... stored - protected final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; - final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; - final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; - - //initialization! - void initialize() throws Exception - { - GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); - - //make a list of players who own claims - Vector playerNames = new Vector(); - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - - //ignore admin claims - if(claim.isAdminClaim()) continue; - - if(!playerNames.contains(claim.ownerName)) - playerNames.add(claim.ownerName); - } - - GriefPrevention.AddLogEntry(playerNames.size() + " players have staked claims."); - - //load up all the messages from messages.yml - this.loadMessages(); - - //collect garbage, since lots of stuff was loaded into memory and then tossed out - System.gc(); - } - - //removes cached player data from memory - synchronized void clearCachedPlayerData(String playerName) - { - this.playerNameToPlayerDataMap.remove(playerName); - } - - //gets the number of bonus blocks a player has from his permissions - synchronized int getGroupBonusBlocks(String playerName) - { - int bonusBlocks = 0; - Set keys = permissionToBonusBlocksMap.keySet(); - Iterator iterator = keys.iterator(); - while(iterator.hasNext()) - { - String groupName = iterator.next(); - Player player = GriefPrevention.instance.getServer().getPlayer(playerName); - if(player.hasPermission(groupName)) - { - bonusBlocks += this.permissionToBonusBlocksMap.get(groupName); - } - } - - return bonusBlocks; - } - - //grants a group (players with a specific permission) bonus claim blocks as long as they're still members of the group - synchronized public int adjustGroupBonusBlocks(String groupName, int amount) - { - Integer currentValue = this.permissionToBonusBlocksMap.get(groupName); - if(currentValue == null) currentValue = 0; - - currentValue += amount; - this.permissionToBonusBlocksMap.put(groupName, currentValue); - - //write changes to storage to ensure they don't get lost - this.saveGroupBonusBlocks(groupName, currentValue); - - return currentValue; - } - - abstract void saveGroupBonusBlocks(String groupName, int amount); - - synchronized public void changeClaimOwner(Claim claim, String newOwnerName) throws Exception - { - //if it's a subdivision, throw an exception - if(claim.parent != null) - { - throw new Exception("Subdivisions can't be transferred. Only top-level claims may change owners."); - } - - //otherwise update information - - //determine current claim owner - PlayerData ownerData = null; - if(!claim.isAdminClaim()) - { - ownerData = this.getPlayerData(claim.ownerName); - } - - //determine new owner - PlayerData newOwnerData = this.getPlayerData(newOwnerName); - - //transfer - claim.ownerName = newOwnerName; - this.saveClaim(claim); - - //adjust blocks and other records - if(ownerData != null) - { - ownerData.claims.remove(claim); - ownerData.bonusClaimBlocks -= claim.getArea(); - this.savePlayerData(claim.ownerName, ownerData); - } - - newOwnerData.claims.add(claim); - newOwnerData.bonusClaimBlocks += claim.getArea(); - this.savePlayerData(newOwnerName, newOwnerData); - } - - //adds a claim to the datastore, making it an effective claim - synchronized void addClaim(Claim newClaim) - { - //subdivisions are easy - if(newClaim.parent != null) - { - newClaim.parent.children.add(newClaim); - newClaim.inDataStore = true; - this.saveClaim(newClaim); - return; - } - - //add it and mark it as added - int j = 0; - while(j < this.claims.size() && !this.claims.get(j).greaterThan(newClaim)) j++; - if(j < this.claims.size()) - this.claims.add(j, newClaim); - else - this.claims.add(this.claims.size(), newClaim); - newClaim.inDataStore = true; - - //except for administrative claims (which have no owner), update the owner's playerData with the new claim - if(!newClaim.isAdminClaim()) - { - PlayerData ownerData = this.getPlayerData(newClaim.getOwnerName()); - ownerData.claims.add(newClaim); - this.savePlayerData(newClaim.getOwnerName(), ownerData); - } - - //make sure the claim is saved to disk - this.saveClaim(newClaim); - } - - //turns a location into a string, useful in data storage - private String locationStringDelimiter = ";"; - String locationToString(Location location) - { - StringBuilder stringBuilder = new StringBuilder(location.getWorld().getName()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockX()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockY()); - stringBuilder.append(locationStringDelimiter); - stringBuilder.append(location.getBlockZ()); - - return stringBuilder.toString(); - } - - //turns a location string back into a location - Location locationFromString(String string) throws Exception - { - //split the input string on the space - String [] elements = string.split(locationStringDelimiter); - - //expect four elements - world name, X, Y, and Z, respectively - if(elements.length != 4) - { - throw new Exception("Expected four distinct parts to the location string."); - } - - String worldName = elements[0]; - String xString = elements[1]; - String yString = elements[2]; - String zString = elements[3]; - - //identify world the claim is in - World world = GriefPrevention.instance.getServer().getWorld(worldName); - if(world == null) - { - throw new Exception("World not found: \"" + worldName + "\""); - } - - //convert those numerical strings to integer values - int x = Integer.parseInt(xString); - int y = Integer.parseInt(yString); - int z = Integer.parseInt(zString); - - return new Location(world, x, y, z); - } - - //saves any changes to a claim to secondary storage - synchronized public void saveClaim(Claim claim) - { - //subdivisions don't save to their own files, but instead live in their parent claim's file - //so any attempt to save a subdivision will save its parent (and thus the subdivision) - if(claim.parent != null) - { - this.saveClaim(claim.parent); - return; - } - - //otherwise get a unique identifier for the claim which will be used to name the file on disk - if(claim.id == null) - { - claim.id = this.nextClaimID; - this.incrementNextClaimID(); - } - - this.writeClaimToStorage(claim); - } - - abstract void writeClaimToStorage(Claim claim); - - //increments the claim ID and updates secondary storage to be sure it's saved - abstract void incrementNextClaimID(); - - //retrieves player data from memory or secondary storage, as necessary - //if the player has never been on the server before, this will return a fresh player data with default values - synchronized public PlayerData getPlayerData(String playerName) - { - //first, look in memory - PlayerData playerData = this.playerNameToPlayerDataMap.get(playerName); - - //if not there, look in secondary storage - if(playerData == null) - { - playerData = this.getPlayerDataFromStorage(playerName); - playerData.playerName = playerName; - - //find all the claims belonging to this player and note them for future reference - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - if(claim.ownerName.equals(playerName)) - { - playerData.claims.add(claim); - } - } - - //shove that new player data into the hash map cache - this.playerNameToPlayerDataMap.put(playerName, playerData); - } - - //try the hash map again. if it's STILL not there, we have a bug to fix - return this.playerNameToPlayerDataMap.get(playerName); - } - - abstract PlayerData getPlayerDataFromStorage(String playerName); - - //deletes a claim or subdivision - synchronized public void deleteClaim(Claim claim) - { - //subdivisions are simple - just remove them from their parent claim and save that claim - if(claim.parent != null) - { - Claim parentClaim = claim.parent; - parentClaim.children.remove(claim); - this.saveClaim(parentClaim); - return; - } - - //remove from memory - for(int i = 0; i < this.claims.size(); i++) - { - if(claims.get(i).id.equals(claim.id)) - { - this.claims.remove(i); - claim.inDataStore = false; - for(int j = 0; j < claim.children.size(); j++) - { - claim.children.get(j).inDataStore = false; - } - break; - } - } - - //remove from secondary storage - this.deleteClaimFromSecondaryStorage(claim); - - //update player data, except for administrative claims, which have no owner - if(!claim.isAdminClaim()) - { - PlayerData ownerData = this.getPlayerData(claim.getOwnerName()); - for(int i = 0; i < ownerData.claims.size(); i++) - { - if(ownerData.claims.get(i).id.equals(claim.id)) - { - ownerData.claims.remove(i); - break; - } - } - this.savePlayerData(claim.getOwnerName(), ownerData); - } - } - - abstract void deleteClaimFromSecondaryStorage(Claim claim); - - //gets the claim at a specific location - //ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim - //cachedClaim can be NULL, but will help performance if you have a reasonable guess about which claim the location is in - synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, Claim cachedClaim) - { - //check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done - if(cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) return cachedClaim; - - //the claims list is ordered by greater boundary corner - //create a temporary "fake" claim in memory for comparison purposes - Claim tempClaim = new Claim(); - tempClaim.lesserBoundaryCorner = location; - - //otherwise, search all existing claims until we find the right claim - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - - //if we reach a claim which is greater than the temp claim created above, there's definitely no claim - //in the collection which includes our location - if(claim.greaterThan(tempClaim)) return null; - - //find a top level claim - if(claim.contains(location, ignoreHeight, false)) - { - //when we find a top level claim, if the location is in one of its subdivisions, - //return the SUBDIVISION, not the top level claim - for(int j = 0; j < claim.children.size(); j++) - { - Claim subdivision = claim.children.get(j); - if(subdivision.contains(location, ignoreHeight, false)) return subdivision; - } - - return claim; - } - } - - //if no claim found, return null - return null; - } - - //creates a claim. - //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim - //otherwise, returns a success along with a reference to the new claim - //use ownerName == "" for administrative claims - //for top level claims, pass parent == NULL - //DOES adjust claim blocks available on success (players can go into negative quantity available) - //does NOT check a player has permission to create a claim, or enough claim blocks. - //does NOT check minimum claim size constraints - //does NOT visualize the new claim for any players - synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, String ownerName, Claim parent, Long id) - { - CreateClaimResult result = new CreateClaimResult(); - - int smallx, bigx, smally, bigy, smallz, bigz; - - //determine small versus big inputs - if(x1 < x2) - { - smallx = x1; - bigx = x2; - } - else - { - smallx = x2; - bigx = x1; - } - - if(y1 < y2) - { - smally = y1; - bigy = y2; - } - else - { - smally = y2; - bigy = y1; - } - - if(z1 < z2) - { - smallz = z1; - bigz = z2; - } - else - { - smallz = z2; - bigz = z1; - } - - //creative mode claims always go to bedrock - if(GriefPrevention.instance.config_claims_enabledCreativeWorlds.contains(world)) - { - smally = 2; - } - - //create a new claim instance (but don't save it, yet) - Claim newClaim = new Claim( - new Location(world, smallx, smally, smallz), - new Location(world, bigx, bigy, bigz), - ownerName, - new String [] {}, - new String [] {}, - new String [] {}, - new String [] {}, - id); - - newClaim.parent = parent; - - //ensure this new claim won't overlap any existing claims - ArrayList claimsToCheck; - if(newClaim.parent != null) - { - claimsToCheck = newClaim.parent.children; - } - else - { - claimsToCheck = this.claims; - } - - for(int i = 0; i < claimsToCheck.size(); i++) - { - Claim otherClaim = claimsToCheck.get(i); - - //if we find an existing claim which will be overlapped - if(otherClaim.overlaps(newClaim)) - { - //result = fail, return conflicting claim - result.succeeded = false; - result.claim = otherClaim; - return result; - } - } - - //otherwise add this new claim to the data store to make it effective - this.addClaim(newClaim); - - //then return success along with reference to new claim - result.succeeded = true; - result.claim = newClaim; - return result; - } - - //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them - public abstract void savePlayerData(String playerName, PlayerData playerData); - - //extends a claim to a new depth - //respects the max depth config variable - synchronized public void extendClaim(Claim claim, int newDepth) - { - if(newDepth < GriefPrevention.instance.config_claims_maxDepth) newDepth = GriefPrevention.instance.config_claims_maxDepth; - - if(claim.parent != null) claim = claim.parent; - - //delete the claim - this.deleteClaim(claim); - - //re-create it at the new depth - claim.lesserBoundaryCorner.setY(newDepth); - claim.greaterBoundaryCorner.setY(newDepth); - - //make all subdivisions reach to the same depth - for(int i = 0; i < claim.children.size(); i++) - { - claim.children.get(i).lesserBoundaryCorner.setY(newDepth); - claim.children.get(i).greaterBoundaryCorner.setY(newDepth); - } - - //save changes - this.addClaim(claim); - } - - //starts a siege on a claim - //does NOT check siege cooldowns, see onCooldown() below - synchronized public void startSiege(Player attacker, Player defender, Claim defenderClaim) - { - //fill-in the necessary SiegeData instance - SiegeData siegeData = new SiegeData(attacker, defender, defenderClaim); - PlayerData attackerData = this.getPlayerData(attacker.getName()); - PlayerData defenderData = this.getPlayerData(defender.getName()); - attackerData.siegeData = siegeData; - defenderData.siegeData = siegeData; - defenderClaim.siegeData = siegeData; - - //start a task to monitor the siege - //why isn't this a "repeating" task? - //because depending on the status of the siege at the time the task runs, there may or may not be a reason to run the task again - SiegeCheckupTask task = new SiegeCheckupTask(siegeData); - siegeData.checkupTaskID = GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 30); - } - - //ends a siege - //either winnerName or loserName can be null, but not both - synchronized public void endSiege(SiegeData siegeData, String winnerName, String loserName, boolean death) - { - boolean grantAccess = false; - - //determine winner and loser - if(winnerName == null && loserName != null) - { - if(siegeData.attacker.getName().equals(loserName)) - { - winnerName = siegeData.defender.getName(); - } - else - { - winnerName = siegeData.attacker.getName(); - } - } - else if(winnerName != null && loserName == null) - { - if(siegeData.attacker.getName().equals(winnerName)) - { - loserName = siegeData.defender.getName(); - } - else - { - loserName = siegeData.attacker.getName(); - } - } - - //if the attacker won, plan to open the doors for looting - if(siegeData.attacker.getName().equals(winnerName)) - { - grantAccess = true; - } - - PlayerData attackerData = this.getPlayerData(siegeData.attacker.getName()); - attackerData.siegeData = null; - - PlayerData defenderData = this.getPlayerData(siegeData.defender.getName()); - defenderData.siegeData = null; - - //start a cooldown for this attacker/defender pair - Long now = Calendar.getInstance().getTimeInMillis(); - Long cooldownEnd = now + 1000 * 60 * 60; //one hour from now - this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + siegeData.defender.getName(), cooldownEnd); - - //start cooldowns for every attacker/involved claim pair - for(int i = 0; i < siegeData.claims.size(); i++) - { - Claim claim = siegeData.claims.get(i); - claim.siegeData = null; - this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + claim.ownerName, cooldownEnd); - - //if doors should be opened for looting, do that now - if(grantAccess) - { - claim.doorsOpen = true; - } - } - - //cancel the siege checkup task - GriefPrevention.instance.getServer().getScheduler().cancelTask(siegeData.checkupTaskID); - - //notify everyone who won and lost - if(winnerName != null && loserName != null) - { - GriefPrevention.instance.getServer().broadcastMessage(winnerName + " defeated " + loserName + " in siege warfare!"); - } - - //if the claim should be opened to looting - if(grantAccess) - { - Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); - if(winner != null) - { - //notify the winner - GriefPrevention.sendMessage(winner, TextMode.Success, Messages.SiegeWinDoorsOpen); - - //schedule a task to secure the claims in about 5 minutes - SecureClaimTask task = new SecureClaimTask(siegeData); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 5); - } - } - - //if the siege ended due to death, transfer inventory to winner - if(death) - { - Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); - Player loser = GriefPrevention.instance.getServer().getPlayer(loserName); - if(winner != null && loser != null) - { - //get loser's inventory, then clear it - ItemStack [] loserItems = loser.getInventory().getContents(); - loser.getInventory().clear(); - - //try to add it to the winner's inventory - for(int j = 0; j < loserItems.length; j++) - { - if(loserItems[j] == null || loserItems[j].getType() == Material.AIR || loserItems[j].getAmount() == 0) continue; - - HashMap wontFitItems = winner.getInventory().addItem(loserItems[j]); - - //drop any remainder on the ground at his feet - Object [] keys = wontFitItems.keySet().toArray(); - Location winnerLocation = winner.getLocation(); - for(int i = 0; i < keys.length; i++) - { - Integer key = (Integer)keys[i]; - winnerLocation.getWorld().dropItemNaturally(winnerLocation, wontFitItems.get(key)); - } - } - } - } - } - - //timestamp for each siege cooldown to end - private HashMap siegeCooldownRemaining = new HashMap(); - - //whether or not a sieger can siege a particular victim or claim, considering only cooldowns - synchronized public boolean onCooldown(Player attacker, Player defender, Claim defenderClaim) - { - Long cooldownEnd = null; - - //look for an attacker/defender cooldown - if(this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()) != null) - { - cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()); - - if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) - { - return true; - } - - //if found but expired, remove it - this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defender.getName()); - } - - //look for an attacker/claim cooldown - if(cooldownEnd == null && this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName) != null) - { - cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName); - - if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) - { - return true; - } - - //if found but expired, remove it - this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defenderClaim.ownerName); - } - - return false; - } - - //extend a siege, if it's possible to do so - synchronized void tryExtendSiege(Player player, Claim claim) - { - PlayerData playerData = this.getPlayerData(player.getName()); - - //player must be sieged - if(playerData.siegeData == null) return; - - //claim isn't already under the same siege - if(playerData.siegeData.claims.contains(claim)) return; - - //admin claims can't be sieged - if(claim.isAdminClaim()) return; - - //player must have some level of permission to be sieged in a claim - if(claim.allowAccess(player) != null) return; - - //otherwise extend the siege - playerData.siegeData.claims.add(claim); - claim.siegeData = playerData.siegeData; - } - - //deletes all claims owned by a player - synchronized public void deleteClaimsForPlayer(String playerName, boolean deleteCreativeClaims) - { - //make a list of the player's claims - ArrayList claimsToDelete = new ArrayList(); - for(int i = 0; i < this.claims.size(); i++) - { - Claim claim = this.claims.get(i); - if(claim.ownerName.equals(playerName) && (deleteCreativeClaims || !GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))) - claimsToDelete.add(claim); - } - - //delete them one by one - for(int i = 0; i < claimsToDelete.size(); i++) - { - Claim claim = claimsToDelete.get(i); - claim.removeSurfaceFluids(null); - - this.deleteClaim(claim); - - //if in a creative mode world, delete the claim - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.instance.restoreClaim(claim, 0); - } - } - } - - //tries to resize a claim - //see CreateClaim() for details on return value - synchronized public CreateClaimResult resizeClaim(Claim claim, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) - { - //remove old claim - this.deleteClaim(claim); - - //try to create this new claim, ignoring the original when checking for overlap - CreateClaimResult result = this.createClaim(claim.getLesserBoundaryCorner().getWorld(), newx1, newx2, newy1, newy2, newz1, newz2, claim.ownerName, claim.parent, claim.id); - - //if succeeded - if(result.succeeded) - { - //copy permissions from old claim - ArrayList builders = new ArrayList(); - ArrayList containers = new ArrayList(); - ArrayList accessors = new ArrayList(); - ArrayList managers = new ArrayList(); - claim.getPermissions(builders, containers, accessors, managers); - - for(int i = 0; i < builders.size(); i++) - result.claim.setPermission(builders.get(i), ClaimPermission.Build); - - for(int i = 0; i < containers.size(); i++) - result.claim.setPermission(containers.get(i), ClaimPermission.Inventory); - - for(int i = 0; i < accessors.size(); i++) - result.claim.setPermission(accessors.get(i), ClaimPermission.Access); - - for(int i = 0; i < managers.size(); i++) - { - result.claim.managers.add(managers.get(i)); - } - - //copy subdivisions from old claim - for(int i = 0; i < claim.children.size(); i++) - { - Claim subdivision = claim.children.get(i); - subdivision.parent = result.claim; - result.claim.children.add(subdivision); - } - - //save those changes - this.saveClaim(result.claim); - } - - else - { - //put original claim back - this.addClaim(claim); - } - - return result; - } - - private void loadMessages() - { - Messages [] messageIDs = Messages.values(); - this.messages = new String[Messages.values().length]; - - HashMap defaults = new HashMap(); - - //initialize defaults - this.addDefault(defaults, Messages.RespectingClaims, "Now respecting claims.", null); - this.addDefault(defaults, Messages.IgnoringClaims, "Now ignoring claims.", null); - this.addDefault(defaults, Messages.NoCreativeUnClaim, "You can't unclaim this land. You can only make this claim larger or create additional claims.", null); - this.addDefault(defaults, Messages.SuccessfulAbandon, "Claims abandoned. You now have {0} available claim blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.RestoreNatureActivate, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop.", null); - this.addDefault(defaults, Messages.RestoreNatureAggressiveActivate, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop.", null); - this.addDefault(defaults, Messages.FillModeActive, "Fill mode activated with radius {0}. Right click an area to fill.", "0: fill radius"); - this.addDefault(defaults, Messages.TransferClaimPermission, "That command requires the administrative claims permission.", null); - this.addDefault(defaults, Messages.TransferClaimMissing, "There's no claim here. Stand in the administrative claim you want to transfer.", null); - this.addDefault(defaults, Messages.TransferClaimAdminOnly, "Only administrative claims may be transferred to a player.", null); - this.addDefault(defaults, Messages.PlayerNotFound, "Player not found.", null); - this.addDefault(defaults, Messages.TransferTopLevel, "Only top level claims (not subdivisions) may be transferred. Stand outside of the subdivision and try again.", null); - this.addDefault(defaults, Messages.TransferSuccess, "Claim transferred.", null); - this.addDefault(defaults, Messages.TrustListNoClaim, "Stand inside the claim you're curious about.", null); - this.addDefault(defaults, Messages.ClearPermsOwnerOnly, "Only the claim owner can clear all permissions.", null); - this.addDefault(defaults, Messages.UntrustIndividualAllClaims, "Revoked {0}'s access to ALL your claims. To set permissions for a single claim, stand inside it.", "0: untrusted player"); - this.addDefault(defaults, Messages.UntrustEveryoneAllClaims, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it.", null); - this.addDefault(defaults, Messages.NoPermissionTrust, "You don't have {0}'s permission to manage permissions here.", "0: claim owner's name"); - this.addDefault(defaults, Messages.ClearPermissionsOneClaim, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them.", null); - this.addDefault(defaults, Messages.UntrustIndividualSingleClaim, "Revoked {0}'s access to this claim. To set permissions for a ALL your claims, stand outside them.", "0: untrusted player"); - this.addDefault(defaults, Messages.OnlySellBlocks, "Claim blocks may only be sold, not purchased.", null); - this.addDefault(defaults, Messages.BlockPurchaseCost, "Each claim block costs {0}. Your balance is {1}.", "0: cost of one block; 1: player's account balance"); - this.addDefault(defaults, Messages.ClaimBlockLimit, "You've reached your claim block limit. You can't purchase more.", null); - this.addDefault(defaults, Messages.InsufficientFunds, "You don't have enough money. You need {0}, but you only have {1}.", "0: total cost; 1: player's account balance"); - this.addDefault(defaults, Messages.PurchaseConfirmation, "Withdrew {0} from your account. You now have {1} available claim blocks.", "0: total cost; 1: remaining blocks"); - this.addDefault(defaults, Messages.OnlyPurchaseBlocks, "Claim blocks may only be purchased, not sold.", null); - this.addDefault(defaults, Messages.BlockSaleValue, "Each claim block is worth {0}. You have {1} available for sale.", "0: block value; 1: available blocks"); - this.addDefault(defaults, Messages.NotEnoughBlocksForSale, "You don't have that many claim blocks available for sale.", null); - this.addDefault(defaults, Messages.BlockSaleConfirmation, "Deposited {0} in your account. You now have {1} available claim blocks.", "0: amount deposited; 1: remaining blocks"); - this.addDefault(defaults, Messages.AdminClaimsMode, "Administrative claims mode active. Any claims created will be free and editable by other administrators.", null); - this.addDefault(defaults, Messages.BasicClaimsMode, "Returned to basic claim creation mode.", null); - this.addDefault(defaults, Messages.SubdivisionMode, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit.", null); - this.addDefault(defaults, Messages.SubdivisionDemo, "Land Claim Help: http://tinyurl.com/7urdtue", null); - this.addDefault(defaults, Messages.DeleteClaimMissing, "There's no claim here.", null); - this.addDefault(defaults, Messages.DeletionSubdivisionWarning, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again.", null); - this.addDefault(defaults, Messages.DeleteSuccess, "Claim deleted.", null); - this.addDefault(defaults, Messages.CantDeleteAdminClaim, "You don't have permission to delete administrative claims.", null); - this.addDefault(defaults, Messages.DeleteAllSuccess, "Deleted all of {0}'s claims.", "0: owner's name"); - this.addDefault(defaults, Messages.NoDeletePermission, "You don't have permission to delete claims.", null); - this.addDefault(defaults, Messages.AllAdminDeleted, "Deleted all administrative claims.", null); - this.addDefault(defaults, Messages.AdjustBlocksSuccess, "Adjusted {0}'s bonus claim blocks by {1}. New total bonus blocks: {2}.", "0: player; 1: adjustment; 2: new total"); - this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); - this.addDefault(defaults, Messages.TrappedOnCooldown, "You used /trapped within the last {0} hours. You have to wait about {1} more minutes before using it again.", "0: default cooldown hours; 1: remaining minutes"); - this.addDefault(defaults, Messages.RescuePending, "If you stay put for 10 seconds, you'll be teleported out. Please wait.", null); - this.addDefault(defaults, Messages.NonSiegeWorld, "Siege is disabled here.", null); - this.addDefault(defaults, Messages.AlreadySieging, "You're already involved in a siege.", null); - this.addDefault(defaults, Messages.AlreadyUnderSiegePlayer, "{0} is already under siege. Join the party!", "0: defending player"); - this.addDefault(defaults, Messages.NotSiegableThere, "{0} isn't protected there.", "0: defending player"); - this.addDefault(defaults, Messages.SiegeTooFarAway, "You're too far away to siege.", null); - this.addDefault(defaults, Messages.NoSiegeDefenseless, "That player is defenseless. Go pick on somebody else.", null); - this.addDefault(defaults, Messages.AlreadyUnderSiegeArea, "That area is already under siege. Join the party!", null); - this.addDefault(defaults, Messages.NoSiegeAdminClaim, "Siege is disabled in this area.", null); - this.addDefault(defaults, Messages.SiegeOnCooldown, "You're still on siege cooldown for this defender or claim. Find another victim.", null); - this.addDefault(defaults, Messages.SiegeAlert, "You're under siege! If you log out now, you will die. You must defeat {0}, wait for him to give up, or escape.", "0: attacker name"); - this.addDefault(defaults, Messages.SiegeConfirmed, "The siege has begun! If you log out now, you will die. You must defeat {0}, chase him away, or admit defeat and walk away.", "0: defender name"); - this.addDefault(defaults, Messages.AbandonClaimMissing, "Stand in the claim you want to delete, or consider /AbandonAllClaims.", null); - this.addDefault(defaults, Messages.NotYourClaim, "This isn't your claim.", null); - this.addDefault(defaults, Messages.DeleteTopLevelClaim, "To delete a subdivision, stand inside it. Otherwise, use /AbandonTopLevelClaim to delete this claim and all subdivisions.", null); - this.addDefault(defaults, Messages.AbandonSuccess, "Claim abandoned. You now have {0} available claim blocks.", "0: remaining claim blocks"); - this.addDefault(defaults, Messages.CantGrantThatPermission, "You can't grant a permission you don't have yourself.", null); - this.addDefault(defaults, Messages.GrantPermissionNoClaim, "Stand inside the claim where you want to grant permission.", null); - this.addDefault(defaults, Messages.GrantPermissionConfirmation, "Granted {0} permission to {1} {2}.", "0: target player; 1: permission description; 2: scope (changed claims)"); - this.addDefault(defaults, Messages.ManageUniversalPermissionsInstruction, "To manage permissions for ALL your claims, stand outside them.", null); - this.addDefault(defaults, Messages.ManageOneClaimPermissionsInstruction, "To manage permissions for a specific claim, stand inside it.", null); - this.addDefault(defaults, Messages.CollectivePublic, "the public", "as in 'granted the public permission to...'"); - this.addDefault(defaults, Messages.BuildPermission, "build", null); - this.addDefault(defaults, Messages.ContainersPermission, "access containers and animals", null); - this.addDefault(defaults, Messages.AccessPermission, "use buttons and levers", null); - this.addDefault(defaults, Messages.PermissionsPermission, "manage permissions", null); - this.addDefault(defaults, Messages.LocationCurrentClaim, "in this claim", null); - this.addDefault(defaults, Messages.LocationAllClaims, "in all your claims", null); - this.addDefault(defaults, Messages.PvPImmunityStart, "You're protected from attack by other players as long as your inventory is empty.", null); - this.addDefault(defaults, Messages.SiegeNoDrop, "You can't give away items while involved in a siege.", null); - this.addDefault(defaults, Messages.DonateItemsInstruction, "To give away the item(s) in your hand, left-click the chest again.", null); - this.addDefault(defaults, Messages.ChestFull, "This chest is full.", null); - this.addDefault(defaults, Messages.DonationSuccess, "Item(s) transferred to chest!", null); - this.addDefault(defaults, Messages.PlayerTooCloseForFire, "You can't start a fire this close to {0}.", "0: other player's name"); - this.addDefault(defaults, Messages.TooDeepToClaim, "This chest can't be protected because it's too deep underground. Consider moving it.", null); - this.addDefault(defaults, Messages.ChestClaimConfirmation, "This chest is protected.", null); - this.addDefault(defaults, Messages.AutomaticClaimNotification, "This chest and nearby blocks are protected from breakage and theft. The temporary gold and glowstone blocks mark the protected area. To toggle them on and off, right-click with a stick.", null); - this.addDefault(defaults, Messages.TrustCommandAdvertisement, "Use the /trust command to grant other players access.", null); - this.addDefault(defaults, Messages.GoldenShovelAdvertisement, "To claim more land, you need a golden shovel. When you equip one, you'll get more information.", null); - this.addDefault(defaults, Messages.UnprotectedChestWarning, "This chest is NOT protected. Consider expanding an existing claim or creating a new one.", null); - this.addDefault(defaults, Messages.ThatPlayerPvPImmune, "You can't injure defenseless players.", null); - this.addDefault(defaults, Messages.CantFightWhileImmune, "You can't fight someone while you're protected from PvP.", null); - this.addDefault(defaults, Messages.NoDamageClaimedEntity, "That belongs to {0}.", "0: owner name"); - this.addDefault(defaults, Messages.ShovelBasicClaimMode, "Shovel returned to basic claims mode.", null); - this.addDefault(defaults, Messages.RemainingBlocks, "You may claim up to {0} more blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.CreativeBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/c7bajb8", null); - this.addDefault(defaults, Messages.SurvivalBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/6nkwegj", null); - this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped", "When mentioned in chat, players get information about the /trapped command."); - this.addDefault(defaults, Messages.TrappedInstructions, "Are you trapped in someone's claim? Consider the /trapped command.", null); - this.addDefault(defaults, Messages.PvPNoDrop, "You can't drop items while in PvP combat.", null); - this.addDefault(defaults, Messages.SiegeNoTeleport, "You can't teleport out of a besieged area.", null); - this.addDefault(defaults, Messages.BesiegedNoTeleport, "You can't teleport into a besieged area.", null); - this.addDefault(defaults, Messages.SiegeNoContainers, "You can't access containers while involved in a siege.", null); - this.addDefault(defaults, Messages.PvPNoContainers, "You can't access containers during PvP combat.", null); - this.addDefault(defaults, Messages.PvPImmunityEnd, "Now you can fight with other players.", null); - this.addDefault(defaults, Messages.NoBedPermission, "{0} hasn't given you permission to sleep here.", "0: claim owner"); - this.addDefault(defaults, Messages.NoWildernessBuckets, "You may only dump buckets inside your claim(s) or underground.", null); - this.addDefault(defaults, Messages.NoLavaNearOtherPlayer, "You can't place lava this close to {0}.", "0: nearby player"); - this.addDefault(defaults, Messages.TooFarAway, "That's too far away.", null); - this.addDefault(defaults, Messages.BlockNotClaimed, "No one has claimed this block.", null); - this.addDefault(defaults, Messages.BlockClaimed, "That block has been claimed by {0}.", "0: claim owner"); - this.addDefault(defaults, Messages.SiegeNoShovel, "You can't use your shovel tool while involved in a siege.", null); - this.addDefault(defaults, Messages.RestoreNaturePlayerInChunk, "Unable to restore. {0} is in that chunk.", "0: nearby player"); - this.addDefault(defaults, Messages.NoCreateClaimPermission, "You don't have permission to claim land.", null); - this.addDefault(defaults, Messages.ResizeClaimTooSmall, "This new size would be too small. Claims must be at least {0} x {0}.", "0: minimum claim size"); - this.addDefault(defaults, Messages.ResizeNeedMoreBlocks, "You don't have enough blocks for this size. You need {0} more.", "0: how many needed"); - this.addDefault(defaults, Messages.ClaimResizeSuccess, "Claim resized. You now have {0} available claim blocks.", "0: remaining blocks"); - this.addDefault(defaults, Messages.ResizeFailOverlap, "Can't resize here because it would overlap another nearby claim.", null); - this.addDefault(defaults, Messages.ResizeStart, "Resizing claim. Use your shovel again at the new location for this corner.", null); - this.addDefault(defaults, Messages.ResizeFailOverlapSubdivision, "You can't create a subdivision here because it would overlap another subdivision. Consider /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); - this.addDefault(defaults, Messages.SubdivisionStart, "Subdivision corner set! Use your shovel at the location for the opposite corner of this new subdivision.", null); - this.addDefault(defaults, Messages.CreateSubdivisionOverlap, "Your selected area overlaps another subdivision.", null); - this.addDefault(defaults, Messages.SubdivisionSuccess, "Subdivision created! Use /trust to share it with friends.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlap, "You can't create a claim here because it would overlap your other claim. Use /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlapOtherPlayer, "You can't create a claim here because it would overlap {0}'s claim.", "0: other claim owner"); - this.addDefault(defaults, Messages.ClaimsDisabledWorld, "Land claims are disabled in this world.", null); - this.addDefault(defaults, Messages.ClaimStart, "Claim corner set! Use the shovel again at the opposite corner to claim a rectangle of land. To cancel, put your shovel away.", null); - this.addDefault(defaults, Messages.NewClaimTooSmall, "This claim would be too small. Any claim must be at least {0} x {0}.", "0: minimum claim size"); - this.addDefault(defaults, Messages.CreateClaimInsufficientBlocks, "You don't have enough blocks to claim that entire area. You need {0} more blocks.", "0: additional blocks needed"); - this.addDefault(defaults, Messages.AbandonClaimAdvertisement, "To delete another claim and free up some blocks, use /AbandonClaim.", null); - this.addDefault(defaults, Messages.CreateClaimFailOverlapShort, "Your selected area overlaps an existing claim.", null); - this.addDefault(defaults, Messages.CreateClaimSuccess, "Claim created! Use /trust to share it with friends.", null); - this.addDefault(defaults, Messages.SiegeWinDoorsOpen, "Congratulations! Buttons and levers are temporarily unlocked (five minutes).", null); - this.addDefault(defaults, Messages.RescueAbortedMoved, "You moved! Rescue cancelled.", null); - this.addDefault(defaults, Messages.SiegeDoorsLockedEjection, "Looting time is up! Ejected from the claim.", null); - this.addDefault(defaults, Messages.NoModifyDuringSiege, "Claims can't be modified while under siege.", null); - this.addDefault(defaults, Messages.OnlyOwnersModifyClaims, "Only {0} can modify this claim.", "0: owner name"); - this.addDefault(defaults, Messages.NoBuildUnderSiege, "This claim is under siege by {0}. No one can build here.", "0: attacker name"); - this.addDefault(defaults, Messages.NoBuildPvP, "You can't build in claims during PvP combat.", null); - this.addDefault(defaults, Messages.NoBuildPermission, "You don't have {0}'s permission to build here.", "0: owner name"); - this.addDefault(defaults, Messages.NonSiegeMaterial, "That material is too tough to break.", null); - this.addDefault(defaults, Messages.NoOwnerBuildUnderSiege, "You can't make changes while under siege.", null); - this.addDefault(defaults, Messages.NoAccessPermission, "You don't have {0}'s permission to use that.", "0: owner name. access permission controls buttons, levers, and beds"); - this.addDefault(defaults, Messages.NoContainersSiege, "This claim is under siege by {0}. No one can access containers here right now.", "0: attacker name"); - this.addDefault(defaults, Messages.NoContainersPermission, "You don't have {0}'s permission to use that.", "0: owner's name. containers also include crafting blocks"); - this.addDefault(defaults, Messages.OwnerNameForAdminClaims, "an administrator", "as in 'You don't have an administrator's permission to build here.'"); - this.addDefault(defaults, Messages.ClaimTooSmallForEntities, "This claim isn't big enough for that. Try enlarging it.", null); - this.addDefault(defaults, Messages.TooManyEntitiesInClaim, "This claim has too many entities already. Try enlarging the claim or removing some animals, monsters, paintings, or minecarts.", null); - this.addDefault(defaults, Messages.YouHaveNoClaims, "You don't have any land claims.", null); - this.addDefault(defaults, Messages.ConfirmFluidRemoval, "Abandoning this claim will remove all your lava and water. If you're sure, use /AbandonClaim again.", null); - this.addDefault(defaults, Messages.AutoBanNotify, "Auto-banned {0}({1}). See logs for details.", null); - this.addDefault(defaults, Messages.AdjustGroupBlocksSuccess, "Adjusted bonus claim blocks for players with the {0} permission by {1}. New total: {2}.", "0: permission; 1: adjustment amount; 2: new total bonus"); - this.addDefault(defaults, Messages.InvalidPermissionID, "Please specify a player name, or a permission in [brackets].", null); - this.addDefault(defaults, Messages.UntrustOwnerOnly, "Only {0} can revoke permissions here.", "0: claim owner's name"); - this.addDefault(defaults, Messages.HowToClaimRegex, "(^|.*\\W)how\\W.*\\W(claim|protect|lock)(\\W.*|$)", "This is a Java Regular Expression. Look it up before editing! It's used to tell players about the demo video when they ask how to claim land."); - this.addDefault(defaults, Messages.NoBuildOutsideClaims, "You can't build here unless you claim some land first.", null); - this.addDefault(defaults, Messages.PlayerOfflineTime, " Last login: {0} days ago.", "0: number of full days since last login"); - this.addDefault(defaults, Messages.BuildingOutsideClaims, "Other players can undo your work here! Consider claiming this area to protect your work.", null); - this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin, or consider /kill if you don't want to wait.", null); - this.addDefault(defaults, Messages.CommandBannedInPvP, "You can't use that command while in PvP combat.", null); - this.addDefault(defaults, Messages.UnclaimCleanupWarning, "The land you've unclaimed may be changed by other players or cleaned up by administrators. If you've built something there you want to keep, you should reclaim it.", null); - this.addDefault(defaults, Messages.BuySellNotConfigured, "Sorry, buying anhd selling claim blocks is disabled.", null); - this.addDefault(defaults, Messages.NoTeleportPvPCombat, "You can't teleport while fighting another player.", null); - this.addDefault(defaults, Messages.NoTNTDamageAboveSeaLevel, "Warning: TNT will not destroy blocks above sea level.", null); - this.addDefault(defaults, Messages.NoTNTDamageClaims, "Warning: TNT will not destroy claimed blocks.", null); - - //load the config file - FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); - - //for each message ID - for(int i = 0; i < messageIDs.length; i++) - { - //get default for this message - Messages messageID = messageIDs[i]; - CustomizableMessage messageData = defaults.get(messageID.name()); - - //if default is missing, log an error and use some fake data for now so that the plugin can run - if(messageData == null) - { - GriefPrevention.AddLogEntry("Missing message for " + messageID.name() + ". Please contact the developer."); - messageData = new CustomizableMessage(messageID, "Missing message! ID: " + messageID.name() + ". Please contact a server admin.", null); - } - - //read the message from the file, use default if necessary - this.messages[messageID.ordinal()] = config.getString("Messages." + messageID.name() + ".Text", messageData.text); - config.set("Messages." + messageID.name() + ".Text", this.messages[messageID.ordinal()]); - - if(messageData.notes != null) - { - messageData.notes = config.getString("Messages." + messageID.name() + ".Notes", messageData.notes); - config.set("Messages." + messageID.name() + ".Notes", messageData.notes); - } - } - - //save any changes - try - { - config.save(DataStore.messagesFilePath); - } - catch(IOException exception) - { - GriefPrevention.AddLogEntry("Unable to write to the configuration file at \"" + DataStore.messagesFilePath + "\""); - } - - defaults.clear(); - System.gc(); - } - - private void addDefault(HashMap defaults, - Messages id, String text, String notes) - { - CustomizableMessage message = new CustomizableMessage(id, text, notes); - defaults.put(id.name(), message); - } - - synchronized public String getMessage(Messages messageID, String... args) - { - String message = messages[messageID.ordinal()]; - - for(int i = 0; i < args.length; i++) - { - String param = args[i]; - message = message.replace("{" + i + "}", param); - } - - return message; - } - - abstract void close(); -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.io.*; +import java.util.*; + +import org.bukkit.*; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +//singleton class which manages all GriefPrevention data (except for config options) +public abstract class DataStore +{ + //in-memory cache for player data + protected HashMap playerNameToPlayerDataMap = new HashMap(); + + //in-memory cache for group (permission-based) data + protected HashMap permissionToBonusBlocksMap = new HashMap(); + + //in-memory cache for claim data + ArrayList claims = new ArrayList(); + + //in-memory cache for messages + private String [] messages; + + //next claim ID + Long nextClaimID = (long)0; + + //path information, for where stuff stored on disk is well... stored + protected final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; + final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; + final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; + + //initialization! + void initialize() throws Exception + { + GriefPrevention.AddLogEntry(this.claims.size() + " total claims loaded."); + + //make a list of players who own claims + Vector playerNames = new Vector(); + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + + //ignore admin claims + if(claim.isAdminClaim()) continue; + + if(!playerNames.contains(claim.ownerName)) + playerNames.add(claim.ownerName); + } + + GriefPrevention.AddLogEntry(playerNames.size() + " players have staked claims."); + + //load up all the messages from messages.yml + this.loadMessages(); + + //collect garbage, since lots of stuff was loaded into memory and then tossed out + System.gc(); + } + + //removes cached player data from memory + synchronized void clearCachedPlayerData(String playerName) + { + this.playerNameToPlayerDataMap.remove(playerName); + } + + //gets the number of bonus blocks a player has from his permissions + synchronized int getGroupBonusBlocks(String playerName) + { + int bonusBlocks = 0; + Set keys = permissionToBonusBlocksMap.keySet(); + Iterator iterator = keys.iterator(); + while(iterator.hasNext()) + { + String groupName = iterator.next(); + Player player = GriefPrevention.instance.getServer().getPlayer(playerName); + if(player.hasPermission(groupName)) + { + bonusBlocks += this.permissionToBonusBlocksMap.get(groupName); + } + } + + return bonusBlocks; + } + + //grants a group (players with a specific permission) bonus claim blocks as long as they're still members of the group + synchronized public int adjustGroupBonusBlocks(String groupName, int amount) + { + Integer currentValue = this.permissionToBonusBlocksMap.get(groupName); + if(currentValue == null) currentValue = 0; + + currentValue += amount; + this.permissionToBonusBlocksMap.put(groupName, currentValue); + + //write changes to storage to ensure they don't get lost + this.saveGroupBonusBlocks(groupName, currentValue); + + return currentValue; + } + + abstract void saveGroupBonusBlocks(String groupName, int amount); + + synchronized public void changeClaimOwner(Claim claim, String newOwnerName) throws Exception + { + //if it's a subdivision, throw an exception + if(claim.parent != null) + { + throw new Exception("Subdivisions can't be transferred. Only top-level claims may change owners."); + } + + //otherwise update information + + //determine current claim owner + PlayerData ownerData = null; + if(!claim.isAdminClaim()) + { + ownerData = this.getPlayerData(claim.ownerName); + } + + //determine new owner + PlayerData newOwnerData = this.getPlayerData(newOwnerName); + + //transfer + claim.ownerName = newOwnerName; + this.saveClaim(claim); + + //adjust blocks and other records + if(ownerData != null) + { + ownerData.claims.remove(claim); + ownerData.bonusClaimBlocks -= claim.getArea(); + this.savePlayerData(claim.ownerName, ownerData); + } + + newOwnerData.claims.add(claim); + newOwnerData.bonusClaimBlocks += claim.getArea(); + this.savePlayerData(newOwnerName, newOwnerData); + } + + //adds a claim to the datastore, making it an effective claim + synchronized void addClaim(Claim newClaim) + { + //subdivisions are easy + if(newClaim.parent != null) + { + newClaim.parent.children.add(newClaim); + newClaim.inDataStore = true; + this.saveClaim(newClaim); + return; + } + + //add it and mark it as added + int j = 0; + while(j < this.claims.size() && !this.claims.get(j).greaterThan(newClaim)) j++; + if(j < this.claims.size()) + this.claims.add(j, newClaim); + else + this.claims.add(this.claims.size(), newClaim); + newClaim.inDataStore = true; + + //except for administrative claims (which have no owner), update the owner's playerData with the new claim + if(!newClaim.isAdminClaim()) + { + PlayerData ownerData = this.getPlayerData(newClaim.getOwnerName()); + ownerData.claims.add(newClaim); + this.savePlayerData(newClaim.getOwnerName(), ownerData); + } + + //make sure the claim is saved to disk + this.saveClaim(newClaim); + } + + //turns a location into a string, useful in data storage + private String locationStringDelimiter = ";"; + String locationToString(Location location) + { + StringBuilder stringBuilder = new StringBuilder(location.getWorld().getName()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockX()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockY()); + stringBuilder.append(locationStringDelimiter); + stringBuilder.append(location.getBlockZ()); + + return stringBuilder.toString(); + } + + //turns a location string back into a location + Location locationFromString(String string) throws Exception + { + //split the input string on the space + String [] elements = string.split(locationStringDelimiter); + + //expect four elements - world name, X, Y, and Z, respectively + if(elements.length != 4) + { + throw new Exception("Expected four distinct parts to the location string."); + } + + String worldName = elements[0]; + String xString = elements[1]; + String yString = elements[2]; + String zString = elements[3]; + + //identify world the claim is in + World world = GriefPrevention.instance.getServer().getWorld(worldName); + if(world == null) + { + throw new Exception("World not found: \"" + worldName + "\""); + } + + //convert those numerical strings to integer values + int x = Integer.parseInt(xString); + int y = Integer.parseInt(yString); + int z = Integer.parseInt(zString); + + return new Location(world, x, y, z); + } + + //saves any changes to a claim to secondary storage + synchronized public void saveClaim(Claim claim) + { + //subdivisions don't save to their own files, but instead live in their parent claim's file + //so any attempt to save a subdivision will save its parent (and thus the subdivision) + if(claim.parent != null) + { + this.saveClaim(claim.parent); + return; + } + + //otherwise get a unique identifier for the claim which will be used to name the file on disk + if(claim.id == null) + { + claim.id = this.nextClaimID; + this.incrementNextClaimID(); + } + + this.writeClaimToStorage(claim); + } + + abstract void writeClaimToStorage(Claim claim); + + //increments the claim ID and updates secondary storage to be sure it's saved + abstract void incrementNextClaimID(); + + //retrieves player data from memory or secondary storage, as necessary + //if the player has never been on the server before, this will return a fresh player data with default values + synchronized public PlayerData getPlayerData(String playerName) + { + //first, look in memory + PlayerData playerData = this.playerNameToPlayerDataMap.get(playerName); + + //if not there, look in secondary storage + if(playerData == null) + { + playerData = this.getPlayerDataFromStorage(playerName); + playerData.playerName = playerName; + + //find all the claims belonging to this player and note them for future reference + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + if(claim.ownerName.equals(playerName)) + { + playerData.claims.add(claim); + } + } + + //shove that new player data into the hash map cache + this.playerNameToPlayerDataMap.put(playerName, playerData); + } + + //try the hash map again. if it's STILL not there, we have a bug to fix + return this.playerNameToPlayerDataMap.get(playerName); + } + + abstract PlayerData getPlayerDataFromStorage(String playerName); + + //deletes a claim or subdivision + synchronized public void deleteClaim(Claim claim) + { + //subdivisions are simple - just remove them from their parent claim and save that claim + if(claim.parent != null) + { + Claim parentClaim = claim.parent; + parentClaim.children.remove(claim); + this.saveClaim(parentClaim); + return; + } + + //remove from memory + for(int i = 0; i < this.claims.size(); i++) + { + if(claims.get(i).id.equals(claim.id)) + { + this.claims.remove(i); + claim.inDataStore = false; + for(int j = 0; j < claim.children.size(); j++) + { + claim.children.get(j).inDataStore = false; + } + break; + } + } + + //remove from secondary storage + this.deleteClaimFromSecondaryStorage(claim); + + //update player data, except for administrative claims, which have no owner + if(!claim.isAdminClaim()) + { + PlayerData ownerData = this.getPlayerData(claim.getOwnerName()); + for(int i = 0; i < ownerData.claims.size(); i++) + { + if(ownerData.claims.get(i).id.equals(claim.id)) + { + ownerData.claims.remove(i); + break; + } + } + this.savePlayerData(claim.getOwnerName(), ownerData); + } + } + + abstract void deleteClaimFromSecondaryStorage(Claim claim); + + //gets the claim at a specific location + //ignoreHeight = TRUE means that a location UNDER an existing claim will return the claim + //cachedClaim can be NULL, but will help performance if you have a reasonable guess about which claim the location is in + synchronized public Claim getClaimAt(Location location, boolean ignoreHeight, Claim cachedClaim) + { + //check cachedClaim guess first. if it's in the datastore and the location is inside it, we're done + if(cachedClaim != null && cachedClaim.inDataStore && cachedClaim.contains(location, ignoreHeight, true)) return cachedClaim; + + //the claims list is ordered by greater boundary corner + //create a temporary "fake" claim in memory for comparison purposes + Claim tempClaim = new Claim(); + tempClaim.lesserBoundaryCorner = location; + + //otherwise, search all existing claims until we find the right claim + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + + //if we reach a claim which is greater than the temp claim created above, there's definitely no claim + //in the collection which includes our location + if(claim.greaterThan(tempClaim)) return null; + + //find a top level claim + if(claim.contains(location, ignoreHeight, false)) + { + //when we find a top level claim, if the location is in one of its subdivisions, + //return the SUBDIVISION, not the top level claim + for(int j = 0; j < claim.children.size(); j++) + { + Claim subdivision = claim.children.get(j); + if(subdivision.contains(location, ignoreHeight, false)) return subdivision; + } + + return claim; + } + } + + //if no claim found, return null + return null; + } + + //creates a claim. + //if the new claim would overlap an existing claim, returns a failure along with a reference to the existing claim + //otherwise, returns a success along with a reference to the new claim + //use ownerName == "" for administrative claims + //for top level claims, pass parent == NULL + //DOES adjust claim blocks available on success (players can go into negative quantity available) + //does NOT check a player has permission to create a claim, or enough claim blocks. + //does NOT check minimum claim size constraints + //does NOT visualize the new claim for any players + synchronized public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, String ownerName, Claim parent, Long id) + { + CreateClaimResult result = new CreateClaimResult(); + + int smallx, bigx, smally, bigy, smallz, bigz; + + //determine small versus big inputs + if(x1 < x2) + { + smallx = x1; + bigx = x2; + } + else + { + smallx = x2; + bigx = x1; + } + + if(y1 < y2) + { + smally = y1; + bigy = y2; + } + else + { + smally = y2; + bigy = y1; + } + + if(z1 < z2) + { + smallz = z1; + bigz = z2; + } + else + { + smallz = z2; + bigz = z1; + } + + //creative mode claims always go to bedrock + if(GriefPrevention.instance.config_claims_enabledCreativeWorlds.contains(world)) + { + smally = 2; + } + + //create a new claim instance (but don't save it, yet) + Claim newClaim = new Claim( + new Location(world, smallx, smally, smallz), + new Location(world, bigx, bigy, bigz), + ownerName, + new String [] {}, + new String [] {}, + new String [] {}, + new String [] {}, + id); + + newClaim.parent = parent; + + //ensure this new claim won't overlap any existing claims + ArrayList claimsToCheck; + if(newClaim.parent != null) + { + claimsToCheck = newClaim.parent.children; + } + else + { + claimsToCheck = this.claims; + } + + for(int i = 0; i < claimsToCheck.size(); i++) + { + Claim otherClaim = claimsToCheck.get(i); + + //if we find an existing claim which will be overlapped + if(otherClaim.overlaps(newClaim)) + { + //result = fail, return conflicting claim + result.succeeded = false; + result.claim = otherClaim; + return result; + } + } + + //otherwise add this new claim to the data store to make it effective + this.addClaim(newClaim); + + //then return success along with reference to new claim + result.succeeded = true; + result.claim = newClaim; + return result; + } + + //saves changes to player data to secondary storage. MUST be called after you're done making changes, otherwise a reload will lose them + public abstract void savePlayerData(String playerName, PlayerData playerData); + + //extends a claim to a new depth + //respects the max depth config variable + synchronized public void extendClaim(Claim claim, int newDepth) + { + if(newDepth < GriefPrevention.instance.config_claims_maxDepth) newDepth = GriefPrevention.instance.config_claims_maxDepth; + + if(claim.parent != null) claim = claim.parent; + + //delete the claim + this.deleteClaim(claim); + + //re-create it at the new depth + claim.lesserBoundaryCorner.setY(newDepth); + claim.greaterBoundaryCorner.setY(newDepth); + + //make all subdivisions reach to the same depth + for(int i = 0; i < claim.children.size(); i++) + { + claim.children.get(i).lesserBoundaryCorner.setY(newDepth); + claim.children.get(i).greaterBoundaryCorner.setY(newDepth); + } + + //save changes + this.addClaim(claim); + } + + //starts a siege on a claim + //does NOT check siege cooldowns, see onCooldown() below + synchronized public void startSiege(Player attacker, Player defender, Claim defenderClaim) + { + //fill-in the necessary SiegeData instance + SiegeData siegeData = new SiegeData(attacker, defender, defenderClaim); + PlayerData attackerData = this.getPlayerData(attacker.getName()); + PlayerData defenderData = this.getPlayerData(defender.getName()); + attackerData.siegeData = siegeData; + defenderData.siegeData = siegeData; + defenderClaim.siegeData = siegeData; + + //start a task to monitor the siege + //why isn't this a "repeating" task? + //because depending on the status of the siege at the time the task runs, there may or may not be a reason to run the task again + SiegeCheckupTask task = new SiegeCheckupTask(siegeData); + siegeData.checkupTaskID = GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 30); + } + + //ends a siege + //either winnerName or loserName can be null, but not both + synchronized public void endSiege(SiegeData siegeData, String winnerName, String loserName, boolean death) + { + boolean grantAccess = false; + + //determine winner and loser + if(winnerName == null && loserName != null) + { + if(siegeData.attacker.getName().equals(loserName)) + { + winnerName = siegeData.defender.getName(); + } + else + { + winnerName = siegeData.attacker.getName(); + } + } + else if(winnerName != null && loserName == null) + { + if(siegeData.attacker.getName().equals(winnerName)) + { + loserName = siegeData.defender.getName(); + } + else + { + loserName = siegeData.attacker.getName(); + } + } + + //if the attacker won, plan to open the doors for looting + if(siegeData.attacker.getName().equals(winnerName)) + { + grantAccess = true; + } + + PlayerData attackerData = this.getPlayerData(siegeData.attacker.getName()); + attackerData.siegeData = null; + + PlayerData defenderData = this.getPlayerData(siegeData.defender.getName()); + defenderData.siegeData = null; + + //start a cooldown for this attacker/defender pair + Long now = Calendar.getInstance().getTimeInMillis(); + Long cooldownEnd = now + 1000 * 60 * 60; //one hour from now + this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + siegeData.defender.getName(), cooldownEnd); + + //start cooldowns for every attacker/involved claim pair + for(int i = 0; i < siegeData.claims.size(); i++) + { + Claim claim = siegeData.claims.get(i); + claim.siegeData = null; + this.siegeCooldownRemaining.put(siegeData.attacker.getName() + "_" + claim.ownerName, cooldownEnd); + + //if doors should be opened for looting, do that now + if(grantAccess) + { + claim.doorsOpen = true; + } + } + + //cancel the siege checkup task + GriefPrevention.instance.getServer().getScheduler().cancelTask(siegeData.checkupTaskID); + + //notify everyone who won and lost + if(winnerName != null && loserName != null) + { + GriefPrevention.instance.getServer().broadcastMessage(winnerName + " defeated " + loserName + " in siege warfare!"); + } + + //if the claim should be opened to looting + if(grantAccess) + { + Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); + if(winner != null) + { + //notify the winner + GriefPrevention.sendMessage(winner, TextMode.Success, Messages.SiegeWinDoorsOpen); + + //schedule a task to secure the claims in about 5 minutes + SecureClaimTask task = new SecureClaimTask(siegeData); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 5); + } + } + + //if the siege ended due to death, transfer inventory to winner + if(death) + { + Player winner = GriefPrevention.instance.getServer().getPlayer(winnerName); + Player loser = GriefPrevention.instance.getServer().getPlayer(loserName); + if(winner != null && loser != null) + { + //get loser's inventory, then clear it + ItemStack [] loserItems = loser.getInventory().getContents(); + loser.getInventory().clear(); + + //try to add it to the winner's inventory + for(int j = 0; j < loserItems.length; j++) + { + if(loserItems[j] == null || loserItems[j].getType() == Material.AIR || loserItems[j].getAmount() == 0) continue; + + HashMap wontFitItems = winner.getInventory().addItem(loserItems[j]); + + //drop any remainder on the ground at his feet + Object [] keys = wontFitItems.keySet().toArray(); + Location winnerLocation = winner.getLocation(); + for(int i = 0; i < keys.length; i++) + { + Integer key = (Integer)keys[i]; + winnerLocation.getWorld().dropItemNaturally(winnerLocation, wontFitItems.get(key)); + } + } + } + } + } + + //timestamp for each siege cooldown to end + private HashMap siegeCooldownRemaining = new HashMap(); + + //whether or not a sieger can siege a particular victim or claim, considering only cooldowns + synchronized public boolean onCooldown(Player attacker, Player defender, Claim defenderClaim) + { + Long cooldownEnd = null; + + //look for an attacker/defender cooldown + if(this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()) != null) + { + cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defender.getName()); + + if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) + { + return true; + } + + //if found but expired, remove it + this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defender.getName()); + } + + //look for an attacker/claim cooldown + if(cooldownEnd == null && this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName) != null) + { + cooldownEnd = this.siegeCooldownRemaining.get(attacker.getName() + "_" + defenderClaim.ownerName); + + if(Calendar.getInstance().getTimeInMillis() < cooldownEnd) + { + return true; + } + + //if found but expired, remove it + this.siegeCooldownRemaining.remove(attacker.getName() + "_" + defenderClaim.ownerName); + } + + return false; + } + + //extend a siege, if it's possible to do so + synchronized void tryExtendSiege(Player player, Claim claim) + { + PlayerData playerData = this.getPlayerData(player.getName()); + + //player must be sieged + if(playerData.siegeData == null) return; + + //claim isn't already under the same siege + if(playerData.siegeData.claims.contains(claim)) return; + + //admin claims can't be sieged + if(claim.isAdminClaim()) return; + + //player must have some level of permission to be sieged in a claim + if(claim.allowAccess(player) != null) return; + + //otherwise extend the siege + playerData.siegeData.claims.add(claim); + claim.siegeData = playerData.siegeData; + } + + //deletes all claims owned by a player + synchronized public void deleteClaimsForPlayer(String playerName, boolean deleteCreativeClaims) + { + //make a list of the player's claims + ArrayList claimsToDelete = new ArrayList(); + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + if(claim.ownerName.equals(playerName) && (deleteCreativeClaims || !GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))) + claimsToDelete.add(claim); + } + + //delete them one by one + for(int i = 0; i < claimsToDelete.size(); i++) + { + Claim claim = claimsToDelete.get(i); + claim.removeSurfaceFluids(null); + + this.deleteClaim(claim); + + //if in a creative mode world, delete the claim + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + GriefPrevention.instance.restoreClaim(claim, 0); + } + } + } + + //tries to resize a claim + //see CreateClaim() for details on return value + synchronized public CreateClaimResult resizeClaim(Claim claim, int newx1, int newx2, int newy1, int newy2, int newz1, int newz2) + { + //remove old claim + this.deleteClaim(claim); + + //try to create this new claim, ignoring the original when checking for overlap + CreateClaimResult result = this.createClaim(claim.getLesserBoundaryCorner().getWorld(), newx1, newx2, newy1, newy2, newz1, newz2, claim.ownerName, claim.parent, claim.id); + + //if succeeded + if(result.succeeded) + { + //copy permissions from old claim + ArrayList builders = new ArrayList(); + ArrayList containers = new ArrayList(); + ArrayList accessors = new ArrayList(); + ArrayList managers = new ArrayList(); + claim.getPermissions(builders, containers, accessors, managers); + + for(int i = 0; i < builders.size(); i++) + result.claim.setPermission(builders.get(i), ClaimPermission.Build); + + for(int i = 0; i < containers.size(); i++) + result.claim.setPermission(containers.get(i), ClaimPermission.Inventory); + + for(int i = 0; i < accessors.size(); i++) + result.claim.setPermission(accessors.get(i), ClaimPermission.Access); + + for(int i = 0; i < managers.size(); i++) + { + result.claim.managers.add(managers.get(i)); + } + + //copy subdivisions from old claim + for(int i = 0; i < claim.children.size(); i++) + { + Claim subdivision = claim.children.get(i); + subdivision.parent = result.claim; + result.claim.children.add(subdivision); + } + + //save those changes + this.saveClaim(result.claim); + } + + else + { + //put original claim back + this.addClaim(claim); + } + + return result; + } + + private void loadMessages() + { + Messages [] messageIDs = Messages.values(); + this.messages = new String[Messages.values().length]; + + HashMap defaults = new HashMap(); + + //initialize defaults + this.addDefault(defaults, Messages.RespectingClaims, "Now respecting claims.", null); + this.addDefault(defaults, Messages.IgnoringClaims, "Now ignoring claims.", null); + this.addDefault(defaults, Messages.NoCreativeUnClaim, "You can't unclaim this land. You can only make this claim larger or create additional claims.", null); + this.addDefault(defaults, Messages.SuccessfulAbandon, "Claims abandoned. You now have {0} available claim blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.RestoreNatureActivate, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop.", null); + this.addDefault(defaults, Messages.RestoreNatureAggressiveActivate, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop.", null); + this.addDefault(defaults, Messages.FillModeActive, "Fill mode activated with radius {0}. Right click an area to fill.", "0: fill radius"); + this.addDefault(defaults, Messages.TransferClaimPermission, "That command requires the administrative claims permission.", null); + this.addDefault(defaults, Messages.TransferClaimMissing, "There's no claim here. Stand in the administrative claim you want to transfer.", null); + this.addDefault(defaults, Messages.TransferClaimAdminOnly, "Only administrative claims may be transferred to a player.", null); + this.addDefault(defaults, Messages.PlayerNotFound, "Player not found.", null); + this.addDefault(defaults, Messages.TransferTopLevel, "Only top level claims (not subdivisions) may be transferred. Stand outside of the subdivision and try again.", null); + this.addDefault(defaults, Messages.TransferSuccess, "Claim transferred.", null); + this.addDefault(defaults, Messages.TrustListNoClaim, "Stand inside the claim you're curious about.", null); + this.addDefault(defaults, Messages.ClearPermsOwnerOnly, "Only the claim owner can clear all permissions.", null); + this.addDefault(defaults, Messages.UntrustIndividualAllClaims, "Revoked {0}'s access to ALL your claims. To set permissions for a single claim, stand inside it.", "0: untrusted player"); + this.addDefault(defaults, Messages.UntrustEveryoneAllClaims, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it.", null); + this.addDefault(defaults, Messages.NoPermissionTrust, "You don't have {0}'s permission to manage permissions here.", "0: claim owner's name"); + this.addDefault(defaults, Messages.ClearPermissionsOneClaim, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them.", null); + this.addDefault(defaults, Messages.UntrustIndividualSingleClaim, "Revoked {0}'s access to this claim. To set permissions for a ALL your claims, stand outside them.", "0: untrusted player"); + this.addDefault(defaults, Messages.OnlySellBlocks, "Claim blocks may only be sold, not purchased.", null); + this.addDefault(defaults, Messages.BlockPurchaseCost, "Each claim block costs {0}. Your balance is {1}.", "0: cost of one block; 1: player's account balance"); + this.addDefault(defaults, Messages.ClaimBlockLimit, "You've reached your claim block limit. You can't purchase more.", null); + this.addDefault(defaults, Messages.InsufficientFunds, "You don't have enough money. You need {0}, but you only have {1}.", "0: total cost; 1: player's account balance"); + this.addDefault(defaults, Messages.PurchaseConfirmation, "Withdrew {0} from your account. You now have {1} available claim blocks.", "0: total cost; 1: remaining blocks"); + this.addDefault(defaults, Messages.OnlyPurchaseBlocks, "Claim blocks may only be purchased, not sold.", null); + this.addDefault(defaults, Messages.BlockSaleValue, "Each claim block is worth {0}. You have {1} available for sale.", "0: block value; 1: available blocks"); + this.addDefault(defaults, Messages.NotEnoughBlocksForSale, "You don't have that many claim blocks available for sale.", null); + this.addDefault(defaults, Messages.BlockSaleConfirmation, "Deposited {0} in your account. You now have {1} available claim blocks.", "0: amount deposited; 1: remaining blocks"); + this.addDefault(defaults, Messages.AdminClaimsMode, "Administrative claims mode active. Any claims created will be free and editable by other administrators.", null); + this.addDefault(defaults, Messages.BasicClaimsMode, "Returned to basic claim creation mode.", null); + this.addDefault(defaults, Messages.SubdivisionMode, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit.", null); + this.addDefault(defaults, Messages.SubdivisionDemo, "Land Claim Help: http://tinyurl.com/7urdtue", null); + this.addDefault(defaults, Messages.DeleteClaimMissing, "There's no claim here.", null); + this.addDefault(defaults, Messages.DeletionSubdivisionWarning, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again.", null); + this.addDefault(defaults, Messages.DeleteSuccess, "Claim deleted.", null); + this.addDefault(defaults, Messages.CantDeleteAdminClaim, "You don't have permission to delete administrative claims.", null); + this.addDefault(defaults, Messages.DeleteAllSuccess, "Deleted all of {0}'s claims.", "0: owner's name"); + this.addDefault(defaults, Messages.NoDeletePermission, "You don't have permission to delete claims.", null); + this.addDefault(defaults, Messages.AllAdminDeleted, "Deleted all administrative claims.", null); + this.addDefault(defaults, Messages.AdjustBlocksSuccess, "Adjusted {0}'s bonus claim blocks by {1}. New total bonus blocks: {2}.", "0: player; 1: adjustment; 2: new total"); + this.addDefault(defaults, Messages.NotTrappedHere, "You can build here. Save yourself.", null); + this.addDefault(defaults, Messages.TrappedOnCooldown, "You used /trapped within the last {0} hours. You have to wait about {1} more minutes before using it again.", "0: default cooldown hours; 1: remaining minutes"); + this.addDefault(defaults, Messages.RescuePending, "If you stay put for 10 seconds, you'll be teleported out. Please wait.", null); + this.addDefault(defaults, Messages.NonSiegeWorld, "Siege is disabled here.", null); + this.addDefault(defaults, Messages.AlreadySieging, "You're already involved in a siege.", null); + this.addDefault(defaults, Messages.AlreadyUnderSiegePlayer, "{0} is already under siege. Join the party!", "0: defending player"); + this.addDefault(defaults, Messages.NotSiegableThere, "{0} isn't protected there.", "0: defending player"); + this.addDefault(defaults, Messages.SiegeTooFarAway, "You're too far away to siege.", null); + this.addDefault(defaults, Messages.NoSiegeDefenseless, "That player is defenseless. Go pick on somebody else.", null); + this.addDefault(defaults, Messages.AlreadyUnderSiegeArea, "That area is already under siege. Join the party!", null); + this.addDefault(defaults, Messages.NoSiegeAdminClaim, "Siege is disabled in this area.", null); + this.addDefault(defaults, Messages.SiegeOnCooldown, "You're still on siege cooldown for this defender or claim. Find another victim.", null); + this.addDefault(defaults, Messages.SiegeAlert, "You're under siege! If you log out now, you will die. You must defeat {0}, wait for him to give up, or escape.", "0: attacker name"); + this.addDefault(defaults, Messages.SiegeConfirmed, "The siege has begun! If you log out now, you will die. You must defeat {0}, chase him away, or admit defeat and walk away.", "0: defender name"); + this.addDefault(defaults, Messages.AbandonClaimMissing, "Stand in the claim you want to delete, or consider /AbandonAllClaims.", null); + this.addDefault(defaults, Messages.NotYourClaim, "This isn't your claim.", null); + this.addDefault(defaults, Messages.DeleteTopLevelClaim, "To delete a subdivision, stand inside it. Otherwise, use /AbandonTopLevelClaim to delete this claim and all subdivisions.", null); + this.addDefault(defaults, Messages.AbandonSuccess, "Claim abandoned. You now have {0} available claim blocks.", "0: remaining claim blocks"); + this.addDefault(defaults, Messages.CantGrantThatPermission, "You can't grant a permission you don't have yourself.", null); + this.addDefault(defaults, Messages.GrantPermissionNoClaim, "Stand inside the claim where you want to grant permission.", null); + this.addDefault(defaults, Messages.GrantPermissionConfirmation, "Granted {0} permission to {1} {2}.", "0: target player; 1: permission description; 2: scope (changed claims)"); + this.addDefault(defaults, Messages.ManageUniversalPermissionsInstruction, "To manage permissions for ALL your claims, stand outside them.", null); + this.addDefault(defaults, Messages.ManageOneClaimPermissionsInstruction, "To manage permissions for a specific claim, stand inside it.", null); + this.addDefault(defaults, Messages.CollectivePublic, "the public", "as in 'granted the public permission to...'"); + this.addDefault(defaults, Messages.BuildPermission, "build", null); + this.addDefault(defaults, Messages.ContainersPermission, "access containers and animals", null); + this.addDefault(defaults, Messages.AccessPermission, "use buttons and levers", null); + this.addDefault(defaults, Messages.PermissionsPermission, "manage permissions", null); + this.addDefault(defaults, Messages.LocationCurrentClaim, "in this claim", null); + this.addDefault(defaults, Messages.LocationAllClaims, "in all your claims", null); + this.addDefault(defaults, Messages.PvPImmunityStart, "You're protected from attack by other players as long as your inventory is empty.", null); + this.addDefault(defaults, Messages.SiegeNoDrop, "You can't give away items while involved in a siege.", null); + this.addDefault(defaults, Messages.DonateItemsInstruction, "To give away the item(s) in your hand, left-click the chest again.", null); + this.addDefault(defaults, Messages.ChestFull, "This chest is full.", null); + this.addDefault(defaults, Messages.DonationSuccess, "Item(s) transferred to chest!", null); + this.addDefault(defaults, Messages.PlayerTooCloseForFire, "You can't start a fire this close to {0}.", "0: other player's name"); + this.addDefault(defaults, Messages.TooDeepToClaim, "This chest can't be protected because it's too deep underground. Consider moving it.", null); + this.addDefault(defaults, Messages.ChestClaimConfirmation, "This chest is protected.", null); + this.addDefault(defaults, Messages.AutomaticClaimNotification, "This chest and nearby blocks are protected from breakage and theft. The temporary gold and glowstone blocks mark the protected area. To toggle them on and off, right-click with a stick.", null); + this.addDefault(defaults, Messages.TrustCommandAdvertisement, "Use the /trust command to grant other players access.", null); + this.addDefault(defaults, Messages.GoldenShovelAdvertisement, "To claim more land, you need a golden shovel. When you equip one, you'll get more information.", null); + this.addDefault(defaults, Messages.UnprotectedChestWarning, "This chest is NOT protected. Consider expanding an existing claim or creating a new one.", null); + this.addDefault(defaults, Messages.ThatPlayerPvPImmune, "You can't injure defenseless players.", null); + this.addDefault(defaults, Messages.CantFightWhileImmune, "You can't fight someone while you're protected from PvP.", null); + this.addDefault(defaults, Messages.NoDamageClaimedEntity, "That belongs to {0}.", "0: owner name"); + this.addDefault(defaults, Messages.ShovelBasicClaimMode, "Shovel returned to basic claims mode.", null); + this.addDefault(defaults, Messages.RemainingBlocks, "You may claim up to {0} more blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.CreativeBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/c7bajb8", null); + this.addDefault(defaults, Messages.SurvivalBasicsDemoAdvertisement, "Land Claim Help: http://tinyurl.com/6nkwegj", null); + this.addDefault(defaults, Messages.TrappedChatKeyword, "trapped", "When mentioned in chat, players get information about the /trapped command."); + this.addDefault(defaults, Messages.TrappedInstructions, "Are you trapped in someone's claim? Consider the /trapped command.", null); + this.addDefault(defaults, Messages.PvPNoDrop, "You can't drop items while in PvP combat.", null); + this.addDefault(defaults, Messages.SiegeNoTeleport, "You can't teleport out of a besieged area.", null); + this.addDefault(defaults, Messages.BesiegedNoTeleport, "You can't teleport into a besieged area.", null); + this.addDefault(defaults, Messages.SiegeNoContainers, "You can't access containers while involved in a siege.", null); + this.addDefault(defaults, Messages.PvPNoContainers, "You can't access containers during PvP combat.", null); + this.addDefault(defaults, Messages.PvPImmunityEnd, "Now you can fight with other players.", null); + this.addDefault(defaults, Messages.NoBedPermission, "{0} hasn't given you permission to sleep here.", "0: claim owner"); + this.addDefault(defaults, Messages.NoWildernessBuckets, "You may only dump buckets inside your claim(s) or underground.", null); + this.addDefault(defaults, Messages.NoLavaNearOtherPlayer, "You can't place lava this close to {0}.", "0: nearby player"); + this.addDefault(defaults, Messages.TooFarAway, "That's too far away.", null); + this.addDefault(defaults, Messages.BlockNotClaimed, "No one has claimed this block.", null); + this.addDefault(defaults, Messages.BlockClaimed, "That block has been claimed by {0}.", "0: claim owner"); + this.addDefault(defaults, Messages.SiegeNoShovel, "You can't use your shovel tool while involved in a siege.", null); + this.addDefault(defaults, Messages.RestoreNaturePlayerInChunk, "Unable to restore. {0} is in that chunk.", "0: nearby player"); + this.addDefault(defaults, Messages.NoCreateClaimPermission, "You don't have permission to claim land.", null); + this.addDefault(defaults, Messages.ResizeClaimTooSmall, "This new size would be too small. Claims must be at least {0} x {0}.", "0: minimum claim size"); + this.addDefault(defaults, Messages.ResizeNeedMoreBlocks, "You don't have enough blocks for this size. You need {0} more.", "0: how many needed"); + this.addDefault(defaults, Messages.ClaimResizeSuccess, "Claim resized. You now have {0} available claim blocks.", "0: remaining blocks"); + this.addDefault(defaults, Messages.ResizeFailOverlap, "Can't resize here because it would overlap another nearby claim.", null); + this.addDefault(defaults, Messages.ResizeStart, "Resizing claim. Use your shovel again at the new location for this corner.", null); + this.addDefault(defaults, Messages.ResizeFailOverlapSubdivision, "You can't create a subdivision here because it would overlap another subdivision. Consider /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); + this.addDefault(defaults, Messages.SubdivisionStart, "Subdivision corner set! Use your shovel at the location for the opposite corner of this new subdivision.", null); + this.addDefault(defaults, Messages.CreateSubdivisionOverlap, "Your selected area overlaps another subdivision.", null); + this.addDefault(defaults, Messages.SubdivisionSuccess, "Subdivision created! Use /trust to share it with friends.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlap, "You can't create a claim here because it would overlap your other claim. Use /abandonclaim to delete it, or use your shovel at a corner to resize it.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlapOtherPlayer, "You can't create a claim here because it would overlap {0}'s claim.", "0: other claim owner"); + this.addDefault(defaults, Messages.ClaimsDisabledWorld, "Land claims are disabled in this world.", null); + this.addDefault(defaults, Messages.ClaimStart, "Claim corner set! Use the shovel again at the opposite corner to claim a rectangle of land. To cancel, put your shovel away.", null); + this.addDefault(defaults, Messages.NewClaimTooSmall, "This claim would be too small. Any claim must be at least {0} x {0}.", "0: minimum claim size"); + this.addDefault(defaults, Messages.CreateClaimInsufficientBlocks, "You don't have enough blocks to claim that entire area. You need {0} more blocks.", "0: additional blocks needed"); + this.addDefault(defaults, Messages.AbandonClaimAdvertisement, "To delete another claim and free up some blocks, use /AbandonClaim.", null); + this.addDefault(defaults, Messages.CreateClaimFailOverlapShort, "Your selected area overlaps an existing claim.", null); + this.addDefault(defaults, Messages.CreateClaimSuccess, "Claim created! Use /trust to share it with friends.", null); + this.addDefault(defaults, Messages.SiegeWinDoorsOpen, "Congratulations! Buttons and levers are temporarily unlocked (five minutes).", null); + this.addDefault(defaults, Messages.RescueAbortedMoved, "You moved! Rescue cancelled.", null); + this.addDefault(defaults, Messages.SiegeDoorsLockedEjection, "Looting time is up! Ejected from the claim.", null); + this.addDefault(defaults, Messages.NoModifyDuringSiege, "Claims can't be modified while under siege.", null); + this.addDefault(defaults, Messages.OnlyOwnersModifyClaims, "Only {0} can modify this claim.", "0: owner name"); + this.addDefault(defaults, Messages.NoBuildUnderSiege, "This claim is under siege by {0}. No one can build here.", "0: attacker name"); + this.addDefault(defaults, Messages.NoBuildPvP, "You can't build in claims during PvP combat.", null); + this.addDefault(defaults, Messages.NoBuildPermission, "You don't have {0}'s permission to build here.", "0: owner name"); + this.addDefault(defaults, Messages.NonSiegeMaterial, "That material is too tough to break.", null); + this.addDefault(defaults, Messages.NoOwnerBuildUnderSiege, "You can't make changes while under siege.", null); + this.addDefault(defaults, Messages.NoAccessPermission, "You don't have {0}'s permission to use that.", "0: owner name. access permission controls buttons, levers, and beds"); + this.addDefault(defaults, Messages.NoContainersSiege, "This claim is under siege by {0}. No one can access containers here right now.", "0: attacker name"); + this.addDefault(defaults, Messages.NoContainersPermission, "You don't have {0}'s permission to use that.", "0: owner's name. containers also include crafting blocks"); + this.addDefault(defaults, Messages.OwnerNameForAdminClaims, "an administrator", "as in 'You don't have an administrator's permission to build here.'"); + this.addDefault(defaults, Messages.ClaimTooSmallForEntities, "This claim isn't big enough for that. Try enlarging it.", null); + this.addDefault(defaults, Messages.TooManyEntitiesInClaim, "This claim has too many entities already. Try enlarging the claim or removing some animals, monsters, paintings, or minecarts.", null); + this.addDefault(defaults, Messages.YouHaveNoClaims, "You don't have any land claims.", null); + this.addDefault(defaults, Messages.ConfirmFluidRemoval, "Abandoning this claim will remove all your lava and water. If you're sure, use /AbandonClaim again.", null); + this.addDefault(defaults, Messages.AutoBanNotify, "Auto-banned {0}({1}). See logs for details.", null); + this.addDefault(defaults, Messages.AdjustGroupBlocksSuccess, "Adjusted bonus claim blocks for players with the {0} permission by {1}. New total: {2}.", "0: permission; 1: adjustment amount; 2: new total bonus"); + this.addDefault(defaults, Messages.InvalidPermissionID, "Please specify a player name, or a permission in [brackets].", null); + this.addDefault(defaults, Messages.UntrustOwnerOnly, "Only {0} can revoke permissions here.", "0: claim owner's name"); + this.addDefault(defaults, Messages.HowToClaimRegex, "(^|.*\\W)how\\W.*\\W(claim|protect|lock)(\\W.*|$)", "This is a Java Regular Expression. Look it up before editing! It's used to tell players about the demo video when they ask how to claim land."); + this.addDefault(defaults, Messages.NoBuildOutsideClaims, "You can't build here unless you claim some land first.", null); + this.addDefault(defaults, Messages.PlayerOfflineTime, " Last login: {0} days ago.", "0: number of full days since last login"); + this.addDefault(defaults, Messages.BuildingOutsideClaims, "Other players can undo your work here! Consider claiming this area to protect your work.", null); + this.addDefault(defaults, Messages.TrappedWontWorkHere, "Sorry, unable to find a safe location to teleport you to. Contact an admin, or consider /kill if you don't want to wait.", null); + this.addDefault(defaults, Messages.CommandBannedInPvP, "You can't use that command while in PvP combat.", null); + this.addDefault(defaults, Messages.UnclaimCleanupWarning, "The land you've unclaimed may be changed by other players or cleaned up by administrators. If you've built something there you want to keep, you should reclaim it.", null); + this.addDefault(defaults, Messages.BuySellNotConfigured, "Sorry, buying anhd selling claim blocks is disabled.", null); + this.addDefault(defaults, Messages.NoTeleportPvPCombat, "You can't teleport while fighting another player.", null); + this.addDefault(defaults, Messages.NoTNTDamageAboveSeaLevel, "Warning: TNT will not destroy blocks above sea level.", null); + this.addDefault(defaults, Messages.NoTNTDamageClaims, "Warning: TNT will not destroy claimed blocks.", null); + this.addDefault(defaults, Messages.IgnoreClaimsAdvertisement, "To override, use /IgnoreClaims.", null); + + //load the config file + FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); + + //for each message ID + for(int i = 0; i < messageIDs.length; i++) + { + //get default for this message + Messages messageID = messageIDs[i]; + CustomizableMessage messageData = defaults.get(messageID.name()); + + //if default is missing, log an error and use some fake data for now so that the plugin can run + if(messageData == null) + { + GriefPrevention.AddLogEntry("Missing message for " + messageID.name() + ". Please contact the developer."); + messageData = new CustomizableMessage(messageID, "Missing message! ID: " + messageID.name() + ". Please contact a server admin.", null); + } + + //read the message from the file, use default if necessary + this.messages[messageID.ordinal()] = config.getString("Messages." + messageID.name() + ".Text", messageData.text); + config.set("Messages." + messageID.name() + ".Text", this.messages[messageID.ordinal()]); + + if(messageData.notes != null) + { + messageData.notes = config.getString("Messages." + messageID.name() + ".Notes", messageData.notes); + config.set("Messages." + messageID.name() + ".Notes", messageData.notes); + } + } + + //save any changes + try + { + config.save(DataStore.messagesFilePath); + } + catch(IOException exception) + { + GriefPrevention.AddLogEntry("Unable to write to the configuration file at \"" + DataStore.messagesFilePath + "\""); + } + + defaults.clear(); + System.gc(); + } + + private void addDefault(HashMap defaults, + Messages id, String text, String notes) + { + CustomizableMessage message = new CustomizableMessage(id, text, notes); + defaults.put(id.name(), message); + } + + synchronized public String getMessage(Messages messageID, String... args) + { + String message = messages[messageID.ordinal()]; + + for(int i = 0; i < args.length; i++) + { + String param = args[i]; + message = message.replace("{" + i + "}", param); + } + + return message; + } + + abstract void close(); +} diff --git a/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java b/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java index 43e64a8..d8b281c 100644 --- a/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java @@ -1,523 +1,529 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2012 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.Calendar; -import java.util.List; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.entity.Arrow; -import org.bukkit.entity.Creature; -import org.bukkit.entity.Creeper; -import org.bukkit.entity.Enderman; -import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Monster; -import org.bukkit.entity.Player; -import org.bukkit.entity.ThrownPotion; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; -import org.bukkit.event.entity.EntityBreakDoorEvent; -import org.bukkit.event.entity.EntityChangeBlockEvent; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.entity.EntityInteractEvent; -import org.bukkit.event.entity.ExpBottleEvent; -import org.bukkit.event.entity.ItemSpawnEvent; -import org.bukkit.event.painting.PaintingBreakByEntityEvent; -import org.bukkit.event.painting.PaintingBreakEvent; -import org.bukkit.event.painting.PaintingPlaceEvent; -import org.bukkit.event.vehicle.VehicleDamageEvent; - -//handles events related to entities -class EntityEventHandler implements Listener -{ - //convenience reference for the singleton datastore - private DataStore dataStore; - - public EntityEventHandler(DataStore dataStore) - { - this.dataStore = dataStore; - } - - //don't allow endermen to change blocks - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onEntityChangeBLock(EntityChangeBlockEvent event) - { - if(!GriefPrevention.instance.config_endermenMoveBlocks && event.getEntityType() == EntityType.ENDERMAN) - { - event.setCancelled(true); - } - } - - //don't allow zombies to break down doors - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onZombieBreakDoor(EntityBreakDoorEvent event) - { - if(!GriefPrevention.instance.config_zombiesBreakDoors) event.setCancelled(true); - } - - //don't allow entities to trample crops - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onEntityInteract(EntityInteractEvent event) - { - if(!GriefPrevention.instance.config_creaturesTrampleCrops && event.getBlock().getType() == Material.SOIL) - { - event.setCancelled(true); - } - } - - //when an entity explodes... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onEntityExplode(EntityExplodeEvent explodeEvent) - { - List blocks = explodeEvent.blockList(); - Location location = explodeEvent.getLocation(); - - //FEATURE: explosions don't destroy blocks when they explode near or above sea level in standard worlds - boolean isCreeper = (explodeEvent.getEntity() != null && explodeEvent.getEntity() instanceof Creeper); - if( location.getWorld().getEnvironment() == Environment.NORMAL && GriefPrevention.instance.config_claims_enabledWorlds.contains(location.getWorld()) && ((isCreeper && GriefPrevention.instance.config_blockSurfaceCreeperExplosions) || (!isCreeper && GriefPrevention.instance.config_blockSurfaceOtherExplosions))) - { - for(int i = 0; i < blocks.size(); i++) - { - Block block = blocks.get(i); - if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; - - if(block.getLocation().getBlockY() > location.getWorld().getSeaLevel() - 7) - { - blocks.remove(i--); - } - } - } - - //special rule for creative worlds: explosions don't destroy anything - if(GriefPrevention.instance.creativeRulesApply(explodeEvent.getLocation())) - { - for(int i = 0; i < blocks.size(); i++) - { - Block block = blocks.get(i); - if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; - - blocks.remove(i--); - } - } - - //FEATURE: explosions don't damage claimed blocks - Claim claim = null; - for(int i = 0; i < blocks.size(); i++) //for each destroyed block - { - Block block = blocks.get(i); - if(block.getType() == Material.AIR) continue; //if it's air, we don't care - - if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; - - claim = this.dataStore.getClaimAt(block.getLocation(), false, claim); - //if the block is claimed, remove it from the list of destroyed blocks - if(claim != null) - { - blocks.remove(i--); - } - - //if the block is not claimed and is a log, trigger the anti-tree-top code - else if(block.getType() == Material.LOG) - { - GriefPrevention.instance.handleLogBroken(block); - } - } - } - - //when an item spawns... - @EventHandler(priority = EventPriority.LOWEST) - public void onItemSpawn(ItemSpawnEvent event) - { - //if in a creative world, cancel the event (don't drop items on the ground) - if(GriefPrevention.instance.creativeRulesApply(event.getLocation())) - { - event.setCancelled(true); - } - } - - //when an experience bottle explodes... - @EventHandler(priority = EventPriority.LOWEST) - public void onExpBottle(ExpBottleEvent event) - { - //if in a creative world, cancel the event (don't drop exp on the ground) - if(GriefPrevention.instance.creativeRulesApply(event.getEntity().getLocation())) - { - event.setExperience(0); - } - } - - //when a creature spawns... - @EventHandler(priority = EventPriority.LOWEST) - public void onEntitySpawn(CreatureSpawnEvent event) - { - LivingEntity entity = event.getEntity(); - - //these rules apply only to creative worlds - if(!GriefPrevention.instance.creativeRulesApply(entity.getLocation())) return; - - //chicken eggs and breeding could potentially make a mess in the wilderness, once griefers get involved - SpawnReason reason = event.getSpawnReason(); - if(reason != SpawnReason.SPAWNER_EGG && reason != SpawnReason.BUILD_IRONGOLEM && reason != SpawnReason.BUILD_SNOWMAN) - { - event.setCancelled(true); - return; - } - - //otherwise, just apply the limit on total entities per claim (and no spawning in the wilderness!) - Claim claim = this.dataStore.getClaimAt(event.getLocation(), false, null); - if(claim == null || claim.allowMoreEntities() != null) - { - event.setCancelled(true); - return; - } - } - - //when an entity dies... - @EventHandler - public void onEntityDeath(EntityDeathEvent event) - { - LivingEntity entity = event.getEntity(); - - //special rule for creative worlds: killed entities don't drop items or experience orbs - if(GriefPrevention.instance.creativeRulesApply(entity.getLocation())) - { - event.setDroppedExp(0); - event.getDrops().clear(); - } - - //FEATURE: when a player is involved in a siege (attacker or defender role) - //his death will end the siege - - if(!(entity instanceof Player)) return; //only tracking players - - Player player = (Player)entity; - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //if involved in a siege - if(playerData.siegeData != null) - { - //don't drop items as usual, they will be sent to the siege winner - event.getDrops().clear(); - - //end it, with the dieing player being the loser - this.dataStore.endSiege(playerData.siegeData, null, player.getName(), true /*ended due to death*/); - } - } - - //when an entity picks up an item - @EventHandler(priority = EventPriority.LOWEST) - public void onEntityPickup(EntityChangeBlockEvent event) - { - //FEATURE: endermen don't steal claimed blocks - - //if its an enderman - if(event.getEntity() instanceof Enderman) - { - //and the block is claimed - if(this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null) != null) - { - //he doesn't get to steal it - event.setCancelled(true); - } - } - } - - //when a painting is broken - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPaintingBreak(PaintingBreakEvent event) - { - //FEATURE: claimed paintings are protected from breakage - - //only allow players to break paintings, not anything else (like water and explosions) - if(!(event instanceof PaintingBreakByEntityEvent)) - { - event.setCancelled(true); - return; - } - - PaintingBreakByEntityEvent entityEvent = (PaintingBreakByEntityEvent)event; - - //who is removing it? - Entity remover = entityEvent.getRemover(); - - //again, making sure the breaker is a player - if(!(remover instanceof Player)) - { - event.setCancelled(true); - return; - } - - //if the player doesn't have build permission, don't allow the breakage - Player playerRemover = (Player)entityEvent.getRemover(); - String noBuildReason = GriefPrevention.instance.allowBuild(playerRemover, event.getPainting().getLocation()); - if(noBuildReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(playerRemover, TextMode.Err, noBuildReason); - } - } - - //when a painting is placed... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPaintingPlace(PaintingPlaceEvent event) - { - //FEATURE: similar to above, placing a painting requires build permission in the claim - - //if the player doesn't have permission, don't allow the placement - String noBuildReason = GriefPrevention.instance.allowBuild(event.getPlayer(), event.getPainting().getLocation()); - if(noBuildReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noBuildReason); - return; - } - - //otherwise, apply entity-count limitations for creative worlds - else if(GriefPrevention.instance.creativeRulesApply(event.getPainting().getLocation())) - { - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, playerData.lastClaim); - if(claim == null) return; - - String noEntitiesReason = claim.allowMoreEntities(); - if(noEntitiesReason != null) - { - GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noEntitiesReason); - event.setCancelled(true); - return; - } - } - } - - //when an entity is damaged - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - public void onEntityDamage (EntityDamageEvent event) - { - //only actually interested in entities damaging entities (ignoring environmental damage) - if(!(event instanceof EntityDamageByEntityEvent)) return; - - //monsters are never protected - if(event.getEntity() instanceof Monster) return; - - EntityDamageByEntityEvent subEvent = (EntityDamageByEntityEvent) event; - - //determine which player is attacking, if any - Player attacker = null; - Arrow arrow = null; - Entity damageSource = subEvent.getDamager(); - if(damageSource instanceof Player) - { - attacker = (Player)damageSource; - } - else if(damageSource instanceof Arrow) - { - arrow = (Arrow)damageSource; - if(arrow.getShooter() instanceof Player) - { - attacker = (Player)arrow.getShooter(); - } - } - else if(damageSource instanceof ThrownPotion) - { - ThrownPotion potion = (ThrownPotion)damageSource; - if(potion.getShooter() instanceof Player) - { - attacker = (Player)potion.getShooter(); - } - } - - //if the attacker is a player and defender is a player (pvp combat) - if(attacker != null && event.getEntity() instanceof Player) - { - //FEATURE: prevent pvp in the first minute after spawn, and prevent pvp when one or both players have no inventory - - //doesn't apply when the attacker has the no pvp immunity permission - //this rule is here to allow server owners to have a world with no spawn camp protection by assigning permissions based on the player's world - if(attacker.hasPermission("griefprevention.nopvpimmunity")) return; - - Player defender = (Player)(event.getEntity()); - - PlayerData defenderData = this.dataStore.getPlayerData(((Player)event.getEntity()).getName()); - PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName()); - - //otherwise if protecting spawning players - if(GriefPrevention.instance.config_pvp_protectFreshSpawns) - { - if(defenderData.pvpImmune) - { - event.setCancelled(true); - GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.ThatPlayerPvPImmune); - return; - } - - if(attackerData.pvpImmune) - { - event.setCancelled(true); - GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.CantFightWhileImmune); - return; - } - } - - //FEATURE: prevent players who very recently participated in pvp combat from hiding inventory to protect it from looting - //FEATURE: prevent players who are in pvp combat from logging out to avoid being defeated - - long now = Calendar.getInstance().getTimeInMillis(); - defenderData.lastPvpTimestamp = now; - defenderData.lastPvpPlayer = attacker.getName(); - attackerData.lastPvpTimestamp = now; - attackerData.lastPvpPlayer = defender.getName(); - } - - //FEATURE: protect claimed animals, boats, minecarts - //NOTE: animals can be lead with wheat, vehicles can be pushed around. - //so unless precautions are taken by the owner, a resourceful thief might find ways to steal anyway - - //if theft protection is enabled - if(event instanceof EntityDamageByEntityEvent) - { - //if the entity is an non-monster creature (remember monsters disqualified above), or a vehicle - if ((subEvent.getEntity() instanceof Creature && GriefPrevention.instance.config_claims_protectCreatures)) - { - Claim cachedClaim = null; - PlayerData playerData = null; - if(attacker != null) - { - playerData = this.dataStore.getPlayerData(attacker.getName()); - cachedClaim = playerData.lastClaim; - } - - Claim claim = this.dataStore.getClaimAt(event.getEntity().getLocation(), false, cachedClaim); - - //if it's claimed - if(claim != null) - { - //if damaged by anything other than a player, cancel the event - if(attacker == null) - { - event.setCancelled(true); - } - - //otherwise the player damaging the entity must have permission - else - { - String noContainersReason = claim.allowContainers(attacker); - if(noContainersReason != null) - { - event.setCancelled(true); - - //kill the arrow to avoid infinite bounce between crowded together animals - if(arrow != null) arrow.remove(); - - GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName()); - } - - //cache claim for later - if(playerData != null) - { - playerData.lastClaim = claim; - } - } - } - } - } - } - - //when a vehicle is damaged - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onVehicleDamage (VehicleDamageEvent event) - { - //all of this is anti theft code - if(!GriefPrevention.instance.config_claims_preventTheft) return; - - //determine which player is attacking, if any - Player attacker = null; - Entity damageSource = event.getAttacker(); - if(damageSource instanceof Player) - { - attacker = (Player)damageSource; - } - else if(damageSource instanceof Arrow) - { - Arrow arrow = (Arrow)damageSource; - if(arrow.getShooter() instanceof Player) - { - attacker = (Player)arrow.getShooter(); - } - } - else if(damageSource instanceof ThrownPotion) - { - ThrownPotion potion = (ThrownPotion)damageSource; - if(potion.getShooter() instanceof Player) - { - attacker = (Player)potion.getShooter(); - } - } - - //NOTE: vehicles can be pushed around. - //so unless precautions are taken by the owner, a resourceful thief might find ways to steal anyway - Claim cachedClaim = null; - PlayerData playerData = null; - if(attacker != null) - { - playerData = this.dataStore.getPlayerData(attacker.getName()); - cachedClaim = playerData.lastClaim; - } - - Claim claim = this.dataStore.getClaimAt(event.getVehicle().getLocation(), false, cachedClaim); - - //if it's claimed - if(claim != null) - { - //if damaged by anything other than a player, cancel the event - if(attacker == null) - { - event.setCancelled(true); - } - - //otherwise the player damaging the entity must have permission - else - { - String noContainersReason = claim.allowContainers(attacker); - if(noContainersReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName()); - } - - //cache claim for later - if(playerData != null) - { - playerData.lastClaim = claim; - } - } - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.Calendar; +import java.util.List; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Creature; +import org.bukkit.entity.Creeper; +import org.bukkit.entity.Enderman; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Monster; +import org.bukkit.entity.Player; +import org.bukkit.entity.ThrownPotion; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; +import org.bukkit.event.entity.EntityBreakDoorEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.EntityInteractEvent; +import org.bukkit.event.entity.ExpBottleEvent; +import org.bukkit.event.entity.ItemSpawnEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.hanging.HangingBreakEvent; +import org.bukkit.event.hanging.HangingPlaceEvent; +import org.bukkit.event.vehicle.VehicleDamageEvent; + +//handles events related to entities +class EntityEventHandler implements Listener +{ + //convenience reference for the singleton datastore + private DataStore dataStore; + + public EntityEventHandler(DataStore dataStore) + { + this.dataStore = dataStore; + } + + //don't allow endermen to change blocks + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onEntityChangeBLock(EntityChangeBlockEvent event) + { + if(!GriefPrevention.instance.config_endermenMoveBlocks && event.getEntityType() == EntityType.ENDERMAN) + { + event.setCancelled(true); + } + + //don't allow the wither to break blocks, when the wither is determined, too expensive to constantly check for claimed blocks + else if(event.getEntityType() == EntityType.WITHER) + { + event.setCancelled(true); + } + } + + //don't allow zombies to break down doors + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onZombieBreakDoor(EntityBreakDoorEvent event) + { + if(!GriefPrevention.instance.config_zombiesBreakDoors) event.setCancelled(true); + } + + //don't allow entities to trample crops + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onEntityInteract(EntityInteractEvent event) + { + if(!GriefPrevention.instance.config_creaturesTrampleCrops && event.getBlock().getType() == Material.SOIL) + { + event.setCancelled(true); + } + } + + //when an entity explodes... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onEntityExplode(EntityExplodeEvent explodeEvent) + { + List blocks = explodeEvent.blockList(); + Location location = explodeEvent.getLocation(); + + //FEATURE: explosions don't destroy blocks when they explode near or above sea level in standard worlds + boolean isCreeper = (explodeEvent.getEntity() != null && explodeEvent.getEntity() instanceof Creeper); + if( location.getWorld().getEnvironment() == Environment.NORMAL && GriefPrevention.instance.config_claims_enabledWorlds.contains(location.getWorld()) && ((isCreeper && GriefPrevention.instance.config_blockSurfaceCreeperExplosions) || (!isCreeper && GriefPrevention.instance.config_blockSurfaceOtherExplosions))) + { + for(int i = 0; i < blocks.size(); i++) + { + Block block = blocks.get(i); + if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; + + if(block.getLocation().getBlockY() > GriefPrevention.instance.getSeaLevel(location.getWorld()) - 7) + { + blocks.remove(i--); + } + } + } + + //special rule for creative worlds: explosions don't destroy anything + if(GriefPrevention.instance.creativeRulesApply(explodeEvent.getLocation())) + { + for(int i = 0; i < blocks.size(); i++) + { + Block block = blocks.get(i); + if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; + + blocks.remove(i--); + } + } + + //FEATURE: explosions don't damage claimed blocks + Claim claim = null; + for(int i = 0; i < blocks.size(); i++) //for each destroyed block + { + Block block = blocks.get(i); + if(block.getType() == Material.AIR) continue; //if it's air, we don't care + + if(GriefPrevention.instance.config_mods_explodableIds.Contains(new MaterialInfo(block.getTypeId(), block.getData(), null))) continue; + + claim = this.dataStore.getClaimAt(block.getLocation(), false, claim); + //if the block is claimed, remove it from the list of destroyed blocks + if(claim != null) + { + blocks.remove(i--); + } + + //if the block is not claimed and is a log, trigger the anti-tree-top code + else if(block.getType() == Material.LOG) + { + GriefPrevention.instance.handleLogBroken(block); + } + } + } + + //when an item spawns... + @EventHandler(priority = EventPriority.LOWEST) + public void onItemSpawn(ItemSpawnEvent event) + { + //if in a creative world, cancel the event (don't drop items on the ground) + if(GriefPrevention.instance.creativeRulesApply(event.getLocation())) + { + event.setCancelled(true); + } + } + + //when an experience bottle explodes... + @EventHandler(priority = EventPriority.LOWEST) + public void onExpBottle(ExpBottleEvent event) + { + //if in a creative world, cancel the event (don't drop exp on the ground) + if(GriefPrevention.instance.creativeRulesApply(event.getEntity().getLocation())) + { + event.setExperience(0); + } + } + + //when a creature spawns... + @EventHandler(priority = EventPriority.LOWEST) + public void onEntitySpawn(CreatureSpawnEvent event) + { + LivingEntity entity = event.getEntity(); + + //these rules apply only to creative worlds + if(!GriefPrevention.instance.creativeRulesApply(entity.getLocation())) return; + + //chicken eggs and breeding could potentially make a mess in the wilderness, once griefers get involved + SpawnReason reason = event.getSpawnReason(); + if(reason != SpawnReason.SPAWNER_EGG && reason != SpawnReason.BUILD_IRONGOLEM && reason != SpawnReason.BUILD_SNOWMAN) + { + event.setCancelled(true); + return; + } + + //otherwise, just apply the limit on total entities per claim (and no spawning in the wilderness!) + Claim claim = this.dataStore.getClaimAt(event.getLocation(), false, null); + if(claim == null || claim.allowMoreEntities() != null) + { + event.setCancelled(true); + return; + } + } + + //when an entity dies... + @EventHandler + public void onEntityDeath(EntityDeathEvent event) + { + LivingEntity entity = event.getEntity(); + + //special rule for creative worlds: killed entities don't drop items or experience orbs + if(GriefPrevention.instance.creativeRulesApply(entity.getLocation())) + { + event.setDroppedExp(0); + event.getDrops().clear(); + } + + //FEATURE: when a player is involved in a siege (attacker or defender role) + //his death will end the siege + + if(!(entity instanceof Player)) return; //only tracking players + + Player player = (Player)entity; + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //if involved in a siege + if(playerData.siegeData != null) + { + //don't drop items as usual, they will be sent to the siege winner + event.getDrops().clear(); + + //end it, with the dieing player being the loser + this.dataStore.endSiege(playerData.siegeData, null, player.getName(), true /*ended due to death*/); + } + } + + //when an entity picks up an item + @EventHandler(priority = EventPriority.LOWEST) + public void onEntityPickup(EntityChangeBlockEvent event) + { + //FEATURE: endermen don't steal claimed blocks + + //if its an enderman + if(event.getEntity() instanceof Enderman) + { + //and the block is claimed + if(this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null) != null) + { + //he doesn't get to steal it + event.setCancelled(true); + } + } + } + + //when a painting is broken + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onHangingBreak(HangingBreakEvent event) + { + //FEATURE: claimed paintings are protected from breakage + + //only allow players to break paintings, not anything else (like water and explosions) + if(!(event instanceof HangingBreakByEntityEvent)) + { + event.setCancelled(true); + return; + } + + HangingBreakByEntityEvent entityEvent = (HangingBreakByEntityEvent)event; + + //who is removing it? + Entity remover = entityEvent.getRemover(); + + //again, making sure the breaker is a player + if(!(remover instanceof Player)) + { + event.setCancelled(true); + return; + } + + //if the player doesn't have build permission, don't allow the breakage + Player playerRemover = (Player)entityEvent.getRemover(); + String noBuildReason = GriefPrevention.instance.allowBuild(playerRemover, event.getEntity().getLocation()); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(playerRemover, TextMode.Err, noBuildReason); + } + } + + //when a painting is placed... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPaintingPlace(HangingPlaceEvent event) + { + //FEATURE: similar to above, placing a painting requires build permission in the claim + + //if the player doesn't have permission, don't allow the placement + String noBuildReason = GriefPrevention.instance.allowBuild(event.getPlayer(), event.getEntity().getLocation()); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noBuildReason); + return; + } + + //otherwise, apply entity-count limitations for creative worlds + else if(GriefPrevention.instance.creativeRulesApply(event.getEntity().getLocation())) + { + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, playerData.lastClaim); + if(claim == null) return; + + String noEntitiesReason = claim.allowMoreEntities(); + if(noEntitiesReason != null) + { + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noEntitiesReason); + event.setCancelled(true); + return; + } + } + } + + //when an entity is damaged + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onEntityDamage (EntityDamageEvent event) + { + //only actually interested in entities damaging entities (ignoring environmental damage) + if(!(event instanceof EntityDamageByEntityEvent)) return; + + //monsters are never protected + if(event.getEntity() instanceof Monster) return; + + EntityDamageByEntityEvent subEvent = (EntityDamageByEntityEvent) event; + + //determine which player is attacking, if any + Player attacker = null; + Arrow arrow = null; + Entity damageSource = subEvent.getDamager(); + if(damageSource instanceof Player) + { + attacker = (Player)damageSource; + } + else if(damageSource instanceof Arrow) + { + arrow = (Arrow)damageSource; + if(arrow.getShooter() instanceof Player) + { + attacker = (Player)arrow.getShooter(); + } + } + else if(damageSource instanceof ThrownPotion) + { + ThrownPotion potion = (ThrownPotion)damageSource; + if(potion.getShooter() instanceof Player) + { + attacker = (Player)potion.getShooter(); + } + } + + //if the attacker is a player and defender is a player (pvp combat) + if(attacker != null && event.getEntity() instanceof Player) + { + //FEATURE: prevent pvp in the first minute after spawn, and prevent pvp when one or both players have no inventory + + //doesn't apply when the attacker has the no pvp immunity permission + //this rule is here to allow server owners to have a world with no spawn camp protection by assigning permissions based on the player's world + if(attacker.hasPermission("griefprevention.nopvpimmunity")) return; + + Player defender = (Player)(event.getEntity()); + + PlayerData defenderData = this.dataStore.getPlayerData(((Player)event.getEntity()).getName()); + PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName()); + + //otherwise if protecting spawning players + if(GriefPrevention.instance.config_pvp_protectFreshSpawns) + { + if(defenderData.pvpImmune) + { + event.setCancelled(true); + GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.ThatPlayerPvPImmune); + return; + } + + if(attackerData.pvpImmune) + { + event.setCancelled(true); + GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.CantFightWhileImmune); + return; + } + } + + //FEATURE: prevent players who very recently participated in pvp combat from hiding inventory to protect it from looting + //FEATURE: prevent players who are in pvp combat from logging out to avoid being defeated + + long now = Calendar.getInstance().getTimeInMillis(); + defenderData.lastPvpTimestamp = now; + defenderData.lastPvpPlayer = attacker.getName(); + attackerData.lastPvpTimestamp = now; + attackerData.lastPvpPlayer = defender.getName(); + } + + //FEATURE: protect claimed animals, boats, minecarts + //NOTE: animals can be lead with wheat, vehicles can be pushed around. + //so unless precautions are taken by the owner, a resourceful thief might find ways to steal anyway + + //if theft protection is enabled + if(event instanceof EntityDamageByEntityEvent) + { + //if the entity is an non-monster creature (remember monsters disqualified above), or a vehicle + if ((subEvent.getEntity() instanceof Creature && GriefPrevention.instance.config_claims_protectCreatures)) + { + Claim cachedClaim = null; + PlayerData playerData = null; + if(attacker != null) + { + playerData = this.dataStore.getPlayerData(attacker.getName()); + cachedClaim = playerData.lastClaim; + } + + Claim claim = this.dataStore.getClaimAt(event.getEntity().getLocation(), false, cachedClaim); + + //if it's claimed + if(claim != null) + { + //if damaged by anything other than a player, cancel the event + if(attacker == null) + { + event.setCancelled(true); + } + + //otherwise the player damaging the entity must have permission + else + { + String noContainersReason = claim.allowContainers(attacker); + if(noContainersReason != null) + { + event.setCancelled(true); + + //kill the arrow to avoid infinite bounce between crowded together animals + if(arrow != null) arrow.remove(); + + GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName()); + } + + //cache claim for later + if(playerData != null) + { + playerData.lastClaim = claim; + } + } + } + } + } + } + + //when a vehicle is damaged + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onVehicleDamage (VehicleDamageEvent event) + { + //all of this is anti theft code + if(!GriefPrevention.instance.config_claims_preventTheft) return; + + //determine which player is attacking, if any + Player attacker = null; + Entity damageSource = event.getAttacker(); + if(damageSource instanceof Player) + { + attacker = (Player)damageSource; + } + else if(damageSource instanceof Arrow) + { + Arrow arrow = (Arrow)damageSource; + if(arrow.getShooter() instanceof Player) + { + attacker = (Player)arrow.getShooter(); + } + } + else if(damageSource instanceof ThrownPotion) + { + ThrownPotion potion = (ThrownPotion)damageSource; + if(potion.getShooter() instanceof Player) + { + attacker = (Player)potion.getShooter(); + } + } + + //NOTE: vehicles can be pushed around. + //so unless precautions are taken by the owner, a resourceful thief might find ways to steal anyway + Claim cachedClaim = null; + PlayerData playerData = null; + if(attacker != null) + { + playerData = this.dataStore.getPlayerData(attacker.getName()); + cachedClaim = playerData.lastClaim; + } + + Claim claim = this.dataStore.getClaimAt(event.getVehicle().getLocation(), false, cachedClaim); + + //if it's claimed + if(claim != null) + { + //if damaged by anything other than a player, cancel the event + if(attacker == null) + { + event.setCancelled(true); + } + + //otherwise the player damaging the entity must have permission + else + { + String noContainersReason = claim.allowContainers(attacker); + if(noContainersReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(attacker, TextMode.Err, Messages.NoDamageClaimedEntity, claim.getOwnerName()); + } + + //cache claim for later + if(playerData != null) + { + playerData.lastClaim = claim; + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java index af44f57..22997a1 100644 --- a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java +++ b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java @@ -1,2471 +1,2506 @@ -/* - 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.HashMap; -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.World.Environment; -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_protectCreatures; //whether claimed animals may be injured by players without permission - 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 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 boolean config_claims_allowUnclaimInCreative; //whether players may unclaim land (resize or abandon) in creative mode - - public boolean config_claims_noBuildOutsideClaims; //whether players can build in survival worlds outside their claimed areas - - public int config_claims_trappedCooldownHours; //number of hours between uses of the /trapped command - - 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_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 ArrayList config_pvp_blockedCommands; //list of commands which may not be used during pvp combat - - 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_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_blockWildernessWaterBuckets; //whether players can dump water buckets outside their claims - 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_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 ArrayList config_eavesdrop_whisperCommands; //list of whisper commands to eavesdrop on - - public boolean config_smartBan; //whether to ban accounts which very likely owned by a banned player - - public boolean config_endermenMoveBlocks; //whether or not endermen may move blocks around - public boolean config_creaturesTrampleCrops; //whether or not non-player entities may trample crops - public boolean config_zombiesBreakDoors; //whether or not hard-mode zombies may break down wooden doors - - public MaterialCollection config_mods_accessTrustIds; //list of block IDs which should require /accesstrust for player interaction - public MaterialCollection config_mods_containerTrustIds; //list of block IDs which should require /containertrust for player interaction - public List config_mods_ignoreClaimsAccounts; //list of player names which ALWAYS ignore claims - public MaterialCollection config_mods_explodableIds; //list of block IDs which can be destroyed by explosions, even in claimed areas - - public boolean config_claims_warnOnBuildOutside; //whether players should be warned when they're building in an unclaimed area - - public HashMap config_seaLevelOverride; //override for sea level, because bukkit doesn't report the right value for all situations - - //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); - } - } - - //sea level - this.config_seaLevelOverride = new HashMap(); - for(int i = 0; i < worlds.size(); i++) - { - int seaLevelOverride = config.getInt("GriefPrevention.SeaLevelOverrides." + worlds.get(i).getName(), -1); - config.set("GriefPrevention.SeaLevelOverrides." + worlds.get(i).getName(), seaLevelOverride); - this.config_seaLevelOverride.put(worlds.get(i).getName(), seaLevelOverride); - } - - this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); - this.config_claims_protectCreatures = config.getBoolean("GriefPrevention.Claims.ProtectCreatures", 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_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_claims_noBuildOutsideClaims = config.getBoolean("GriefPrevention.Claims.NoSurvivalBuildingOutsideClaims", false); - this.config_claims_warnOnBuildOutside = config.getBoolean("GriefPrevention.Claims.WarnWhenBuildingOutsideClaims", true); - this.config_claims_allowUnclaimInCreative = config.getBoolean("GriefPrevention.Claims.AllowUnclaimingCreativeModeLand", true); - - 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); - String bannedPvPCommandsList = config.getString("GriefPrevention.PvP.BlockedSlashCommands", "/home;/vanish;/spawn;/tpa"); - - 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_blockSurfaceCreeperExplosions = config.getBoolean("GriefPrevention.BlockSurfaceCreeperExplosions", true); - this.config_blockSurfaceOtherExplosions = config.getBoolean("GriefPrevention.BlockSurfaceOtherExplosions", true); - this.config_blockWildernessWaterBuckets = config.getBoolean("GriefPrevention.LimitSurfaceWaterBuckets", true); - this.config_blockSkyTrees = config.getBoolean("GriefPrevention.LimitSkyTrees", 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); - String whisperCommandsToMonitor = config.getString("GriefPrevention.WhisperCommands", "/tell;/pm;/r"); - - this.config_smartBan = config.getBoolean("GriefPrevention.SmartBan", true); - - this.config_endermenMoveBlocks = config.getBoolean("GriefPrevention.EndermenMoveBlocks", false); - this.config_creaturesTrampleCrops = config.getBoolean("GriefPrevention.CreaturesTrampleCrops", false); - this.config_zombiesBreakDoors = config.getBoolean("GriefPrevention.HardModeZombiesBreakDoors", false); - - this.config_mods_ignoreClaimsAccounts = config.getStringList("GriefPrevention.Mods.PlayersIgnoringAllClaims"); - - if(this.config_mods_ignoreClaimsAccounts == null) this.config_mods_ignoreClaimsAccounts = new ArrayList(); - - this.config_mods_accessTrustIds = new MaterialCollection(); - List accessTrustStrings = config.getStringList("GriefPrevention.Mods.BlockIdsRequiringAccessTrust"); - - //default values for access trust mod blocks - if(accessTrustStrings == null || accessTrustStrings.size() == 0) - { - //none by default - } - - this.parseMaterialListFromConfig(accessTrustStrings, this.config_mods_accessTrustIds); - - this.config_mods_containerTrustIds = new MaterialCollection(); - List containerTrustStrings = config.getStringList("GriefPrevention.Mods.BlockIdsRequiringContainerTrust"); - - //default values for container trust mod blocks - if(containerTrustStrings == null || containerTrustStrings.size() == 0) - { - containerTrustStrings.add(new MaterialInfo(227, "Battery Box").toString()); - containerTrustStrings.add(new MaterialInfo(130, "Transmutation Tablet").toString()); - containerTrustStrings.add(new MaterialInfo(128, "Alchemical Chest and Energy Condenser").toString()); - containerTrustStrings.add(new MaterialInfo(181, "Various Chests").toString()); - containerTrustStrings.add(new MaterialInfo(178, "Ender Chest").toString()); - containerTrustStrings.add(new MaterialInfo(150, "Various BuildCraft Gadgets").toString()); - containerTrustStrings.add(new MaterialInfo(155, "Filler").toString()); - containerTrustStrings.add(new MaterialInfo(157, "Builder").toString()); - containerTrustStrings.add(new MaterialInfo(158, "Template Drawing Table").toString()); - containerTrustStrings.add(new MaterialInfo(126, "Various EE Gadgets").toString()); - containerTrustStrings.add(new MaterialInfo(138, "Various RedPower Gadgets").toString()); - containerTrustStrings.add(new MaterialInfo(137, "BuildCraft Project Table and Furnaces").toString()); - containerTrustStrings.add(new MaterialInfo(250, "Various IC2 Machines").toString()); - containerTrustStrings.add(new MaterialInfo(161, "BuildCraft Engines").toString()); - containerTrustStrings.add(new MaterialInfo(169, "Automatic Crafting Table").toString()); - containerTrustStrings.add(new MaterialInfo(177, "Wireless Components").toString()); - containerTrustStrings.add(new MaterialInfo(183, "Solar Arrays").toString()); - containerTrustStrings.add(new MaterialInfo(187, "Charging Benches").toString()); - containerTrustStrings.add(new MaterialInfo(188, "More IC2 Machines").toString()); - containerTrustStrings.add(new MaterialInfo(190, "Generators, Fabricators, Strainers").toString()); - containerTrustStrings.add(new MaterialInfo(194, "More Gadgets").toString()); - containerTrustStrings.add(new MaterialInfo(207, "Computer").toString()); - containerTrustStrings.add(new MaterialInfo(208, "Computer Peripherals").toString()); - containerTrustStrings.add(new MaterialInfo(246, "IC2 Generators").toString()); - containerTrustStrings.add(new MaterialInfo(24303, "Teleport Pipe").toString()); - containerTrustStrings.add(new MaterialInfo(24304, "Waterproof Teleport Pipe").toString()); - containerTrustStrings.add(new MaterialInfo(24305, "Power Teleport Pipe").toString()); - containerTrustStrings.add(new MaterialInfo(4311, "Diamond Sorting Pipe").toString()); - containerTrustStrings.add(new MaterialInfo(216, "Turtle").toString()); - - } - - //parse the strings from the config file - this.parseMaterialListFromConfig(containerTrustStrings, this.config_mods_containerTrustIds); - - this.config_mods_explodableIds = new MaterialCollection(); - List explodableStrings = config.getStringList("GriefPrevention.Mods.BlockIdsExplodable"); - - //default values for explodable mod blocks - if(explodableStrings == null || explodableStrings.size() == 0) - { - explodableStrings.add(new MaterialInfo(161, "BuildCraft Engines").toString()); - explodableStrings.add(new MaterialInfo(246, (byte)5 ,"Nuclear Reactor").toString()); - } - - //parse the strings from the config file - this.parseMaterialListFromConfig(explodableStrings, this.config_mods_explodableIds); - - //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.GOLD_SPADE.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.GOLD_SPADE; - } - - //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); - } - } - - //optional database settings - String databaseUrl = config.getString("GriefPrevention.Database.URL", ""); - String databaseUserName = config.getString("GriefPrevention.Database.UserName", ""); - String databasePassword = config.getString("GriefPrevention.Database.Password", ""); - - 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.ProtectCreatures", this.config_claims_protectCreatures); - config.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); - config.set("GriefPrevention.Claims.LockWoodenDoors", this.config_claims_lockWoodenDoors); - config.set("GriefPrevention.Claims.LockTrapDoors", this.config_claims_lockTrapDoors); - config.set("GriefPrevention.Claims.LockFenceGates", this.config_claims_lockFenceGates); - 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.Claims.InvestigationTool", this.config_claims_investigationTool.name()); - config.set("GriefPrevention.Claims.ModificationTool", this.config_claims_modificationTool.name()); - config.set("GriefPrevention.Claims.NoSurvivalBuildingOutsideClaims", this.config_claims_noBuildOutsideClaims); - config.set("GriefPrevention.Claims.WarnWhenBuildingOutsideClaims", this.config_claims_warnOnBuildOutside); - config.set("GriefPrevention.Claims.AllowUnclaimingCreativeModeLand", this.config_claims_allowUnclaimInCreative); - - 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.PvP.BlockedSlashCommands", bannedPvPCommandsList); - - 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.BlockSurfaceCreeperExplosions", this.config_blockSurfaceCreeperExplosions); - config.set("GriefPrevention.BlockSurfaceOtherExplosions", this.config_blockSurfaceOtherExplosions); - config.set("GriefPrevention.LimitSurfaceWaterBuckets", this.config_blockWildernessWaterBuckets); - config.set("GriefPrevention.LimitSkyTrees", this.config_blockSkyTrees); - - 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.WhisperCommands", whisperCommandsToMonitor); - config.set("GriefPrevention.SmartBan", this.config_smartBan); - - config.set("GriefPrevention.Siege.Worlds", siegeEnabledWorldNames); - config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList); - - config.set("GriefPrevention.EndermenMoveBlocks", this.config_endermenMoveBlocks); - config.set("GriefPrevention.CreaturesTrampleCrops", this.config_creaturesTrampleCrops); - config.set("GriefPrevention.HardModeZombiesBreakDoors", this.config_zombiesBreakDoors); - - config.set("GriefPrevention.Database.URL", databaseUrl); - config.set("GriefPrevention.Database.UserName", databaseUserName); - config.set("GriefPrevention.Database.Password", databasePassword); - - config.set("GriefPrevention.Mods.BlockIdsRequiringAccessTrust", this.config_mods_accessTrustIds); - config.set("GriefPrevention.Mods.BlockIdsRequiringContainerTrust", this.config_mods_containerTrustIds); - config.set("GriefPrevention.Mods.BlockIdsExplodable", this.config_mods_explodableIds); - config.set("GriefPrevention.Mods.PlayersIgnoringAllClaims", this.config_mods_ignoreClaimsAccounts); - config.set("GriefPrevention.Mods.BlockIdsRequiringAccessTrust", accessTrustStrings); - config.set("GriefPrevention.Mods.BlockIdsRequiringContainerTrust", containerTrustStrings); - config.set("GriefPrevention.Mods.BlockIdsExplodable", explodableStrings); - - 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()); - } - - //try to parse the list of commands which should be included in eavesdropping - this.config_eavesdrop_whisperCommands = new ArrayList(); - commands = whisperCommandsToMonitor.split(";"); - for(int i = 0; i < commands.length; i++) - { - this.config_eavesdrop_whisperCommands.add(commands[i].trim()); - } - - //try to parse the list of commands which should be banned during pvp combat - this.config_pvp_blockedCommands = new ArrayList(); - commands = bannedPvPCommandsList.split(";"); - for(int i = 0; i < commands.length; i++) - { - this.config_pvp_blockedCommands.add(commands[i].trim()); - } - - //when datastore initializes, it loads player and claim data, and posts some stats to the log - if(databaseUrl.length() > 0) - { - try - { - DatabaseDataStore databaseStore = new DatabaseDataStore(databaseUrl, databaseUserName, 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(); - flatFileStore.migrateData(databaseStore); - GriefPrevention.AddLogEntry("Data migration process complete. Reloading data from the database..."); - databaseStore.close(); - databaseStore = new DatabaseDataStore(databaseUrl, databaseUserName, databasePassword); - } - - 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."); - 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) - { - try - { - this.dataStore = new FlatFileDataStore(); - } - catch(Exception e) - { - GriefPrevention.AddLogEntry("Unable to initialize the file system data store. Details:"); - GriefPrevention.AddLogEntry(e.getMessage()); - } - } - - //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); - } - - //start the recurring cleanup event for entities in creative worlds - EntityCleanupTask task = new EntityCleanupTask(0); - this.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L); - - //start recurring cleanup scan for unused claims belonging to inactive players - CleanupUnusedClaimsTask task2 = new CleanupUnusedClaimsTask(); - this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task2, 20L * 60 * 2, 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, Messages.RespectingClaims); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.IgnoringClaims); - } - - return true; - } - - //abandonallclaims - else if(cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) - { - if(args.length != 0) return false; - - if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); - 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, Messages.YouHaveNoClaims); - return true; - } - - //delete them - this.dataStore.deleteClaimsForPlayer(player.getName(), 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.getName()); - 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.getName()); - 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.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, 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) - { - //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, Messages.TransferClaimPermission); - 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, Messages.TransferClaimMissing); - return true; - } - else if(!claim.isAdminClaim()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TransferClaimAdminOnly); - return true; - } - - OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); - if(targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - return true; - } - - //change ownerhsip - try - { - this.dataStore.changeClaimOwner(claim, targetPlayer.getName()); - } - catch(Exception 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 " + 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, Messages.TrustListNoClaim); - 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 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.allowEdit(player) == 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.resolvePlayer(args[0]); - if(!clearPermissions && otherPlayer == null && !args[0].equals("public")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - 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, Messages.UntrustIndividualAllClaims, args[0]); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustEveryoneAllClaims); - } - } - - //otherwise, apply changes to only this claim - else if(claim.allowGrantPermission(player) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); - } - else - { - //if clearing all - if(clearPermissions) - { - claim.clearPermissions(); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClearPermissionsOneClaim); - } - - //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, Messages.UntrustIndividualSingleClaim, args[0]); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustOwnerOnly, claim.getOwnerName()); - } - } - - //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) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); - return true; - } - - //if purchase disabled, send error message - if(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlySellBlocks); - return true; - } - - //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(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, Messages.ClaimBlockLimit); - 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 - } - - if(blockCount <= 0) - { - return false; - } - - //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, Messages.InsufficientFunds, String.valueOf(totalCost), String.valueOf(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, 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 - if(GriefPrevention.economy == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); - 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.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, 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; - economy.depositPlayer(player.getName(), totalValue); - - //subtract blocks - playerData.accruedClaimBlocks -= blockCount; - this.dataStore.savePlayerData(player.getName(), 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.getName()); - 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.getName()); - 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.getName()); - playerData.shovelMode = ShovelMode.Subdivide; - playerData.claimSubdividing = null; - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionMode); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionDemo); - - 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.getName()); - if(claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.DeletionSubdivisionWarning); - playerData.warnedAboutMajorDeletion = true; - } - else - { - claim.removeSurfaceFluids(null); - this.dataStore.deleteClaim(claim); - - //if in a creative mode world, delete the claim - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.instance.restoreClaim(claim, 0); - } - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteSuccess); - 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, Messages.CantDeleteAdminClaim); - } - } - - 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.resolvePlayer(args[0]); - if(otherPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - return true; - } - - //delete all that player's claims - this.dataStore.deleteClaimsForPlayer(otherPlayer.getName(), true); - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteAllSuccess, otherPlayer.getName()); - if(player != null) - { - GriefPrevention.AddLogEntry(player.getName() + " deleted all claims belonging to " + otherPlayer.getName() + "."); - - //revert any current visualization - Visualization.Revert(player); - } - - return true; - } - - //claimslist - else if(cmd.getName().equalsIgnoreCase("claimslist")) - { - //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, Messages.PlayerNotFound); - return true; - } - - //load the player's data - PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getName()); - GriefPrevention.sendMessage(player, TextMode.Instr, " " + playerData.accruedClaimBlocks + "(+" + playerData.bonusClaimBlocks + ")=" + (playerData.accruedClaimBlocks + playerData.bonusClaimBlocks)); - for(int i = 0; i < playerData.claims.size(); i++) - { - Claim claim = playerData.claims.get(i); - GriefPrevention.sendMessage(player, TextMode.Instr, " (-" + claim.getArea() + ") " + getfriendlyLocationString(claim.getLesserBoundaryCorner())); - } - - if(playerData.claims.size() > 0) - GriefPrevention.sendMessage(player, TextMode.Instr, " =" + playerData.getRemainingClaimBlocks()); - - //drop the data we just loaded, if the player isn't online - if(!otherPlayer.isOnline()) - this.dataStore.clearCachedPlayerData(otherPlayer.getName()); - - return true; - } - - //deathblow [recipientPlayer] - else if(cmd.getName().equalsIgnoreCase("deathblow")) - { - //requires at least one parameter, the target player's name - if(args.length < 1) return false; - - //try to find that player - Player targetPlayer = this.getServer().getPlayer(args[0]); - if(targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - return true; - } - - //try to find the recipient player, if specified - Player recipientPlayer = null; - if(args.length > 1) - { - recipientPlayer = this.getServer().getPlayer(args[1]); - if(recipientPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - return true; - } - } - - //if giving inventory to another player, teleport the target player to that receiving player - if(recipientPlayer != null) - { - targetPlayer.teleport(recipientPlayer); - } - - //otherwise, plan to "pop" the player in place - else - { - //if in a normal world, shoot him up to the sky first, so his items will fall on the surface. - if(targetPlayer.getWorld().getEnvironment() == Environment.NORMAL) - { - Location location = targetPlayer.getLocation(); - location.setY(location.getWorld().getMaxHeight()); - targetPlayer.teleport(location); - } - } - - //kill target player - targetPlayer.setHealth(0); - - //log entry - if(player != null) - { - GriefPrevention.AddLogEntry(player.getName() + " used /DeathBlow to kill " + targetPlayer.getName() + "."); - } - else - { - GriefPrevention.AddLogEntry("Killed " + targetPlayer.getName() + "."); - } - - return true; - } - - //deletealladminclaims - else if(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("", true); //empty string for owner name indicates an administrative claim - - GriefPrevention.sendMessage(player, TextMode.Success, Messages.AllAdminDeleted); - if(player != null) - { - GriefPrevention.AddLogEntry(player.getName() + " deleted all administrative claims."); - - //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 = this.resolvePlayer(args[0]); - if(targetPlayer == null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - return true; - } - - //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, Messages.AdjustBlocksSuccess, targetPlayer.getName(), String.valueOf(adjustment), String.valueOf(playerData.bonusClaimBlocks)); - if(player != null) 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, Messages.NotTrappedHere); - return true; - } - - //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) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); - 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, Messages.TrappedOnCooldown, String.valueOf(this.config_claims_trappedCooldownHours), String.valueOf((nextTrappedUsage - now) / (1000 * 60) + 1)); - 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()); - 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, Messages.NonSiegeWorld); - 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, Messages.AlreadySieging); - 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, Messages.PlayerNotFound); - 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, Messages.AlreadyUnderSiegePlayer); - return true; - } - - //victim must not be pvp immune - if(defenderData.pvpImmune) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoSiegeDefenseless); - 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, Messages.NotSiegableThere); - return true; - } - - //attacker must be close to the claim he wants to siege - if(!defenderClaim.isNear(attacker.getLocation(), 25)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeTooFarAway); - return true; - } - - //claim can't be under siege already - if(defenderClaim.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.AlreadyUnderSiegeArea); - return true; - } - - //can't siege admin claims - if(defenderClaim.isAdminClaim()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoSiegeAdminClaim); - return true; - } - - //can't be on cooldown - if(dataStore.onCooldown(attacker, defender, defenderClaim)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeOnCooldown); - return true; - } - - //start the siege - dataStore.startSiege(attacker, defender, defenderClaim); - - //confirmation message for attacker, warning message for defender - GriefPrevention.sendMessage(defender, TextMode.Warn, Messages.SiegeAlert, attacker.getName()); - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SiegeConfirmed, defender.getName()); - } - - 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) - { - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //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.allowEdit(player) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); - } - - //don't allow abandon of creative mode claims - else if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && this.creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); - } - - //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; - } - - //if the claim has lots of surface water or some surface lava, warn the player it will be cleaned up - else if(!playerData.warnedAboutMajorDeletion && claim.hasSurfaceFluids()) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.ConfirmFluidRemoval); - playerData.warnedAboutMajorDeletion = true; - } - - else - { - //delete it - claim.removeSurfaceFluids(null); - this.dataStore.deleteClaim(claim); - - //if in a creative mode world, restore the claim area - if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) - { - GriefPrevention.AddLogEntry(player.getName() + " abandoned a claim @ " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); - GriefPrevention.instance.restoreClaim(claim, 20L * 60 * 2); - } - - //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 - 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; - 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.resolvePlayer(recipientName); - if(otherPlayer == null && !recipientName.equals("public") && !recipientName.equals("all")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); - 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, Messages.NoPermissionTrust, claim.getOwnerName()); - 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, 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; - } - - //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 = 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 //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 - 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() - { - //save data for any online players - Player [] players = this.getServer().getOnlinePlayers(); - for(int i = 0; i < players.length; i++) - { - Player player = players[i]; - String playerName = player.getName(); - PlayerData playerData = this.dataStore.getPlayerData(playerName); - this.dataStore.savePlayerData(playerName, playerData); - } - - this.dataStore.close(); - - 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 player is in creative mode, do nothing - if(player.getGameMode() == GameMode.CREATIVE) return; - - //if anti spawn camping feature is not enabled, do nothing - if(!this.config_pvp_protectFreshSpawns) return; - - //if the player has the damage any player permission enabled, do nothing - if(player.hasPermission("griefprevention.nopvpimmunity")) 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, Messages.PvPImmunityStart); - } - - //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, Messages messageID, String... args) - { - sendMessage(player, color, messageID, 0, args); - } - - //sends a color-coded message to a player - 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 - static void sendMessage(Player player, ChatColor color, String message) - { - if(player == null) - { - GriefPrevention.AddLogEntry(color + message); - } - else - { - player.sendMessage(color + message); - } - } - - static void sendMessage(Player player, ChatColor color, String message, long delayInTicks) - { - SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message); - if(delayInTicks > 0) - { - GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); - } - else - { - task.run(); - } - } - - //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); - - //exception: administrators in ignore claims mode and special player accounts created by server mods - if(playerData.ignoreClaims || GriefPrevention.instance.config_mods_ignoreClaimsAccounts.contains(player.getName())) return null; - - //wilderness rules - if(claim == null) - { - //no building in the wilderness in creative mode - if(this.creativeRulesApply(location)) - { - return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.CreativeBasicsDemoAdvertisement); - - } - - //no building in survival wilderness when that is configured - else if(this.config_claims_noBuildOutsideClaims && this.config_claims_enabledWorlds.contains(location.getWorld())) - { - return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.SurvivalBasicsDemoAdvertisement); - } - - else - { - //but it's fine in survival mode - return null; - } - } - - //if not in the wilderness, then apply claim rules (permissions, etc) - else - { - //cache the claim for later reference - playerData.lastClaim = claim; - 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); - - //exception: administrators in ignore claims mode, and special player accounts created by server mods - if(playerData.ignoreClaims || GriefPrevention.instance.config_mods_ignoreClaimsAccounts.contains(player.getName())) return null; - - //wilderness rules - if(claim == null) - { - //no building in the wilderness in creative mode - if(this.creativeRulesApply(location)) - { - return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.CreativeBasicsDemoAdvertisement); - } - - else if(this.config_claims_noBuildOutsideClaims && this.config_claims_enabledWorlds.contains(location.getWorld())) - { - return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.SurvivalBasicsDemoAdvertisement); - } - - //but it's fine in survival mode - else - { - return null; - } - } - else - { - //cache the claim for later reference - playerData.lastClaim = claim; - - //if not in the wilderness, then apply claim rules (permissions, etc) - return claim.allowBreak(player, location.getBlock().getType()); - } - } - - //restores nature in multiple chunks, as described by a claim instance - //this restores all chunks which have ANY number of claim blocks from this claim in them - //if the claim is still active (in the data store), then the claimed blocks will not be changed (only the area bordering the claim) - public void restoreClaim(Claim claim, long delayInTicks) - { - //admin claims aren't automatically cleaned up when deleted or abandoned - if(claim.isAdminClaim()) return; - - //it's too expensive to do this for huge claims - if(claim.getArea() > 10000) return; - - Chunk lesserChunk = claim.getLesserBoundaryCorner().getChunk(); - Chunk greaterChunk = claim.getGreaterBoundaryCorner().getChunk(); - - for(int x = lesserChunk.getX(); x <= greaterChunk.getX(); x++) - for(int z = lesserChunk.getZ(); z <= greaterChunk.getZ(); z++) - { - Chunk chunk = lesserChunk.getWorld().getChunkAt(x, z); - this.restoreChunk(chunk, chunk.getWorld().getSeaLevel() - 15, false, delayInTicks, null); - } - } - - 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.getTypeId(), block.getData()); - } - } - } - - //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, chunk.getWorld().getSeaLevel(), aggressiveMode, GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner), playerReceivingVisualization); - GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); - } - - private void parseMaterialListFromConfig(List stringsToParse, MaterialCollection materialCollection) - { - materialCollection.clear(); - - //for each string in the list - for(int i = 0; i < stringsToParse.size(); i++) - { - //try to parse the string value into a material info - MaterialInfo materialInfo = MaterialInfo.fromString(stringsToParse.get(i)); - - //null value returned indicates an error parsing the string from the config file - if(materialInfo == null) - { - //show error in log - GriefPrevention.AddLogEntry("ERROR: Unable to read a material entry from the config file. Please update your config.yml."); - - //update string, which will go out to config file to help user find the error entry - if(!stringsToParse.get(i).contains("can't")) - { - stringsToParse.set(i, stringsToParse.get(i) + " <-- can't understand this entry, see BukkitDev documentation"); - } - } - - //otherwise store the valid entry in config data - else - { - materialCollection.Add(materialInfo); - } - } - } - - public int getSeaLevel(World world) - { - Integer overrideValue = this.config_seaLevelOverride.get(world.getName()); - if(overrideValue == null || overrideValue == -1) - { - return world.getSeaLevel(); - } - else - { - return overrideValue; - } - } +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +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.World.Environment; +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_protectCreatures; //whether claimed animals may be injured by players without permission + 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 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 boolean config_claims_allowUnclaimInCreative; //whether players may unclaim land (resize or abandon) in creative mode + + public boolean config_claims_noBuildOutsideClaims; //whether players can build in survival worlds outside their claimed areas + + 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_creativeAutoNatureRestoration; //whether creative claims will be automatically restored to nature when auto-deleted + + public int config_claims_trappedCooldownHours; //number of hours between uses of the /trapped command + + 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_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 ArrayList config_pvp_blockedCommands; //list of commands which may not be used during pvp combat + + 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_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_blockWildernessWaterBuckets; //whether players can dump water buckets outside their claims + 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_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 ArrayList config_eavesdrop_whisperCommands; //list of whisper commands to eavesdrop on + + public boolean config_smartBan; //whether to ban accounts which very likely owned by a banned player + + public boolean config_endermenMoveBlocks; //whether or not endermen may move blocks around + public boolean config_creaturesTrampleCrops; //whether or not non-player entities may trample crops + public boolean config_zombiesBreakDoors; //whether or not hard-mode zombies may break down wooden doors + + public MaterialCollection config_mods_accessTrustIds; //list of block IDs which should require /accesstrust for player interaction + public MaterialCollection config_mods_containerTrustIds; //list of block IDs which should require /containertrust for player interaction + public List config_mods_ignoreClaimsAccounts; //list of player names which ALWAYS ignore claims + public MaterialCollection config_mods_explodableIds; //list of block IDs which can be destroyed by explosions, even in claimed areas + + public boolean config_claims_warnOnBuildOutside; //whether players should be warned when they're building in an unclaimed area + + public HashMap config_seaLevelOverride; //override for sea level, because bukkit doesn't report the right value for all situations + + //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); + } + } + + //sea level + this.config_seaLevelOverride = new HashMap(); + for(int i = 0; i < worlds.size(); i++) + { + int seaLevelOverride = config.getInt("GriefPrevention.SeaLevelOverrides." + worlds.get(i).getName(), -1); + config.set("GriefPrevention.SeaLevelOverrides." + worlds.get(i).getName(), seaLevelOverride); + this.config_seaLevelOverride.put(worlds.get(i).getName(), seaLevelOverride); + } + + this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); + this.config_claims_protectCreatures = config.getBoolean("GriefPrevention.Claims.ProtectCreatures", 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_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_trappedCooldownHours = config.getInt("GriefPrevention.Claims.TrappedCommandCooldownHours", 8); + this.config_claims_noBuildOutsideClaims = config.getBoolean("GriefPrevention.Claims.NoSurvivalBuildingOutsideClaims", false); + this.config_claims_warnOnBuildOutside = config.getBoolean("GriefPrevention.Claims.WarnWhenBuildingOutsideClaims", true); + this.config_claims_allowUnclaimInCreative = config.getBoolean("GriefPrevention.Claims.AllowUnclaimingCreativeModeLand", true); + + this.config_claims_chestClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.ChestClaimDays", 7); + config.set("GriefPrevention.Claims.Expiration.ChestClaimDays", this.config_claims_chestClaimExpirationDays); + + this.config_claims_unusedClaimExpirationDays = config.getInt("GriefPrevention.Claims.Expiration.UnusedClaimDays", 14); + config.set("GriefPrevention.Claims.Expiration.UnusedClaimDays", this.config_claims_unusedClaimExpirationDays); + + this.config_claims_expirationDays = config.getInt("GriefPrevention.Claims.Expiration.AllClaimDays", 0); + config.set("GriefPrevention.Claims.Expiration.AllClaimDays", this.config_claims_expirationDays); + + this.config_claims_survivalAutoNatureRestoration = config.getBoolean("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", false); + config.set("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.SurvivalWorlds", this.config_claims_survivalAutoNatureRestoration); + + this.config_claims_creativeAutoNatureRestoration = config.getBoolean("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.CreativeWorlds", true); + config.set("GriefPrevention.Claims.Expiration.AutomaticNatureRestoration.CreativeWorlds", this.config_claims_creativeAutoNatureRestoration); + + 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); + String bannedPvPCommandsList = config.getString("GriefPrevention.PvP.BlockedSlashCommands", "/home;/vanish;/spawn;/tpa"); + + 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_blockSurfaceCreeperExplosions = config.getBoolean("GriefPrevention.BlockSurfaceCreeperExplosions", true); + this.config_blockSurfaceOtherExplosions = config.getBoolean("GriefPrevention.BlockSurfaceOtherExplosions", true); + this.config_blockWildernessWaterBuckets = config.getBoolean("GriefPrevention.LimitSurfaceWaterBuckets", true); + this.config_blockSkyTrees = config.getBoolean("GriefPrevention.LimitSkyTrees", 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); + String whisperCommandsToMonitor = config.getString("GriefPrevention.WhisperCommands", "/tell;/pm;/r"); + + this.config_smartBan = config.getBoolean("GriefPrevention.SmartBan", true); + + this.config_endermenMoveBlocks = config.getBoolean("GriefPrevention.EndermenMoveBlocks", false); + this.config_creaturesTrampleCrops = config.getBoolean("GriefPrevention.CreaturesTrampleCrops", false); + this.config_zombiesBreakDoors = config.getBoolean("GriefPrevention.HardModeZombiesBreakDoors", false); + + this.config_mods_ignoreClaimsAccounts = config.getStringList("GriefPrevention.Mods.PlayersIgnoringAllClaims"); + + if(this.config_mods_ignoreClaimsAccounts == null) this.config_mods_ignoreClaimsAccounts = new ArrayList(); + + this.config_mods_accessTrustIds = new MaterialCollection(); + List accessTrustStrings = config.getStringList("GriefPrevention.Mods.BlockIdsRequiringAccessTrust"); + + //default values for access trust mod blocks + if(accessTrustStrings == null || accessTrustStrings.size() == 0) + { + //none by default + } + + this.parseMaterialListFromConfig(accessTrustStrings, this.config_mods_accessTrustIds); + + this.config_mods_containerTrustIds = new MaterialCollection(); + List containerTrustStrings = config.getStringList("GriefPrevention.Mods.BlockIdsRequiringContainerTrust"); + + //default values for container trust mod blocks + if(containerTrustStrings == null || containerTrustStrings.size() == 0) + { + containerTrustStrings.add(new MaterialInfo(227, "Battery Box").toString()); + containerTrustStrings.add(new MaterialInfo(130, "Transmutation Tablet").toString()); + containerTrustStrings.add(new MaterialInfo(128, "Alchemical Chest and Energy Condenser").toString()); + containerTrustStrings.add(new MaterialInfo(181, "Various Chests").toString()); + containerTrustStrings.add(new MaterialInfo(178, "Ender Chest").toString()); + containerTrustStrings.add(new MaterialInfo(150, "Various BuildCraft Gadgets").toString()); + containerTrustStrings.add(new MaterialInfo(155, "Filler").toString()); + containerTrustStrings.add(new MaterialInfo(157, "Builder").toString()); + containerTrustStrings.add(new MaterialInfo(158, "Template Drawing Table").toString()); + containerTrustStrings.add(new MaterialInfo(126, "Various EE Gadgets").toString()); + containerTrustStrings.add(new MaterialInfo(138, "Various RedPower Gadgets").toString()); + containerTrustStrings.add(new MaterialInfo(137, "BuildCraft Project Table and Furnaces").toString()); + containerTrustStrings.add(new MaterialInfo(250, "Various IC2 Machines").toString()); + containerTrustStrings.add(new MaterialInfo(161, "BuildCraft Engines").toString()); + containerTrustStrings.add(new MaterialInfo(169, "Automatic Crafting Table").toString()); + containerTrustStrings.add(new MaterialInfo(177, "Wireless Components").toString()); + containerTrustStrings.add(new MaterialInfo(183, "Solar Arrays").toString()); + containerTrustStrings.add(new MaterialInfo(187, "Charging Benches").toString()); + containerTrustStrings.add(new MaterialInfo(188, "More IC2 Machines").toString()); + containerTrustStrings.add(new MaterialInfo(190, "Generators, Fabricators, Strainers").toString()); + containerTrustStrings.add(new MaterialInfo(194, "More Gadgets").toString()); + containerTrustStrings.add(new MaterialInfo(207, "Computer").toString()); + containerTrustStrings.add(new MaterialInfo(208, "Computer Peripherals").toString()); + containerTrustStrings.add(new MaterialInfo(246, "IC2 Generators").toString()); + containerTrustStrings.add(new MaterialInfo(24303, "Teleport Pipe").toString()); + containerTrustStrings.add(new MaterialInfo(24304, "Waterproof Teleport Pipe").toString()); + containerTrustStrings.add(new MaterialInfo(24305, "Power Teleport Pipe").toString()); + containerTrustStrings.add(new MaterialInfo(4311, "Diamond Sorting Pipe").toString()); + containerTrustStrings.add(new MaterialInfo(216, "Turtle").toString()); + + } + + //parse the strings from the config file + this.parseMaterialListFromConfig(containerTrustStrings, this.config_mods_containerTrustIds); + + this.config_mods_explodableIds = new MaterialCollection(); + List explodableStrings = config.getStringList("GriefPrevention.Mods.BlockIdsExplodable"); + + //default values for explodable mod blocks + if(explodableStrings == null || explodableStrings.size() == 0) + { + explodableStrings.add(new MaterialInfo(161, "BuildCraft Engines").toString()); + explodableStrings.add(new MaterialInfo(246, (byte)5 ,"Nuclear Reactor").toString()); + } + + //parse the strings from the config file + this.parseMaterialListFromConfig(explodableStrings, this.config_mods_explodableIds); + + //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.GOLD_SPADE.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.GOLD_SPADE; + } + + //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); + } + } + + //optional database settings + String databaseUrl = config.getString("GriefPrevention.Database.URL", ""); + String databaseUserName = config.getString("GriefPrevention.Database.UserName", ""); + String databasePassword = config.getString("GriefPrevention.Database.Password", ""); + + 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.ProtectCreatures", this.config_claims_protectCreatures); + config.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); + config.set("GriefPrevention.Claims.LockWoodenDoors", this.config_claims_lockWoodenDoors); + config.set("GriefPrevention.Claims.LockTrapDoors", this.config_claims_lockTrapDoors); + config.set("GriefPrevention.Claims.LockFenceGates", this.config_claims_lockFenceGates); + 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.Claims.InvestigationTool", this.config_claims_investigationTool.name()); + config.set("GriefPrevention.Claims.ModificationTool", this.config_claims_modificationTool.name()); + config.set("GriefPrevention.Claims.NoSurvivalBuildingOutsideClaims", this.config_claims_noBuildOutsideClaims); + config.set("GriefPrevention.Claims.WarnWhenBuildingOutsideClaims", this.config_claims_warnOnBuildOutside); + config.set("GriefPrevention.Claims.AllowUnclaimingCreativeModeLand", this.config_claims_allowUnclaimInCreative); + + 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.PvP.BlockedSlashCommands", bannedPvPCommandsList); + + 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.BlockSurfaceCreeperExplosions", this.config_blockSurfaceCreeperExplosions); + config.set("GriefPrevention.BlockSurfaceOtherExplosions", this.config_blockSurfaceOtherExplosions); + config.set("GriefPrevention.LimitSurfaceWaterBuckets", this.config_blockWildernessWaterBuckets); + config.set("GriefPrevention.LimitSkyTrees", this.config_blockSkyTrees); + + 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.WhisperCommands", whisperCommandsToMonitor); + config.set("GriefPrevention.SmartBan", this.config_smartBan); + + config.set("GriefPrevention.Siege.Worlds", siegeEnabledWorldNames); + config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList); + + config.set("GriefPrevention.EndermenMoveBlocks", this.config_endermenMoveBlocks); + config.set("GriefPrevention.CreaturesTrampleCrops", this.config_creaturesTrampleCrops); + config.set("GriefPrevention.HardModeZombiesBreakDoors", this.config_zombiesBreakDoors); + + config.set("GriefPrevention.Database.URL", databaseUrl); + config.set("GriefPrevention.Database.UserName", databaseUserName); + config.set("GriefPrevention.Database.Password", databasePassword); + + config.set("GriefPrevention.Mods.BlockIdsRequiringAccessTrust", this.config_mods_accessTrustIds); + config.set("GriefPrevention.Mods.BlockIdsRequiringContainerTrust", this.config_mods_containerTrustIds); + config.set("GriefPrevention.Mods.BlockIdsExplodable", this.config_mods_explodableIds); + config.set("GriefPrevention.Mods.PlayersIgnoringAllClaims", this.config_mods_ignoreClaimsAccounts); + config.set("GriefPrevention.Mods.BlockIdsRequiringAccessTrust", accessTrustStrings); + config.set("GriefPrevention.Mods.BlockIdsRequiringContainerTrust", containerTrustStrings); + config.set("GriefPrevention.Mods.BlockIdsExplodable", explodableStrings); + + 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()); + } + + //try to parse the list of commands which should be included in eavesdropping + this.config_eavesdrop_whisperCommands = new ArrayList(); + commands = whisperCommandsToMonitor.split(";"); + for(int i = 0; i < commands.length; i++) + { + this.config_eavesdrop_whisperCommands.add(commands[i].trim()); + } + + //try to parse the list of commands which should be banned during pvp combat + this.config_pvp_blockedCommands = new ArrayList(); + commands = bannedPvPCommandsList.split(";"); + for(int i = 0; i < commands.length; i++) + { + this.config_pvp_blockedCommands.add(commands[i].trim()); + } + + //when datastore initializes, it loads player and claim data, and posts some stats to the log + if(databaseUrl.length() > 0) + { + try + { + DatabaseDataStore databaseStore = new DatabaseDataStore(databaseUrl, databaseUserName, 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(); + flatFileStore.migrateData(databaseStore); + GriefPrevention.AddLogEntry("Data migration process complete. Reloading data from the database..."); + databaseStore.close(); + databaseStore = new DatabaseDataStore(databaseUrl, databaseUserName, databasePassword); + } + + 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."); + 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) + { + try + { + this.dataStore = new FlatFileDataStore(); + } + catch(Exception e) + { + GriefPrevention.AddLogEntry("Unable to initialize the file system data store. Details:"); + GriefPrevention.AddLogEntry(e.getMessage()); + } + } + + //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); + } + + //start the recurring cleanup event for entities in creative worlds + EntityCleanupTask task = new EntityCleanupTask(0); + this.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L); + + //start recurring cleanup scan for unused claims belonging to inactive players + CleanupUnusedClaimsTask task2 = new CleanupUnusedClaimsTask(); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task2, 20L * 60 * 2, 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, Messages.RespectingClaims); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.IgnoringClaims); + } + + return true; + } + + //abandonallclaims + else if(cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) + { + if(args.length != 0) return false; + + if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); + 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, Messages.YouHaveNoClaims); + return true; + } + + //delete them + this.dataStore.deleteClaimsForPlayer(player.getName(), 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.getName()); + 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.getName()); + 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.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, 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) + { + //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, Messages.TransferClaimPermission); + 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, Messages.TransferClaimMissing); + return true; + } + else if(!claim.isAdminClaim()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TransferClaimAdminOnly); + return true; + } + + OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); + if(targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + return true; + } + + //change ownerhsip + try + { + this.dataStore.changeClaimOwner(claim, targetPlayer.getName()); + } + catch(Exception 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 " + 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, Messages.TrustListNoClaim); + 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 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); + + //bracket any permissions + if(args[0].contains(".")) + { + args[0] = "[" + args[0] + "]"; + } + + //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, Messages.ClearPermsOwnerOnly); + return true; + } + } + + else + { + //validate player argument or group argument + if(!args[0].startsWith("[") || !args[0].endsWith("]")) + { + otherPlayer = this.resolvePlayer(args[0]); + if(!clearPermissions && otherPlayer == null && !args[0].equals("public")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + 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, Messages.UntrustIndividualAllClaims, args[0]); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustEveryoneAllClaims); + } + } + + //otherwise, apply changes to only this claim + else if(claim.allowGrantPermission(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoPermissionTrust, claim.getOwnerName()); + } + else + { + //if clearing all + if(clearPermissions) + { + claim.clearPermissions(); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClearPermissionsOneClaim); + } + + //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, Messages.UntrustIndividualSingleClaim, args[0]); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.UntrustOwnerOnly, claim.getOwnerName()); + } + } + + //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) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); + return true; + } + + //if purchase disabled, send error message + if(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.OnlySellBlocks); + return true; + } + + //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(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, Messages.ClaimBlockLimit); + 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 + } + + if(blockCount <= 0) + { + return false; + } + + //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, Messages.InsufficientFunds, String.valueOf(totalCost), String.valueOf(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, 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 + if(GriefPrevention.economy == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BuySellNotConfigured); + 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.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, 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; + economy.depositPlayer(player.getName(), totalValue); + + //subtract blocks + playerData.accruedClaimBlocks -= blockCount; + this.dataStore.savePlayerData(player.getName(), 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.getName()); + 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.getName()); + 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.getName()); + playerData.shovelMode = ShovelMode.Subdivide; + playerData.claimSubdividing = null; + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionMode); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionDemo); + + 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.getName()); + if(claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.DeletionSubdivisionWarning); + playerData.warnedAboutMajorDeletion = true; + } + else + { + claim.removeSurfaceFluids(null); + this.dataStore.deleteClaim(claim); + + //if in a creative mode world, delete the claim + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + GriefPrevention.instance.restoreClaim(claim, 0); + } + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteSuccess); + 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, Messages.CantDeleteAdminClaim); + } + } + + 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.resolvePlayer(args[0]); + if(otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + return true; + } + + //delete all that player's claims + this.dataStore.deleteClaimsForPlayer(otherPlayer.getName(), true); + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.DeleteAllSuccess, otherPlayer.getName()); + if(player != null) + { + GriefPrevention.AddLogEntry(player.getName() + " deleted all claims belonging to " + otherPlayer.getName() + "."); + + //revert any current visualization + Visualization.Revert(player); + } + + return true; + } + + //claimslist + else if(cmd.getName().equalsIgnoreCase("claimslist")) + { + //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, Messages.PlayerNotFound); + return true; + } + + //load the player's data + PlayerData playerData = this.dataStore.getPlayerData(otherPlayer.getName()); + GriefPrevention.sendMessage(player, TextMode.Instr, " " + playerData.accruedClaimBlocks + "(+" + playerData.bonusClaimBlocks + ")=" + (playerData.accruedClaimBlocks + playerData.bonusClaimBlocks)); + for(int i = 0; i < playerData.claims.size(); i++) + { + Claim claim = playerData.claims.get(i); + GriefPrevention.sendMessage(player, TextMode.Instr, " (-" + claim.getArea() + ") " + getfriendlyLocationString(claim.getLesserBoundaryCorner())); + } + + if(playerData.claims.size() > 0) + GriefPrevention.sendMessage(player, TextMode.Instr, " =" + playerData.getRemainingClaimBlocks()); + + //drop the data we just loaded, if the player isn't online + if(!otherPlayer.isOnline()) + this.dataStore.clearCachedPlayerData(otherPlayer.getName()); + + return true; + } + + //deathblow [recipientPlayer] + else if(cmd.getName().equalsIgnoreCase("deathblow")) + { + //requires at least one parameter, the target player's name + if(args.length < 1) return false; + + //try to find that player + Player targetPlayer = this.getServer().getPlayer(args[0]); + if(targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + return true; + } + + //try to find the recipient player, if specified + Player recipientPlayer = null; + if(args.length > 1) + { + recipientPlayer = this.getServer().getPlayer(args[1]); + if(recipientPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + return true; + } + } + + //if giving inventory to another player, teleport the target player to that receiving player + if(recipientPlayer != null) + { + targetPlayer.teleport(recipientPlayer); + } + + //otherwise, plan to "pop" the player in place + else + { + //if in a normal world, shoot him up to the sky first, so his items will fall on the surface. + if(targetPlayer.getWorld().getEnvironment() == Environment.NORMAL) + { + Location location = targetPlayer.getLocation(); + location.setY(location.getWorld().getMaxHeight()); + targetPlayer.teleport(location); + } + } + + //kill target player + targetPlayer.setHealth(0); + + //log entry + if(player != null) + { + GriefPrevention.AddLogEntry(player.getName() + " used /DeathBlow to kill " + targetPlayer.getName() + "."); + } + else + { + GriefPrevention.AddLogEntry("Killed " + targetPlayer.getName() + "."); + } + + return true; + } + + //deletealladminclaims + else if(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("", true); //empty string for owner name indicates an administrative claim + + GriefPrevention.sendMessage(player, TextMode.Success, Messages.AllAdminDeleted); + if(player != null) + { + GriefPrevention.AddLogEntry(player.getName() + " deleted all administrative claims."); + + //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 = this.resolvePlayer(args[0]); + if(targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + return true; + } + + //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, Messages.AdjustBlocksSuccess, targetPlayer.getName(), String.valueOf(adjustment), String.valueOf(playerData.bonusClaimBlocks)); + if(player != null) 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, Messages.NotTrappedHere); + return true; + } + + //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) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TrappedWontWorkHere); + 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, Messages.TrappedOnCooldown, String.valueOf(this.config_claims_trappedCooldownHours), String.valueOf((nextTrappedUsage - now) / (1000 * 60) + 1)); + 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()); + 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, Messages.NonSiegeWorld); + 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, Messages.AlreadySieging); + 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, Messages.PlayerNotFound); + 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, Messages.AlreadyUnderSiegePlayer); + return true; + } + + //victim must not be pvp immune + if(defenderData.pvpImmune) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoSiegeDefenseless); + 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, Messages.NotSiegableThere); + return true; + } + + //attacker must be close to the claim he wants to siege + if(!defenderClaim.isNear(attacker.getLocation(), 25)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeTooFarAway); + return true; + } + + //claim can't be under siege already + if(defenderClaim.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.AlreadyUnderSiegeArea); + return true; + } + + //can't siege admin claims + if(defenderClaim.isAdminClaim()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoSiegeAdminClaim); + return true; + } + + //can't be on cooldown + if(dataStore.onCooldown(attacker, defender, defenderClaim)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeOnCooldown); + return true; + } + + //start the siege + dataStore.startSiege(attacker, defender, defenderClaim); + + //confirmation message for attacker, warning message for defender + GriefPrevention.sendMessage(defender, TextMode.Warn, Messages.SiegeAlert, attacker.getName()); + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SiegeConfirmed, defender.getName()); + } + + 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) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //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.allowEdit(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NotYourClaim); + } + + //don't allow abandon of creative mode claims + else if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && this.creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); + } + + //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; + } + + //if the claim has lots of surface water or some surface lava, warn the player it will be cleaned up + else if(!playerData.warnedAboutMajorDeletion && claim.hasSurfaceFluids()) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.ConfirmFluidRemoval); + playerData.warnedAboutMajorDeletion = true; + } + + else + { + //delete it + claim.removeSurfaceFluids(null); + this.dataStore.deleteClaim(claim); + + //if in a creative mode world, restore the claim area + if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner())) + { + GriefPrevention.AddLogEntry(player.getName() + " abandoned a claim @ " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); + GriefPrevention.instance.restoreClaim(claim, 20L * 60 * 2); + } + + //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 + 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; + 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 if(recipientName.contains(".")) + { + permission = recipientName; + } + + else + { + otherPlayer = this.resolvePlayer(recipientName); + if(otherPlayer == null && !recipientName.equals("public") && !recipientName.equals("all")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerNotFound); + 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, Messages.NoPermissionTrust, claim.getOwnerName()); + 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, 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; + } + + //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 = 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 //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 + 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() + { + //save data for any online players + Player [] players = this.getServer().getOnlinePlayers(); + for(int i = 0; i < players.length; i++) + { + Player player = players[i]; + String playerName = player.getName(); + PlayerData playerData = this.dataStore.getPlayerData(playerName); + this.dataStore.savePlayerData(playerName, playerData); + } + + this.dataStore.close(); + + 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 player is in creative mode, do nothing + if(player.getGameMode() == GameMode.CREATIVE) return; + + //if anti spawn camping feature is not enabled, do nothing + if(!this.config_pvp_protectFreshSpawns) return; + + //if the player has the damage any player permission enabled, do nothing + if(player.hasPermission("griefprevention.nopvpimmunity")) 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, Messages.PvPImmunityStart); + } + + //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, Messages messageID, String... args) + { + sendMessage(player, color, messageID, 0, args); + } + + //sends a color-coded message to a player + 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 + static void sendMessage(Player player, ChatColor color, String message) + { + if(player == null) + { + GriefPrevention.AddLogEntry(color + message); + } + else + { + player.sendMessage(color + message); + } + } + + static void sendMessage(Player player, ChatColor color, String message, long delayInTicks) + { + SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message); + if(delayInTicks > 0) + { + GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); + } + else + { + task.run(); + } + } + + //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); + + //exception: administrators in ignore claims mode and special player accounts created by server mods + if(playerData.ignoreClaims || GriefPrevention.instance.config_mods_ignoreClaimsAccounts.contains(player.getName())) return null; + + //wilderness rules + if(claim == null) + { + //no building in the wilderness in creative mode + if(this.creativeRulesApply(location)) + { + String reason = this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.CreativeBasicsDemoAdvertisement); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + this.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + //no building in survival wilderness when that is configured + else if(this.config_claims_noBuildOutsideClaims && this.config_claims_enabledWorlds.contains(location.getWorld())) + { + return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.SurvivalBasicsDemoAdvertisement); + } + + else + { + //but it's fine in survival mode + return null; + } + } + + //if not in the wilderness, then apply claim rules (permissions, etc) + else + { + //cache the claim for later reference + playerData.lastClaim = claim; + 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); + + //exception: administrators in ignore claims mode, and special player accounts created by server mods + if(playerData.ignoreClaims || GriefPrevention.instance.config_mods_ignoreClaimsAccounts.contains(player.getName())) return null; + + //wilderness rules + if(claim == null) + { + //no building in the wilderness in creative mode + if(this.creativeRulesApply(location)) + { + String reason = this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.CreativeBasicsDemoAdvertisement); + if(player.hasPermission("griefprevention.ignoreclaims")) + reason += " " + this.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement); + return reason; + } + + else if(this.config_claims_noBuildOutsideClaims && this.config_claims_enabledWorlds.contains(location.getWorld())) + { + return this.dataStore.getMessage(Messages.NoBuildOutsideClaims) + " " + this.dataStore.getMessage(Messages.SurvivalBasicsDemoAdvertisement); + } + + //but it's fine in survival mode + else + { + return null; + } + } + else + { + //cache the claim for later reference + playerData.lastClaim = claim; + + //if not in the wilderness, then apply claim rules (permissions, etc) + return claim.allowBreak(player, location.getBlock().getType()); + } + } + + //restores nature in multiple chunks, as described by a claim instance + //this restores all chunks which have ANY number of claim blocks from this claim in them + //if the claim is still active (in the data store), then the claimed blocks will not be changed (only the area bordering the claim) + public void restoreClaim(Claim claim, long delayInTicks) + { + //admin claims aren't automatically cleaned up when deleted or abandoned + if(claim.isAdminClaim()) return; + + //it's too expensive to do this for huge claims + if(claim.getArea() > 10000) return; + + Chunk lesserChunk = claim.getLesserBoundaryCorner().getChunk(); + Chunk greaterChunk = claim.getGreaterBoundaryCorner().getChunk(); + + for(int x = lesserChunk.getX(); x <= greaterChunk.getX(); x++) + for(int z = lesserChunk.getZ(); z <= greaterChunk.getZ(); z++) + { + Chunk chunk = lesserChunk.getWorld().getChunkAt(x, z); + this.restoreChunk(chunk, this.getSeaLevel(chunk.getWorld()) - 15, false, delayInTicks, null); + } + } + + 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.getTypeId(), block.getData()); + } + } + } + + //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, GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner), playerReceivingVisualization); + GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task, delayInTicks); + } + + private void parseMaterialListFromConfig(List stringsToParse, MaterialCollection materialCollection) + { + materialCollection.clear(); + + //for each string in the list + for(int i = 0; i < stringsToParse.size(); i++) + { + //try to parse the string value into a material info + MaterialInfo materialInfo = MaterialInfo.fromString(stringsToParse.get(i)); + + //null value returned indicates an error parsing the string from the config file + if(materialInfo == null) + { + //show error in log + GriefPrevention.AddLogEntry("ERROR: Unable to read a material entry from the config file. Please update your config.yml."); + + //update string, which will go out to config file to help user find the error entry + if(!stringsToParse.get(i).contains("can't")) + { + stringsToParse.set(i, stringsToParse.get(i) + " <-- can't understand this entry, see BukkitDev documentation"); + } + } + + //otherwise store the valid entry in config data + else + { + materialCollection.Add(materialInfo); + } + } + } + + public int getSeaLevel(World world) + { + Integer overrideValue = this.config_seaLevelOverride.get(world.getName()); + if(overrideValue == null || overrideValue == -1) + { + return world.getSeaLevel(); + } + else + { + return overrideValue; + } + } } \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/Messages.java b/src/me/ryanhamshire/GriefPrevention/Messages.java index 1e30ebb..1c7d1a2 100644 --- a/src/me/ryanhamshire/GriefPrevention/Messages.java +++ b/src/me/ryanhamshire/GriefPrevention/Messages.java @@ -1,6 +1,6 @@ -package me.ryanhamshire.GriefPrevention; - -public enum Messages -{ - RespectingClaims, IgnoringClaims, SuccessfulAbandon, RestoreNatureActivate, RestoreNatureAggressiveActivate, FillModeActive, TransferClaimPermission, TransferClaimMissing, TransferClaimAdminOnly, PlayerNotFound, TransferTopLevel, TransferSuccess, TrustListNoClaim, ClearPermsOwnerOnly, UntrustIndividualAllClaims, UntrustEveryoneAllClaims, NoPermissionTrust, ClearPermissionsOneClaim, UntrustIndividualSingleClaim, OnlySellBlocks, BlockPurchaseCost, ClaimBlockLimit, InsufficientFunds, PurchaseConfirmation, OnlyPurchaseBlocks, BlockSaleValue, NotEnoughBlocksForSale, BlockSaleConfirmation, AdminClaimsMode, BasicClaimsMode, SubdivisionMode, SubdivisionDemo, DeleteClaimMissing, DeletionSubdivisionWarning, DeleteSuccess, CantDeleteAdminClaim, DeleteAllSuccess, NoDeletePermission, AllAdminDeleted, AdjustBlocksSuccess, NotTrappedHere, TrappedOnCooldown, RescuePending, NonSiegeWorld, AlreadySieging, NotSiegableThere, SiegeTooFarAway, NoSiegeDefenseless, AlreadyUnderSiegePlayer, AlreadyUnderSiegeArea, NoSiegeAdminClaim, SiegeOnCooldown, SiegeAlert, SiegeConfirmed, AbandonClaimMissing, NotYourClaim, DeleteTopLevelClaim, AbandonSuccess, CantGrantThatPermission, GrantPermissionNoClaim, GrantPermissionConfirmation, ManageUniversalPermissionsInstruction, ManageOneClaimPermissionsInstruction, CollectivePublic, BuildPermission, ContainersPermission, AccessPermission, PermissionsPermission, LocationCurrentClaim, LocationAllClaims, PvPImmunityStart, SiegeNoDrop, DonateItemsInstruction, ChestFull, DonationSuccess, PlayerTooCloseForFire, TooDeepToClaim, ChestClaimConfirmation, AutomaticClaimNotification, TrustCommandAdvertisement, GoldenShovelAdvertisement, UnprotectedChestWarning, ThatPlayerPvPImmune, CantFightWhileImmune, NoDamageClaimedEntity, ShovelBasicClaimMode, RemainingBlocks, CreativeBasicsDemoAdvertisement, SurvivalBasicsDemoAdvertisement, TrappedChatKeyword, TrappedInstructions, PvPNoDrop, SiegeNoTeleport, BesiegedNoTeleport, SiegeNoContainers, PvPNoContainers, PvPImmunityEnd, NoBedPermission, NoWildernessBuckets, NoLavaNearOtherPlayer, TooFarAway, BlockNotClaimed, BlockClaimed, SiegeNoShovel, RestoreNaturePlayerInChunk, NoCreateClaimPermission, ResizeClaimTooSmall, ResizeNeedMoreBlocks, NoCreativeUnClaim, ClaimResizeSuccess, ResizeFailOverlap, ResizeStart, ResizeFailOverlapSubdivision, SubdivisionStart, CreateSubdivisionOverlap, SubdivisionSuccess, CreateClaimFailOverlap, CreateClaimFailOverlapOtherPlayer, ClaimsDisabledWorld, ClaimStart, NewClaimTooSmall, CreateClaimInsufficientBlocks, AbandonClaimAdvertisement, CreateClaimFailOverlapShort, CreateClaimSuccess, SiegeWinDoorsOpen, RescueAbortedMoved, SiegeDoorsLockedEjection, NoModifyDuringSiege, OnlyOwnersModifyClaims, NoBuildUnderSiege, NoBuildPvP, NoBuildPermission, NonSiegeMaterial, NoOwnerBuildUnderSiege, NoAccessPermission, NoContainersSiege, NoContainersPermission, OwnerNameForAdminClaims, ClaimTooSmallForEntities, TooManyEntitiesInClaim, YouHaveNoClaims, ConfirmFluidRemoval, AutoBanNotify, AdjustGroupBlocksSuccess, InvalidPermissionID, UntrustOwnerOnly, HowToClaimRegex, NoBuildOutsideClaims, PlayerOfflineTime, BuildingOutsideClaims, TrappedWontWorkHere, CommandBannedInPvP, UnclaimCleanupWarning, BuySellNotConfigured, NoTeleportPvPCombat, NoTNTDamageAboveSeaLevel, NoTNTDamageClaims -} +package me.ryanhamshire.GriefPrevention; + +public enum Messages +{ + RespectingClaims, IgnoringClaims, SuccessfulAbandon, RestoreNatureActivate, RestoreNatureAggressiveActivate, FillModeActive, TransferClaimPermission, TransferClaimMissing, TransferClaimAdminOnly, PlayerNotFound, TransferTopLevel, TransferSuccess, TrustListNoClaim, ClearPermsOwnerOnly, UntrustIndividualAllClaims, UntrustEveryoneAllClaims, NoPermissionTrust, ClearPermissionsOneClaim, UntrustIndividualSingleClaim, OnlySellBlocks, BlockPurchaseCost, ClaimBlockLimit, InsufficientFunds, PurchaseConfirmation, OnlyPurchaseBlocks, BlockSaleValue, NotEnoughBlocksForSale, BlockSaleConfirmation, AdminClaimsMode, BasicClaimsMode, SubdivisionMode, SubdivisionDemo, DeleteClaimMissing, DeletionSubdivisionWarning, DeleteSuccess, CantDeleteAdminClaim, DeleteAllSuccess, NoDeletePermission, AllAdminDeleted, AdjustBlocksSuccess, NotTrappedHere, TrappedOnCooldown, RescuePending, NonSiegeWorld, AlreadySieging, NotSiegableThere, SiegeTooFarAway, NoSiegeDefenseless, AlreadyUnderSiegePlayer, AlreadyUnderSiegeArea, NoSiegeAdminClaim, SiegeOnCooldown, SiegeAlert, SiegeConfirmed, AbandonClaimMissing, NotYourClaim, DeleteTopLevelClaim, AbandonSuccess, CantGrantThatPermission, GrantPermissionNoClaim, GrantPermissionConfirmation, ManageUniversalPermissionsInstruction, ManageOneClaimPermissionsInstruction, CollectivePublic, BuildPermission, ContainersPermission, AccessPermission, PermissionsPermission, LocationCurrentClaim, LocationAllClaims, PvPImmunityStart, SiegeNoDrop, DonateItemsInstruction, ChestFull, DonationSuccess, PlayerTooCloseForFire, TooDeepToClaim, ChestClaimConfirmation, AutomaticClaimNotification, TrustCommandAdvertisement, GoldenShovelAdvertisement, UnprotectedChestWarning, ThatPlayerPvPImmune, CantFightWhileImmune, NoDamageClaimedEntity, ShovelBasicClaimMode, RemainingBlocks, CreativeBasicsDemoAdvertisement, SurvivalBasicsDemoAdvertisement, TrappedChatKeyword, TrappedInstructions, PvPNoDrop, SiegeNoTeleport, BesiegedNoTeleport, SiegeNoContainers, PvPNoContainers, PvPImmunityEnd, NoBedPermission, NoWildernessBuckets, NoLavaNearOtherPlayer, TooFarAway, BlockNotClaimed, BlockClaimed, SiegeNoShovel, RestoreNaturePlayerInChunk, NoCreateClaimPermission, ResizeClaimTooSmall, ResizeNeedMoreBlocks, NoCreativeUnClaim, ClaimResizeSuccess, ResizeFailOverlap, ResizeStart, ResizeFailOverlapSubdivision, SubdivisionStart, CreateSubdivisionOverlap, SubdivisionSuccess, CreateClaimFailOverlap, CreateClaimFailOverlapOtherPlayer, ClaimsDisabledWorld, ClaimStart, NewClaimTooSmall, CreateClaimInsufficientBlocks, AbandonClaimAdvertisement, CreateClaimFailOverlapShort, CreateClaimSuccess, SiegeWinDoorsOpen, RescueAbortedMoved, SiegeDoorsLockedEjection, NoModifyDuringSiege, OnlyOwnersModifyClaims, NoBuildUnderSiege, NoBuildPvP, NoBuildPermission, NonSiegeMaterial, NoOwnerBuildUnderSiege, NoAccessPermission, NoContainersSiege, NoContainersPermission, OwnerNameForAdminClaims, ClaimTooSmallForEntities, TooManyEntitiesInClaim, YouHaveNoClaims, ConfirmFluidRemoval, AutoBanNotify, AdjustGroupBlocksSuccess, InvalidPermissionID, UntrustOwnerOnly, HowToClaimRegex, NoBuildOutsideClaims, PlayerOfflineTime, BuildingOutsideClaims, TrappedWontWorkHere, CommandBannedInPvP, UnclaimCleanupWarning, BuySellNotConfigured, NoTeleportPvPCombat, NoTNTDamageAboveSeaLevel, NoTNTDamageClaims, IgnoreClaimsAdvertisement +} diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java index ed09c8b..745288d 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -1,1703 +1,1717 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.bukkit.ChatColor; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World.Environment; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Animals; -import org.bukkit.entity.Boat; -import org.bukkit.entity.Entity; -import org.bukkit.entity.PoweredMinecart; -import org.bukkit.entity.StorageMinecart; -import org.bukkit.entity.Player; -import org.bukkit.entity.Vehicle; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.*; -import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; - -class PlayerEventHandler implements Listener -{ - private DataStore dataStore; - - //list of temporarily banned ip's - private ArrayList tempBannedIps = new ArrayList(); - - //number of milliseconds in a day - private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; - - //timestamps of login and logout notifications in the last minute - private ArrayList recentLoginLogoutNotifications = new ArrayList(); - - //regex pattern for the "how do i claim land?" scanner - private Pattern howToClaimPattern = null; - - //typical constructor, yawn - PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) - { - this.dataStore = dataStore; - } - - //when a player chats, monitor for spam - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - void onPlayerChat (AsyncPlayerChatEvent event) - { - Player player = event.getPlayer(); - if(!player.isOnline()) - { - event.setCancelled(true); - return; - } - - String message = event.getMessage(); - - event.setCancelled(this.handlePlayerChat(player, message, event)); - } - - //returns true if the message should be sent, false if it should be muted - private boolean handlePlayerChat(Player player, String message, PlayerEvent event) - { - //FEATURE: automatically educate players about claiming land - //watching for message format how*claim*, and will send a link to the basics video - if(this.howToClaimPattern == null) - { - this.howToClaimPattern = Pattern.compile(this.dataStore.getMessage(Messages.HowToClaimRegex), Pattern.CASE_INSENSITIVE); - } - - if(this.howToClaimPattern.matcher(message).matches()) - { - if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.CreativeBasicsDemoAdvertisement, 10L); - } - else - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsDemoAdvertisement, 10L); - } - } - - //FEATURE: automatically educate players about the /trapped command - //check for "trapped" or "stuck" to educate players about the /trapped command - if(message.contains("trapped") || message.contains("stuck") || message.contains(this.dataStore.getMessage(Messages.TrappedChatKeyword))) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrappedInstructions, 10L); - } - - //FEATURE: monitor for chat and command spam - - if(!GriefPrevention.instance.config_spam_enabled) return false; - - //if the player has permission to spam, don't bother even examining the message - if(player.hasPermission("griefprevention.spam")) return false; - - //remedy any CAPS SPAM without bothering to fault the player for it - if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message)) - { - if(event instanceof AsyncPlayerChatEvent) - { - ((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase()); - } - } - - //where other types of spam are concerned, casing isn't significant - message = message.toLowerCase(); - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - boolean spam = false; - boolean muted = false; - - //check message content and timing - long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime(); - - //if the message came too close to the last one - if(millisecondsSinceLastMessage < 2000) - { - //increment the spam counter - playerData.spamCount++; - spam = true; - } - - //if it's very similar to the last message - if(!muted && this.stringsAreSimilar(message, playerData.lastMessage)) - { - playerData.spamCount++; - spam = true; - muted = true; - } - - //filter IP addresses - if(!muted) - { - Pattern ipAddressPattern = Pattern.compile("\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}"); - Matcher matcher = ipAddressPattern.matcher(message); - - //if it looks like an IP address - while(matcher.find()) - { - //and it's not in the list of allowed IP addresses - if(!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) - { - //log entry - GriefPrevention.AddLogEntry("Muted IP address from " + player.getName() + ": " + message); - - //spam notation - playerData.spamCount++; - spam = true; - - //block message - muted = true; - } - } - } - - //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish) - if(!muted && message.length() > 5) - { - int symbolsCount = 0; - int whitespaceCount = 0; - for(int i = 0; i < message.length(); i++) - { - char character = message.charAt(i); - if(!(Character.isLetterOrDigit(character))) - { - symbolsCount++; - } - - if(Character.isWhitespace(character)) - { - whitespaceCount++; - } - } - - if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10)) - { - spam = true; - if(playerData.spamCount > 0) muted = true; - playerData.spamCount++; - } - } - - //very short messages close together are spam - if(!muted && message.length() < 5 && millisecondsSinceLastMessage < 5000) - { - spam = true; - playerData.spamCount++; - } - - //if the message was determined to be a spam, consider taking action - if(!player.hasPermission("griefprevention.spam") && spam) - { - //anything above level 8 for a player which has received a warning... kick or if enabled, ban - if(playerData.spamCount > 8 && playerData.spamWarned) - { - if(GriefPrevention.instance.config_spam_banOffenders) - { - //log entry - GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); - - //ban - GriefPrevention.instance.getServer().getOfflinePlayer(player.getName()).setBanned(true); - - //kick - player.kickPlayer(GriefPrevention.instance.config_spam_banMessage); - } - else - { - player.kickPlayer(""); - } - - return true; - } - - //cancel any messages while at or above the third spam level and issue warnings - //anything above level 2, mute and warn - if(playerData.spamCount >= 3) - { - muted = true; - if(!playerData.spamWarned) - { - GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L); - GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); - playerData.spamWarned = true; - } - } - - if(muted) - { - //make a log entry - GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); - - //send a fake message so the player doesn't realize he's muted - //less information for spammers = less effective spam filter dodging - player.sendMessage("<" + player.getName() + "> " + message); - - //cancelling the event guarantees other players don't receive the message - return true; - } - } - - //otherwise if not a spam, reset the spam counter for this player - else - { - playerData.spamCount = 0; - playerData.spamWarned = false; - } - - //in any case, record the timestamp of this message and also its content for next time - playerData.lastMessageTimestamp = new Date(); - playerData.lastMessage = message; - - return false; - } - - //if two strings are 75% identical, they're too close to follow each other in the chat - private boolean stringsAreSimilar(String message, String lastMessage) - { - //determine which is shorter - String shorterString, longerString; - if(lastMessage.length() < message.length()) - { - shorterString = lastMessage; - longerString = message; - } - else - { - shorterString = message; - longerString = lastMessage; - } - - if(shorterString.length() <= 5) return shorterString.equals(longerString); - - //set similarity tolerance - int maxIdenticalCharacters = longerString.length() - longerString.length() / 4; - - //trivial check on length - if(shorterString.length() < maxIdenticalCharacters) return false; - - //compare forward - int identicalCount = 0; - for(int i = 0; i < shorterString.length(); i++) - { - if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++; - if(identicalCount > maxIdenticalCharacters) return true; - } - - //compare backward - for(int i = 0; i < shorterString.length(); i++) - { - if(shorterString.charAt(shorterString.length() - i - 1) == longerString.charAt(longerString.length() - i - 1)) identicalCount++; - if(identicalCount > maxIdenticalCharacters) return true; - } - - return false; - } - - //when a player uses a slash command... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) - { - String [] args = event.getMessage().split(" "); - - //if eavesdrop enabled, eavesdrop - String command = args[0].toLowerCase(); - if(GriefPrevention.instance.config_eavesdrop && GriefPrevention.instance.config_eavesdrop_whisperCommands.contains(command) && !event.getPlayer().hasPermission("griefprevention.eavesdrop") && args.length > 1) - { - StringBuilder logMessageBuilder = new StringBuilder(); - logMessageBuilder.append("[[").append(event.getPlayer().getName()).append("]] "); - - for(int i = 1; i < args.length; i++) - { - logMessageBuilder.append(args[i]).append(" "); - } - - String logMessage = logMessageBuilder.toString(); - - GriefPrevention.AddLogEntry(logMessage.toString()); - - Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); - for(int i = 0; i < players.length; i++) - { - Player player = players[i]; - if(player.hasPermission("griefprevention.eavesdrop") && !player.getName().equalsIgnoreCase(args[1])) - { - player.sendMessage(ChatColor.GRAY + logMessage); - } - } - } - - //if in pvp, block any pvp-banned slash commands - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if(playerData.inPvpCombat() && GriefPrevention.instance.config_pvp_blockedCommands.contains(command)) - { - event.setCancelled(true); - GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); - return; - } - - //if anti spam enabled, check for spam - if(!GriefPrevention.instance.config_spam_enabled) return; - - //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) - if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains(args[0])) - { - event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); - } - } - - //when a player attempts to join the server... - @EventHandler(priority = EventPriority.HIGHEST) - void onPlayerLogin (PlayerLoginEvent event) - { - Player player = event.getPlayer(); - - //all this is anti-spam code - if(GriefPrevention.instance.config_spam_enabled) - { - //FEATURE: login cooldown to prevent login/logout spam with custom clients - - //if allowed to join and login cooldown enabled - if(GriefPrevention.instance.config_spam_loginCooldownMinutes > 0 && event.getResult() == Result.ALLOWED) - { - //determine how long since last login and cooldown remaining - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - long millisecondsSinceLastLogin = (new Date()).getTime() - playerData.lastLogin.getTime(); - long minutesSinceLastLogin = millisecondsSinceLastLogin / 1000 / 60; - long cooldownRemaining = GriefPrevention.instance.config_spam_loginCooldownMinutes - minutesSinceLastLogin; - - //if cooldown remaining and player doesn't have permission to spam - if(cooldownRemaining > 0 && !player.hasPermission("griefprevention.spam")) - { - //DAS BOOT! - event.setResult(Result.KICK_OTHER); - event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again."); - event.disallow(event.getResult(), event.getKickMessage()); - return; - } - } - } - - //remember the player's ip address - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - playerData.ipAddress = event.getAddress(); - - //FEATURE: auto-ban accounts who use an IP address which was very recently used by another banned account - if(GriefPrevention.instance.config_smartBan && !player.hasPlayedBefore()) - { - //if logging-in account is banned, remember IP address for later - long now = Calendar.getInstance().getTimeInMillis(); - if(event.getResult() == Result.KICK_BANNED) - { - this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); - } - - //otherwise if not banned - else - { - //search temporarily banned IP addresses for this one - for(int i = 0; i < this.tempBannedIps.size(); i++) - { - IpBanInfo info = this.tempBannedIps.get(i); - String address = info.address.toString(); - - //eliminate any expired entries - if(now > info.expirationTimestamp) - { - this.tempBannedIps.remove(i--); - } - - //if we find a match - else if(address.equals(playerData.ipAddress.toString())) - { - //if the account associated with the IP ban has been pardoned, remove all ip bans for that ip and we're done - OfflinePlayer bannedPlayer = GriefPrevention.instance.getServer().getOfflinePlayer(info.bannedAccountName); - if(!bannedPlayer.isBanned()) - { - for(int j = 0; j < this.tempBannedIps.size(); j++) - { - IpBanInfo info2 = this.tempBannedIps.get(j); - if(info2.address.toString().equals(address)) - { - OfflinePlayer bannedAccount = GriefPrevention.instance.getServer().getOfflinePlayer(info2.bannedAccountName); - bannedAccount.setBanned(false); - this.tempBannedIps.remove(j--); - } - } - - break; - } - - //otherwise if that account is still banned, ban this account, too - else - { - player.setBanned(true); - event.setResult(Result.KICK_BANNED); - event.disallow(event.getResult(), ""); - GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ")."); - - //notify any online ops - Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); - for(int k = 0; k < players.length; k++) - { - if(players[k].isOp()) - { - GriefPrevention.sendMessage(players[k], TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); - } - } - - break; - } - } - } - } - } - } - - //when a player spawns, conditionally apply temporary pvp protection - @EventHandler(ignoreCancelled = true) - void onPlayerRespawn (PlayerRespawnEvent event) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(event.getPlayer().getName()); - playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); - GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); - } - - //when a player successfully joins the server... - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - void onPlayerJoin(PlayerJoinEvent event) - { - String playerName = event.getPlayer().getName(); - - //note login time - PlayerData playerData = this.dataStore.getPlayerData(playerName); - playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); - playerData.lastLogin = new Date(); - this.dataStore.savePlayerData(playerName, playerData); - - //if player has never played on the server before, may need pvp protection - if(!event.getPlayer().hasPlayedBefore()) - { - GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); - } - - //silence notifications when they're coming too fast - if(event.getJoinMessage() != null && this.shouldSilenceNotification()) - { - event.setJoinMessage(null); - } - } - - //when a player quits... - @EventHandler(priority = EventPriority.HIGHEST) - void onPlayerQuit(PlayerQuitEvent event) - { - Player player = event.getPlayer(); - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //if banned, add IP to the temporary IP ban list - if(player.isBanned() && playerData.ipAddress != null) - { - long now = Calendar.getInstance().getTimeInMillis(); - this.tempBannedIps.add(new IpBanInfo(playerData.ipAddress, now + this.MILLISECONDS_IN_DAY, player.getName())); - } - - //silence notifications when they're coming too fast - if(event.getQuitMessage() != null && this.shouldSilenceNotification()) - { - event.setQuitMessage(null); - } - - //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately - this.dataStore.savePlayerData(player.getName(), playerData); - - this.onPlayerDisconnect(event.getPlayer(), event.getQuitMessage()); - } - - //helper for above - private void onPlayerDisconnect(Player player, String notificationMessage) - { - String playerName = player.getName(); - PlayerData playerData = this.dataStore.getPlayerData(playerName); - - //FEATURE: players in pvp combat when they log out will die - if(GriefPrevention.instance.config_pvp_punishLogout && playerData.inPvpCombat()) - { - player.setHealth(0); - } - - //FEATURE: during a siege, any player who logs out dies and forfeits the siege - - //if player was involved in a siege, he forfeits - if(playerData.siegeData != null) - { - if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message - } - - //drop data about this player - this.dataStore.clearCachedPlayerData(player.getName()); - } - - //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute - private boolean shouldSilenceNotification() - { - final long ONE_MINUTE = 60000; - final int MAX_ALLOWED = 20; - Long now = Calendar.getInstance().getTimeInMillis(); - - //eliminate any expired entries (longer than a minute ago) - for(int i = 0; i < this.recentLoginLogoutNotifications.size(); i++) - { - Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i); - if(now - notificationTimestamp > ONE_MINUTE) - { - this.recentLoginLogoutNotifications.remove(i--); - } - else - { - break; - } - } - - //add the new entry - this.recentLoginLogoutNotifications.add(now); - - return this.recentLoginLogoutNotifications.size() > MAX_ALLOWED; - } - - //when a player drops an item - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerDropItem(PlayerDropItemEvent event) - { - Player player = event.getPlayer(); - - //in creative worlds, dropping items is blocked - if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) - { - event.setCancelled(true); - return; - } - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - - //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide - //them or give them away to other players before they are defeated - - //if in combat, don't let him drop it - if(!GriefPrevention.instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop); - event.setCancelled(true); - } - - //if he's under siege, don't let him drop it - else if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); - event.setCancelled(true); - } - } - - //when a player teleports - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerTeleport(PlayerTeleportEvent event) - { - //FEATURE: prevent teleport abuse to win sieges - - //these rules only apply to non-ender-pearl teleportation - if(event.getCause() == TeleportCause.ENDER_PEARL) return; - - Player player = event.getPlayer(); - - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if(playerData.inPvpCombat()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoTeleportPvPCombat); - event.setCancelled(true); - return; - } - - Location source = event.getFrom(); - Claim sourceClaim = this.dataStore.getClaimAt(source, false, null); - if(sourceClaim != null && sourceClaim.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoTeleport); - event.setCancelled(true); - return; - } - - Location destination = event.getTo(); - Claim destinationClaim = this.dataStore.getClaimAt(destination, false, null); - if(destinationClaim != null && destinationClaim.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BesiegedNoTeleport); - event.setCancelled(true); - return; - } - } - - //when a player interacts with an entity... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerInteractEntity(PlayerInteractEntityEvent event) - { - Player player = event.getPlayer(); - Entity entity = event.getRightClicked(); - - //don't allow container access during pvp combat - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if((entity instanceof StorageMinecart || entity instanceof PoweredMinecart)) - { - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); - event.setCancelled(true); - return; - } - - if(playerData.inPvpCombat()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers); - event.setCancelled(true); - return; - } - } - - //if the entity is a vehicle and we're preventing theft in claims - if(GriefPrevention.instance.config_claims_preventTheft && entity instanceof Vehicle) - { - //if the entity is in a claim - Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null); - if(claim != null) - { - //for storage and powered minecarts, apply container rules (this is a potential theft) - if(entity instanceof StorageMinecart || entity instanceof PoweredMinecart) - { - String noContainersReason = claim.allowContainers(player); - if(noContainersReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); - event.setCancelled(true); - } - } - - //for boats, apply access rules - else if(entity instanceof Boat) - { - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - player.sendMessage(noAccessReason); - event.setCancelled(true); - } - } - - //if the entity is an animal, apply container rules - else if(entity instanceof Animals) - { - if(claim.allowContainers(player) != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity); - event.setCancelled(true); - } - } - } - } - } - - //when a player picks up an item... - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerPickupItem(PlayerPickupItemEvent event) - { - Player player = event.getPlayer(); - - if(!event.getPlayer().getWorld().getPVP()) return; - - //if we're preventing spawn camping and the player was previously empty handed... - if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (player.getItemInHand().getType() == Material.AIR)) - { - //if that player is currently immune to pvp - PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); - if(playerData.pvpImmune) - { - //if it's been less than 10 seconds since the last time he spawned, don't pick up the item - long now = Calendar.getInstance().getTimeInMillis(); - long elapsedSinceLastSpawn = now - playerData.lastSpawn; - if(elapsedSinceLastSpawn < 10000) - { - event.setCancelled(true); - return; - } - - //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot - playerData.pvpImmune = false; - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); - } - } - } - - //when a player switches in-hand items - @EventHandler(ignoreCancelled = true) - public void onItemHeldChange(PlayerItemHeldEvent event) - { - Player player = event.getPlayer(); - - //if he's switching to the golden shovel - ItemStack newItemStack = player.getInventory().getItem(event.getNewSlot()); - if(newItemStack != null && newItemStack.getType() == GriefPrevention.instance.config_claims_modificationTool) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); - - //always reset to basic claims mode - if(playerData.shovelMode != ShovelMode.Basic) - { - playerData.shovelMode = ShovelMode.Basic; - GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShovelBasicClaimMode); - } - - //reset any work he might have been doing - playerData.lastShovelLocation = null; - playerData.claimResizing = null; - - //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam - if(GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) - { - EquipShovelProcessingTask task = new EquipShovelProcessingTask(player); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 15L); //15L is approx. 3/4 of a second - } - } - } - - //block players from entering beds they don't have permission for - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent) - { - if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return; - - Player player = bedEvent.getPlayer(); - Block block = bedEvent.getBed(); - - //if the bed is in a claim - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); - if(claim != null) - { - //if the player doesn't have access in that claim, tell him so and prevent him from sleeping in the bed - if(claim.allowAccess(player) != null) - { - bedEvent.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoBedPermission, claim.getOwnerName()); - } - } - } - - //block use of buckets within other players' claims - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) - { - Player player = bucketEvent.getPlayer(); - Block block = bucketEvent.getBlockClicked().getRelative(bucketEvent.getBlockFace()); - int minLavaDistance = 10; - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - bucketEvent.setCancelled(true); - return; - } - - //if the bucket is being used in a claim, allow for dumping lava closer to other players - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - minLavaDistance = 3; - } - - //otherwise no wilderness dumping (unless underground) in worlds where claims are enabled - else if(GriefPrevention.instance.config_claims_enabledWorlds.contains(block.getWorld())) - { - if(block.getY() >= block.getWorld().getSeaLevel() - 5 && !player.hasPermission("griefprevention.lava")) - { - if(bucketEvent.getBucket() == Material.LAVA_BUCKET || GriefPrevention.instance.config_blockWildernessWaterBuckets) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); - bucketEvent.setCancelled(true); - return; - } - } - } - - //lava buckets can't be dumped near other players unless pvp is on - if(!block.getWorld().getPVP() && !player.hasPermission("griefprevention.lava")) - { - if(bucketEvent.getBucket() == Material.LAVA_BUCKET) - { - List players = block.getWorld().getPlayers(); - for(int i = 0; i < players.size(); i++) - { - Player otherPlayer = players.get(i); - Location location = otherPlayer.getLocation(); - if(!otherPlayer.equals(player) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, otherPlayer.getName()); - bucketEvent.setCancelled(true); - return; - } - } - } - } - } - - //see above - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) - { - Player player = bucketEvent.getPlayer(); - Block block = bucketEvent.getBlockClicked(); - - //make sure the player is allowed to build at the location - String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - bucketEvent.setCancelled(true); - return; - } - } - - //when a player interacts with the world - @EventHandler(priority = EventPriority.LOWEST) - void onPlayerInteract(PlayerInteractEvent event) - { - Player player = event.getPlayer(); - - //determine target block. FEATURE: shovel and string can be used from a distance away - Block clickedBlock = null; - - try - { - clickedBlock = event.getClickedBlock(); //null returned here means interacting with air - if(clickedBlock == null || clickedBlock.getType() == Material.SNOW) - { - //try to find a far away non-air block along line of sight - HashSet transparentMaterials = new HashSet(); - transparentMaterials.add(Byte.valueOf((byte)Material.AIR.getId())); - transparentMaterials.add(Byte.valueOf((byte)Material.SNOW.getId())); - transparentMaterials.add(Byte.valueOf((byte)Material.LONG_GRASS.getId())); - clickedBlock = player.getTargetBlock(transparentMaterials, 250); - } - } - catch(Exception e) //an exception intermittently comes from getTargetBlock(). when it does, just ignore the event - { - return; - } - - //if no block, stop here - if(clickedBlock == null) - { - return; - } - - Material clickedBlockType = clickedBlock.getType(); - - //apply rules for putting out fires (requires build permission) - PlayerData playerData = this.dataStore.getPlayerData(player.getName()); - if(event.getClickedBlock() != null && event.getClickedBlock().getRelative(event.getBlockFace()).getType() == Material.FIRE) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noBuildReason = claim.allowBuild(player); - if(noBuildReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - return; - } - } - } - - //apply rules for containers and crafting blocks - if( GriefPrevention.instance.config_claims_preventTheft && ( - event.getAction() == Action.RIGHT_CLICK_BLOCK && ( - clickedBlock.getState() instanceof InventoryHolder || - clickedBlockType == Material.WORKBENCH || - clickedBlockType == Material.ENDER_CHEST || - clickedBlockType == Material.DISPENSER || - clickedBlockType == Material.BREWING_STAND || - clickedBlockType == Material.JUKEBOX || - clickedBlockType == Material.ENCHANTMENT_TABLE || - GriefPrevention.instance.config_mods_containerTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))))) - { - //block container use while under siege, so players can't hide items from attackers - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); - event.setCancelled(true); - return; - } - - //block container use during pvp combat, same reason - if(playerData.inPvpCombat()) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers); - event.setCancelled(true); - return; - } - - //otherwise check permissions for the claim the player is in - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noContainersReason = claim.allowContainers(player); - if(noContainersReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); - return; - } - } - - //if the event hasn't been cancelled, then the player is allowed to use the container - //so drop any pvp protection - if(playerData.pvpImmune) - { - playerData.pvpImmune = false; - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); - } - } - - //otherwise apply rules for doors, if configured that way - else if((GriefPrevention.instance.config_claims_lockWoodenDoors && clickedBlockType == Material.WOODEN_DOOR) || - (GriefPrevention.instance.config_claims_lockTrapDoors && clickedBlockType == Material.TRAP_DOOR) || - (GriefPrevention.instance.config_claims_lockFenceGates && clickedBlockType == Material.FENCE_GATE)) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); - return; - } - } - } - - //otherwise apply rules for buttons and switches - else if(GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == null || clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.LEVER || GriefPrevention.instance.config_mods_accessTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null)))) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - playerData.lastClaim = claim; - - String noAccessReason = claim.allowAccess(player); - if(noAccessReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); - return; - } - } - } - - //apply rule for players trampling tilled soil back to dirt (never allow it) - //NOTE: that this event applies only to players. monsters and animals can still trample. - else if(event.getAction() == Action.PHYSICAL && clickedBlockType == Material.SOIL) - { - event.setCancelled(true); - return; - } - - //apply rule for note blocks and repeaters - else if(clickedBlockType == Material.NOTE_BLOCK || clickedBlockType == Material.DIODE_BLOCK_ON || clickedBlockType == Material.DIODE_BLOCK_OFF) - { - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - String noBuildReason = claim.allowBuild(player); - if(noBuildReason != null) - { - event.setCancelled(true); - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - return; - } - } - } - - //otherwise handle right click (shovel, string, bonemeal) - else - { - //ignore all actions except right-click on a block or in the air - Action action = event.getAction(); - if(action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; - - //what's the player holding? - Material materialInHand = player.getItemInHand().getType(); - - //if it's bonemeal or a boat, check for build permission (ink sac == bone meal, must be a Bukkit bug?) - if(materialInHand == Material.INK_SACK || materialInHand == Material.BOAT) - { - String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - } - - return; - } - - //if it's a spawn egg, minecart, or boat, and this is a creative world, apply special rules - else if((materialInHand == Material.MONSTER_EGG || materialInHand == Material.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.BOAT) && GriefPrevention.instance.creativeRulesApply(clickedBlock.getLocation())) - { - //player needs build permission at this location - String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); - if(noBuildReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); - event.setCancelled(true); - return; - } - - //enforce limit on total number of entities in this claim - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim == null) return; - - String noEntitiesReason = claim.allowMoreEntities(); - if(noEntitiesReason != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason); - event.setCancelled(true); - return; - } - - return; - } - - //if he's investigating a claim - else if(materialInHand == GriefPrevention.instance.config_claims_investigationTool) - { - //air indicates too far away - if(clickedBlockType == Material.AIR) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); - return; - } - - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, playerData.lastClaim); - - //no claim case - if(claim == null) - { - GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockNotClaimed); - Visualization.Revert(player); - } - - //claim case - else - { - playerData.lastClaim = claim; - GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockClaimed, claim.getOwnerName()); - - //visualize boundary - Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - - //if can resize this claim, tell about the boundaries - if(claim.allowEdit(player) == null) - { - GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea()); - } - - //if deleteclaims permission, tell about the player's offline time - if(!claim.isAdminClaim() && player.hasPermission("griefprevention.deleteclaims")) - { - PlayerData otherPlayerData = this.dataStore.getPlayerData(claim.getOwnerName()); - Date lastLogin = otherPlayerData.lastLogin; - Date now = new Date(); - long daysElapsed = (now.getTime() - lastLogin.getTime()) / (1000 * 60 * 60 * 24); - - GriefPrevention.sendMessage(player, TextMode.Info, Messages.PlayerOfflineTime, String.valueOf(daysElapsed)); - - //drop the data we just loaded, if the player isn't online - if(GriefPrevention.instance.getServer().getPlayerExact(claim.getOwnerName()) == null) - this.dataStore.clearCachedPlayerData(claim.getOwnerName()); - } - } - - return; - } - - //if it's a golden shovel - else if(materialInHand != GriefPrevention.instance.config_claims_modificationTool) return; - - //disable golden shovel while under siege - if(playerData.siegeData != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoShovel); - event.setCancelled(true); - return; - } - - //can't use the shovel from too far away - if(clickedBlockType == Material.AIR) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); - return; - } - - //if the player is in restore nature mode, do only that - String playerName = player.getName(); - playerData = this.dataStore.getPlayerData(player.getName()); - if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive) - { - //if the clicked block is in a claim, visualize that claim and deliver an error message - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); - if(claim != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName()); - Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - - return; - } - - //figure out which chunk to repair - Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); - - //start the repair process - - //set boundaries for processing - int miny = clickedBlock.getY(); - - //if not in aggressive mode, extend the selection down to a little below sea level - if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive)) - { - if(miny > chunk.getWorld().getSeaLevel() - 10) - { - miny = chunk.getWorld().getSeaLevel() - 10; - } - } - - GriefPrevention.instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player); - - return; - } - - //if in restore nature fill mode - if(playerData.shovelMode == ShovelMode.RestoreNatureFill) - { - ArrayList allowedFillBlocks = new ArrayList(); - Environment environment = clickedBlock.getWorld().getEnvironment(); - if(environment == Environment.NETHER) - { - allowedFillBlocks.add(Material.NETHERRACK); - } - else if(environment == Environment.THE_END) - { - allowedFillBlocks.add(Material.ENDER_STONE); - } - else - { - allowedFillBlocks.add(Material.GRASS); - allowedFillBlocks.add(Material.DIRT); - allowedFillBlocks.add(Material.STONE); - allowedFillBlocks.add(Material.SAND); - allowedFillBlocks.add(Material.SANDSTONE); - allowedFillBlocks.add(Material.ICE); - } - - Block centerBlock = clickedBlock; - - int maxHeight = centerBlock.getY(); - int minx = centerBlock.getX() - playerData.fillRadius; - int maxx = centerBlock.getX() + playerData.fillRadius; - int minz = centerBlock.getZ() - playerData.fillRadius; - int maxz = centerBlock.getZ() + playerData.fillRadius; - int minHeight = maxHeight - 10; - if(minHeight < 0) minHeight = 0; - - Claim cachedClaim = null; - for(int x = minx; x <= maxx; x++) - { - for(int z = minz; z <= maxz; z++) - { - //circular brush - Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z); - if(location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue; - - //default fill block is initially the first from the allowed fill blocks list above - Material defaultFiller = allowedFillBlocks.get(0); - - //prefer to use the block the player clicked on, if it's an acceptable fill block - if(allowedFillBlocks.contains(centerBlock.getType())) - { - defaultFiller = centerBlock.getType(); - } - - //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler - else if(centerBlock.getType() == Material.WATER || centerBlock.getType() == Material.STATIONARY_WATER) - { - Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation()); - while(!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10) - { - block = block.getRelative(BlockFace.DOWN); - } - if(allowedFillBlocks.contains(block.getType())) - { - defaultFiller = block.getType(); - } - } - - //fill bottom to top - for(int y = minHeight; y <= maxHeight; y++) - { - Block block = centerBlock.getWorld().getBlockAt(x, y, z); - - //respect claims - Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim); - if(claim != null) - { - cachedClaim = claim; - break; - } - - //only replace air, spilling water, snow, long grass - if(block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.STATIONARY_WATER && block.getData() != 0) || block.getType() == Material.LONG_GRASS) - { - //if the top level, always use the default filler picked above - if(y == maxHeight) - { - block.setType(defaultFiller); - } - - //otherwise look to neighbors for an appropriate fill block - else - { - Block eastBlock = block.getRelative(BlockFace.EAST); - Block westBlock = block.getRelative(BlockFace.WEST); - Block northBlock = block.getRelative(BlockFace.NORTH); - Block southBlock = block.getRelative(BlockFace.SOUTH); - - //first, check lateral neighbors (ideally, want to keep natural layers) - if(allowedFillBlocks.contains(eastBlock.getType())) - { - block.setType(eastBlock.getType()); - } - else if(allowedFillBlocks.contains(westBlock.getType())) - { - block.setType(westBlock.getType()); - } - else if(allowedFillBlocks.contains(northBlock.getType())) - { - block.setType(northBlock.getType()); - } - else if(allowedFillBlocks.contains(southBlock.getType())) - { - block.setType(southBlock.getType()); - } - - //if all else fails, use the default filler selected above - else - { - block.setType(defaultFiller); - } - } - } - } - } - } - - return; - } - - //if the player doesn't have claims permission, don't do anything - if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims")) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission); - return; - } - - //if he's resizing a claim and that claim hasn't been deleted since he started resizing it - if(playerData.claimResizing != null && playerData.claimResizing.inDataStore) - { - if(clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return; - - //figure out what the coords of his new claim would be - int newx1, newx2, newz1, newz2, newy1, newy2; - if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX()) - { - newx1 = clickedBlock.getX(); - } - else - { - newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX(); - } - - if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockX()) - { - newx2 = clickedBlock.getX(); - } - else - { - newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX(); - } - - if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ()) - { - newz1 = clickedBlock.getZ(); - } - else - { - newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ(); - } - - if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ()) - { - newz2 = clickedBlock.getZ(); - } - else - { - newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ(); - } - - newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY(); - newy2 = clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance; - - //for top level claims, apply size rules and claim blocks requirement - if(playerData.claimResizing.parent == null) - { - //measure new claim, apply size rules - int newWidth = (Math.abs(newx1 - newx2) + 1); - int newHeight = (Math.abs(newz1 - newz2) + 1); - - if(!playerData.claimResizing.isAdminClaim() && (newWidth < GriefPrevention.instance.config_claims_minSize || newHeight < GriefPrevention.instance.config_claims_minSize)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); - return; - } - - //make sure player has enough blocks to make up the difference - if(!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) - { - int newArea = newWidth * newHeight; - int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; - - if(blocksRemainingAfter < 0) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); - return; - } - } - } - - //special rules for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. - //rule1: in creative mode, top-level claims can't be moved or resized smaller. - //rule2: in any mode, shrinking a claim removes any surface fluids - Claim oldClaim = playerData.claimResizing; - boolean smaller = false; - if(oldClaim.parent == null) - { - //temporary claim instance, just for checking contains() - Claim newClaim = new Claim( - new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), - new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), - "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null); - - //if the new claim is smaller - if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) - { - smaller = true; - - //enforce creative mode rule - if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && !player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation())) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); - return; - } - - //remove surface fluids about to be unclaimed - oldClaim.removeSurfaceFluids(newClaim); - } - } - - //ask the datastore to try and resize the claim, this checks for conflicts with other claims - CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim(playerData.claimResizing, newx1, newx2, newy1, newy2, newz1, newz2); - - if(result.succeeded) - { - //inform and show the player - GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(playerData.getRemainingClaimBlocks())); - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - - //if resizing someone else's claim, make a log entry - if(!playerData.claimResizing.ownerName.equals(playerName)) - { - GriefPrevention.AddLogEntry(playerName + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); - } - - //if in a creative mode world and shrinking an existing claim, restore any unclaimed area - if(smaller && GriefPrevention.instance.creativeRulesApply(oldClaim.getLesserBoundaryCorner())) - { - GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); - GriefPrevention.instance.restoreClaim(oldClaim, 20L * 60 * 2); //2 minutes - GriefPrevention.AddLogEntry(player.getName() + " shrank a claim @ " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.getLesserBoundaryCorner())); - } - - //clean up - playerData.claimResizing = null; - playerData.lastShovelLocation = null; - } - else - { - //inform player - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); - - //show the player the conflicting claim - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - - return; - } - - //otherwise, since not currently resizing a claim, must be starting a resize, creating a new claim, or creating a subdivision - Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), true /*ignore height*/, playerData.lastClaim); - - //if within an existing claim, he's not creating a new one - if(claim != null) - { - //if the player has permission to edit the claim or subdivision - String noEditReason = claim.allowEdit(player); - if(noEditReason == null) - { - //if he clicked on a corner, start resizing it - if((clickedBlock.getX() == claim.getLesserBoundaryCorner().getBlockX() || clickedBlock.getX() == claim.getGreaterBoundaryCorner().getBlockX()) && (clickedBlock.getZ() == claim.getLesserBoundaryCorner().getBlockZ() || clickedBlock.getZ() == claim.getGreaterBoundaryCorner().getBlockZ())) - { - playerData.claimResizing = claim; - playerData.lastShovelLocation = clickedBlock.getLocation(); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ResizeStart); - } - - //if he didn't click on a corner and is in subdivision mode, he's creating a new subdivision - else if(playerData.shovelMode == ShovelMode.Subdivide) - { - //if it's the first click, he's trying to start a new subdivision - if(playerData.lastShovelLocation == null) - { - //if the clicked claim was a subdivision, tell him he can't start a new subdivision here - if(claim.parent != null) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapSubdivision); - } - - //otherwise start a new subdivision - else - { - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionStart); - playerData.lastShovelLocation = clickedBlock.getLocation(); - playerData.claimSubdividing = claim; - } - } - - //otherwise, he's trying to finish creating a subdivision by setting the other boundary corner - else - { - //if last shovel location was in a different world, assume the player is starting the create-claim workflow over - if(!playerData.lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) - { - playerData.lastShovelLocation = null; - this.onPlayerInteract(event); - return; - } - - //try to create a new claim (will return null if this subdivision overlaps another) - CreateClaimResult result = this.dataStore.createClaim( - player.getWorld(), - playerData.lastShovelLocation.getBlockX(), clickedBlock.getX(), - playerData.lastShovelLocation.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, - playerData.lastShovelLocation.getBlockZ(), clickedBlock.getZ(), - "--subdivision--", //owner name is not used for subdivisions - playerData.claimSubdividing, - null); - - //if it didn't succeed, tell the player why - if(!result.succeeded) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap); - - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - - return; - } - - //otherwise, advise him on the /trust command and show him his new subdivision - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess); - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - playerData.lastShovelLocation = null; - playerData.claimSubdividing = null; - } - } - } - - //otherwise tell him he can't create a claim here, and show him the existing claim - //also advise him to consider /abandonclaim or resizing the existing claim - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlap); - Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - } - } - - //otherwise tell the player he can't claim here because it's someone else's claim, and show him the claim - else - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapOtherPlayer, claim.getOwnerName()); - Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - } - - return; - } - - //otherwise, the player isn't in an existing claim! - - //if he hasn't already start a claim with a previous shovel action - Location lastShovelLocation = playerData.lastShovelLocation; - if(lastShovelLocation == null) - { - //if claims are not enabled in this world and it's not an administrative claim, display an error message and stop - if(!GriefPrevention.instance.claimsEnabledForWorld(player.getWorld()) && playerData.shovelMode != ShovelMode.Admin) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); - return; - } - - //remember it, and start him on the new claim - playerData.lastShovelLocation = clickedBlock.getLocation(); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimStart); - - //show him where he's working - Visualization visualization = Visualization.FromClaim(new Claim(clickedBlock.getLocation(), clickedBlock.getLocation(), "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null), clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation()); - Visualization.Apply(player, visualization); - } - - //otherwise, he's trying to finish creating a claim by setting the other boundary corner - else - { - //if last shovel location was in a different world, assume the player is starting the create-claim workflow over - if(!lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) - { - playerData.lastShovelLocation = null; - this.onPlayerInteract(event); - return; - } - - //apply minimum claim dimensions rule - int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1; - int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1; - - if(playerData.shovelMode != ShovelMode.Admin && (newClaimWidth < GriefPrevention.instance.config_claims_minSize || newClaimHeight < GriefPrevention.instance.config_claims_minSize)) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); - return; - } - - //if not an administrative claim, verify the player has enough claim blocks for this new claim - if(playerData.shovelMode != ShovelMode.Admin) - { - int newClaimArea = newClaimWidth * newClaimHeight; - int remainingBlocks = playerData.getRemainingClaimBlocks(); - if(newClaimArea > remainingBlocks) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks)); - GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimAdvertisement); - return; - } - } - else - { - playerName = ""; - } - - //try to create a new claim (will return null if this claim overlaps another) - CreateClaimResult result = this.dataStore.createClaim( - player.getWorld(), - lastShovelLocation.getBlockX(), clickedBlock.getX(), - lastShovelLocation.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, - lastShovelLocation.getBlockZ(), clickedBlock.getZ(), - playerName, - null, null); - - //if it didn't succeed, tell the player why - if(!result.succeeded) - { - GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); - - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); - Visualization.Apply(player, visualization); - - return; - } - - //otherwise, advise him on the /trust command and show him his new claim - else - { - GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess); - Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); - Visualization.Apply(player, visualization); - playerData.lastShovelLocation = null; - } - } - } - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World.Environment; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Boat; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Hanging; +import org.bukkit.entity.PoweredMinecart; +import org.bukkit.entity.StorageMinecart; +import org.bukkit.entity.Player; +import org.bukkit.entity.Vehicle; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.*; +import org.bukkit.event.player.PlayerLoginEvent.Result; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +class PlayerEventHandler implements Listener +{ + private DataStore dataStore; + + //list of temporarily banned ip's + private ArrayList tempBannedIps = new ArrayList(); + + //number of milliseconds in a day + private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; + + //timestamps of login and logout notifications in the last minute + private ArrayList recentLoginLogoutNotifications = new ArrayList(); + + //regex pattern for the "how do i claim land?" scanner + private Pattern howToClaimPattern = null; + + //typical constructor, yawn + PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) + { + this.dataStore = dataStore; + } + + //when a player chats, monitor for spam + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + void onPlayerChat (AsyncPlayerChatEvent event) + { + Player player = event.getPlayer(); + if(!player.isOnline()) + { + event.setCancelled(true); + return; + } + + String message = event.getMessage(); + + event.setCancelled(this.handlePlayerChat(player, message, event)); + } + + //returns true if the message should be sent, false if it should be muted + private boolean handlePlayerChat(Player player, String message, PlayerEvent event) + { + //FEATURE: automatically educate players about claiming land + //watching for message format how*claim*, and will send a link to the basics video + if(this.howToClaimPattern == null) + { + this.howToClaimPattern = Pattern.compile(this.dataStore.getMessage(Messages.HowToClaimRegex), Pattern.CASE_INSENSITIVE); + } + + if(this.howToClaimPattern.matcher(message).matches()) + { + if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.CreativeBasicsDemoAdvertisement, 10L); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.SurvivalBasicsDemoAdvertisement, 10L); + } + } + + //FEATURE: automatically educate players about the /trapped command + //check for "trapped" or "stuck" to educate players about the /trapped command + if(message.contains("trapped") || message.contains("stuck") || message.contains(this.dataStore.getMessage(Messages.TrappedChatKeyword))) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.TrappedInstructions, 10L); + } + + //FEATURE: monitor for chat and command spam + + if(!GriefPrevention.instance.config_spam_enabled) return false; + + //if the player has permission to spam, don't bother even examining the message + if(player.hasPermission("griefprevention.spam")) return false; + + //remedy any CAPS SPAM without bothering to fault the player for it + if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message)) + { + if(event instanceof AsyncPlayerChatEvent) + { + ((AsyncPlayerChatEvent)event).setMessage(message.toLowerCase()); + } + } + + //where other types of spam are concerned, casing isn't significant + message = message.toLowerCase(); + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + boolean spam = false; + boolean muted = false; + + //check message content and timing + long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime(); + + //if the message came too close to the last one + if(millisecondsSinceLastMessage < 2000) + { + //increment the spam counter + playerData.spamCount++; + spam = true; + } + + //if it's very similar to the last message + if(!muted && this.stringsAreSimilar(message, playerData.lastMessage)) + { + playerData.spamCount++; + spam = true; + muted = true; + } + + //filter IP addresses + if(!muted) + { + Pattern ipAddressPattern = Pattern.compile("\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}\\D{1,3}\\d{1,4}"); + Matcher matcher = ipAddressPattern.matcher(message); + + //if it looks like an IP address + while(matcher.find()) + { + //and it's not in the list of allowed IP addresses + if(!GriefPrevention.instance.config_spam_allowedIpAddresses.contains(matcher.group())) + { + //log entry + GriefPrevention.AddLogEntry("Muted IP address from " + player.getName() + ": " + message); + + //spam notation + playerData.spamCount++; + spam = true; + + //block message + muted = true; + } + } + } + + //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish) + if(!muted && message.length() > 5) + { + int symbolsCount = 0; + int whitespaceCount = 0; + for(int i = 0; i < message.length(); i++) + { + char character = message.charAt(i); + if(!(Character.isLetterOrDigit(character))) + { + symbolsCount++; + } + + if(Character.isWhitespace(character)) + { + whitespaceCount++; + } + } + + if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10)) + { + spam = true; + if(playerData.spamCount > 0) muted = true; + playerData.spamCount++; + } + } + + //very short messages close together are spam + if(!muted && message.length() < 5 && millisecondsSinceLastMessage < 5000) + { + spam = true; + playerData.spamCount++; + } + + //if the message was determined to be a spam, consider taking action + if(!player.hasPermission("griefprevention.spam") && spam) + { + //anything above level 8 for a player which has received a warning... kick or if enabled, ban + if(playerData.spamCount > 8 && playerData.spamWarned) + { + if(GriefPrevention.instance.config_spam_banOffenders) + { + //log entry + GriefPrevention.AddLogEntry("Banning " + player.getName() + " for spam."); + + //ban + GriefPrevention.instance.getServer().getOfflinePlayer(player.getName()).setBanned(true); + + //kick + player.kickPlayer(GriefPrevention.instance.config_spam_banMessage); + } + else + { + player.kickPlayer(""); + } + + return true; + } + + //cancel any messages while at or above the third spam level and issue warnings + //anything above level 2, mute and warn + if(playerData.spamCount >= 3) + { + muted = true; + if(!playerData.spamWarned) + { + GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage, 10L); + GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); + playerData.spamWarned = true; + } + } + + if(muted) + { + //make a log entry + GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); + + //send a fake message so the player doesn't realize he's muted + //less information for spammers = less effective spam filter dodging + player.sendMessage("<" + player.getName() + "> " + message); + + //cancelling the event guarantees other players don't receive the message + return true; + } + } + + //otherwise if not a spam, reset the spam counter for this player + else + { + playerData.spamCount = 0; + playerData.spamWarned = false; + } + + //in any case, record the timestamp of this message and also its content for next time + playerData.lastMessageTimestamp = new Date(); + playerData.lastMessage = message; + + return false; + } + + //if two strings are 75% identical, they're too close to follow each other in the chat + private boolean stringsAreSimilar(String message, String lastMessage) + { + //determine which is shorter + String shorterString, longerString; + if(lastMessage.length() < message.length()) + { + shorterString = lastMessage; + longerString = message; + } + else + { + shorterString = message; + longerString = lastMessage; + } + + if(shorterString.length() <= 5) return shorterString.equals(longerString); + + //set similarity tolerance + int maxIdenticalCharacters = longerString.length() - longerString.length() / 4; + + //trivial check on length + if(shorterString.length() < maxIdenticalCharacters) return false; + + //compare forward + int identicalCount = 0; + for(int i = 0; i < shorterString.length(); i++) + { + if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++; + if(identicalCount > maxIdenticalCharacters) return true; + } + + //compare backward + for(int i = 0; i < shorterString.length(); i++) + { + if(shorterString.charAt(shorterString.length() - i - 1) == longerString.charAt(longerString.length() - i - 1)) identicalCount++; + if(identicalCount > maxIdenticalCharacters) return true; + } + + return false; + } + + //when a player uses a slash command... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) + { + String [] args = event.getMessage().split(" "); + + //if eavesdrop enabled, eavesdrop + String command = args[0].toLowerCase(); + if(GriefPrevention.instance.config_eavesdrop && GriefPrevention.instance.config_eavesdrop_whisperCommands.contains(command) && !event.getPlayer().hasPermission("griefprevention.eavesdrop") && args.length > 1) + { + StringBuilder logMessageBuilder = new StringBuilder(); + logMessageBuilder.append("[[").append(event.getPlayer().getName()).append("]] "); + + for(int i = 1; i < args.length; i++) + { + logMessageBuilder.append(args[i]).append(" "); + } + + String logMessage = logMessageBuilder.toString(); + + GriefPrevention.AddLogEntry(logMessage.toString()); + + Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); + for(int i = 0; i < players.length; i++) + { + Player player = players[i]; + if(player.hasPermission("griefprevention.eavesdrop") && !player.getName().equalsIgnoreCase(args[1])) + { + player.sendMessage(ChatColor.GRAY + logMessage); + } + } + } + + //if in pvp, block any pvp-banned slash commands + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if(playerData.inPvpCombat() && GriefPrevention.instance.config_pvp_blockedCommands.contains(command)) + { + event.setCancelled(true); + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, Messages.CommandBannedInPvP); + return; + } + + //if anti spam enabled, check for spam + if(!GriefPrevention.instance.config_spam_enabled) return; + + //if the slash command used is in the list of monitored commands, treat it like a chat message (see above) + if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains(args[0])) + { + event.setCancelled(this.handlePlayerChat(event.getPlayer(), event.getMessage(), event)); + } + } + + //when a player attempts to join the server... + @EventHandler(priority = EventPriority.HIGHEST) + void onPlayerLogin (PlayerLoginEvent event) + { + Player player = event.getPlayer(); + + //all this is anti-spam code + if(GriefPrevention.instance.config_spam_enabled) + { + //FEATURE: login cooldown to prevent login/logout spam with custom clients + + //if allowed to join and login cooldown enabled + if(GriefPrevention.instance.config_spam_loginCooldownMinutes > 0 && event.getResult() == Result.ALLOWED) + { + //determine how long since last login and cooldown remaining + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + long millisecondsSinceLastLogin = (new Date()).getTime() - playerData.lastLogin.getTime(); + long minutesSinceLastLogin = millisecondsSinceLastLogin / 1000 / 60; + long cooldownRemaining = GriefPrevention.instance.config_spam_loginCooldownMinutes - minutesSinceLastLogin; + + //if cooldown remaining and player doesn't have permission to spam + if(cooldownRemaining > 0 && !player.hasPermission("griefprevention.spam")) + { + //DAS BOOT! + event.setResult(Result.KICK_OTHER); + event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again."); + event.disallow(event.getResult(), event.getKickMessage()); + return; + } + } + } + + //remember the player's ip address + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.ipAddress = event.getAddress(); + + //FEATURE: auto-ban accounts who use an IP address which was very recently used by another banned account + if(GriefPrevention.instance.config_smartBan && !player.hasPlayedBefore()) + { + //if logging-in account is banned, remember IP address for later + long now = Calendar.getInstance().getTimeInMillis(); + if(event.getResult() == Result.KICK_BANNED) + { + this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName())); + } + + //otherwise if not banned + else + { + //search temporarily banned IP addresses for this one + for(int i = 0; i < this.tempBannedIps.size(); i++) + { + IpBanInfo info = this.tempBannedIps.get(i); + String address = info.address.toString(); + + //eliminate any expired entries + if(now > info.expirationTimestamp) + { + this.tempBannedIps.remove(i--); + } + + //if we find a match + else if(address.equals(playerData.ipAddress.toString())) + { + //if the account associated with the IP ban has been pardoned, remove all ip bans for that ip and we're done + OfflinePlayer bannedPlayer = GriefPrevention.instance.getServer().getOfflinePlayer(info.bannedAccountName); + if(!bannedPlayer.isBanned()) + { + for(int j = 0; j < this.tempBannedIps.size(); j++) + { + IpBanInfo info2 = this.tempBannedIps.get(j); + if(info2.address.toString().equals(address)) + { + OfflinePlayer bannedAccount = GriefPrevention.instance.getServer().getOfflinePlayer(info2.bannedAccountName); + bannedAccount.setBanned(false); + this.tempBannedIps.remove(j--); + } + } + + break; + } + + //otherwise if that account is still banned, ban this account, too + else + { + player.setBanned(true); + event.setResult(Result.KICK_BANNED); + event.disallow(event.getResult(), ""); + GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ")."); + + //notify any online ops + Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); + for(int k = 0; k < players.length; k++) + { + if(players[k].isOp()) + { + GriefPrevention.sendMessage(players[k], TextMode.Success, Messages.AutoBanNotify, player.getName(), info.bannedAccountName); + } + } + + break; + } + } + } + } + } + } + + //when a player spawns, conditionally apply temporary pvp protection + @EventHandler(ignoreCancelled = true) + void onPlayerRespawn (PlayerRespawnEvent event) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(event.getPlayer().getName()); + playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); + GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); + } + + //when a player successfully joins the server... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + void onPlayerJoin(PlayerJoinEvent event) + { + String playerName = event.getPlayer().getName(); + + //note login time + PlayerData playerData = this.dataStore.getPlayerData(playerName); + playerData.lastSpawn = Calendar.getInstance().getTimeInMillis(); + playerData.lastLogin = new Date(); + this.dataStore.savePlayerData(playerName, playerData); + + //if player has never played on the server before, may need pvp protection + if(!event.getPlayer().hasPlayedBefore()) + { + GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); + } + + //silence notifications when they're coming too fast + if(event.getJoinMessage() != null && this.shouldSilenceNotification()) + { + event.setJoinMessage(null); + } + } + + //when a player quits... + @EventHandler(priority = EventPriority.HIGHEST) + void onPlayerQuit(PlayerQuitEvent event) + { + Player player = event.getPlayer(); + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //if banned, add IP to the temporary IP ban list + if(player.isBanned() && playerData.ipAddress != null) + { + long now = Calendar.getInstance().getTimeInMillis(); + this.tempBannedIps.add(new IpBanInfo(playerData.ipAddress, now + this.MILLISECONDS_IN_DAY, player.getName())); + } + + //silence notifications when they're coming too fast + if(event.getQuitMessage() != null && this.shouldSilenceNotification()) + { + event.setQuitMessage(null); + } + + //make sure his data is all saved - he might have accrued some claim blocks while playing that were not saved immediately + this.dataStore.savePlayerData(player.getName(), playerData); + + this.onPlayerDisconnect(event.getPlayer(), event.getQuitMessage()); + } + + //helper for above + private void onPlayerDisconnect(Player player, String notificationMessage) + { + String playerName = player.getName(); + PlayerData playerData = this.dataStore.getPlayerData(playerName); + + //FEATURE: players in pvp combat when they log out will die + if(GriefPrevention.instance.config_pvp_punishLogout && playerData.inPvpCombat()) + { + player.setHealth(0); + } + + //FEATURE: during a siege, any player who logs out dies and forfeits the siege + + //if player was involved in a siege, he forfeits + if(playerData.siegeData != null) + { + if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message + } + + //drop data about this player + this.dataStore.clearCachedPlayerData(player.getName()); + } + + //determines whether or not a login or logout notification should be silenced, depending on how many there have been in the last minute + private boolean shouldSilenceNotification() + { + final long ONE_MINUTE = 60000; + final int MAX_ALLOWED = 20; + Long now = Calendar.getInstance().getTimeInMillis(); + + //eliminate any expired entries (longer than a minute ago) + for(int i = 0; i < this.recentLoginLogoutNotifications.size(); i++) + { + Long notificationTimestamp = this.recentLoginLogoutNotifications.get(i); + if(now - notificationTimestamp > ONE_MINUTE) + { + this.recentLoginLogoutNotifications.remove(i--); + } + else + { + break; + } + } + + //add the new entry + this.recentLoginLogoutNotifications.add(now); + + return this.recentLoginLogoutNotifications.size() > MAX_ALLOWED; + } + + //when a player drops an item + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerDropItem(PlayerDropItemEvent event) + { + Player player = event.getPlayer(); + + //in creative worlds, dropping items is blocked + if(GriefPrevention.instance.creativeRulesApply(player.getLocation())) + { + event.setCancelled(true); + return; + } + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //FEATURE: players under siege or in PvP combat, can't throw items on the ground to hide + //them or give them away to other players before they are defeated + + //if in combat, don't let him drop it + if(!GriefPrevention.instance.config_pvp_allowCombatItemDrop && playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoDrop); + event.setCancelled(true); + } + + //if he's under siege, don't let him drop it + else if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoDrop); + event.setCancelled(true); + } + } + + //when a player teleports + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerTeleport(PlayerTeleportEvent event) + { + //FEATURE: prevent teleport abuse to win sieges + + //these rules only apply to non-ender-pearl teleportation + if(event.getCause() == TeleportCause.ENDER_PEARL) return; + + Player player = event.getPlayer(); + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoTeleportPvPCombat); + event.setCancelled(true); + return; + } + + Location source = event.getFrom(); + Claim sourceClaim = this.dataStore.getClaimAt(source, false, null); + if(sourceClaim != null && sourceClaim.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoTeleport); + event.setCancelled(true); + return; + } + + Location destination = event.getTo(); + Claim destinationClaim = this.dataStore.getClaimAt(destination, false, null); + if(destinationClaim != null && destinationClaim.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BesiegedNoTeleport); + event.setCancelled(true); + return; + } + } + + //when a player interacts with an entity... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) + { + Player player = event.getPlayer(); + Entity entity = event.getRightClicked(); + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //don't allow interaction with item frames in claimed areas without build permission + if(entity instanceof Hanging) + { + String noBuildReason = GriefPrevention.instance.allowBuild(player, entity.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + return; + } + } + + //don't allow container access during pvp combat + if((entity instanceof StorageMinecart || entity instanceof PoweredMinecart)) + { + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); + event.setCancelled(true); + return; + } + + if(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers); + event.setCancelled(true); + return; + } + } + + //if the entity is a vehicle and we're preventing theft in claims + if(GriefPrevention.instance.config_claims_preventTheft && entity instanceof Vehicle) + { + //if the entity is in a claim + Claim claim = this.dataStore.getClaimAt(entity.getLocation(), false, null); + if(claim != null) + { + //for storage and powered minecarts, apply container rules (this is a potential theft) + if(entity instanceof StorageMinecart || entity instanceof PoweredMinecart) + { + String noContainersReason = claim.allowContainers(player); + if(noContainersReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); + event.setCancelled(true); + } + } + + //for boats, apply access rules + else if(entity instanceof Boat) + { + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + player.sendMessage(noAccessReason); + event.setCancelled(true); + } + } + + //if the entity is an animal, apply container rules + else if(entity instanceof Animals) + { + if(claim.allowContainers(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoDamageClaimedEntity); + event.setCancelled(true); + } + } + } + } + } + + //when a player picks up an item... + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerPickupItem(PlayerPickupItemEvent event) + { + Player player = event.getPlayer(); + + if(!event.getPlayer().getWorld().getPVP()) return; + + //if we're preventing spawn camping and the player was previously empty handed... + if(GriefPrevention.instance.config_pvp_protectFreshSpawns && (player.getItemInHand().getType() == Material.AIR)) + { + //if that player is currently immune to pvp + PlayerData playerData = this.dataStore.getPlayerData(event.getPlayer().getName()); + if(playerData.pvpImmune) + { + //if it's been less than 10 seconds since the last time he spawned, don't pick up the item + long now = Calendar.getInstance().getTimeInMillis(); + long elapsedSinceLastSpawn = now - playerData.lastSpawn; + if(elapsedSinceLastSpawn < 10000) + { + event.setCancelled(true); + return; + } + + //otherwise take away his immunity. he may be armed now. at least, he's worth killing for some loot + playerData.pvpImmune = false; + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); + } + } + } + + //when a player switches in-hand items + @EventHandler(ignoreCancelled = true) + public void onItemHeldChange(PlayerItemHeldEvent event) + { + Player player = event.getPlayer(); + + //if he's switching to the golden shovel + ItemStack newItemStack = player.getInventory().getItem(event.getNewSlot()); + if(newItemStack != null && newItemStack.getType() == GriefPrevention.instance.config_claims_modificationTool) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + + //always reset to basic claims mode + if(playerData.shovelMode != ShovelMode.Basic) + { + playerData.shovelMode = ShovelMode.Basic; + GriefPrevention.sendMessage(player, TextMode.Info, Messages.ShovelBasicClaimMode); + } + + //reset any work he might have been doing + playerData.lastShovelLocation = null; + playerData.claimResizing = null; + + //give the player his available claim blocks count and claiming instructions, but only if he keeps the shovel equipped for a minimum time, to avoid mouse wheel spam + if(GriefPrevention.instance.claimsEnabledForWorld(player.getWorld())) + { + EquipShovelProcessingTask task = new EquipShovelProcessingTask(player); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 15L); //15L is approx. 3/4 of a second + } + } + } + + //block players from entering beds they don't have permission for + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent) + { + if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return; + + Player player = bedEvent.getPlayer(); + Block block = bedEvent.getBed(); + + //if the bed is in a claim + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null) + { + //if the player doesn't have access in that claim, tell him so and prevent him from sleeping in the bed + if(claim.allowAccess(player) != null) + { + bedEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoBedPermission, claim.getOwnerName()); + } + } + } + + //block use of buckets within other players' claims + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) + { + Player player = bucketEvent.getPlayer(); + Block block = bucketEvent.getBlockClicked().getRelative(bucketEvent.getBlockFace()); + int minLavaDistance = 10; + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + bucketEvent.setCancelled(true); + return; + } + + //if the bucket is being used in a claim, allow for dumping lava closer to other players + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + minLavaDistance = 3; + } + + //otherwise no wilderness dumping (unless underground) in worlds where claims are enabled + else if(GriefPrevention.instance.config_claims_enabledWorlds.contains(block.getWorld())) + { + if(block.getY() >= GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5 && !player.hasPermission("griefprevention.lava")) + { + if(bucketEvent.getBucket() == Material.LAVA_BUCKET || GriefPrevention.instance.config_blockWildernessWaterBuckets) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoWildernessBuckets); + bucketEvent.setCancelled(true); + return; + } + } + } + + //lava buckets can't be dumped near other players unless pvp is on + if(!block.getWorld().getPVP() && !player.hasPermission("griefprevention.lava")) + { + if(bucketEvent.getBucket() == Material.LAVA_BUCKET) + { + List players = block.getWorld().getPlayers(); + for(int i = 0; i < players.size(); i++) + { + Player otherPlayer = players.get(i); + Location location = otherPlayer.getLocation(); + if(!otherPlayer.equals(player) && block.getY() >= location.getBlockY() - 1 && location.distanceSquared(block.getLocation()) < minLavaDistance * minLavaDistance) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoLavaNearOtherPlayer, otherPlayer.getName()); + bucketEvent.setCancelled(true); + return; + } + } + } + } + } + + //see above + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) + { + Player player = bucketEvent.getPlayer(); + Block block = bucketEvent.getBlockClicked(); + + //make sure the player is allowed to build at the location + String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + bucketEvent.setCancelled(true); + return; + } + } + + //when a player interacts with the world + @EventHandler(priority = EventPriority.LOWEST) + void onPlayerInteract(PlayerInteractEvent event) + { + Player player = event.getPlayer(); + + //determine target block. FEATURE: shovel and string can be used from a distance away + Block clickedBlock = null; + + try + { + clickedBlock = event.getClickedBlock(); //null returned here means interacting with air + if(clickedBlock == null || clickedBlock.getType() == Material.SNOW) + { + //try to find a far away non-air block along line of sight + HashSet transparentMaterials = new HashSet(); + transparentMaterials.add(Byte.valueOf((byte)Material.AIR.getId())); + transparentMaterials.add(Byte.valueOf((byte)Material.SNOW.getId())); + transparentMaterials.add(Byte.valueOf((byte)Material.LONG_GRASS.getId())); + clickedBlock = player.getTargetBlock(transparentMaterials, 250); + } + } + catch(Exception e) //an exception intermittently comes from getTargetBlock(). when it does, just ignore the event + { + return; + } + + //if no block, stop here + if(clickedBlock == null) + { + return; + } + + Material clickedBlockType = clickedBlock.getType(); + + //apply rules for putting out fires (requires build permission) + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(event.getClickedBlock() != null && event.getClickedBlock().getRelative(event.getBlockFace()).getType() == Material.FIRE) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noBuildReason = claim.allowBuild(player); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + return; + } + } + } + + //apply rules for containers and crafting blocks + if( GriefPrevention.instance.config_claims_preventTheft && ( + event.getAction() == Action.RIGHT_CLICK_BLOCK && ( + clickedBlock.getState() instanceof InventoryHolder || + clickedBlockType == Material.WORKBENCH || + clickedBlockType == Material.ENDER_CHEST || + clickedBlockType == Material.DISPENSER || + clickedBlockType == Material.ANVIL || + clickedBlockType == Material.BREWING_STAND || + clickedBlockType == Material.JUKEBOX || + clickedBlockType == Material.ENCHANTMENT_TABLE || + GriefPrevention.instance.config_mods_containerTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null))))) + { + //block container use while under siege, so players can't hide items from attackers + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoContainers); + event.setCancelled(true); + return; + } + + //block container use during pvp combat, same reason + if(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.PvPNoContainers); + event.setCancelled(true); + return; + } + + //otherwise check permissions for the claim the player is in + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noContainersReason = claim.allowContainers(player); + if(noContainersReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noContainersReason); + return; + } + } + + //if the event hasn't been cancelled, then the player is allowed to use the container + //so drop any pvp protection + if(playerData.pvpImmune) + { + playerData.pvpImmune = false; + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd); + } + } + + //otherwise apply rules for doors, if configured that way + else if((GriefPrevention.instance.config_claims_lockWoodenDoors && clickedBlockType == Material.WOODEN_DOOR) || + (GriefPrevention.instance.config_claims_lockTrapDoors && clickedBlockType == Material.TRAP_DOOR) || + (GriefPrevention.instance.config_claims_lockFenceGates && clickedBlockType == Material.FENCE_GATE)) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); + return; + } + } + } + + //otherwise apply rules for buttons and switches + else if(GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == null || clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.WOOD_BUTTON || clickedBlockType == Material.LEVER || GriefPrevention.instance.config_mods_accessTrustIds.Contains(new MaterialInfo(clickedBlock.getTypeId(), clickedBlock.getData(), null)))) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + playerData.lastClaim = claim; + + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); + return; + } + } + } + + //apply rule for players trampling tilled soil back to dirt (never allow it) + //NOTE: that this event applies only to players. monsters and animals can still trample. + else if(event.getAction() == Action.PHYSICAL && clickedBlockType == Material.SOIL) + { + event.setCancelled(true); + return; + } + + //apply rule for note blocks and repeaters + else if(clickedBlockType == Material.NOTE_BLOCK || clickedBlockType == Material.DIODE_BLOCK_ON || clickedBlockType == Material.DIODE_BLOCK_OFF) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + String noBuildReason = claim.allowBuild(player); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + return; + } + } + } + + //otherwise handle right click (shovel, string, bonemeal) + else + { + //ignore all actions except right-click on a block or in the air + Action action = event.getAction(); + if(action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; + + //what's the player holding? + Material materialInHand = player.getItemInHand().getType(); + + //if it's bonemeal or a boat, check for build permission (ink sac == bone meal, must be a Bukkit bug?) + if(materialInHand == Material.INK_SACK || materialInHand == Material.BOAT) + { + String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + } + + return; + } + + //if it's a spawn egg, minecart, or boat, and this is a creative world, apply special rules + else if((materialInHand == Material.MONSTER_EGG || materialInHand == Material.MINECART || materialInHand == Material.POWERED_MINECART || materialInHand == Material.STORAGE_MINECART || materialInHand == Material.BOAT) && GriefPrevention.instance.creativeRulesApply(clickedBlock.getLocation())) + { + //player needs build permission at this location + String noBuildReason = GriefPrevention.instance.allowBuild(player, clickedBlock.getLocation()); + if(noBuildReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + event.setCancelled(true); + return; + } + + //enforce limit on total number of entities in this claim + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim == null) return; + + String noEntitiesReason = claim.allowMoreEntities(); + if(noEntitiesReason != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, noEntitiesReason); + event.setCancelled(true); + return; + } + + return; + } + + //if he's investigating a claim + else if(materialInHand == GriefPrevention.instance.config_claims_investigationTool) + { + //air indicates too far away + if(clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); + return; + } + + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, playerData.lastClaim); + + //no claim case + if(claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockNotClaimed); + Visualization.Revert(player); + } + + //claim case + else + { + playerData.lastClaim = claim; + GriefPrevention.sendMessage(player, TextMode.Info, Messages.BlockClaimed, claim.getOwnerName()); + + //visualize boundary + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + + //if can resize this claim, tell about the boundaries + if(claim.allowEdit(player) == null) + { + GriefPrevention.sendMessage(player, TextMode.Info, " " + claim.getWidth() + "x" + claim.getHeight() + "=" + claim.getArea()); + } + + //if deleteclaims permission, tell about the player's offline time + if(!claim.isAdminClaim() && player.hasPermission("griefprevention.deleteclaims")) + { + PlayerData otherPlayerData = this.dataStore.getPlayerData(claim.getOwnerName()); + Date lastLogin = otherPlayerData.lastLogin; + Date now = new Date(); + long daysElapsed = (now.getTime() - lastLogin.getTime()) / (1000 * 60 * 60 * 24); + + GriefPrevention.sendMessage(player, TextMode.Info, Messages.PlayerOfflineTime, String.valueOf(daysElapsed)); + + //drop the data we just loaded, if the player isn't online + if(GriefPrevention.instance.getServer().getPlayerExact(claim.getOwnerName()) == null) + this.dataStore.clearCachedPlayerData(claim.getOwnerName()); + } + } + + return; + } + + //if it's a golden shovel + else if(materialInHand != GriefPrevention.instance.config_claims_modificationTool) return; + + //disable golden shovel while under siege + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeNoShovel); + event.setCancelled(true); + return; + } + + //can't use the shovel from too far away + if(clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.TooFarAway); + return; + } + + //if the player is in restore nature mode, do only that + String playerName = player.getName(); + playerData = this.dataStore.getPlayerData(player.getName()); + if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive) + { + //if the clicked block is in a claim, visualize that claim and deliver an error message + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); + if(claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.BlockClaimed, claim.getOwnerName()); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + + return; + } + + //figure out which chunk to repair + Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); + + //start the repair process + + //set boundaries for processing + int miny = clickedBlock.getY(); + + //if not in aggressive mode, extend the selection down to a little below sea level + if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive)) + { + if(miny > GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10) + { + miny = GriefPrevention.instance.getSeaLevel(chunk.getWorld()) - 10; + } + } + + GriefPrevention.instance.restoreChunk(chunk, miny, playerData.shovelMode == ShovelMode.RestoreNatureAggressive, 0, player); + + return; + } + + //if in restore nature fill mode + if(playerData.shovelMode == ShovelMode.RestoreNatureFill) + { + ArrayList allowedFillBlocks = new ArrayList(); + Environment environment = clickedBlock.getWorld().getEnvironment(); + if(environment == Environment.NETHER) + { + allowedFillBlocks.add(Material.NETHERRACK); + } + else if(environment == Environment.THE_END) + { + allowedFillBlocks.add(Material.ENDER_STONE); + } + else + { + allowedFillBlocks.add(Material.GRASS); + allowedFillBlocks.add(Material.DIRT); + allowedFillBlocks.add(Material.STONE); + allowedFillBlocks.add(Material.SAND); + allowedFillBlocks.add(Material.SANDSTONE); + allowedFillBlocks.add(Material.ICE); + } + + Block centerBlock = clickedBlock; + + int maxHeight = centerBlock.getY(); + int minx = centerBlock.getX() - playerData.fillRadius; + int maxx = centerBlock.getX() + playerData.fillRadius; + int minz = centerBlock.getZ() - playerData.fillRadius; + int maxz = centerBlock.getZ() + playerData.fillRadius; + int minHeight = maxHeight - 10; + if(minHeight < 0) minHeight = 0; + + Claim cachedClaim = null; + for(int x = minx; x <= maxx; x++) + { + for(int z = minz; z <= maxz; z++) + { + //circular brush + Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z); + if(location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue; + + //default fill block is initially the first from the allowed fill blocks list above + Material defaultFiller = allowedFillBlocks.get(0); + + //prefer to use the block the player clicked on, if it's an acceptable fill block + if(allowedFillBlocks.contains(centerBlock.getType())) + { + defaultFiller = centerBlock.getType(); + } + + //if the player clicks on water, try to sink through the water to find something underneath that's useful for a filler + else if(centerBlock.getType() == Material.WATER || centerBlock.getType() == Material.STATIONARY_WATER) + { + Block block = centerBlock.getWorld().getBlockAt(centerBlock.getLocation()); + while(!allowedFillBlocks.contains(block.getType()) && block.getY() > centerBlock.getY() - 10) + { + block = block.getRelative(BlockFace.DOWN); + } + if(allowedFillBlocks.contains(block.getType())) + { + defaultFiller = block.getType(); + } + } + + //fill bottom to top + for(int y = minHeight; y <= maxHeight; y++) + { + Block block = centerBlock.getWorld().getBlockAt(x, y, z); + + //respect claims + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim); + if(claim != null) + { + cachedClaim = claim; + break; + } + + //only replace air, spilling water, snow, long grass + if(block.getType() == Material.AIR || block.getType() == Material.SNOW || (block.getType() == Material.STATIONARY_WATER && block.getData() != 0) || block.getType() == Material.LONG_GRASS) + { + //if the top level, always use the default filler picked above + if(y == maxHeight) + { + block.setType(defaultFiller); + } + + //otherwise look to neighbors for an appropriate fill block + else + { + Block eastBlock = block.getRelative(BlockFace.EAST); + Block westBlock = block.getRelative(BlockFace.WEST); + Block northBlock = block.getRelative(BlockFace.NORTH); + Block southBlock = block.getRelative(BlockFace.SOUTH); + + //first, check lateral neighbors (ideally, want to keep natural layers) + if(allowedFillBlocks.contains(eastBlock.getType())) + { + block.setType(eastBlock.getType()); + } + else if(allowedFillBlocks.contains(westBlock.getType())) + { + block.setType(westBlock.getType()); + } + else if(allowedFillBlocks.contains(northBlock.getType())) + { + block.setType(northBlock.getType()); + } + else if(allowedFillBlocks.contains(southBlock.getType())) + { + block.setType(southBlock.getType()); + } + + //if all else fails, use the default filler selected above + else + { + block.setType(defaultFiller); + } + } + } + } + } + } + + return; + } + + //if the player doesn't have claims permission, don't do anything + if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreateClaimPermission); + return; + } + + //if he's resizing a claim and that claim hasn't been deleted since he started resizing it + if(playerData.claimResizing != null && playerData.claimResizing.inDataStore) + { + if(clickedBlock.getLocation().equals(playerData.lastShovelLocation)) return; + + //figure out what the coords of his new claim would be + int newx1, newx2, newz1, newz2, newy1, newy2; + if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getLesserBoundaryCorner().getBlockX()) + { + newx1 = clickedBlock.getX(); + } + else + { + newx1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockX(); + } + + if(playerData.lastShovelLocation.getBlockX() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockX()) + { + newx2 = clickedBlock.getX(); + } + else + { + newx2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockX(); + } + + if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getLesserBoundaryCorner().getBlockZ()) + { + newz1 = clickedBlock.getZ(); + } + else + { + newz1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockZ(); + } + + if(playerData.lastShovelLocation.getBlockZ() == playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ()) + { + newz2 = clickedBlock.getZ(); + } + else + { + newz2 = playerData.claimResizing.getGreaterBoundaryCorner().getBlockZ(); + } + + newy1 = playerData.claimResizing.getLesserBoundaryCorner().getBlockY(); + newy2 = clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance; + + //for top level claims, apply size rules and claim blocks requirement + if(playerData.claimResizing.parent == null) + { + //measure new claim, apply size rules + int newWidth = (Math.abs(newx1 - newx2) + 1); + int newHeight = (Math.abs(newz1 - newz2) + 1); + + if(!playerData.claimResizing.isAdminClaim() && (newWidth < GriefPrevention.instance.config_claims_minSize || newHeight < GriefPrevention.instance.config_claims_minSize)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); + return; + } + + //make sure player has enough blocks to make up the difference + if(!playerData.claimResizing.isAdminClaim() && player.getName().equals(playerData.claimResizing.getOwnerName())) + { + int newArea = newWidth * newHeight; + int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; + + if(blocksRemainingAfter < 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeNeedMoreBlocks, String.valueOf(Math.abs(blocksRemainingAfter))); + return; + } + } + } + + //special rules for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries. + //rule1: in creative mode, top-level claims can't be moved or resized smaller. + //rule2: in any mode, shrinking a claim removes any surface fluids + Claim oldClaim = playerData.claimResizing; + boolean smaller = false; + if(oldClaim.parent == null) + { + //temporary claim instance, just for checking contains() + Claim newClaim = new Claim( + new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), + new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), + "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null); + + //if the new claim is smaller + if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) + { + smaller = true; + + //enforce creative mode rule + if(!GriefPrevention.instance.config_claims_allowUnclaimInCreative && !player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation())) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoCreativeUnClaim); + return; + } + + //remove surface fluids about to be unclaimed + oldClaim.removeSurfaceFluids(newClaim); + } + } + + //ask the datastore to try and resize the claim, this checks for conflicts with other claims + CreateClaimResult result = GriefPrevention.instance.dataStore.resizeClaim(playerData.claimResizing, newx1, newx2, newy1, newy2, newz1, newz2); + + if(result.succeeded) + { + //inform and show the player + GriefPrevention.sendMessage(player, TextMode.Success, Messages.ClaimResizeSuccess, String.valueOf(playerData.getRemainingClaimBlocks())); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + + //if resizing someone else's claim, make a log entry + if(!playerData.claimResizing.ownerName.equals(playerName)) + { + GriefPrevention.AddLogEntry(playerName + " resized " + playerData.claimResizing.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.lesserBoundaryCorner) + "."); + } + + //if in a creative mode world and shrinking an existing claim, restore any unclaimed area + if(smaller && GriefPrevention.instance.creativeRulesApply(oldClaim.getLesserBoundaryCorner())) + { + GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnclaimCleanupWarning); + GriefPrevention.instance.restoreClaim(oldClaim, 20L * 60 * 2); //2 minutes + GriefPrevention.AddLogEntry(player.getName() + " shrank a claim @ " + GriefPrevention.getfriendlyLocationString(playerData.claimResizing.getLesserBoundaryCorner())); + } + + //clean up + playerData.claimResizing = null; + playerData.lastShovelLocation = null; + } + else + { + //inform player + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlap); + + //show the player the conflicting claim + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + + return; + } + + //otherwise, since not currently resizing a claim, must be starting a resize, creating a new claim, or creating a subdivision + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), true /*ignore height*/, playerData.lastClaim); + + //if within an existing claim, he's not creating a new one + if(claim != null) + { + //if the player has permission to edit the claim or subdivision + String noEditReason = claim.allowEdit(player); + if(noEditReason == null) + { + //if he clicked on a corner, start resizing it + if((clickedBlock.getX() == claim.getLesserBoundaryCorner().getBlockX() || clickedBlock.getX() == claim.getGreaterBoundaryCorner().getBlockX()) && (clickedBlock.getZ() == claim.getLesserBoundaryCorner().getBlockZ() || clickedBlock.getZ() == claim.getGreaterBoundaryCorner().getBlockZ())) + { + playerData.claimResizing = claim; + playerData.lastShovelLocation = clickedBlock.getLocation(); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ResizeStart); + } + + //if he didn't click on a corner and is in subdivision mode, he's creating a new subdivision + else if(playerData.shovelMode == ShovelMode.Subdivide) + { + //if it's the first click, he's trying to start a new subdivision + if(playerData.lastShovelLocation == null) + { + //if the clicked claim was a subdivision, tell him he can't start a new subdivision here + if(claim.parent != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ResizeFailOverlapSubdivision); + } + + //otherwise start a new subdivision + else + { + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SubdivisionStart); + playerData.lastShovelLocation = clickedBlock.getLocation(); + playerData.claimSubdividing = claim; + } + } + + //otherwise, he's trying to finish creating a subdivision by setting the other boundary corner + else + { + //if last shovel location was in a different world, assume the player is starting the create-claim workflow over + if(!playerData.lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) + { + playerData.lastShovelLocation = null; + this.onPlayerInteract(event); + return; + } + + //try to create a new claim (will return null if this subdivision overlaps another) + CreateClaimResult result = this.dataStore.createClaim( + player.getWorld(), + playerData.lastShovelLocation.getBlockX(), clickedBlock.getX(), + playerData.lastShovelLocation.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, + playerData.lastShovelLocation.getBlockZ(), clickedBlock.getZ(), + "--subdivision--", //owner name is not used for subdivisions + playerData.claimSubdividing, + null); + + //if it didn't succeed, tell the player why + if(!result.succeeded) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateSubdivisionOverlap); + + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + + return; + } + + //otherwise, advise him on the /trust command and show him his new subdivision + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.SubdivisionSuccess); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + playerData.lastShovelLocation = null; + playerData.claimSubdividing = null; + } + } + } + + //otherwise tell him he can't create a claim here, and show him the existing claim + //also advise him to consider /abandonclaim or resizing the existing claim + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlap); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + } + } + + //otherwise tell the player he can't claim here because it's someone else's claim, and show him the claim + else + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapOtherPlayer, claim.getOwnerName()); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + } + + return; + } + + //otherwise, the player isn't in an existing claim! + + //if he hasn't already start a claim with a previous shovel action + Location lastShovelLocation = playerData.lastShovelLocation; + if(lastShovelLocation == null) + { + //if claims are not enabled in this world and it's not an administrative claim, display an error message and stop + if(!GriefPrevention.instance.claimsEnabledForWorld(player.getWorld()) && playerData.shovelMode != ShovelMode.Admin) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.ClaimsDisabledWorld); + return; + } + + //remember it, and start him on the new claim + playerData.lastShovelLocation = clickedBlock.getLocation(); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimStart); + + //show him where he's working + Visualization visualization = Visualization.FromClaim(new Claim(clickedBlock.getLocation(), clickedBlock.getLocation(), "", new String[]{}, new String[]{}, new String[]{}, new String[]{}, null), clickedBlock.getY(), VisualizationType.RestoreNature, player.getLocation()); + Visualization.Apply(player, visualization); + } + + //otherwise, he's trying to finish creating a claim by setting the other boundary corner + else + { + //if last shovel location was in a different world, assume the player is starting the create-claim workflow over + if(!lastShovelLocation.getWorld().equals(clickedBlock.getWorld())) + { + playerData.lastShovelLocation = null; + this.onPlayerInteract(event); + return; + } + + //apply minimum claim dimensions rule + int newClaimWidth = Math.abs(playerData.lastShovelLocation.getBlockX() - clickedBlock.getX()) + 1; + int newClaimHeight = Math.abs(playerData.lastShovelLocation.getBlockZ() - clickedBlock.getZ()) + 1; + + if(playerData.shovelMode != ShovelMode.Admin && (newClaimWidth < GriefPrevention.instance.config_claims_minSize || newClaimHeight < GriefPrevention.instance.config_claims_minSize)) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NewClaimTooSmall, String.valueOf(GriefPrevention.instance.config_claims_minSize)); + return; + } + + //if not an administrative claim, verify the player has enough claim blocks for this new claim + if(playerData.shovelMode != ShovelMode.Admin) + { + int newClaimArea = newClaimWidth * newClaimHeight; + int remainingBlocks = playerData.getRemainingClaimBlocks(); + if(newClaimArea > remainingBlocks) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimInsufficientBlocks, String.valueOf(newClaimArea - remainingBlocks)); + GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AbandonClaimAdvertisement); + return; + } + } + else + { + playerName = ""; + } + + //try to create a new claim (will return null if this claim overlaps another) + CreateClaimResult result = this.dataStore.createClaim( + player.getWorld(), + lastShovelLocation.getBlockX(), clickedBlock.getX(), + lastShovelLocation.getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, clickedBlock.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance, + lastShovelLocation.getBlockZ(), clickedBlock.getZ(), + playerName, + null, null); + + //if it didn't succeed, tell the player why + if(!result.succeeded) + { + GriefPrevention.sendMessage(player, TextMode.Err, Messages.CreateClaimFailOverlapShort); + + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim, player.getLocation()); + Visualization.Apply(player, visualization); + + return; + } + + //otherwise, advise him on the /trust command and show him his new claim + else + { + GriefPrevention.sendMessage(player, TextMode.Success, Messages.CreateClaimSuccess); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim, player.getLocation()); + Visualization.Apply(player, visualization); + playerData.lastShovelLocation = null; + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java b/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java index a8216ac..e09e1ac 100644 --- a/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java +++ b/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java @@ -1,112 +1,120 @@ -/* - 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 org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Animals; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; - -//this main thread task takes the output from the RestoreNatureProcessingTask\ -//and updates the world accordingly -class RestoreNatureExecutionTask implements Runnable -{ - //results from processing thread - //will be applied to the world - private BlockSnapshot[][][] snapshots; - - //boundaries for changes - private int miny; - private Location lesserCorner; - private Location greaterCorner; - - //player who should be notified about the result (will see a visualization when the restoration is complete) - private Player player; - - public RestoreNatureExecutionTask(BlockSnapshot[][][] snapshots, int miny, Location lesserCorner, Location greaterCorner, Player player) - { - this.snapshots = snapshots; - this.miny = miny; - this.lesserCorner = lesserCorner; - this.greaterCorner = greaterCorner; - this.player = player; - } - - @Override - public void run() - { - //apply changes to the world, but ONLY to unclaimed blocks - //note that the edge of the results is not applied (the 1-block-wide band around the outside of the chunk) - //those data were sent to the processing thread for referernce purposes, but aren't part of the area selected for restoration - Claim cachedClaim = null; - for(int x = 1; x < this.snapshots.length - 1; x++) - { - for(int z = 1; z < this.snapshots[0][0].length; z++) - { - for(int y = this.miny; y < this.snapshots[0].length; y++) - { - BlockSnapshot blockUpdate = this.snapshots[x][y][z]; - Block currentBlock = blockUpdate.location.getBlock(); - if(blockUpdate.typeId != currentBlock.getTypeId() || blockUpdate.data != currentBlock.getData()) - { - Claim claim = GriefPrevention.instance.dataStore.getClaimAt(blockUpdate.location, false, cachedClaim); - if(claim != null) - { - cachedClaim = claim; - break; - } - - currentBlock.setTypeId(blockUpdate.typeId); - currentBlock.setData(blockUpdate.data); - } - } - } - } - - //clean up any entities in the chunk, ensure no players are suffocated - Chunk chunk = this.lesserCorner.getChunk(); - Entity [] entities = chunk.getEntities(); - for(int i = 0; i < entities.length; i++) - { - Entity entity = entities[i]; - if(!(entity instanceof Player || entity instanceof Animals)) - { - entity.remove(); - } - else - { - Block feetBlock = entity.getLocation().getBlock(); - feetBlock.setType(Material.AIR); - feetBlock.getRelative(BlockFace.UP).setType(Material.AIR); - } - } - - //show visualization to player - if(player != null) - { - Claim claim = new Claim(lesserCorner, greaterCorner, "", new String[] {}, new String[] {}, new String[] {}, new String[] {}, null); - Visualization visualization = Visualization.FromClaim(claim, player.getLocation().getBlockY(), VisualizationType.RestoreNature, player.getLocation()); - Visualization.Apply(player, visualization); - } - } -} +/* + 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 org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Hanging; +import org.bukkit.entity.Player; + +//this main thread task takes the output from the RestoreNatureProcessingTask\ +//and updates the world accordingly +class RestoreNatureExecutionTask implements Runnable +{ + //results from processing thread + //will be applied to the world + private BlockSnapshot[][][] snapshots; + + //boundaries for changes + private int miny; + private Location lesserCorner; + private Location greaterCorner; + + //player who should be notified about the result (will see a visualization when the restoration is complete) + private Player player; + + public RestoreNatureExecutionTask(BlockSnapshot[][][] snapshots, int miny, Location lesserCorner, Location greaterCorner, Player player) + { + this.snapshots = snapshots; + this.miny = miny; + this.lesserCorner = lesserCorner; + this.greaterCorner = greaterCorner; + this.player = player; + } + + @Override + public void run() + { + //apply changes to the world, but ONLY to unclaimed blocks + //note that the edge of the results is not applied (the 1-block-wide band around the outside of the chunk) + //those data were sent to the processing thread for referernce purposes, but aren't part of the area selected for restoration + Claim cachedClaim = null; + for(int x = 1; x < this.snapshots.length - 1; x++) + { + for(int z = 1; z < this.snapshots[0][0].length; z++) + { + for(int y = this.miny; y < this.snapshots[0].length; y++) + { + BlockSnapshot blockUpdate = this.snapshots[x][y][z]; + Block currentBlock = blockUpdate.location.getBlock(); + if(blockUpdate.typeId != currentBlock.getTypeId() || blockUpdate.data != currentBlock.getData()) + { + Claim claim = GriefPrevention.instance.dataStore.getClaimAt(blockUpdate.location, false, cachedClaim); + if(claim != null) + { + cachedClaim = claim; + break; + } + + currentBlock.setTypeId(blockUpdate.typeId); + currentBlock.setData(blockUpdate.data); + } + } + } + } + + //clean up any entities in the chunk, ensure no players are suffocated + Chunk chunk = this.lesserCorner.getChunk(); + Entity [] entities = chunk.getEntities(); + for(int i = 0; i < entities.length; i++) + { + Entity entity = entities[i]; + if(!(entity instanceof Player || entity instanceof Animals)) + { + //hanging entities (paintings, item frames) are protected when they're in land claims + if(!(entity instanceof Hanging) || GriefPrevention.instance.dataStore.getClaimAt(entity.getLocation(), false, null) == null) + { + //everything else is removed + entity.remove(); + } + } + + //for players, always ensure there's air where the player is standing + else + { + Block feetBlock = entity.getLocation().getBlock(); + feetBlock.setType(Material.AIR); + feetBlock.getRelative(BlockFace.UP).setType(Material.AIR); + } + } + + //show visualization to player who started the restoration + if(player != null) + { + Claim claim = new Claim(lesserCorner, greaterCorner, "", new String[] {}, new String[] {}, new String[] {}, new String[] {}, null); + Visualization visualization = Visualization.FromClaim(claim, player.getLocation().getBlockY(), VisualizationType.RestoreNature, player.getLocation()); + Visualization.Apply(player, visualization); + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java index 2e222fe..d271ac5 100644 --- a/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java +++ b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java @@ -1,740 +1,749 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World.Environment; -import org.bukkit.block.Biome; -import org.bukkit.entity.Player; - -//non-main-thread task which processes world data to repair the unnatural -//after processing is complete, creates a main thread task to make the necessary changes to the world -class RestoreNatureProcessingTask implements Runnable -{ - //world information captured from the main thread - //will be updated and sent back to main thread to be applied to the world - private BlockSnapshot[][][] snapshots; - - //other information collected from the main thread. - //not to be updated, only to be passed back to main thread to provide some context about the operation - private int miny; - private Environment environment; - private Location lesserBoundaryCorner; - private Location greaterBoundaryCorner; - private Player player; //absolutely must not be accessed. not thread safe. - private Biome biome; - private boolean creativeMode; - private int seaLevel; - private boolean aggressiveMode; - - //two lists of materials - private ArrayList notAllowedToHang; //natural blocks which don't naturally hang in their air - private ArrayList playerBlocks; //a "complete" list of player-placed blocks. MUST BE MAINTAINED as patches introduce more - - public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode, Player player) - { - this.snapshots = snapshots; - this.miny = miny; - this.environment = environment; - this.lesserBoundaryCorner = lesserBoundaryCorner; - this.greaterBoundaryCorner = greaterBoundaryCorner; - this.biome = biome; - this.seaLevel = seaLevel; - this.aggressiveMode = aggressiveMode; - this.player = player; - this.creativeMode = creativeMode; - - this.notAllowedToHang = new ArrayList(); - this.notAllowedToHang.add(Material.DIRT.getId()); - this.notAllowedToHang.add(Material.LONG_GRASS.getId()); - this.notAllowedToHang.add(Material.SNOW.getId()); - this.notAllowedToHang.add(Material.LOG.getId()); - - if(this.aggressiveMode) - { - this.notAllowedToHang.add(Material.GRASS.getId()); - this.notAllowedToHang.add(Material.STONE.getId()); - } - - this.playerBlocks = new ArrayList(); - this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome)); - - //in aggressive or creative world mode, also treat these blocks as user placed, to be removed - //this is helpful in the few cases where griefers intentionally use natural blocks to grief, - //like a single-block tower of iron ore or a giant penis constructed with melons - if(this.aggressiveMode || this.creativeMode) - { - this.playerBlocks.add(Material.IRON_ORE.getId()); - this.playerBlocks.add(Material.GOLD_ORE.getId()); - this.playerBlocks.add(Material.DIAMOND_ORE.getId()); - this.playerBlocks.add(Material.MELON_BLOCK.getId()); - this.playerBlocks.add(Material.MELON_STEM.getId()); - this.playerBlocks.add(Material.BEDROCK.getId()); - this.playerBlocks.add(Material.COAL_ORE.getId()); - this.playerBlocks.add(Material.PUMPKIN.getId()); - this.playerBlocks.add(Material.PUMPKIN_STEM.getId()); - this.playerBlocks.add(Material.MELON.getId()); - } - - if(this.aggressiveMode) - { - this.playerBlocks.add(Material.LEAVES.getId()); - this.playerBlocks.add(Material.LOG.getId()); - this.playerBlocks.add(Material.VINE.getId()); - } - } - - @Override - public void run() - { - //order is important! - - //remove sandstone which appears to be unnatural - this.removeSandstone(); - - //remove any blocks which are definitely player placed - this.removePlayerBlocks(); - - //reduce large outcroppings of stone, sandstone - this.reduceStone(); - - //reduce logs, except in jungle biomes - this.reduceLogs(); - - //remove natural blocks which are unnaturally hanging in the air - this.removeHanging(); - - //remove natural blocks which are unnaturally stacked high - this.removeWallsAndTowers(); - - //fill unnatural thin trenches and single-block potholes - this.fillHolesAndTrenches(); - - //fill water depressions and fix unnatural surface ripples - this.fixWater(); - - //remove water/lava above sea level - this.removeDumpedFluids(); - - //cover over any gaping holes in creative mode worlds - if(this.creativeMode && this.environment == Environment.NORMAL) - { - this.fillBigHoles(); - } - - //cover surface stone and gravel with sand or grass, as the biome requires - this.coverSurfaceStone(); - - //reset leaves to NOT player placed so that they may decay as usual - this.resetLeaves(); - - //schedule main thread task to apply the result to the world - RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player); - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task); - } - - private void resetLeaves() - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = this.seaLevel - 1; y < snapshots[0].length; y++) - { - //note: see minecraft wiki data values for leaves - BlockSnapshot block = snapshots[x][y][z]; - if(block.typeId == Material.LEAVES.getId()) - { - //clear "player placed" bit - block.data = (byte)(block.data & ~(1<<2)); - - //set the "check for natural decay" bit - block.data = (byte)(block.data | (1<<3)); - } - } - } - } - } - - private void fillBigHoles() - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - //replace air, lava, or running water at sea level with stone - if(this.snapshots[x][this.seaLevel - 2][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 2][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 2][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 2][z].data != 0)) - { - this.snapshots[x][this.seaLevel - 2][z].typeId = Material.STONE.getId(); - } - - //do the same for one layer beneath that (because a future restoration step may convert surface stone to sand, which falls down) - if(this.snapshots[x][this.seaLevel - 3][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 3][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 3][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 3][z].data != 0)) - { - this.snapshots[x][this.seaLevel - 3][z].typeId = Material.STONE.getId(); - } - } - } - } - - //converts sandstone adjacent to sand to sand, and any other sandstone to air - private void removeSandstone() - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = snapshots[0].length - 2; y > miny; y--) - { - if(snapshots[x][y][z].typeId != Material.SANDSTONE.getId()) continue; - - BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; - BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; - BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; - BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; - BlockSnapshot underBlock = this.snapshots[x][y - 1][z]; - BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z]; - - //skip blocks which may cause a cave-in - if(aboveBlock.typeId == Material.SAND.getId() && underBlock.typeId == Material.AIR.getId()) continue; - - //count adjacent non-air/non-leaf blocks - if( leftBlock.typeId == Material.SAND.getId() || - rightBlock.typeId == Material.SAND.getId() || - upBlock.typeId == Material.SAND.getId() || - downBlock.typeId == Material.SAND.getId() || - aboveBlock.typeId == Material.SAND.getId() || - underBlock.typeId == Material.SAND.getId()) - { - snapshots[x][y][z].typeId = Material.SAND.getId(); - } - else - { - snapshots[x][y][z].typeId = Material.AIR.getId(); - } - } - } - } - } - - private void reduceStone() - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - int thisy = this.highestY(x, z, true); - - while(thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].typeId == Material.STONE.getId() || this.snapshots[x][thisy][z].typeId == Material.SANDSTONE.getId())) - { - BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z]; - BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z]; - BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1]; - BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1]; - - //count adjacent non-air/non-leaf blocks - byte adjacentBlockCount = 0; - if(leftBlock.typeId != Material.AIR.getId() && leftBlock.typeId != Material.LEAVES.getId() && leftBlock.typeId != Material.VINE.getId()) - { - adjacentBlockCount++; - } - if(rightBlock.typeId != Material.AIR.getId() && rightBlock.typeId != Material.LEAVES.getId() && rightBlock.typeId != Material.VINE.getId()) - { - adjacentBlockCount++; - } - if(downBlock.typeId != Material.AIR.getId() && downBlock.typeId != Material.LEAVES.getId() && downBlock.typeId != Material.VINE.getId()) - { - adjacentBlockCount++; - } - if(upBlock.typeId != Material.AIR.getId() && upBlock.typeId != Material.LEAVES.getId() && upBlock.typeId != Material.VINE.getId()) - { - adjacentBlockCount++; - } - - if(adjacentBlockCount < 3) - { - this.snapshots[x][thisy][z].typeId = Material.AIR.getId(); - } - - thisy--; - } - } - } - } - - private void reduceLogs() - { - boolean jungleBiome = this.biome == Biome.JUNGLE || this.biome == Biome.JUNGLE_HILLS; - - //scan all blocks above sea level - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = this.seaLevel - 1; y < snapshots[0].length; y++) - { - BlockSnapshot block = snapshots[x][y][z]; - - //skip non-logs - if(block.typeId != Material.LOG.getId()) continue; - - //if in jungle biome, skip jungle logs - if(jungleBiome && block.data == 3) continue; - - //examine adjacent blocks for logs - BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; - BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; - BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; - BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; - - //if any, remove the log - if(leftBlock.typeId == Material.LOG.getId() || rightBlock.typeId == Material.LOG.getId() || upBlock.typeId == Material.LOG.getId() || downBlock.typeId == Material.LOG.getId()) - { - this.snapshots[x][y][z].typeId = Material.AIR.getId(); - } - } - } - } - } - - private void removePlayerBlocks() - { - int miny = this.miny; - if(miny < 1) miny = 1; - - //remove all player blocks - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = miny; y < snapshots[0].length - 1; y++) - { - BlockSnapshot block = snapshots[x][y][z]; - if(this.playerBlocks.contains(block.typeId)) - { - block.typeId = Material.AIR.getId(); - } - } - } - } - } - - private void removeHanging() - { - int miny = this.miny; - if(miny < 1) miny = 1; - - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = miny; y < snapshots[0].length - 1; y++) - { - BlockSnapshot block = snapshots[x][y][z]; - BlockSnapshot underBlock = snapshots[x][y - 1][z]; - - if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.STATIONARY_WATER.getId() || underBlock.typeId == Material.STATIONARY_LAVA.getId() || underBlock.typeId == Material.LEAVES.getId()) - { - if(this.notAllowedToHang.contains(block.typeId)) - { - block.typeId = Material.AIR.getId(); - } - } - } - } - } - } - - private void removeWallsAndTowers() - { - int [] excludedBlocksArray = new int [] - { - Material.CACTUS.getId(), - Material.LONG_GRASS.getId(), - Material.RED_MUSHROOM.getId(), - Material.BROWN_MUSHROOM.getId(), - Material.DEAD_BUSH.getId(), - Material.SAPLING.getId(), - Material.YELLOW_FLOWER.getId(), - Material.RED_ROSE.getId(), - Material.SUGAR_CANE_BLOCK.getId(), - Material.VINE.getId(), - Material.PUMPKIN.getId(), - Material.WATER_LILY.getId(), - Material.LEAVES.getId() - }; - - ArrayList excludedBlocks = new ArrayList(); - for(int i = 0; i < excludedBlocksArray.length; i++) excludedBlocks.add(excludedBlocksArray[i]); - - boolean changed; - do - { - changed = false; - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - int thisy = this.highestY(x, z, false); - if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue; - - int righty = this.highestY(x + 1, z, false); - int lefty = this.highestY(x - 1, z, false); - while(lefty < thisy && righty < thisy) - { - this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); - changed = true; - } - - int upy = this.highestY(x, z + 1, false); - int downy = this.highestY(x, z - 1, false); - while(upy < thisy && downy < thisy) - { - this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); - changed = true; - } - } - } - }while(changed); - } - - private void coverSurfaceStone() - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - int y = this.highestY(x, z, true); - BlockSnapshot block = snapshots[x][y][z]; - - if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.SOIL.getId() || block.typeId == Material.DIRT.getId() || block.typeId == Material.SANDSTONE.getId()) - { - if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH) - { - this.snapshots[x][y][z].typeId = Material.SAND.getId(); - } - else - { - this.snapshots[x][y][z].typeId = Material.GRASS.getId(); - } - } - } - } - } - - private void fillHolesAndTrenches() - { - ArrayList fillableBlocks = new ArrayList(); - fillableBlocks.add(Material.AIR.getId()); - fillableBlocks.add(Material.STATIONARY_WATER.getId()); - fillableBlocks.add(Material.STATIONARY_LAVA.getId()); - fillableBlocks.add(Material.LONG_GRASS.getId()); - - ArrayList notSuitableForFillBlocks = new ArrayList(); - notSuitableForFillBlocks.add(Material.LONG_GRASS.getId()); - notSuitableForFillBlocks.add(Material.CACTUS.getId()); - notSuitableForFillBlocks.add(Material.STATIONARY_WATER.getId()); - notSuitableForFillBlocks.add(Material.STATIONARY_LAVA.getId()); - - boolean changed; - do - { - changed = false; - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = 0; y < snapshots[0].length - 1; y++) - { - BlockSnapshot block = this.snapshots[x][y][z]; - if(!fillableBlocks.contains(block.typeId)) continue; - - BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; - BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; - - if(!fillableBlocks.contains(leftBlock.typeId) && !fillableBlocks.contains(rightBlock.typeId)) - { - if(!notSuitableForFillBlocks.contains(rightBlock.typeId)) - { - block.typeId = rightBlock.typeId; - changed = true; - } - } - - BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; - BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; - - if(!fillableBlocks.contains(upBlock.typeId) && !fillableBlocks.contains(downBlock.typeId)) - { - if(!notSuitableForFillBlocks.contains(downBlock.typeId)) - { - block.typeId = downBlock.typeId; - changed = true; - } - } - } - } - } - }while(changed); - } - - private void fixWater() - { - int miny = this.miny; - if(miny < 1) miny = 1; - - boolean changed; - - //remove hanging water or lava - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = miny; y < snapshots[0].length - 1; y++) - { - BlockSnapshot block = this.snapshots[x][y][z]; - BlockSnapshot underBlock = this.snapshots[x][y][z]; - if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId()) - { - if(underBlock.typeId == Material.AIR.getId() || (underBlock.data != 0)) - { - block.typeId = Material.AIR.getId(); - } - } - } - } - } - - //fill water depressions - do - { - changed = false; - for(int y = this.seaLevel - 10; y <= this.seaLevel; y++) - { - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - BlockSnapshot block = snapshots[x][y][z]; - - //only consider air blocks and flowing water blocks for upgrade to water source blocks - if(block.typeId == Material.AIR.getId() || (block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0)) - { - BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; - BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; - BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; - BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; - BlockSnapshot underBlock = this.snapshots[x][y - 1][z]; - - //block underneath MUST be source water - if(underBlock.typeId != Material.STATIONARY_WATER.getId() || underBlock.data != 0) continue; - - //count adjacent source water blocks - byte adjacentSourceWaterCount = 0; - if(leftBlock.typeId == Material.STATIONARY_WATER.getId() && leftBlock.data == 0) - { - adjacentSourceWaterCount++; - } - if(rightBlock.typeId == Material.STATIONARY_WATER.getId() && rightBlock.data == 0) - { - adjacentSourceWaterCount++; - } - if(upBlock.typeId == Material.STATIONARY_WATER.getId() && upBlock.data == 0) - { - adjacentSourceWaterCount++; - } - if(downBlock.typeId == Material.STATIONARY_WATER.getId() && downBlock.data == 0) - { - adjacentSourceWaterCount++; - } - - //at least two adjacent blocks must be source water - if(adjacentSourceWaterCount >= 2) - { - block.typeId = Material.STATIONARY_WATER.getId(); - block.data = 0; - changed = true; - } - } - } - } - } - }while(changed); - } - - private void removeDumpedFluids() - { - //remove any surface water or lava above sea level, presumed to be placed by players - //sometimes, this is naturally generated. but replacing it is very easy with a bucket, so overall this is a good plan - if(this.environment == Environment.NETHER) return; - for(int x = 1; x < snapshots.length - 1; x++) - { - for(int z = 1; z < snapshots[0][0].length - 1; z++) - { - for(int y = this.seaLevel - 1; y < snapshots[0].length - 1; y++) - { - BlockSnapshot block = snapshots[x][y][z]; - if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId() || - block.typeId == Material.WATER.getId() || block.typeId == Material.LAVA.getId()) - { - block.typeId = Material.AIR.getId(); - } - } - } - } - } - - private int highestY(int x, int z, boolean ignoreLeaves) - { - int y; - for(y = snapshots[0].length - 1; y > 0; y--) - { - BlockSnapshot block = this.snapshots[x][y][z]; - if(block.typeId != Material.AIR.getId() && - !(ignoreLeaves && block.typeId == Material.SNOW.getId()) && - !(ignoreLeaves && block.typeId == Material.LEAVES.getId()) && - !(block.typeId == Material.STATIONARY_WATER.getId()) && - !(block.typeId == Material.WATER.getId()) && - !(block.typeId == Material.LAVA.getId()) && - !(block.typeId == Material.STATIONARY_LAVA.getId())) - { - return y; - } - } - - return y; - } - - static ArrayList getPlayerBlocks(Environment environment, Biome biome) - { - //NOTE on this list. why not make a list of natural blocks? - //answer: better to leave a few player blocks than to remove too many natural blocks. remember we're "restoring nature" - //a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal - ArrayList playerBlocks = new ArrayList(); - playerBlocks.add(Material.FIRE.getId()); - playerBlocks.add(Material.BED_BLOCK.getId()); - playerBlocks.add(Material.WOOD.getId()); - playerBlocks.add(Material.BOOKSHELF.getId()); - playerBlocks.add(Material.BREWING_STAND.getId()); - playerBlocks.add(Material.BRICK.getId()); - playerBlocks.add(Material.COBBLESTONE.getId()); - playerBlocks.add(Material.GLASS.getId()); - playerBlocks.add(Material.LAPIS_BLOCK.getId()); - playerBlocks.add(Material.DISPENSER.getId()); - playerBlocks.add(Material.NOTE_BLOCK.getId()); - playerBlocks.add(Material.POWERED_RAIL.getId()); - playerBlocks.add(Material.DETECTOR_RAIL.getId()); - playerBlocks.add(Material.PISTON_STICKY_BASE.getId()); - playerBlocks.add(Material.PISTON_BASE.getId()); - playerBlocks.add(Material.PISTON_EXTENSION.getId()); - playerBlocks.add(Material.WOOL.getId()); - playerBlocks.add(Material.PISTON_MOVING_PIECE.getId()); - playerBlocks.add(Material.GOLD_BLOCK.getId()); - playerBlocks.add(Material.IRON_BLOCK.getId()); - playerBlocks.add(Material.DOUBLE_STEP.getId()); - playerBlocks.add(Material.STEP.getId()); - playerBlocks.add(Material.CROPS.getId()); - playerBlocks.add(Material.TNT.getId()); - playerBlocks.add(Material.MOSSY_COBBLESTONE.getId()); - playerBlocks.add(Material.TORCH.getId()); - playerBlocks.add(Material.FIRE.getId()); - playerBlocks.add(Material.WOOD_STAIRS.getId()); - playerBlocks.add(Material.CHEST.getId()); - playerBlocks.add(Material.REDSTONE_WIRE.getId()); - playerBlocks.add(Material.DIAMOND_BLOCK.getId()); - playerBlocks.add(Material.WORKBENCH.getId()); - playerBlocks.add(Material.FURNACE.getId()); - playerBlocks.add(Material.BURNING_FURNACE.getId()); - playerBlocks.add(Material.WOODEN_DOOR.getId()); - playerBlocks.add(Material.SIGN_POST.getId()); - playerBlocks.add(Material.LADDER.getId()); - playerBlocks.add(Material.RAILS.getId()); - playerBlocks.add(Material.COBBLESTONE_STAIRS.getId()); - playerBlocks.add(Material.WALL_SIGN.getId()); - playerBlocks.add(Material.STONE_PLATE.getId()); - playerBlocks.add(Material.LEVER.getId()); - playerBlocks.add(Material.IRON_DOOR_BLOCK.getId()); - playerBlocks.add(Material.WOOD_PLATE.getId()); - playerBlocks.add(Material.REDSTONE_TORCH_ON.getId()); - playerBlocks.add(Material.REDSTONE_TORCH_OFF.getId()); - playerBlocks.add(Material.STONE_BUTTON.getId()); - playerBlocks.add(Material.SNOW_BLOCK.getId()); - playerBlocks.add(Material.JUKEBOX.getId()); - playerBlocks.add(Material.FENCE.getId()); - playerBlocks.add(Material.PORTAL.getId()); - playerBlocks.add(Material.JACK_O_LANTERN.getId()); - playerBlocks.add(Material.CAKE_BLOCK.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_OFF.getId()); - playerBlocks.add(Material.TRAP_DOOR.getId()); - playerBlocks.add(Material.SMOOTH_BRICK.getId()); - playerBlocks.add(Material.HUGE_MUSHROOM_1.getId()); - playerBlocks.add(Material.HUGE_MUSHROOM_2.getId()); - playerBlocks.add(Material.IRON_FENCE.getId()); - playerBlocks.add(Material.THIN_GLASS.getId()); - playerBlocks.add(Material.MELON_STEM.getId()); - playerBlocks.add(Material.FENCE_GATE.getId()); - playerBlocks.add(Material.BRICK_STAIRS.getId()); - playerBlocks.add(Material.SMOOTH_STAIRS.getId()); - playerBlocks.add(Material.ENCHANTMENT_TABLE.getId()); - playerBlocks.add(Material.BREWING_STAND.getId()); - playerBlocks.add(Material.CAULDRON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); - playerBlocks.add(Material.WEB.getId()); - playerBlocks.add(Material.SPONGE.getId()); - playerBlocks.add(Material.GRAVEL.getId()); - playerBlocks.add(Material.EMERALD_BLOCK.getId()); - playerBlocks.add(Material.SANDSTONE.getId()); - playerBlocks.add(Material.WOOD_STEP.getId()); - playerBlocks.add(Material.WOOD_DOUBLE_STEP.getId()); - playerBlocks.add(Material.ENDER_CHEST.getId()); - - //these are unnatural in the standard world, but not in the nether - if(environment != Environment.NETHER) - { - playerBlocks.add(Material.NETHERRACK.getId()); - playerBlocks.add(Material.SOUL_SAND.getId()); - playerBlocks.add(Material.GLOWSTONE.getId()); - playerBlocks.add(Material.NETHER_BRICK.getId()); - playerBlocks.add(Material.NETHER_FENCE.getId()); - playerBlocks.add(Material.NETHER_BRICK_STAIRS.getId()); - } - - //these are unnatural in the standard and nether worlds, but not in the end - if(environment != Environment.THE_END) - { - playerBlocks.add(Material.OBSIDIAN.getId()); - playerBlocks.add(Material.ENDER_STONE.getId()); - playerBlocks.add(Material.ENDER_PORTAL_FRAME.getId()); - } - - //these are unnatural in sandy biomes, but not elsewhere - if(biome == Biome.DESERT || biome == Biome.DESERT_HILLS || biome == Biome.BEACH || environment != Environment.NORMAL) - { - playerBlocks.add(Material.LEAVES.getId()); - playerBlocks.add(Material.LOG.getId()); - } - - return playerBlocks; - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.ArrayList; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World.Environment; +import org.bukkit.block.Biome; +import org.bukkit.entity.Player; + +//non-main-thread task which processes world data to repair the unnatural +//after processing is complete, creates a main thread task to make the necessary changes to the world +class RestoreNatureProcessingTask implements Runnable +{ + //world information captured from the main thread + //will be updated and sent back to main thread to be applied to the world + private BlockSnapshot[][][] snapshots; + + //other information collected from the main thread. + //not to be updated, only to be passed back to main thread to provide some context about the operation + private int miny; + private Environment environment; + private Location lesserBoundaryCorner; + private Location greaterBoundaryCorner; + private Player player; //absolutely must not be accessed. not thread safe. + private Biome biome; + private boolean creativeMode; + private int seaLevel; + private boolean aggressiveMode; + + //two lists of materials + private ArrayList notAllowedToHang; //natural blocks which don't naturally hang in their air + private ArrayList playerBlocks; //a "complete" list of player-placed blocks. MUST BE MAINTAINED as patches introduce more + + public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode, Player player) + { + this.snapshots = snapshots; + this.miny = miny; + this.environment = environment; + this.lesserBoundaryCorner = lesserBoundaryCorner; + this.greaterBoundaryCorner = greaterBoundaryCorner; + this.biome = biome; + this.seaLevel = seaLevel; + this.aggressiveMode = aggressiveMode; + this.player = player; + this.creativeMode = creativeMode; + + this.notAllowedToHang = new ArrayList(); + this.notAllowedToHang.add(Material.DIRT.getId()); + this.notAllowedToHang.add(Material.LONG_GRASS.getId()); + this.notAllowedToHang.add(Material.SNOW.getId()); + this.notAllowedToHang.add(Material.LOG.getId()); + + if(this.aggressiveMode) + { + this.notAllowedToHang.add(Material.GRASS.getId()); + this.notAllowedToHang.add(Material.STONE.getId()); + } + + this.playerBlocks = new ArrayList(); + this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome)); + + //in aggressive or creative world mode, also treat these blocks as user placed, to be removed + //this is helpful in the few cases where griefers intentionally use natural blocks to grief, + //like a single-block tower of iron ore or a giant penis constructed with melons + if(this.aggressiveMode || this.creativeMode) + { + this.playerBlocks.add(Material.IRON_ORE.getId()); + this.playerBlocks.add(Material.GOLD_ORE.getId()); + this.playerBlocks.add(Material.DIAMOND_ORE.getId()); + this.playerBlocks.add(Material.MELON_BLOCK.getId()); + this.playerBlocks.add(Material.MELON_STEM.getId()); + this.playerBlocks.add(Material.BEDROCK.getId()); + this.playerBlocks.add(Material.COAL_ORE.getId()); + this.playerBlocks.add(Material.PUMPKIN.getId()); + this.playerBlocks.add(Material.PUMPKIN_STEM.getId()); + this.playerBlocks.add(Material.MELON.getId()); + } + + if(this.aggressiveMode) + { + this.playerBlocks.add(Material.LEAVES.getId()); + this.playerBlocks.add(Material.LOG.getId()); + this.playerBlocks.add(Material.VINE.getId()); + } + } + + @Override + public void run() + { + //order is important! + + //remove sandstone which appears to be unnatural + this.removeSandstone(); + + //remove any blocks which are definitely player placed + this.removePlayerBlocks(); + + //reduce large outcroppings of stone, sandstone + this.reduceStone(); + + //reduce logs, except in jungle biomes + this.reduceLogs(); + + //remove natural blocks which are unnaturally hanging in the air + this.removeHanging(); + + //remove natural blocks which are unnaturally stacked high + this.removeWallsAndTowers(); + + //fill unnatural thin trenches and single-block potholes + this.fillHolesAndTrenches(); + + //fill water depressions and fix unnatural surface ripples + this.fixWater(); + + //remove water/lava above sea level + this.removeDumpedFluids(); + + //cover over any gaping holes in creative mode worlds + if(this.creativeMode && this.environment == Environment.NORMAL) + { + this.fillBigHoles(); + } + + //cover surface stone and gravel with sand or grass, as the biome requires + this.coverSurfaceStone(); + + //remove any player-placed leaves + this.removePlayerLeaves(); + + //schedule main thread task to apply the result to the world + RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task); + } + + private void removePlayerLeaves() + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = this.seaLevel - 1; y < snapshots[0].length; y++) + { + //note: see minecraft wiki data values for leaves + BlockSnapshot block = snapshots[x][y][z]; + if(block.typeId == Material.LEAVES.getId() && (block.data & 0x4) != 0) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + + private void fillBigHoles() + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + //replace air, lava, or running water at sea level with stone + if(this.snapshots[x][this.seaLevel - 2][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 2][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 2][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 2][z].data != 0)) + { + this.snapshots[x][this.seaLevel - 2][z].typeId = Material.STONE.getId(); + } + + //do the same for one layer beneath that (because a future restoration step may convert surface stone to sand, which falls down) + if(this.snapshots[x][this.seaLevel - 3][z].typeId == Material.AIR.getId() || this.snapshots[x][this.seaLevel - 3][z].typeId == Material.LAVA.getId() || (this.snapshots[x][this.seaLevel - 3][z].typeId == Material.WATER.getId() || this.snapshots[x][this.seaLevel - 3][z].data != 0)) + { + this.snapshots[x][this.seaLevel - 3][z].typeId = Material.STONE.getId(); + } + } + } + } + + //converts sandstone adjacent to sand to sand, and any other sandstone to air + private void removeSandstone() + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = snapshots[0].length - 2; y > miny; y--) + { + if(snapshots[x][y][z].typeId != Material.SANDSTONE.getId()) continue; + + BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; + BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; + BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; + BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; + BlockSnapshot underBlock = this.snapshots[x][y - 1][z]; + BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z]; + + //skip blocks which may cause a cave-in + if(aboveBlock.typeId == Material.SAND.getId() && underBlock.typeId == Material.AIR.getId()) continue; + + //count adjacent non-air/non-leaf blocks + if( leftBlock.typeId == Material.SAND.getId() || + rightBlock.typeId == Material.SAND.getId() || + upBlock.typeId == Material.SAND.getId() || + downBlock.typeId == Material.SAND.getId() || + aboveBlock.typeId == Material.SAND.getId() || + underBlock.typeId == Material.SAND.getId()) + { + snapshots[x][y][z].typeId = Material.SAND.getId(); + } + else + { + snapshots[x][y][z].typeId = Material.AIR.getId(); + } + } + } + } + } + + private void reduceStone() + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + int thisy = this.highestY(x, z, true); + + while(thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].typeId == Material.STONE.getId() || this.snapshots[x][thisy][z].typeId == Material.SANDSTONE.getId())) + { + BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z]; + BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z]; + BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1]; + BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1]; + + //count adjacent non-air/non-leaf blocks + byte adjacentBlockCount = 0; + if(leftBlock.typeId != Material.AIR.getId() && leftBlock.typeId != Material.LEAVES.getId() && leftBlock.typeId != Material.VINE.getId()) + { + adjacentBlockCount++; + } + if(rightBlock.typeId != Material.AIR.getId() && rightBlock.typeId != Material.LEAVES.getId() && rightBlock.typeId != Material.VINE.getId()) + { + adjacentBlockCount++; + } + if(downBlock.typeId != Material.AIR.getId() && downBlock.typeId != Material.LEAVES.getId() && downBlock.typeId != Material.VINE.getId()) + { + adjacentBlockCount++; + } + if(upBlock.typeId != Material.AIR.getId() && upBlock.typeId != Material.LEAVES.getId() && upBlock.typeId != Material.VINE.getId()) + { + adjacentBlockCount++; + } + + if(adjacentBlockCount < 3) + { + this.snapshots[x][thisy][z].typeId = Material.AIR.getId(); + } + + thisy--; + } + } + } + } + + private void reduceLogs() + { + boolean jungleBiome = this.biome == Biome.JUNGLE || this.biome == Biome.JUNGLE_HILLS; + + //scan all blocks above sea level + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = this.seaLevel - 1; y < snapshots[0].length; y++) + { + BlockSnapshot block = snapshots[x][y][z]; + + //skip non-logs + if(block.typeId != Material.LOG.getId()) continue; + + //if in jungle biome, skip jungle logs + if(jungleBiome && block.data == 3) continue; + + //examine adjacent blocks for logs + BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; + BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; + BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; + BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; + + //if any, remove the log + if(leftBlock.typeId == Material.LOG.getId() || rightBlock.typeId == Material.LOG.getId() || upBlock.typeId == Material.LOG.getId() || downBlock.typeId == Material.LOG.getId()) + { + this.snapshots[x][y][z].typeId = Material.AIR.getId(); + } + } + } + } + } + + private void removePlayerBlocks() + { + int miny = this.miny; + if(miny < 1) miny = 1; + + //remove all player blocks + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = miny; y < snapshots[0].length - 1; y++) + { + BlockSnapshot block = snapshots[x][y][z]; + if(this.playerBlocks.contains(block.typeId)) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + + private void removeHanging() + { + int miny = this.miny; + if(miny < 1) miny = 1; + + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = miny; y < snapshots[0].length - 1; y++) + { + BlockSnapshot block = snapshots[x][y][z]; + BlockSnapshot underBlock = snapshots[x][y - 1][z]; + + if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.STATIONARY_WATER.getId() || underBlock.typeId == Material.STATIONARY_LAVA.getId() || underBlock.typeId == Material.LEAVES.getId()) + { + if(this.notAllowedToHang.contains(block.typeId)) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + } + + private void removeWallsAndTowers() + { + int [] excludedBlocksArray = new int [] + { + Material.CACTUS.getId(), + Material.LONG_GRASS.getId(), + Material.RED_MUSHROOM.getId(), + Material.BROWN_MUSHROOM.getId(), + Material.DEAD_BUSH.getId(), + Material.SAPLING.getId(), + Material.YELLOW_FLOWER.getId(), + Material.RED_ROSE.getId(), + Material.SUGAR_CANE_BLOCK.getId(), + Material.VINE.getId(), + Material.PUMPKIN.getId(), + Material.WATER_LILY.getId(), + Material.LEAVES.getId() + }; + + ArrayList excludedBlocks = new ArrayList(); + for(int i = 0; i < excludedBlocksArray.length; i++) excludedBlocks.add(excludedBlocksArray[i]); + + boolean changed; + do + { + changed = false; + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + int thisy = this.highestY(x, z, false); + if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue; + + int righty = this.highestY(x + 1, z, false); + int lefty = this.highestY(x - 1, z, false); + while(lefty < thisy && righty < thisy) + { + this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); + changed = true; + } + + int upy = this.highestY(x, z + 1, false); + int downy = this.highestY(x, z - 1, false); + while(upy < thisy && downy < thisy) + { + this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); + changed = true; + } + } + } + }while(changed); + } + + private void coverSurfaceStone() + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + int y = this.highestY(x, z, true); + BlockSnapshot block = snapshots[x][y][z]; + + if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.SOIL.getId() || block.typeId == Material.DIRT.getId() || block.typeId == Material.SANDSTONE.getId()) + { + if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH) + { + this.snapshots[x][y][z].typeId = Material.SAND.getId(); + } + else + { + this.snapshots[x][y][z].typeId = Material.GRASS.getId(); + } + } + } + } + } + + private void fillHolesAndTrenches() + { + ArrayList fillableBlocks = new ArrayList(); + fillableBlocks.add(Material.AIR.getId()); + fillableBlocks.add(Material.STATIONARY_WATER.getId()); + fillableBlocks.add(Material.STATIONARY_LAVA.getId()); + fillableBlocks.add(Material.LONG_GRASS.getId()); + + ArrayList notSuitableForFillBlocks = new ArrayList(); + notSuitableForFillBlocks.add(Material.LONG_GRASS.getId()); + notSuitableForFillBlocks.add(Material.CACTUS.getId()); + notSuitableForFillBlocks.add(Material.STATIONARY_WATER.getId()); + notSuitableForFillBlocks.add(Material.STATIONARY_LAVA.getId()); + + boolean changed; + do + { + changed = false; + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = 0; y < snapshots[0].length - 1; y++) + { + BlockSnapshot block = this.snapshots[x][y][z]; + if(!fillableBlocks.contains(block.typeId)) continue; + + BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; + BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; + + if(!fillableBlocks.contains(leftBlock.typeId) && !fillableBlocks.contains(rightBlock.typeId)) + { + if(!notSuitableForFillBlocks.contains(rightBlock.typeId)) + { + block.typeId = rightBlock.typeId; + changed = true; + } + } + + BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; + BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; + + if(!fillableBlocks.contains(upBlock.typeId) && !fillableBlocks.contains(downBlock.typeId)) + { + if(!notSuitableForFillBlocks.contains(downBlock.typeId)) + { + block.typeId = downBlock.typeId; + changed = true; + } + } + } + } + } + }while(changed); + } + + private void fixWater() + { + int miny = this.miny; + if(miny < 1) miny = 1; + + boolean changed; + + //remove hanging water or lava + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = miny; y < snapshots[0].length - 1; y++) + { + BlockSnapshot block = this.snapshots[x][y][z]; + BlockSnapshot underBlock = this.snapshots[x][y][z]; + if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId()) + { + if(underBlock.typeId == Material.AIR.getId() || (underBlock.data != 0)) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + + //fill water depressions + do + { + changed = false; + for(int y = this.seaLevel - 10; y <= this.seaLevel; y++) + { + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + BlockSnapshot block = snapshots[x][y][z]; + + //only consider air blocks and flowing water blocks for upgrade to water source blocks + if(block.typeId == Material.AIR.getId() || (block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0)) + { + BlockSnapshot leftBlock = this.snapshots[x + 1][y][z]; + BlockSnapshot rightBlock = this.snapshots[x - 1][y][z]; + BlockSnapshot upBlock = this.snapshots[x][y][z + 1]; + BlockSnapshot downBlock = this.snapshots[x][y][z - 1]; + BlockSnapshot underBlock = this.snapshots[x][y - 1][z]; + + //block underneath MUST be source water + if(underBlock.typeId != Material.STATIONARY_WATER.getId() || underBlock.data != 0) continue; + + //count adjacent source water blocks + byte adjacentSourceWaterCount = 0; + if(leftBlock.typeId == Material.STATIONARY_WATER.getId() && leftBlock.data == 0) + { + adjacentSourceWaterCount++; + } + if(rightBlock.typeId == Material.STATIONARY_WATER.getId() && rightBlock.data == 0) + { + adjacentSourceWaterCount++; + } + if(upBlock.typeId == Material.STATIONARY_WATER.getId() && upBlock.data == 0) + { + adjacentSourceWaterCount++; + } + if(downBlock.typeId == Material.STATIONARY_WATER.getId() && downBlock.data == 0) + { + adjacentSourceWaterCount++; + } + + //at least two adjacent blocks must be source water + if(adjacentSourceWaterCount >= 2) + { + block.typeId = Material.STATIONARY_WATER.getId(); + block.data = 0; + changed = true; + } + } + } + } + } + }while(changed); + } + + private void removeDumpedFluids() + { + //remove any surface water or lava above sea level, presumed to be placed by players + //sometimes, this is naturally generated. but replacing it is very easy with a bucket, so overall this is a good plan + if(this.environment == Environment.NETHER) return; + for(int x = 1; x < snapshots.length - 1; x++) + { + for(int z = 1; z < snapshots[0][0].length - 1; z++) + { + for(int y = this.seaLevel - 1; y < snapshots[0].length - 1; y++) + { + BlockSnapshot block = snapshots[x][y][z]; + if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId() || + block.typeId == Material.WATER.getId() || block.typeId == Material.LAVA.getId()) + { + block.typeId = Material.AIR.getId(); + } + } + } + } + } + + private int highestY(int x, int z, boolean ignoreLeaves) + { + int y; + for(y = snapshots[0].length - 1; y > 0; y--) + { + BlockSnapshot block = this.snapshots[x][y][z]; + if(block.typeId != Material.AIR.getId() && + !(ignoreLeaves && block.typeId == Material.SNOW.getId()) && + !(ignoreLeaves && block.typeId == Material.LEAVES.getId()) && + !(block.typeId == Material.STATIONARY_WATER.getId()) && + !(block.typeId == Material.WATER.getId()) && + !(block.typeId == Material.LAVA.getId()) && + !(block.typeId == Material.STATIONARY_LAVA.getId())) + { + return y; + } + } + + return y; + } + + static ArrayList getPlayerBlocks(Environment environment, Biome biome) + { + //NOTE on this list. why not make a list of natural blocks? + //answer: better to leave a few player blocks than to remove too many natural blocks. remember we're "restoring nature" + //a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal + ArrayList playerBlocks = new ArrayList(); + playerBlocks.add(Material.FIRE.getId()); + playerBlocks.add(Material.BED_BLOCK.getId()); + playerBlocks.add(Material.WOOD.getId()); + playerBlocks.add(Material.BOOKSHELF.getId()); + playerBlocks.add(Material.BREWING_STAND.getId()); + playerBlocks.add(Material.BRICK.getId()); + playerBlocks.add(Material.COBBLESTONE.getId()); + playerBlocks.add(Material.GLASS.getId()); + playerBlocks.add(Material.LAPIS_BLOCK.getId()); + playerBlocks.add(Material.DISPENSER.getId()); + playerBlocks.add(Material.NOTE_BLOCK.getId()); + playerBlocks.add(Material.POWERED_RAIL.getId()); + playerBlocks.add(Material.DETECTOR_RAIL.getId()); + playerBlocks.add(Material.PISTON_STICKY_BASE.getId()); + playerBlocks.add(Material.PISTON_BASE.getId()); + playerBlocks.add(Material.PISTON_EXTENSION.getId()); + playerBlocks.add(Material.WOOL.getId()); + playerBlocks.add(Material.PISTON_MOVING_PIECE.getId()); + playerBlocks.add(Material.GOLD_BLOCK.getId()); + playerBlocks.add(Material.IRON_BLOCK.getId()); + playerBlocks.add(Material.DOUBLE_STEP.getId()); + playerBlocks.add(Material.STEP.getId()); + playerBlocks.add(Material.CROPS.getId()); + playerBlocks.add(Material.TNT.getId()); + playerBlocks.add(Material.MOSSY_COBBLESTONE.getId()); + playerBlocks.add(Material.TORCH.getId()); + playerBlocks.add(Material.FIRE.getId()); + playerBlocks.add(Material.WOOD_STAIRS.getId()); + playerBlocks.add(Material.CHEST.getId()); + playerBlocks.add(Material.REDSTONE_WIRE.getId()); + playerBlocks.add(Material.DIAMOND_BLOCK.getId()); + playerBlocks.add(Material.WORKBENCH.getId()); + playerBlocks.add(Material.FURNACE.getId()); + playerBlocks.add(Material.BURNING_FURNACE.getId()); + playerBlocks.add(Material.WOODEN_DOOR.getId()); + playerBlocks.add(Material.SIGN_POST.getId()); + playerBlocks.add(Material.LADDER.getId()); + playerBlocks.add(Material.RAILS.getId()); + playerBlocks.add(Material.COBBLESTONE_STAIRS.getId()); + playerBlocks.add(Material.WALL_SIGN.getId()); + playerBlocks.add(Material.STONE_PLATE.getId()); + playerBlocks.add(Material.LEVER.getId()); + playerBlocks.add(Material.IRON_DOOR_BLOCK.getId()); + playerBlocks.add(Material.WOOD_PLATE.getId()); + playerBlocks.add(Material.REDSTONE_TORCH_ON.getId()); + playerBlocks.add(Material.REDSTONE_TORCH_OFF.getId()); + playerBlocks.add(Material.STONE_BUTTON.getId()); + playerBlocks.add(Material.SNOW_BLOCK.getId()); + playerBlocks.add(Material.JUKEBOX.getId()); + playerBlocks.add(Material.FENCE.getId()); + playerBlocks.add(Material.PORTAL.getId()); + playerBlocks.add(Material.JACK_O_LANTERN.getId()); + playerBlocks.add(Material.CAKE_BLOCK.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_OFF.getId()); + playerBlocks.add(Material.TRAP_DOOR.getId()); + playerBlocks.add(Material.SMOOTH_BRICK.getId()); + playerBlocks.add(Material.HUGE_MUSHROOM_1.getId()); + playerBlocks.add(Material.HUGE_MUSHROOM_2.getId()); + playerBlocks.add(Material.IRON_FENCE.getId()); + playerBlocks.add(Material.THIN_GLASS.getId()); + playerBlocks.add(Material.MELON_STEM.getId()); + playerBlocks.add(Material.FENCE_GATE.getId()); + playerBlocks.add(Material.BRICK_STAIRS.getId()); + playerBlocks.add(Material.SMOOTH_STAIRS.getId()); + playerBlocks.add(Material.ENCHANTMENT_TABLE.getId()); + playerBlocks.add(Material.BREWING_STAND.getId()); + playerBlocks.add(Material.CAULDRON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + playerBlocks.add(Material.WEB.getId()); + playerBlocks.add(Material.SPONGE.getId()); + playerBlocks.add(Material.GRAVEL.getId()); + playerBlocks.add(Material.EMERALD_BLOCK.getId()); + playerBlocks.add(Material.SANDSTONE.getId()); + playerBlocks.add(Material.WOOD_STEP.getId()); + playerBlocks.add(Material.WOOD_DOUBLE_STEP.getId()); + playerBlocks.add(Material.ENDER_CHEST.getId()); + playerBlocks.add(Material.SANDSTONE_STAIRS.getId()); + playerBlocks.add(Material.SPRUCE_WOOD_STAIRS.getId()); + playerBlocks.add(Material.JUNGLE_WOOD_STAIRS.getId()); + playerBlocks.add(Material.COMMAND.getId()); + playerBlocks.add(Material.BEACON.getId()); + playerBlocks.add(Material.COBBLE_WALL.getId()); + playerBlocks.add(Material.FLOWER_POT.getId()); + playerBlocks.add(Material.CARROT.getId()); + playerBlocks.add(Material.POTATO.getId()); + playerBlocks.add(Material.WOOD_BUTTON.getId()); + playerBlocks.add(Material.SKULL.getId()); + playerBlocks.add(Material.ANVIL.getId()); + + + //these are unnatural in the standard world, but not in the nether + if(environment != Environment.NETHER) + { + playerBlocks.add(Material.NETHERRACK.getId()); + playerBlocks.add(Material.SOUL_SAND.getId()); + playerBlocks.add(Material.GLOWSTONE.getId()); + playerBlocks.add(Material.NETHER_BRICK.getId()); + playerBlocks.add(Material.NETHER_FENCE.getId()); + playerBlocks.add(Material.NETHER_BRICK_STAIRS.getId()); + } + + //these are unnatural in the standard and nether worlds, but not in the end + if(environment != Environment.THE_END) + { + playerBlocks.add(Material.OBSIDIAN.getId()); + playerBlocks.add(Material.ENDER_STONE.getId()); + playerBlocks.add(Material.ENDER_PORTAL_FRAME.getId()); + } + + //these are unnatural in sandy biomes, but not elsewhere + if(biome == Biome.DESERT || biome == Biome.DESERT_HILLS || biome == Biome.BEACH || environment != Environment.NORMAL) + { + playerBlocks.add(Material.LEAVES.getId()); + playerBlocks.add(Material.LOG.getId()); + } + + return playerBlocks; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/Visualization.java b/src/me/ryanhamshire/GriefPrevention/Visualization.java index f6fe486..a037219 100644 --- a/src/me/ryanhamshire/GriefPrevention/Visualization.java +++ b/src/me/ryanhamshire/GriefPrevention/Visualization.java @@ -1,225 +1,227 @@ -/* - GriefPrevention Server Plugin for Minecraft - Copyright (C) 2011 Ryan Hamshire - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -package me.ryanhamshire.GriefPrevention; - -import java.util.ArrayList; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Player; - -//represents a visualization sent to a player -//FEATURE: to show players visually where claim boundaries are, we send them fake block change packets -//the result is that those players see new blocks, but the world hasn't been changed. other players can't see the new blocks, either. -public class Visualization -{ - public ArrayList elements = new ArrayList(); - - //sends a visualization to a player - public static void Apply(Player player, Visualization visualization) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); - - //if he has any current visualization, clear it first - if(playerData.currentVisualization != null) - { - Visualization.Revert(player); - } - - //if he's online, create a task to send him the visualization in about half a second - if(player.isOnline()) - { - GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new VisualizationApplicationTask(player, playerData, visualization), 10L); - } - } - - //reverts a visualization by sending another block change list, this time with the real world block values - public static void Revert(Player player) - { - PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); - - Visualization visualization = playerData.currentVisualization; - - if(playerData.currentVisualization != null) - { - if(player.isOnline()) - { - for(int i = 0; i < visualization.elements.size(); i++) - { - VisualizationElement element = visualization.elements.get(i); - Block block = element.location.getBlock(); - player.sendBlockChange(element.location, block.getType(), block.getData()); - } - } - - playerData.currentVisualization = null; - } - } - - //convenience method to build a visualization from a claim - //visualizationType determines the style (gold blocks, silver, red, diamond, etc) - public static Visualization FromClaim(Claim claim, int height, VisualizationType visualizationType, Location locality) - { - //visualize only top level claims - if(claim.parent != null) - { - return FromClaim(claim.parent, height, visualizationType, locality); - } - - Visualization visualization = new Visualization(); - - //add subdivisions first - for(int i = 0; i < claim.children.size(); i++) - { - visualization.addClaimElements(claim.children.get(i), height, VisualizationType.Subdivision, locality); - } - - //add top level last so that it takes precedence (it shows on top when the child claim boundaries overlap with its boundaries) - visualization.addClaimElements(claim, height, visualizationType, locality); - - return visualization; - } - - //adds a claim's visualization to the current visualization - //handy for combining several visualizations together, as when visualization a top level claim with several subdivisions inside - //locality is a performance consideration. only create visualization blocks for around 100 blocks of the locality - private void addClaimElements(Claim claim, int height, VisualizationType visualizationType, Location locality) - { - Location smallXsmallZ = claim.getLesserBoundaryCorner(); - Location bigXbigZ = claim.getGreaterBoundaryCorner(); - World world = smallXsmallZ.getWorld(); - - int smallx = smallXsmallZ.getBlockX(); - int smallz = smallXsmallZ.getBlockZ(); - int bigx = bigXbigZ.getBlockX(); - int bigz = bigXbigZ.getBlockZ(); - - Material cornerMaterial; - Material accentMaterial; - - if(visualizationType == VisualizationType.Claim) - { - cornerMaterial = Material.GLOWSTONE; - accentMaterial = Material.GOLD_BLOCK; - } - - else if(visualizationType == VisualizationType.Subdivision) - { - cornerMaterial = Material.IRON_BLOCK; - accentMaterial = Material.WOOL; - } - - else if(visualizationType == VisualizationType.RestoreNature) - { - cornerMaterial = Material.DIAMOND_BLOCK; - accentMaterial = Material.DIAMOND_BLOCK; - } - - else - { - cornerMaterial = Material.GLOWING_REDSTONE_ORE; - accentMaterial = Material.NETHERRACK; - } - - //bottom left corner - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, smallz), cornerMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx + 1, height, smallz), accentMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, smallz + 1), accentMaterial, (byte)0)); - - //bottom right corner - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, smallz), cornerMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx - 1, height, smallz), accentMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, smallz + 1), accentMaterial, (byte)0)); - - //top right corner - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, bigz), cornerMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx - 1, height, bigz), accentMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, bigz - 1), accentMaterial, (byte)0)); - - //top left corner - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, bigz), cornerMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx + 1, height, bigz), accentMaterial, (byte)0)); - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, bigz - 1), accentMaterial, (byte)0)); - - //locality - int minx = locality.getBlockX() - 100; - int minz = locality.getBlockZ() - 100; - int maxx = locality.getBlockX() + 100; - int maxz = locality.getBlockZ() + 100; - - //top line - for(int x = smallx + 10; x < bigx - 10; x += 10) - { - if(x > minx && x < maxx) - this.elements.add(new VisualizationElement(getVisibleLocation(world, x, height, bigz), accentMaterial, (byte)0)); - } - - //bottom line - for(int x = smallx + 10; x < bigx - 10; x += 10) - { - if(x > minx && x < maxx) - this.elements.add(new VisualizationElement(getVisibleLocation(world, x, height, smallz), accentMaterial, (byte)0)); - } - - //left line - for(int z = smallz + 10; z < bigz - 10; z += 10) - { - if(z > minz && z < maxz) - this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, z), accentMaterial, (byte)0)); - } - - //right line - for(int z = smallz + 10; z < bigz - 10; z += 10) - { - if(z > minz && z < maxz) - this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, z), accentMaterial, (byte)0)); - } - } - - //finds a block the player can probably see. this is how visualizations "cling" to the ground or ceiling - private static Location getVisibleLocation(World world, int x, int y, int z) - { - Block block = world.getBlockAt(x, y, z); - BlockFace direction = (isTransparent(block)) ? BlockFace.DOWN : BlockFace.UP; - - while( block.getY() >= 1 && - block.getY() < world.getMaxHeight() - 1 && - (!isTransparent(block.getRelative(BlockFace.UP)) || isTransparent(block))) - { - block = block.getRelative(direction); - } - - return block.getLocation(); - } - - //helper method for above. allows visualization blocks to sit underneath partly transparent blocks like grass and fence - private static boolean isTransparent(Block block) - { - return ( block.getType() == Material.AIR || - block.getType() == Material.LONG_GRASS || - block.getType() == Material.FENCE || - block.getType() == Material.LEAVES || - block.getType() == Material.RED_ROSE || - block.getType() == Material.CHEST || - block.getType() == Material.YELLOW_FLOWER ); - } -} +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.util.ArrayList; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Player; + +//represents a visualization sent to a player +//FEATURE: to show players visually where claim boundaries are, we send them fake block change packets +//the result is that those players see new blocks, but the world hasn't been changed. other players can't see the new blocks, either. +public class Visualization +{ + public ArrayList elements = new ArrayList(); + + //sends a visualization to a player + public static void Apply(Player player, Visualization visualization) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + + //if he has any current visualization, clear it first + if(playerData.currentVisualization != null) + { + Visualization.Revert(player); + } + + //if he's online, create a task to send him the visualization in about half a second + if(player.isOnline()) + { + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new VisualizationApplicationTask(player, playerData, visualization), 10L); + } + } + + //reverts a visualization by sending another block change list, this time with the real world block values + public static void Revert(Player player) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + + Visualization visualization = playerData.currentVisualization; + + if(playerData.currentVisualization != null) + { + if(player.isOnline()) + { + for(int i = 0; i < visualization.elements.size(); i++) + { + VisualizationElement element = visualization.elements.get(i); + Block block = element.location.getBlock(); + player.sendBlockChange(element.location, block.getType(), block.getData()); + } + } + + playerData.currentVisualization = null; + } + } + + //convenience method to build a visualization from a claim + //visualizationType determines the style (gold blocks, silver, red, diamond, etc) + public static Visualization FromClaim(Claim claim, int height, VisualizationType visualizationType, Location locality) + { + //visualize only top level claims + if(claim.parent != null) + { + return FromClaim(claim.parent, height, visualizationType, locality); + } + + Visualization visualization = new Visualization(); + + //add subdivisions first + for(int i = 0; i < claim.children.size(); i++) + { + visualization.addClaimElements(claim.children.get(i), height, VisualizationType.Subdivision, locality); + } + + //add top level last so that it takes precedence (it shows on top when the child claim boundaries overlap with its boundaries) + visualization.addClaimElements(claim, height, visualizationType, locality); + + return visualization; + } + + //adds a claim's visualization to the current visualization + //handy for combining several visualizations together, as when visualization a top level claim with several subdivisions inside + //locality is a performance consideration. only create visualization blocks for around 100 blocks of the locality + private void addClaimElements(Claim claim, int height, VisualizationType visualizationType, Location locality) + { + Location smallXsmallZ = claim.getLesserBoundaryCorner(); + Location bigXbigZ = claim.getGreaterBoundaryCorner(); + World world = smallXsmallZ.getWorld(); + + int smallx = smallXsmallZ.getBlockX(); + int smallz = smallXsmallZ.getBlockZ(); + int bigx = bigXbigZ.getBlockX(); + int bigz = bigXbigZ.getBlockZ(); + + Material cornerMaterial; + Material accentMaterial; + + if(visualizationType == VisualizationType.Claim) + { + cornerMaterial = Material.GLOWSTONE; + accentMaterial = Material.GOLD_BLOCK; + } + + else if(visualizationType == VisualizationType.Subdivision) + { + cornerMaterial = Material.IRON_BLOCK; + accentMaterial = Material.WOOL; + } + + else if(visualizationType == VisualizationType.RestoreNature) + { + cornerMaterial = Material.DIAMOND_BLOCK; + accentMaterial = Material.DIAMOND_BLOCK; + } + + else + { + cornerMaterial = Material.GLOWING_REDSTONE_ORE; + accentMaterial = Material.NETHERRACK; + } + + //bottom left corner + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, smallz), cornerMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx + 1, height, smallz), accentMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, smallz + 1), accentMaterial, (byte)0)); + + //bottom right corner + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, smallz), cornerMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx - 1, height, smallz), accentMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, smallz + 1), accentMaterial, (byte)0)); + + //top right corner + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, bigz), cornerMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx - 1, height, bigz), accentMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, bigz - 1), accentMaterial, (byte)0)); + + //top left corner + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, bigz), cornerMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx + 1, height, bigz), accentMaterial, (byte)0)); + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, bigz - 1), accentMaterial, (byte)0)); + + //locality + int minx = locality.getBlockX() - 100; + int minz = locality.getBlockZ() - 100; + int maxx = locality.getBlockX() + 100; + int maxz = locality.getBlockZ() + 100; + + //top line + for(int x = smallx + 10; x < bigx - 10; x += 10) + { + if(x > minx && x < maxx) + this.elements.add(new VisualizationElement(getVisibleLocation(world, x, height, bigz), accentMaterial, (byte)0)); + } + + //bottom line + for(int x = smallx + 10; x < bigx - 10; x += 10) + { + if(x > minx && x < maxx) + this.elements.add(new VisualizationElement(getVisibleLocation(world, x, height, smallz), accentMaterial, (byte)0)); + } + + //left line + for(int z = smallz + 10; z < bigz - 10; z += 10) + { + if(z > minz && z < maxz) + this.elements.add(new VisualizationElement(getVisibleLocation(world, smallx, height, z), accentMaterial, (byte)0)); + } + + //right line + for(int z = smallz + 10; z < bigz - 10; z += 10) + { + if(z > minz && z < maxz) + this.elements.add(new VisualizationElement(getVisibleLocation(world, bigx, height, z), accentMaterial, (byte)0)); + } + } + + //finds a block the player can probably see. this is how visualizations "cling" to the ground or ceiling + private static Location getVisibleLocation(World world, int x, int y, int z) + { + Block block = world.getBlockAt(x, y, z); + BlockFace direction = (isTransparent(block)) ? BlockFace.DOWN : BlockFace.UP; + + while( block.getY() >= 1 && + block.getY() < world.getMaxHeight() - 1 && + (!isTransparent(block.getRelative(BlockFace.UP)) || isTransparent(block))) + { + block = block.getRelative(direction); + } + + return block.getLocation(); + } + + //helper method for above. allows visualization blocks to sit underneath partly transparent blocks like grass and fence + private static boolean isTransparent(Block block) + { + return ( block.getType() == Material.AIR || + block.getType() == Material.LONG_GRASS || + block.getType() == Material.FENCE || + block.getType() == Material.LEAVES || + block.getType() == Material.RED_ROSE || + block.getType() == Material.CHEST || + block.getType() == Material.TORCH || + block.getType() == Material.VINE || + block.getType() == Material.YELLOW_FLOWER ); + } +}