From e68fd631946a7277f7f981fe60112eacbc7574ea Mon Sep 17 00:00:00 2001 From: Ryan Hamshire Date: Thu, 29 Mar 2012 17:27:12 -0700 Subject: [PATCH] 3.2 --- README | 0 plugin.yml | 123 ++ .../GriefPrevention/BlockEventHandler.java | 405 +++++ .../GriefPrevention/BlockSnapshot.java | 37 + .../ryanhamshire/GriefPrevention/Claim.java | 554 ++++++ .../GriefPrevention/ClaimPermission.java | 27 + .../GriefPrevention/CreateClaimResult.java | 29 + .../GriefPrevention/DataStore.java | 1069 +++++++++++ .../DeliverClaimBlocksTask.java | 64 + .../GriefPrevention/EntityEventHandler.java | 306 ++++ .../GriefPrevention/GriefPrevention.java | 1568 +++++++++++++++++ .../GriefPrevention/PlayerData.java | 130 ++ .../GriefPrevention/PlayerEventHandler.java | 1097 ++++++++++++ .../GriefPrevention/PlayerRescueTask.java | 69 + .../GriefPrevention/Public API.txt | 41 + .../RestoreNatureExecutionTask.java | 97 + .../RestoreNatureProcessingTask.java | 470 +++++ .../GriefPrevention/SecureClaimTask.java | 56 + .../GriefPrevention/ShovelMode.java | 28 + .../GriefPrevention/SiegeCheckupTask.java | 110 ++ .../GriefPrevention/SiegeData.java | 40 + .../GriefPrevention/TextMode.java | 31 + .../GriefPrevention/TreeCleanupTask.java | 100 ++ .../GriefPrevention/Visualization.java | 214 +++ .../VisualizationApplicationTask.java | 52 + .../GriefPrevention/VisualizationElement.java | 36 + .../GriefPrevention/VisualizationType.java | 28 + 27 files changed, 6781 insertions(+) create mode 100644 README create mode 100644 plugin.yml create mode 100644 src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java create mode 100644 src/me/ryanhamshire/GriefPrevention/BlockSnapshot.java create mode 100644 src/me/ryanhamshire/GriefPrevention/Claim.java create mode 100644 src/me/ryanhamshire/GriefPrevention/ClaimPermission.java create mode 100644 src/me/ryanhamshire/GriefPrevention/CreateClaimResult.java create mode 100644 src/me/ryanhamshire/GriefPrevention/DataStore.java create mode 100644 src/me/ryanhamshire/GriefPrevention/DeliverClaimBlocksTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java create mode 100644 src/me/ryanhamshire/GriefPrevention/GriefPrevention.java create mode 100644 src/me/ryanhamshire/GriefPrevention/PlayerData.java create mode 100644 src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java create mode 100644 src/me/ryanhamshire/GriefPrevention/PlayerRescueTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/Public API.txt create mode 100644 src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/SecureClaimTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/ShovelMode.java create mode 100644 src/me/ryanhamshire/GriefPrevention/SiegeCheckupTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/SiegeData.java create mode 100644 src/me/ryanhamshire/GriefPrevention/TextMode.java create mode 100644 src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/Visualization.java create mode 100644 src/me/ryanhamshire/GriefPrevention/VisualizationApplicationTask.java create mode 100644 src/me/ryanhamshire/GriefPrevention/VisualizationElement.java create mode 100644 src/me/ryanhamshire/GriefPrevention/VisualizationType.java diff --git a/README b/README new file mode 100644 index 0000000..e69de29 diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..d2f51f9 --- /dev/null +++ b/plugin.yml @@ -0,0 +1,123 @@ +name: GriefPrevention +main: me.ryanhamshire.GriefPrevention.GriefPrevention +softdepend: [Vault] +version: 3.2 +commands: + abandonclaim: + description: Deletes a claim. + usage: /abandonclaim + abandonallclaims: + description: Deletes ALL your claims. + usage: /AbandonAllClaims + trust: + description: Grants a player full access to your claim(s). + usage: /Trust See also /UnTrust, /ContainerTrust, /AccessTrust, and /PermissionTrust. + aliases: t + untrust: + description: Revokes a player's access to your claim(s). + usage: /UnTrust + aliases: ut + containertrust: + description: Grants a player access to your containers. + usage: /ContainerTrust + aliases: ct + accesstrust: + description: Grants a player entry to your claim(s) and use of your bed. + usage: /AccessTrust + aliases: at + permissiontrust: + description: Grants a player permission to grant his level of permission to others. + usage: /PermissionTrust + aliases: pt + subdivideclaims: + description: Switches the shovel tool to subdivision mode, used to subdivide your claims. + usage: /SubdivideClaims + aliases: sc + 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 + basicclaims: + description: Switches the shovel tool back to basic claims mode. + usage: /BasicClaims + aliases: bc + buyclaimblocks: + description: Purchases additional claim blocks with server money. Doesn't work on servers without a Vault-compatible economy plugin. + usage: /BuyClaimBlocks + aliases: buyclaim + sellclaimblocks: + description: Sells your claim blocks for server money. Doesn't work on servers without a Vault-compatible economy plugin. + usage: /SellClaimBlocks + aliases: sellclaim + trapped: + description: Ejects you to nearby unclaimed land. Usable once per 8 hours. + usage: /Trapped + trustlist: + description: Lists permissions for the claim you're standing in. + usage: /TrustList + 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: adminclaims +permissions: + griefprevention.createclaims: + description: Grants permission to create claims. + default: op + griefprevention.admin.*: + description: Grants all administrative functionality. + children: + griefprevention.restorenature: true + griefprevention.ignoreclaims: true + griefprevention.adminclaims: true + griefprevention.adjustclaimblocks: true + griefprevention.deleteclaims: true + griefprevention.spam: 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 \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java new file mode 100644 index 0000000..5bf8326 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/BlockEventHandler.java @@ -0,0 +1,405 @@ +/* + 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.List; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.block.Block; +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.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.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +//event handlers related to blocks +public class BlockEventHandler implements Listener +{ + //convenience reference to singleton datastore + private DataStore dataStore; + + //boring typical constructor + public BlockEventHandler(DataStore dataStore) + { + this.dataStore = dataStore; + } + + //when a block is damaged... + @EventHandler(ignoreCancelled = true) + public void onBlockDamaged(BlockDamageEvent event) + { + 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, "You can't give away items while involved in a siege."); + 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, "To give away the item(s) in your hand, left-click the chest again."); + } + + //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, "This chest is full."); + + 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, "Item(s) transferred to chest!"); + } + } + } + + //when a player breaks a block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onBlockBreak(BlockBreakEvent breakEvent) + { + Player player = breakEvent.getPlayer(); + Block block = breakEvent.getBlock(); + 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) + { + //cache the claim for later reference + playerData.lastClaim = claim; + + //check permissions + String noBuildReason = claim.allowBreak(player, block.getType()); + + //if permission to break and breaking UNDER the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY()) + { + if(noBuildReason == null) + { + //extend the claim downward beyond the breakage point + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + } + + //otherwise if not allowed to break blocks here, tell the player why + else if(noBuildReason != null) + { + breakEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + return; + } + } + + //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 block... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onBlockPlace(BlockPlaceEvent placeEvent) + { + Player player = placeEvent.getPlayer(); + Block block = placeEvent.getBlock(); + + //FEATURE: limit fire placement, to prevent PvP-by-fire and general fiery messes + + //if placed block is fire and pvp is off, block it apply limitations based on the block it's placed on + if(block.getType() == Material.FIRE && !block.getWorld().getPVP()) + { + 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()) < 9) + { + player.sendMessage(block.getY() + " " + otherPlayer.getLocation().getBlockY()); + + GriefPrevention.sendMessage(player, TextMode.Err, "You can't start a fire this close to " + otherPlayer.getName() + "."); + 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) + { + playerData.lastClaim = claim; + String noBuildReason = claim.allowBuild(player); + + //if the player has permission for the claim and he's placing UNDER the claim + if(block.getY() < claim.lesserBoundaryCorner.getBlockY()) + { + if(noBuildReason == null) + { + //extend the claim downward + this.dataStore.extendClaim(claim, claim.getLesserBoundaryCorner().getBlockY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance); + } + } + + //otherwise if he doesn't have permission, tell him why + else if(noBuildReason != null) + { + placeEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason); + } + } + + //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, "This chest can't be protected because it's too deep underground. Consider moving it."); + 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); + GriefPrevention.sendMessage(player, TextMode.Success, "This chest is protected."); + } + + //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).succeeded) + { + radius--; + } + + //notify and explain to player + GriefPrevention.sendMessage(player, TextMode.Success, "This chest and nearby blocks are protected from breakage and theft. The gold and glowstone blocks mark the protected area."); + + //show the player the protected area + Claim newClaim = this.dataStore.getClaimAt(block.getLocation(), false, null); + Visualization visualization = Visualization.FromClaim(newClaim, block.getY(), VisualizationType.Claim); + Visualization.Apply(player, visualization); + } + + //instructions for using /trust + GriefPrevention.sendMessage(player, TextMode.Instr, "Use the /trust command to grant other players access."); + + //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, "To claim more land, use a golden shovel."); + } + } + + //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, "This chest is NOT protected. Consider expanding an existing claim or creating a new one."); + } + } + } + + //blocks "pushing" other players' blocks around (pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onBlockPistonExtend (BlockPistonExtendEvent event) + { + //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? + List blocks = event.getBlocks(); + 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); + return; + } + } + } + + //blocks theft by pulling blocks out of a claim (again pistons) + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + 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) + @EventHandler(priority = EventPriority.HIGHEST) + public void onBlockIgnite (BlockIgniteEvent igniteEvent) + { + if(igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL) igniteEvent.setCancelled(true); + } + + //fire doesn't spread, but other blocks still do (mushrooms and vines, for example) + @EventHandler(priority = EventPriority.HIGHEST) + public void onBlockSpread (BlockSpreadEvent spreadEvent) + { + if(spreadEvent.getSource().getType() == Material.FIRE) spreadEvent.setCancelled(true); + } + + //blocks are not destroyed by fire + @EventHandler(priority = EventPriority.HIGHEST) + public void onBlockBurn (BlockBurnEvent burnEvent) + { + burnEvent.setCancelled(true); + } + + //ensures fluids don't flow into claims, unless out of another claim where the owner is trusted to build in the receiving claim + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onBlockFromTo (BlockFromToEvent spreadEvent) + { + //where to? + Block toBlock = spreadEvent.getToBlock(); + Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, null); + + //if spreading into a claim + if(toClaim != null) + { + //from where? + Block fromBlock = spreadEvent.getBlock(); + Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null); + + //who owns the spreading block, if anyone? + OfflinePlayer fromOwner = null; + if(fromClaim != null) + { + //if it's within the same claim, allow it + if(fromClaim == toClaim) return; + + 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); + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/BlockSnapshot.java b/src/me/ryanhamshire/GriefPrevention/BlockSnapshot.java new file mode 100644 index 0000000..ade66d9 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/BlockSnapshot.java @@ -0,0 +1,37 @@ +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2012 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import org.bukkit.Location; + +//basically, just a few data points from a block conveniently encapsulated in a class +//this is used only by the RestoreNature code +public class BlockSnapshot +{ + public Location location; + public int typeId; + public byte data; + + public BlockSnapshot(Location location, int typeId, byte data) + { + this.location = location; + this.typeId = typeId; + this.data = data; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/Claim.java b/src/me/ryanhamshire/GriefPrevention/Claim.java new file mode 100644 index 0000000..f949288 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/Claim.java @@ -0,0 +1,554 @@ +/* + 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 org.bukkit.*; +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 + //IF MODIFIED, THE CLAIM DATA FILE'S NAME WILL CHANGE. ANY MODIFICATIONS MUST BE HANDLED VERY CAREFULLY + Location lesserBoundaryCorner; + Location greaterBoundaryCorner; + + //modification date. this comes from the file timestamp during load, and is updated with runtime changes + public Date modifiedDate; + + //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(); + } + + //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; + } + + //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) + { + //modification date + this.modifiedDate = Calendar.getInstance().getTime(); + + //store corners + this.lesserBoundaryCorner = lesserBoundaryCorner; + this.greaterBoundaryCorner = greaterBoundaryCorner; + + //if trying to create a claim under the max depth, auto-correct y values + if(this.lesserBoundaryCorner.getBlockY() < GriefPrevention.instance.config_claims_maxDepth) + { + this.lesserBoundaryCorner.setY(GriefPrevention.instance.config_claims_maxDepth); + } + + if(this.greaterBoundaryCorner.getBlockY() < GriefPrevention.instance.config_claims_maxDepth) + { + this.greaterBoundaryCorner.setY(GriefPrevention.instance.config_claims_maxDepth); + } + + //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[] {}); + + 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) + { + //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 "Claims can't be modified while under siege."; + } + + //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 "Only " + this.getOwnerName() + " can modify this claim."; + } + + //build permission check + public String allowBuild(Player player) + { + //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 "This claim is under siege by " + this.siegeData.attacker.getName() + ". No one can build here."; + } + + //no building while in pvp combat + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + if(playerData.inPvpCombat()) + { + return "You can't build in claims during PvP combat."; + } + + //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 + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get(player.getName().toLowerCase()); + if(ClaimPermission.Build == permissionLevel) return null; + + //also everyone is a member of the "public", so check for public permission + 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 "You don't have " + this.getOwnerName() + "'s permission to build here."; + } + + //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 "That material is too tough to break."; + } + else if(this.ownerName.equals(player.getName())) + { + return "You can't make changes while under siege."; + } + else + { + return null; + } + } + + //if not under siege, build rules apply + return this.allowBuild(player); + } + + //access permission check + public String allowAccess(Player player) + { + //everyone always has access to admin claims + if(this.isAdminClaim()) return null; + + //following a siege where the defender lost, the claim will allow everyone access for a time + if(this.doorsOpen) 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 + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get(player.getName().toLowerCase()); + if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel || ClaimPermission.Access == permissionLevel) return null; + + //also check for public permission + 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 "You don't have " + this.getOwnerName() + "'s permission to use that."; + } + + //inventory permission check + public String allowContainers(Player player) + { + //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 "This claim is under siege by " + siegeData.attacker.getName() + ". No one can access containers here right now."; + } + + //containers are always accessible in admin claims + if(this.isAdminClaim()) return null; + + //owner and administrators in ignoreclaims mode have access + if(this.ownerName.equals(player.getName()) || GriefPrevention.instance.dataStore.getPlayerData(player.getName()).ignoreClaims) return null; + + //check for explicit individual container or build permission + ClaimPermission permissionLevel = this.playerNameToClaimPermissionMap.get(player.getName().toLowerCase()); + if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel) return null; + + //check for public container or build permission + 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 "You don't have " + this.getOwnerName() + "'s permission to use that."; + } + + //grant permission check, relatively simple + public String allowGrantPermission(Player player) + { + //anyone who can modify the claim, or who's explicitly in the managers (/PermissionTrust) list can do this + if(this.allowEdit(player) == null || this.managers.contains(player.getName())) return null; + + //permission inheritance for subdivisions + if(this.parent != null) + return this.parent.allowGrantPermission(player); + + //generic error message + return "You don't have " + this.getOwnerName() + "'s permission to grant permission here."; + } + + //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 "an administrator"; + + 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. + + //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; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/ClaimPermission.java b/src/me/ryanhamshire/GriefPrevention/ClaimPermission.java new file mode 100644 index 0000000..e480b7f --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/ClaimPermission.java @@ -0,0 +1,27 @@ +/* + 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; + +//basic enum stuff +public enum ClaimPermission +{ + Build, + Inventory, + Access +} diff --git a/src/me/ryanhamshire/GriefPrevention/CreateClaimResult.java b/src/me/ryanhamshire/GriefPrevention/CreateClaimResult.java new file mode 100644 index 0000000..847ac93 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/CreateClaimResult.java @@ -0,0 +1,29 @@ +/* + 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; + +public class CreateClaimResult +{ + //whether or not the creation succeeded (it would fail if the new claim overlapped another existing claim) + public boolean succeeded; + + //when succeeded, this is a reference to the new claim + //when failed, this is a reference to the pre-existing, conflicting claim + public Claim claim; +} diff --git a/src/me/ryanhamshire/GriefPrevention/DataStore.java b/src/me/ryanhamshire/GriefPrevention/DataStore.java new file mode 100644 index 0000000..274ca6c --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/DataStore.java @@ -0,0 +1,1069 @@ +/* + 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.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +import org.bukkit.*; +import org.bukkit.entity.Player; + +//singleton class which manages all GriefPrevention data (except for config options) +public class DataStore +{ + //in-memory cache for player data + private HashMap playerNameToPlayerDataMap = new HashMap(); + + //in-memory cache for claim data + private ArrayList claims = new ArrayList(); + + //path information, for where stuff stored on disk is well... stored + private final static String dataLayerFolderPath = "plugins" + File.separator + "GriefPreventionData"; + private final static String playerDataFolderPath = dataLayerFolderPath + File.separator + "PlayerData"; + private final static String claimDataFolderPath = dataLayerFolderPath + File.separator + "ClaimData"; + final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; + + //initialization! + DataStore() + { + //ensure data folders exist + new File(playerDataFolderPath).mkdirs(); + new File(claimDataFolderPath).mkdirs(); + + //load claims data into memory + File claimDataFolder = new File(claimDataFolderPath); + File [] files = claimDataFolder.listFiles(); + + int loadedClaimCount = 0; + + for(int i = 0; i < files.length; i++) + { + if(files[i].isFile()) //avoids folders + { + BufferedReader inStream = null; + try + { + Claim topLevelClaim = null; + + inStream = new BufferedReader(new FileReader(files[i].getAbsolutePath())); + String line = inStream.readLine(); + + while(line != null) + { + //first line is lesser boundary corner location + Location lesserBoundaryCorner = this.locationFromString(line); + + //second line is greater boundary corner location + line = inStream.readLine(); + Location greaterBoundaryCorner = this.locationFromString(line); + + //third line is owner name + line = inStream.readLine(); + String ownerName = line; + + //fourth line is list of builders + line = inStream.readLine(); + String [] builderNames = line.split(";"); + + //fifth line is list of players who can access containers + line = inStream.readLine(); + String [] containerNames = line.split(";"); + + //sixth line is list of players who can use buttons and switches + line = inStream.readLine(); + String [] accessorNames = line.split(";"); + + //seventh line is list of players who can grant permissions + line = inStream.readLine(); + if(line == null) line = ""; + String [] managerNames = line.split(";"); + + //skip any remaining extra lines, until the "===" string, indicating the end of this claim or subdivision + line = inStream.readLine(); + while(line != null && !line.contains("==========")) + line = inStream.readLine(); + + //build a claim instance from those data + //if this is the first claim loaded from this file, it's the top level claim + if(topLevelClaim == null) + { + //instantiate + topLevelClaim = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, ownerName, builderNames, containerNames, accessorNames, managerNames); + + //search for another claim overlapping this one + Claim conflictClaim = this.getClaimAt(topLevelClaim.lesserBoundaryCorner, true, null); + + //if there is such a claim, delete this file and move on to the next + if(conflictClaim != null) + { + inStream.close(); + files[i].delete(); + line = null; + continue; + } + + //otherwise, add this claim to the claims collection + else + { + topLevelClaim.modifiedDate = new Date(files[i].lastModified()); + this.claims.add(topLevelClaim); + topLevelClaim.inDataStore = true; + } + } + + //otherwise there's already a top level claim, so this must be a subdivision of that top level claim + else + { + Claim subdivision = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, "--subdivision--", builderNames, containerNames, accessorNames, managerNames); + subdivision.modifiedDate = new Date(files[i].lastModified()); + subdivision.parent = topLevelClaim; + topLevelClaim.children.add(subdivision); + subdivision.inDataStore = true; + } + + //move up to the first line in the next subdivision + line = inStream.readLine(); + } + + inStream.close(); + + loadedClaimCount++; + } + + //if there's any problem with the file's content, log an error message and skip it + catch(Exception e) + { + GriefPrevention.AddLogEntry("Unable to load data for claim \"" + files[i].getName() + "\": " + e.getMessage()); + } + + try + { + if(inStream != null) inStream.close(); + } + catch(IOException exception) {} + } + } + + GriefPrevention.AddLogEntry(loadedClaimCount + " 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 each of these players and determine whether his claims should be cleaned up + for(int i = 0; i < playerNames.size(); i++) + { + String playerName = playerNames.get(i); + + PlayerData playerData = this.getPlayerData(playerName); + + int areaOfDefaultClaim = 0; + + //determine area of the default chest claim + if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius >= 0) + { + areaOfDefaultClaim = (int)Math.pow(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius * 2 + 1, 2); + } + + //figure out how long the player has been away + Calendar sevenDaysAgo = Calendar.getInstance(); + sevenDaysAgo.add(Calendar.DATE, -7); + boolean claimsExpired = sevenDaysAgo.getTime().after(playerData.lastLogin); + + //if only one claim, and the player hasn't played in a week + if(claimsExpired && playerData.claims.size() == 1) + { + Claim claim = playerData.claims.get(0); + + //if that's a chest claim, delete it + if(claim.getArea() <= areaOfDefaultClaim) + { + this.deleteClaim(claim); + GriefPrevention.AddLogEntry(" " + playerName + "'s new player claim expired."); + } + } + + //toss that player data out of the cache, it's not needed in memory right now + this.clearCachedPlayerData(playerName); + } + + //collect garbage, since lots of stuff was loaded into memory and then tossed out + System.gc(); + } + + //removes cached player data from memory + void clearCachedPlayerData(String playerName) + { + this.playerNameToPlayerDataMap.remove(playerName); + } + + 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 + 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 + this.claims.add(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 files and data file names + private String locationStringDelimiter = ";"; + private 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 + private 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); + } + + //does the work of actually writing a claim to file + 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 + String claimID = this.getClaimID(claim); + + BufferedWriter outStream = null; + + try + { + //open the claim's file + File claimFile = new File(claimDataFolderPath + File.separator + claimID); + claimFile.createNewFile(); + outStream = new BufferedWriter(new FileWriter(claimFile)); + + this.writeClaimData(claim, outStream); + + for(int i = 0; i < claim.children.size(); i++) + { + //see below for details of writing data to file + this.writeClaimData(claim.children.get(i), outStream); + } + } + + //if any problem, log it + catch(Exception e) + { + GriefPrevention.AddLogEntry("PopulationDensity: Unexpected exception saving data for claim \"" + claimID + "\": " + e.getMessage()); + } + + //close the file + try + { + if(outStream != null) outStream.close(); + } + catch(IOException exception) {} + } + + //actually writes claim data to an output stream + private void writeClaimData(Claim claim, BufferedWriter outStream) throws IOException + { + String claimID = this.getClaimID(claim); + + //first line is lesser boundary corner location + outStream.write(claimID); + outStream.newLine(); + + //second line is greater boundary corner location + outStream.write(this.locationToString(claim.getGreaterBoundaryCorner())); + outStream.newLine(); + + //third line is owner name + outStream.write(claim.ownerName); + outStream.newLine(); + + ArrayList builders = new ArrayList(); + ArrayList containers = new ArrayList(); + ArrayList accessors = new ArrayList(); + ArrayList managers = new ArrayList(); + + claim.getPermissions(builders, containers, accessors, managers); + + //fourth line is list of players with build permission + for(int i = 0; i < builders.size(); i++) + { + outStream.write(builders.get(i) + ";"); + } + outStream.newLine(); + + //fifth line is list of players with container permission + for(int i = 0; i < containers.size(); i++) + { + outStream.write(containers.get(i) + ";"); + } + outStream.newLine(); + + //sixth line is list of players with access permission + for(int i = 0; i < accessors.size(); i++) + { + outStream.write(accessors.get(i) + ";"); + } + outStream.newLine(); + + //seventh line is list of players who may grant permissions for others + for(int i = 0; i < managers.size(); i++) + { + outStream.write(managers.get(i) + ";"); + } + outStream.newLine(); + + //cap each claim with "==========" + outStream.write("=========="); + outStream.newLine(); + } + + //retrieves player data from memory or file, as necessary + //if the player has never been on the server before, this will return a fresh player data with default values + public PlayerData getPlayerData(String playerName) + { + //first, look in memory + PlayerData playerData = this.playerNameToPlayerDataMap.get(playerName); + + //if not there, look on disk + if(playerData == null) + { + File playerFile = new File(playerDataFolderPath + File.separator + playerName); + + playerData = new PlayerData(); + + //if it doesn't exist as a file + if(!playerFile.exists()) + { + //create a file with defaults + this.savePlayerData(playerName, playerData); + } + + //otherwise, read the file + else + { + BufferedReader inStream = null; + try + { + inStream = new BufferedReader(new FileReader(playerFile.getAbsolutePath())); + + //first line is last login timestamp + String lastLoginTimestampString = inStream.readLine(); + + //convert that to a date and store it + DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); + try + { + playerData.lastLogin = dateFormat.parse(lastLoginTimestampString); + } + catch(ParseException parseException) + { + GriefPrevention.AddLogEntry("Unable to load last login for \"" + playerFile.getName() + "\"."); + playerData.lastLogin = null; + } + + //second line is accrued claim blocks + String accruedBlocksString = inStream.readLine(); + + //convert that to a number and store it + playerData.accruedClaimBlocks = Integer.parseInt(accruedBlocksString); + + //third line is any bonus claim blocks granted by administrators + String bonusBlocksString = inStream.readLine(); + + //convert that to a number and store it + playerData.bonusClaimBlocks = Integer.parseInt(bonusBlocksString); + + //fourth line is a double-semicolon-delimited list of claims + String claimsString = inStream.readLine(); + if(claimsString != null && claimsString.length() > 0) + { + String [] claimsStrings = claimsString.split(";;"); + boolean missingClaim = false; + + //search for each claim mentioned in the file + for(int i = 0; i < claimsStrings.length; i++) + { + String claimID = claimsStrings[i]; + if(claimID != null) + { + Claim claim = this.getClaimAt(this.locationFromString(claimID), true /*ignore height*/, null); + + //if the referenced claim exists, add it to the player data instance for later reference + if(claim != null) + { + playerData.claims.add(claim); + } + + //if the claim doesn't seem to exist anymore, plan to drop the reference from the file + else + { + missingClaim = true; + } + } + } + + //if any referenced claims no longer exist, write the player data back to file to eliminate those references + if(missingClaim) + { + this.savePlayerData(playerName, playerData); + } + } + + inStream.close(); + } + + //if there's any problem with the file's content, log an error message + catch(Exception e) + { + GriefPrevention.AddLogEntry("Unable to load data for player \"" + playerName + "\": " + e.getMessage()); + } + + try + { + if(inStream != null) inStream.close(); + } + catch(IOException exception) {} + } + + //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); + } + + //deletes a claim or subdivision + 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; + } + + //otherwise, need to update the data store and ensure the claim's file is deleted + String claimID = this.getClaimID(claim); + + //remove from memory + for(int i = 0; i < this.claims.size(); i++) + { + if(this.getClaimID(this.claims.get(i)).equals(claimID)) + { + 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 disk + File claimFile = new File(claimDataFolderPath + File.separator + claimID); + if(claimFile.exists() && !claimFile.delete()) + { + GriefPrevention.AddLogEntry("Error: Unable to delete claim file \"" + claimFile.getAbsolutePath() + "\"."); + } + + //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(this.getClaimID(ownerData.claims.get(i)).equals(claimID)) + { + ownerData.claims.remove(i); + break; + } + } + this.savePlayerData(claim.getOwnerName(), ownerData); + } + } + + //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 + 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; + + //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); + + //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 + public CreateClaimResult createClaim(World world, int x1, int x2, int y1, int y2, int z1, int z2, String ownerName, Claim parent) + { + 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; + } + + //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 [] {}); + + 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. MUST be called after you're done making changes, otherwise a reload will lose them + public void savePlayerData(String playerName, PlayerData playerData) + { + //never save data for the "administrative" account. an empty string for claim owner indicates an administrative claim + if(playerName.length() == 0) return; + + BufferedWriter outStream = null; + try + { + //open the player's file + File playerDataFile = new File(playerDataFolderPath + File.separator + playerName); + playerDataFile.createNewFile(); + outStream = new BufferedWriter(new FileWriter(playerDataFile)); + + //first line is last login timestamp + if(playerData.lastLogin == null)playerData.lastLogin = new Date(); + DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); + outStream.write(dateFormat.format(playerData.lastLogin)); + outStream.newLine(); + + //second line is accrued claim blocks + outStream.write(String.valueOf(playerData.accruedClaimBlocks)); + outStream.newLine(); + + //third line is bonus claim blocks + outStream.write(String.valueOf(playerData.bonusClaimBlocks)); + outStream.newLine(); + + //fourth line is a double-semicolon-delimited list of claims + if(playerData.claims.size() > 0) + { + outStream.write(this.locationToString(playerData.claims.get(0).getLesserBoundaryCorner())); + for(int i = 1; i < playerData.claims.size(); i++) + { + outStream.write(";;" + this.locationToString(playerData.claims.get(i).getLesserBoundaryCorner())); + } + } + outStream.newLine(); + } + + //if any problem, log it + catch(Exception e) + { + GriefPrevention.AddLogEntry("PopulationDensity: Unexpected exception saving data for player \"" + playerName + "\": " + e.getMessage()); + } + + try + { + //close the file + if(outStream != null) + { + outStream.close(); + } + } + catch(IOException exception){} + } + + //gets a unique identifier for a claim + private String getClaimID(Claim claim) + { + return this.locationToString(claim.getLesserBoundaryCorner()); + } + + //extends a claim to a new depth + //respects the max depth config variable + 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 + 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 * 60); + } + + //ends a siege + //either winnerName or loserName can be null, but not both + public void endSiege(SiegeData siegeData, String winnerName, String loserName) + { + 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, "Congratulations! Buttons and levers are temporarily unlocked (five minutes)."); + + //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); + } + } + } + + //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 + 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 + 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 + public void deleteClaimsForPlayer(String playerName) + { + //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)) + claimsToDelete.add(claim); + } + + //delete them one by one + for(int i = 0; i < claimsToDelete.size(); i++) + { + this.deleteClaim(claimsToDelete.get(i)); + } + } + + //tries to resize a claim + //see CreateClaim() for details on return value + 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); + + //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; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/DeliverClaimBlocksTask.java b/src/me/ryanhamshire/GriefPrevention/DeliverClaimBlocksTask.java new file mode 100644 index 0000000..28d4ecc --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/DeliverClaimBlocksTask.java @@ -0,0 +1,64 @@ +/* + 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.Location; +import org.bukkit.entity.Player; + +//FEATURE: give players claim blocks for playing, as long as they're not away from their computer + +//runs every 5 minutes in the main thread, grants blocks per hour / 12 to each online player who appears to be actively playing +class DeliverClaimBlocksTask implements Runnable +{ + @Override + public void run() + { + Player [] players = GriefPrevention.instance.getServer().getOnlinePlayers(); + + //for each online player + for(int i = 0; i < players.length; i++) + { + Player player = players[i]; + DataStore dataStore = GriefPrevention.instance.dataStore; + PlayerData playerData = dataStore.getPlayerData(player.getName()); + + Location lastLocation = playerData.lastAfkCheckLocation; + try //distance squared will throw an exception if the player has changed worlds + { + //if he's not in a vehicle and has moved at least three blocks since the last check + if(!player.isInsideVehicle() && (lastLocation == null || lastLocation.distanceSquared(player.getLocation()) >= 9)) + { + playerData.accruedClaimBlocks += GriefPrevention.instance.config_claims_blocksAccruedPerHour / 12; + + //respect limits + if(playerData.accruedClaimBlocks > GriefPrevention.instance.config_claims_maxAccruedBlocks) + { + playerData.accruedClaimBlocks = GriefPrevention.instance.config_claims_maxAccruedBlocks; + } + + dataStore.savePlayerData(player.getName(), playerData); + } + } + catch(Exception e) { } + + //remember current location for next time + playerData.lastAfkCheckLocation = player.getLocation(); + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java b/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java new file mode 100644 index 0000000..be61c84 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/EntityEventHandler.java @@ -0,0 +1,306 @@ +/* + 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.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Creeper; +import org.bukkit.entity.Enderman; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.ThrownPotion; +import org.bukkit.entity.Vehicle; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +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.painting.PaintingBreakByEntityEvent; +import org.bukkit.event.painting.PaintingBreakEvent; +import org.bukkit.event.painting.PaintingPlaceEvent; + +//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; + } + + //when an entity explodes... + @EventHandler(ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent explodeEvent) + { + List blocks = explodeEvent.blockList(); + Entity entity = explodeEvent.getEntity(); + + //FEATURE: creepers don't destroy blocks when they explode near or above sea level + + if(GriefPrevention.instance.config_creepersDontDestroySurface && entity instanceof Creeper) + { + if(entity.getLocation().getBlockY() > entity.getLocation().getWorld().getSeaLevel() - 7) + { + blocks.clear(); //explosion still happens, can damage creatures/players, but no blocks will be destroyed + return; + } + } + + //FEATURE: creating an explosion near a claim doesn't damage any of the 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 + + 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 entity dies... + @EventHandler + public void onEntityDeath(EntityDeathEvent event) + { + //FEATURE: when a player is involved in a siege (attacker or defender role) + //his death will end the siege + + LivingEntity entity = event.getEntity(); + 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) + { + //end it, with the dieing player being the loser + this.dataStore.endSiege(playerData.siegeData, null, player.getName()); + } + } + + //when an entity picks up an item + @EventHandler + 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) + 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; + + //which claim is the painting in? + Claim claim = this.dataStore.getClaimAt(event.getPainting().getLocation(), false, null); + if(claim == null) return; + + //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 = claim.allowBuild(playerRemover); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(playerRemover, TextMode.Err, noBuildReason); + } + } + + //when a painting is placed... + @EventHandler(ignoreCancelled = true) + public void onPaintingPlace(PaintingPlaceEvent event) + { + //FEATURE: similar to above, placing a painting requires build permission in the claim + + //which claim is the painting in? + Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null); + if(claim == null) return; + + //if the player doesn't have permission, don't allow the placement + String noBuildReason = claim.allowBuild(event.getPlayer()); + if(noBuildReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(event.getPlayer(), TextMode.Err, noBuildReason); + } + } + + //when an entity is damaged + @EventHandler(ignoreCancelled = true) + public void onEntityDamage (EntityDamageEvent event) + { + //only actually interested in entities damaging entities (ignoring environmental damage) + if(!(event instanceof EntityDamageByEntityEvent)) return; + + EntityDamageByEntityEvent subEvent = (EntityDamageByEntityEvent) event; + + //determine which player is attacking, if any + Player attacker = null; + Entity damageSource = subEvent.getDamager(); + 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(); + } + } + + //if the attacker is a player and defender is a player (pvp combat) + if(attacker != null && event.getEntity() instanceof Player) + { + //if pvp is disabled, cancel the event + if(!event.getEntity().getWorld().getPVP()) + { + event.setCancelled(true); + 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 + Player defender = (Player)(event.getEntity()); + + PlayerData defenderData = this.dataStore.getPlayerData(((Player)event.getEntity()).getName()); + PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName()); + + long now = Calendar.getInstance().getTimeInMillis(); + defenderData.lastPvpTimestamp = now; + defenderData.lastPvpPlayer = attacker.getName(); + attackerData.lastPvpTimestamp = now; + attackerData.lastPvpPlayer = defender.getName(); + + //FEATURE: prevent pvp in the first minute after spawn, and prevent pvp when one or both players have no inventory + + //otherwise if protecting spawning players + if(GriefPrevention.instance.config_pvp_protectFreshSpawns) + { + if(defenderData.pvpImmune) + { + event.setCancelled(true); + return; + } + + if(attackerData.pvpImmune) + { + event.setCancelled(true); + return; + } + } + } + + //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(GriefPrevention.instance.config_claims_preventTheft && event instanceof EntityDamageByEntityEvent) + { + //if the entity is an animal or a vehicle + if (subEvent.getEntity() instanceof Animals || subEvent.getEntity() instanceof Vehicle) + { + Claim claim = this.dataStore.getClaimAt(event.getEntity().getLocation(), false, null); + + //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, "That belongs to " + claim.getOwnerName() + "."); + } + } + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java new file mode 100644 index 0000000..e4a950a --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/GriefPrevention.java @@ -0,0 +1,1568 @@ +/* + GriefPrevention Server Plugin for Minecraft + Copyright (C) 2011 Ryan Hamshire + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +package me.ryanhamshire.GriefPrevention; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Logger; + +import net.milkbowl.vault.economy.Economy; + +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.command.*; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; + +public class GriefPrevention extends JavaPlugin +{ + //for convenience, a reference to the instance of this plugin + public static GriefPrevention instance; + + //for logging to the console and log file + private static Logger log = Logger.getLogger("Minecraft"); + + //this handles data storage, like player and region data + public DataStore dataStore; + + //configuration variables, loaded/saved from a config.yml + public ArrayList config_claims_enabledWorlds; //list of worlds where players can create GriefPrevention claims + + public boolean config_claims_preventTheft; //whether containers and crafting blocks are protectable + public boolean config_claims_preventButtonsSwitches; //whether buttons and switches are protectable + + public int config_claims_initialBlocks; //the number of claim blocks a new player starts with + public int config_claims_blocksAccruedPerHour; //how many additional blocks players get each hour of play (can be zero) + public int config_claims_maxAccruedBlocks; //the limit on accrued blocks (over time). doesn't limit purchased or admin-gifted blocks + public int config_claims_maxDepth; //limit on how deep claims can go + + public int config_claims_automaticClaimsForNewPlayersRadius; //how big automatic new player claims (when they place a chest) should be. 0 to disable + public boolean config_claims_creationRequiresPermission; //whether creating claims with the shovel requires a permission + public int config_claims_claimsExtendIntoGroundDistance; //how far below the shoveled block a new claim will reach + public int config_claims_minSize; //minimum width and height for non-admin claims + + public int config_claims_trappedCooldownHours; //number of hours between uses of the /trapped command + + public ArrayList config_siege_enabledWorlds; //whether or not /siege is enabled on this server + public ArrayList config_siege_blocks; //which blocks will be breakable in siege mode + + public boolean config_spam_enabled; //whether or not to monitor for spam + public int config_spam_loginCooldownMinutes; //how long players must wait between logins. combats login spam. + public ArrayList config_spam_monitorSlashCommands; //the list of slash commands monitored for spam + public boolean config_spam_banOffenders; //whether or not to ban spammers automatically + public String config_spam_banMessage; //message to show an automatically banned player + public String config_spam_warningMessage; //message to show a player who is close to spam level + public String config_spam_allowedIpAddresses; //IP addresses which will not be censored + + public boolean config_pvp_protectFreshSpawns; //whether to make newly spawned players immune until they pick up an item + public boolean config_pvp_punishLogout; //whether to kill players who log out during PvP combat + + public 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_creepersDontDestroySurface; //whether creeper explosions near or above the surface destroy blocks + + //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; + + //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); + } + } + + this.config_claims_preventTheft = config.getBoolean("GriefPrevention.Claims.PreventTheft", true); + this.config_claims_preventButtonsSwitches = config.getBoolean("GriefPrevention.Claims.PreventButtonsSwitches", true); + this.config_claims_initialBlocks = config.getInt("GriefPrevention.Claims.InitialBlocks", 100); + this.config_claims_blocksAccruedPerHour = config.getInt("GriefPrevention.Claims.BlocksAccruedPerHour", 100); + this.config_claims_maxAccruedBlocks = config.getInt("GriefPrevention.Claims.MaxAccruedBlocks", 80000); + this.config_claims_automaticClaimsForNewPlayersRadius = config.getInt("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", 4); + this.config_claims_claimsExtendIntoGroundDistance = config.getInt("GriefPrevention.Claims.ExtendIntoGroundDistance", 5); + this.config_claims_creationRequiresPermission = config.getBoolean("GriefPrevention.Claims.CreationRequiresPermission", false); + this.config_claims_minSize = config.getInt("GriefPrevention.Claims.MinimumSize", 10); + this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", 0); + this.config_claims_trappedCooldownHours = config.getInt("GriefPrevention.Claims.TrappedCommandCooldownHours", 8); + + this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true); + this.config_spam_loginCooldownMinutes = config.getInt("GriefPrevention.Spam.LoginCooldownMinutes", 5); + this.config_spam_warningMessage = config.getString("GriefPrevention.Spam.WarningMessage", "Please reduce your message speed. 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_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_creepersDontDestroySurface = config.getBoolean("GriefPrevention.CreepersDontDestroySurface", true); + + //default for claims 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.WOOD); + this.config_siege_blocks.add(Material.WOOL); + this.config_siege_blocks.add(Material.SNOW); + + //build a default config entry + ArrayList defaultBreakableBlocksList = new ArrayList(); + for(int i = 0; i < this.config_siege_blocks.size(); i++) + { + defaultBreakableBlocksList.add(this.config_siege_blocks.get(i).name()); + } + + //try to load the list from the config file + List breakableBlocksList = config.getStringList("GriefPrevention.Siege.BreakableBlocks"); + + //if it fails, use default list instead + if(breakableBlocksList == null || breakableBlocksList.size() == 0) + { + breakableBlocksList = defaultBreakableBlocksList; + } + + //parse the list of siege-breakable blocks + this.config_siege_blocks = new ArrayList(); + for(int i = 0; i < breakableBlocksList.size(); i++) + { + String blockName = breakableBlocksList.get(i); + Material material = Material.getMaterial(blockName); + if(material == null) + { + GriefPrevention.AddLogEntry("Siege Configuration: Material not found: " + blockName + "."); + } + else + { + this.config_siege_blocks.add(material); + } + } + + config.set("GriefPrevention.Claims.Worlds", claimsEnabledWorldNames); + config.set("GriefPrevention.Claims.PreventTheft", this.config_claims_preventTheft); + config.set("GriefPrevention.Claims.PreventButtonsSwitches", this.config_claims_preventButtonsSwitches); + config.set("GriefPrevention.Claims.InitialBlocks", this.config_claims_initialBlocks); + config.set("GriefPrevention.Claims.BlocksAccruedPerHour", this.config_claims_blocksAccruedPerHour); + config.set("GriefPrevention.Claims.MaxAccruedBlocks", this.config_claims_maxAccruedBlocks); + config.set("GriefPrevention.Claims.AutomaticNewPlayerClaimsRadius", this.config_claims_automaticClaimsForNewPlayersRadius); + config.set("GriefPrevention.Claims.ExtendIntoGroundDistance", this.config_claims_claimsExtendIntoGroundDistance); + config.set("GriefPrevention.Claims.CreationRequiresPermission", this.config_claims_creationRequiresPermission); + config.set("GriefPrevention.Claims.MinimumSize", this.config_claims_minSize); + config.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth); + config.set("GriefPrevention.Claims.TrappedCommandCooldownHours", this.config_claims_trappedCooldownHours); + + config.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled); + config.set("GriefPrevention.Spam.LoginCooldownMinutes", this.config_spam_loginCooldownMinutes); + config.set("GriefPrevention.Spam.MonitorSlashCommands", slashCommandsToMonitor); + config.set("GriefPrevention.Spam.WarningMessage", this.config_spam_warningMessage); + config.set("GriefPrevention.Spam.BanOffenders", this.config_spam_banOffenders); + config.set("GriefPrevention.Spam.BanMessage", this.config_spam_banMessage); + config.set("GriefPrevention.Spam.AllowedIpAddresses", this.config_spam_allowedIpAddresses); + + config.set("GriefPrevention.PvP.ProtectFreshSpawns", this.config_pvp_protectFreshSpawns); + config.set("GriefPrevention.PvP.PunishLogout", this.config_pvp_punishLogout); + + config.set("GriefPrevention.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.CreepersDontDestroySurface", this.config_creepersDontDestroySurface); + + config.set("GriefPrevention.Siege.Enabled", siegeEnabledWorldNames); + config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList); + + try + { + config.save(DataStore.configFilePath); + } + catch(IOException exception) + { + AddLogEntry("Unable to write to the configuration file at \"" + DataStore.configFilePath + "\""); + } + + //try to parse the list of commands which should be monitored for spam + this.config_spam_monitorSlashCommands = new ArrayList(); + String [] commands = slashCommandsToMonitor.split(";"); + for(int i = 0; i < commands.length; i++) + { + this.config_spam_monitorSlashCommands.add(commands[i].trim()); + } + + //when datastore initializes, it loads player and claim data, and posts some stats to the log + this.dataStore = new DataStore(); + + //unless claim block accrual is disabled, start the recurring per 5 minute event to give claim blocks to online players + //20L ~ 1 second + DeliverClaimBlocksTask task = new DeliverClaimBlocksTask(); + this.getServer().getScheduler().scheduleSyncRepeatingTask(this, task, 20L * 60 * 5, 20L * 60 * 5); + + //register for events + PluginManager pluginManager = this.getServer().getPluginManager(); + + //player events + PlayerEventHandler playerEventHandler = new PlayerEventHandler(this.dataStore, this); + pluginManager.registerEvents(playerEventHandler, this); + + //block events + BlockEventHandler blockEventHandler = new BlockEventHandler(this.dataStore); + pluginManager.registerEvents(blockEventHandler, this); + + //entity events + EntityEventHandler entityEventHandler = new EntityEventHandler(this.dataStore); + pluginManager.registerEvents(entityEventHandler, this); + + //if economy is enabled + if(this.config_economy_claimBlocksPurchaseCost > 0 || this.config_economy_claimBlocksSellValue > 0) + { + //try to load Vault + GriefPrevention.AddLogEntry("GriefPrevention requires Vault for economy integration."); + GriefPrevention.AddLogEntry("Attempting to load Vault..."); + RegisteredServiceProvider economyProvider = getServer().getServicesManager().getRegistration(net.milkbowl.vault.economy.Economy.class); + GriefPrevention.AddLogEntry("Vault loaded successfully!"); + + //ask Vault to hook into an economy plugin + GriefPrevention.AddLogEntry("Looking for a Vault-compatible economy plugin..."); + if (economyProvider != null) + { + GriefPrevention.economy = economyProvider.getProvider(); + + //on success, display success message + if(GriefPrevention.economy != null) + { + GriefPrevention.AddLogEntry("Hooked into economy: " + GriefPrevention.economy.getName() + "."); + GriefPrevention.AddLogEntry("Ready to buy/sell claim blocks!"); + } + + //otherwise error message + else + { + GriefPrevention.AddLogEntry("ERROR: Vault was unable to find a supported economy plugin. Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero."); + } + } + + //another error case + else + { + GriefPrevention.AddLogEntry("ERROR: Vault was unable to find a supported economy plugin. Either install a Vault-compatible economy plugin, or set both of the economy config variables to zero."); + } + } + } + + //handles slash commands + public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ + + Player player = null; + if (sender instanceof Player) + { + player = (Player) sender; + } + + //abandonclaim + if(cmd.getName().equalsIgnoreCase("abandonclaim") && player != null) + { + //which claim is being abandoned? + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + if(claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Instr, "Stand in the claim you want to delete, or consider /AbandonAllClaims."); + } + + //verify ownership + else if(claim.allowEdit(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "This isn't your claim."); + } + + else + { + //delete it + this.dataStore.deleteClaim(claim); + + //tell the player how many claim blocks he has left + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + int remainingBlocks = playerData.getRemainingClaimBlocks(); + GriefPrevention.sendMessage(player, TextMode.Success, "Claim abandoned. You now have " + String.valueOf(remainingBlocks) + " available claim blocks."); + + //revert any current visualization + Visualization.Revert(player); + } + + return true; + } + + //ignoreclaims + if(cmd.getName().equalsIgnoreCase("ignoreclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + playerData.ignoreClaims = !playerData.ignoreClaims; + + //toggle ignore claims mode on or off + if(!playerData.ignoreClaims) + { + GriefPrevention.sendMessage(player, TextMode.Success, "Now respecting claims."); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, "Now ignoring claims."); + } + + return true; + } + + //abandonallclaims + else if(cmd.getName().equalsIgnoreCase("abandonallclaims") && player != null) + { + //count claims + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + int originalClaimCount = playerData.claims.size(); + + //check count + if(originalClaimCount == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You haven't claimed any land."); + return true; + } + + //delete them + this.dataStore.deleteClaimsForPlayer(player.getName()); + + //inform the player + int remainingBlocks = playerData.getRemainingClaimBlocks(); + GriefPrevention.sendMessage(player, TextMode.Success, originalClaimCount + " claims abandoned. You now have " + String.valueOf(remainingBlocks) + " available claim blocks."); + + //revert any current visualization + Visualization.Revert(player); + + return true; + } + + //restore nature + else if(cmd.getName().equalsIgnoreCase("restorenature") && player != null) + { + //change shovel mode + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.shovelMode = ShovelMode.RestoreNature; + GriefPrevention.sendMessage(player, TextMode.Instr, "Ready to restore some nature! Right click to restore nature, and use /BasicClaims to stop."); + return true; + } + + //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; + } + + //trustlist + else if(cmd.getName().equalsIgnoreCase("trustlist") && player != null) + { + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true, null); + + //if no claim here, error message + if(claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Stand inside the claim you're curious about."); + return true; + } + + //if no permission to manage permissions, error message + String errorMessage = claim.allowGrantPermission(player); + if(errorMessage != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, errorMessage); + return true; + } + + //otherwise build a list of explicit permissions by permission level + //and send that to the player + ArrayList builders = new ArrayList(); + ArrayList containers = new ArrayList(); + ArrayList accessors = new ArrayList(); + ArrayList managers = new ArrayList(); + claim.getPermissions(builders, containers, accessors, managers); + + player.sendMessage("Explicit permissions here:"); + + StringBuilder permissions = new StringBuilder(); + permissions.append(ChatColor.GOLD + "M: "); + + if(managers.size() > 0) + { + for(int i = 0; i < managers.size(); i++) + permissions.append(managers.get(i) + " "); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.YELLOW + "B: "); + + if(builders.size() > 0) + { + for(int i = 0; i < builders.size(); i++) + permissions.append(builders.get(i) + " "); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.GREEN + "C: "); + + if(containers.size() > 0) + { + for(int i = 0; i < containers.size(); i++) + permissions.append(containers.get(i) + " "); + } + + player.sendMessage(permissions.toString()); + permissions = new StringBuilder(); + permissions.append(ChatColor.BLUE + "A :"); + + if(accessors.size() > 0) + { + for(int i = 0; i < accessors.size(); i++) + permissions.append(accessors.get(i) + " "); + } + + player.sendMessage(permissions.toString()); + + player.sendMessage("(M-anager, B-builder, C-ontainers, A-ccess)"); + + return true; + } + + //untrust + else if(cmd.getName().equalsIgnoreCase("untrust") && player != null) + { + //requires exactly one parameter, the other player's name + if(args.length != 1) return false; + + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + //determine whether a single player or clearing permissions entirely + boolean clearPermissions = false; + OfflinePlayer otherPlayer = null; + if(args[0].equals("all")) + { + if(claim == null || claim.allowEdit(player) == null) + { + clearPermissions = true; + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, "Only the claim owner can clear all permissions."); + return true; + } + } + + else + { + //validate player argument + otherPlayer = this.resolvePlayer(args[0]); + if(!clearPermissions && otherPlayer == null && !args[0].equals("public")) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); + return true; + } + + //correct to proper casing + if(otherPlayer != null) + args[0] = otherPlayer.getName(); + } + + //if no claim here, apply changes to all his claims + if(claim == null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + for(int i = 0; i < playerData.claims.size(); i++) + { + claim = playerData.claims.get(i); + + //if untrusting "all" drop all permissions + if(clearPermissions) + { + claim.clearPermissions(); + } + + //otherwise drop individual permissions + else + { + claim.dropPermission(args[0]); + claim.managers.remove(args[0]); + } + + //save changes + this.dataStore.saveClaim(claim); + } + + //beautify for output + if(args[0].equals("public")) + { + args[0] = "the public"; + } + + //confirmation message + if(!clearPermissions) + { + GriefPrevention.sendMessage(player, TextMode.Success, "Revoked " + args[0] + "'s access to ALL your claims. To set permissions for a single claim, stand inside it."); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Success, "Cleared permissions in ALL your claims. To set permissions for a single claim, stand inside it."); + } + } + + //otherwise, apply changes to only this claim + else if(claim.allowGrantPermission(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have " + claim.getOwnerName() + "'s permission to manage permissions here."); + } + else + { + //if clearing all + if(clearPermissions) + { + claim.clearPermissions(); + GriefPrevention.sendMessage(player, TextMode.Success, "Cleared permissions in this claim. To set permission for ALL your claims, stand outside them."); + } + + //otherwise individual permission drop + else + { + claim.dropPermission(args[0]); + if(claim.allowEdit(player) == null) + { + claim.managers.remove(args[0]); + + //beautify for output + if(args[0].equals("public")) + { + args[0] = "the public"; + } + + GriefPrevention.sendMessage(player, TextMode.Success, "Revoked " + args[0] + "'s access to this claim. To set permissions for a ALL your claims, stand outside them."); + } + } + + //save changes + this.dataStore.saveClaim(claim); + } + + return true; + } + + //accesstrust + else if(cmd.getName().equalsIgnoreCase("accesstrust") && player != null) + { + //requires exactly one parameter, the other player's name + if(args.length != 1) return false; + + this.handleTrustCommand(player, ClaimPermission.Access, args[0]); + + return true; + } + + //containertrust + else if(cmd.getName().equalsIgnoreCase("containertrust") && player != null) + { + //requires exactly one parameter, the other player's name + if(args.length != 1) return false; + + this.handleTrustCommand(player, ClaimPermission.Inventory, args[0]); + + return true; + } + + //permissiontrust + else if(cmd.getName().equalsIgnoreCase("permissiontrust") && player != null) + { + //requires exactly one parameter, the other player's name + if(args.length != 1) return false; + + this.handleTrustCommand(player, null, args[0]); //null indicates permissiontrust to the helper method + + return true; + } + + //buyclaimblocks + else if(cmd.getName().equalsIgnoreCase("buyclaimblocks") && player != null) + { + //if economy is disabled, don't do anything + if(GriefPrevention.economy == null) return true; + + //if purchase disabled, send error message + if(GriefPrevention.instance.config_economy_claimBlocksPurchaseCost == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Claim blocks may only be sold, not purchased."); + return true; + } + + //if no parameter, just tell player cost per block and balance + if(args.length != 1) + { + GriefPrevention.sendMessage(player, TextMode.Info, "Each claim block costs " + GriefPrevention.instance.config_economy_claimBlocksPurchaseCost + ". Your balance is " + GriefPrevention.economy.getBalance(player.getName()) + "."); + return false; + } + + else + { + //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 the player can't afford his purchase, send error message + double balance = economy.getBalance(player.getName()); + double totalCost = blockCount * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost; + if(totalCost > balance) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have enough money. You need " + totalCost + ", but you only have " + balance + "."); + } + + //otherwise carry out transaction + else + { + //withdraw cost + economy.withdrawPlayer(player.getName(), totalCost); + + //add blocks + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.bonusClaimBlocks += blockCount; + this.dataStore.savePlayerData(player.getName(), playerData); + + //inform player + GriefPrevention.sendMessage(player, TextMode.Success, "Withdrew " + totalCost + " from your account. You now have " + playerData.getRemainingClaimBlocks() + " available claim blocks."); + } + + return true; + } + } + + //sellclaimblocks + else if(cmd.getName().equalsIgnoreCase("sellclaimblocks") && player != null) + { + //if economy is disabled, don't do anything + if(GriefPrevention.economy == null) return true; + + //if disabled, error message + if(GriefPrevention.instance.config_economy_claimBlocksSellValue == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Claim blocks may only be purchased, not sold."); + return true; + } + + //load player data + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + int availableBlocks = playerData.getRemainingClaimBlocks(); + + //if no amount provided, just tell player value per block sold, and how many he can sell + if(args.length != 1) + { + GriefPrevention.sendMessage(player, TextMode.Info, "Each claim block is worth " + GriefPrevention.instance.config_economy_claimBlocksSellValue + ". You have " + availableBlocks + " available for sale."); + return false; + } + + //parse number of blocks + int blockCount; + try + { + blockCount = Integer.parseInt(args[0]); + } + catch(NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + //if he doesn't have enough blocks, tell him so + if(blockCount > availableBlocks) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have that many claim blocks available for sale."); + } + + //otherwise carry out the transaction + else + { + //compute value and deposit it + double totalValue = blockCount * GriefPrevention.instance.config_economy_claimBlocksSellValue; + economy.depositPlayer(player.getName(), totalValue); + + //subtract blocks + playerData.bonusClaimBlocks -= blockCount; + this.dataStore.savePlayerData(player.getName(), playerData); + + //inform player + GriefPrevention.sendMessage(player, TextMode.Success, "Deposited " + totalValue + " in your account. You now have " + playerData.getRemainingClaimBlocks() + " available claim blocks."); + } + + return true; + } + + //adminclaims + else if(cmd.getName().equalsIgnoreCase("adminclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.shovelMode = ShovelMode.Admin; + GriefPrevention.sendMessage(player, TextMode.Success, "Administrative claims mode active. Any claims created will be free and editable by other administrators."); + + return true; + } + + //basicclaims + else if(cmd.getName().equalsIgnoreCase("basicclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.shovelMode = ShovelMode.Basic; + playerData.claimSubdividing = null; + GriefPrevention.sendMessage(player, TextMode.Success, "Returned to basic claim creation mode."); + + return true; + } + + //subdivideclaims + else if(cmd.getName().equalsIgnoreCase("subdivideclaims") && player != null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.shovelMode = ShovelMode.Subdivide; + playerData.claimSubdividing = null; + GriefPrevention.sendMessage(player, TextMode.Instr, "Subdivision mode. Use your shovel to create subdivisions in your existing claims. Use /basicclaims to exit."); + + return true; + } + + //deleteclaim + else if(cmd.getName().equalsIgnoreCase("deleteclaim") && player != null) + { + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + if(claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "There's no claim here."); + } + + else + { + //deleting an admin claim additionally requires the adminclaims permission + if(!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims")) + { + this.dataStore.deleteClaim(claim); + GriefPrevention.sendMessage(player, TextMode.Success, "Claim deleted."); + + //revert any current visualization + Visualization.Revert(player); + } + else + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have permission to delete administrative claims."); + } + } + + return true; + } + + //deleteallclaims + else if(cmd.getName().equalsIgnoreCase("deleteallclaims") && player != null) + { + //requires exactly one parameter, the other player's name + if(args.length != 1) return false; + + //try to find that player + OfflinePlayer otherPlayer = this.resolvePlayer(args[0]); + if(otherPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); + return true; + } + + //delete all that player's claims + this.dataStore.deleteClaimsForPlayer(otherPlayer.getName()); + + GriefPrevention.sendMessage(player, TextMode.Success, "Deleted all of " + otherPlayer.getName() + "'s claims."); + + //revert any current visualization + Visualization.Revert(player); + + return true; + } + + //deletealladminclaims + else if(cmd.getName().equalsIgnoreCase("deletealladminclaims") && player != null) + { + if(!player.hasPermission("griefprevention.deleteclaims")) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have permission to delete claims."); + return true; + } + + //delete all admin claims + this.dataStore.deleteClaimsForPlayer(""); //empty string for owner name indicates an administrative claim + + GriefPrevention.sendMessage(player, TextMode.Success, "Deleted all administrative claims."); + + //revert any current visualization + Visualization.Revert(player); + + return true; + } + + //adjustbonusclaimblocks + else if(cmd.getName().equalsIgnoreCase("adjustbonusclaimblocks") && player != null) + { + //requires exactly two parameters, the other player's name and the adjustment + if(args.length != 2) return false; + + //find the specified player + OfflinePlayer targetPlayer = this.resolvePlayer(args[0]); + if(targetPlayer == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Player \"" + args[0] + "\" not found."); + return true; + } + + //parse the adjustment amount + int adjustment; + try + { + adjustment = Integer.parseInt(args[1]); + } + catch(NumberFormatException numberFormatException) + { + return false; //causes usage to be displayed + } + + //give blocks to player + PlayerData playerData = this.dataStore.getPlayerData(targetPlayer.getName()); + playerData.bonusClaimBlocks += adjustment; + this.dataStore.savePlayerData(targetPlayer.getName(), playerData); + + GriefPrevention.sendMessage(player, TextMode.Success, "Adjusted " + targetPlayer.getName() + "'s bonus claim blocks by " + adjustment + ". New total bonus blocks: " + playerData.bonusClaimBlocks + "."); + + return true; + } + + //trapped + else if(cmd.getName().equalsIgnoreCase("trapped") && player != null) + { + //FEATURE: empower players who get "stuck" in an area where they don't have permission to build to save themselves + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + Claim claim = this.dataStore.getClaimAt(player.getLocation(), false, playerData.lastClaim); + + //if another /trapped is pending, ignore this slash command + if(playerData.pendingTrapped) + { + return true; + } + + //if the player isn't in a claim or has permission to build, tell him to man up + if(claim == null || claim.allowBuild(player) == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can build here. Save yourself."); + return true; + } + + //check cooldown + long lastTrappedUsage = playerData.lastTrappedUsage.getTime(); + long nextTrappedUsage = lastTrappedUsage + 1000 * 60 * 60 * this.config_claims_trappedCooldownHours; + long now = Calendar.getInstance().getTimeInMillis(); + if(now < nextTrappedUsage) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You used /trapped within the last " + this.config_claims_trappedCooldownHours + " hours. You have to wait about " + ((nextTrappedUsage - now) / (1000 * 60) + 1) + " more minutes before using it again."); + return true; + } + + //send instructions + GriefPrevention.sendMessage(player, TextMode.Instr, "If you stay put for 10 seconds, you'll be teleported out. Please wait."); + + //create a task to rescue this player in a little while + PlayerRescueTask task = new PlayerRescueTask(player, player.getLocation()); + this.getServer().getScheduler().scheduleSyncDelayedTask(this, task, 200L); //20L ~ 1 second + + return true; + } + + //siege + else if(cmd.getName().equalsIgnoreCase("siege") && player != null) + { + //error message for when siege mode is disabled + if(this.siegeEnabledForWorld(player.getWorld())) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Siege is disabled here."); + return true; + } + + //requires one argument + if(args.length > 1) + { + return false; + } + + //can't start a siege when you're already involved in one + Player attacker = player; + PlayerData attackerData = this.dataStore.getPlayerData(attacker.getName()); + if(attackerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You're already involved in a siege."); + return true; + } + + //if a player name was specified, use that + Player defender = null; + if(args.length >= 1) + { + defender = this.getServer().getPlayer(args[0]); + if(defender == null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); + return true; + } + } + + //otherwise use the last player this player was in pvp combat with + else if(attackerData.lastPvpPlayer.length() > 0) + { + defender = this.getServer().getPlayer(attackerData.lastPvpPlayer); + if(defender == null) + { + return false; + } + } + + else + { + return false; + } + + //victim must not be under siege already + PlayerData defenderData = this.dataStore.getPlayerData(defender.getName()); + if(defenderData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, defender.getName() + " is already under siege. Join the party!"); + return true; + } + + Claim defenderClaim = this.dataStore.getClaimAt(defender.getLocation(), false, null); + + //attacker must be close to the claim he wants to siege + if(!defenderClaim.isNear(attacker.getLocation(), 25)) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You're too far away from " + defender.getName() + " to siege."); + return true; + } + + //claim can't be under siege already + if(defenderClaim.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "That area is already under siege. Join the party!"); + return true; + } + + //can't siege admin claims + if(defenderClaim.isAdminClaim()) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Siege is disabled in this area."); + return true; + } + + //defender must have some level of permission there to be protected + if(defenderClaim == null || defenderClaim.allowAccess(defender) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, defender.getName() + " isn't protected there."); + return true; + } + + //can't be on cooldown + if(dataStore.onCooldown(attacker, defender, defenderClaim)) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You're still on siege cooldown for this defender or claim. Find another victim."); + return true; + } + + //start the siege + dataStore.startSiege(attacker, defender, defenderClaim); + + //confirmation message for attacker, warning message for defender + GriefPrevention.sendMessage(player, TextMode.Warn, "You're under siege! If you log out now, you will die. You must defeat " + attacker.getName() + ", wait for him to give up, or escape."); + GriefPrevention.sendMessage(player, TextMode.Success, "The siege has begun! If you log out now, you will die. You must defeat " + defender.getName() + ", chase him away, or admit defeat and walk away."); + } + + return false; + } + + //helper method keeps the trust commands consistent and eliminates duplicate code + private void handleTrustCommand(Player player, ClaimPermission permissionLevel, String recipientName) + { + //determine which claim the player is standing in + Claim claim = this.dataStore.getClaimAt(player.getLocation(), true /*ignore height*/, null); + + //validate player argument + OfflinePlayer otherPlayer = this.resolvePlayer(recipientName); + if(otherPlayer == null && !recipientName.equals("public")) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Player not found."); + return; + } + + if(otherPlayer != null) + { + recipientName = otherPlayer.getName(); + } + else + { + recipientName = "public"; + } + + //determine which claims should be modified + ArrayList targetClaims = new ArrayList(); + if(claim == null) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + for(int i = 0; i < playerData.claims.size(); i++) + { + targetClaims.add(playerData.claims.get(i)); + } + } + else + { + //check permission here + if(claim.allowGrantPermission(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have " + claim.getOwnerName() + "'s permission to grant permissions here."); + return; + } + + //see if the player has the level of permission he's trying to grant + String errorMessage = null; + + //permission level null indicates granting permission trust + if(permissionLevel == null) + { + errorMessage = claim.allowEdit(player); + if(errorMessage != null) + { + errorMessage = "Only " + claim.getOwnerName() + " can grant /PermissionTrust here."; + } + } + + //otherwise just use the ClaimPermission enum values + else + { + switch(permissionLevel) + { + case Access: + errorMessage = claim.allowAccess(player); + break; + case Inventory: + errorMessage = claim.allowContainers(player); + break; + default: + errorMessage = claim.allowBuild(player); + } + } + + //error message for trying to grant a permission the player doesn't have + if(errorMessage != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, errorMessage + " You can't grant a permission you don't have yourself."); + return; + } + + targetClaims.add(claim); + } + + //if we didn't determine which claims to modify, tell the player to be specific + if(targetClaims.size() == 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Stand inside the claim where you want to grant permission."); + return; + } + + //apply changes + for(int i = 0; i < targetClaims.size(); i++) + { + Claim currentClaim = targetClaims.get(i); + if(permissionLevel == null) + { + if(!currentClaim.managers.contains(recipientName)) + { + currentClaim.managers.add(recipientName); + } + } + else + { + currentClaim.setPermission(recipientName, permissionLevel); + } + this.dataStore.saveClaim(currentClaim); + } + + //notify player + if(recipientName.equals("public")) recipientName = "the public"; + StringBuilder resultString = new StringBuilder(); + resultString.append("Granted " + recipientName + " "); + if(permissionLevel == null) + { + resultString.append("manager status"); + } + else if(permissionLevel == ClaimPermission.Build) + { + resultString.append("permission to build in"); + } + else if(permissionLevel == ClaimPermission.Access) + { + resultString.append("permission to use buttons and levers in"); + } + else if(permissionLevel == ClaimPermission.Inventory) + { + resultString.append("permission to access containers in"); + } + + if(claim == null) + { + resultString.append(" ALL your claims. To modify only one claim, stand inside it."); + } + else + { + resultString.append(" this claim. To modify ALL your claims, stand outside them."); + } + + GriefPrevention.sendMessage(player, TextMode.Success, resultString.toString()); + } + + //helper method to resolve a player by name + private OfflinePlayer resolvePlayer(String name) + { + //try online players first + Player player = this.getServer().getPlayer(name); + if(player != null) return player; + + //then search offline players + OfflinePlayer [] offlinePlayers = this.getServer().getOfflinePlayers(); + for(int i = 0; i < offlinePlayers.length; i++) + { + if(offlinePlayers[i].getName().equalsIgnoreCase(name)) + { + return offlinePlayers[i]; + } + } + + //if none found, return null + return null; + } + + public void onDisable() + { + AddLogEntry("GriefPrevention disabled."); + } + + //called when a player spawns, applies protection for that player if necessary + public void checkPvpProtectionNeeded(Player player) + { + //if pvp is disabled, do nothing + if(!player.getWorld().getPVP()) return; + + //if anti spawn camping feature is not enabled, do nothing + if(!this.config_pvp_protectFreshSpawns) return; + + //check inventory for well, anything + PlayerInventory inventory = player.getInventory(); + ItemStack [] armorStacks = inventory.getArmorContents(); + + //check armor slots, stop if any items are found + for(int i = 0; i < armorStacks.length; i++) + { + if(!(armorStacks[i] == null || armorStacks[i].getType() == Material.AIR)) return; + } + + //check other slots, stop if any items are found + ItemStack [] generalStacks = inventory.getContents(); + for(int i = 0; i < generalStacks.length; i++) + { + if(!(generalStacks[i] == null || generalStacks[i].getType() == Material.AIR)) return; + } + + //otherwise, apply immunity + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + playerData.pvpImmune = true; + + //inform the player + GriefPrevention.sendMessage(player, TextMode.Info, "You have one minute of PvP protection, starting now."); + GriefPrevention.sendMessage(player, TextMode.Info, "After the minute, you can pick up items, but doing so will drop your PvP protection."); + } + + //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 5 mins = 300 seconds ~ 6000L + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, cleanupTask, 6000L); + } + } + + //helper for above, finds the "root" of a stack of logs + //will return null if the stack is determined to not be a natural tree + private Block getRootBlock(Block logBlock) + { + if(logBlock.getType() != Material.LOG) return null; + + //run down through log blocks until finding a non-log block + Block underBlock = logBlock.getRelative(BlockFace.DOWN); + while(underBlock.getType() == Material.LOG) + { + underBlock = underBlock.getRelative(BlockFace.DOWN); + } + + //if this is a standard tree, that block MUST be dirt + if(underBlock.getType() != Material.DIRT) return null; + + //run up through log blocks until finding a non-log block + Block aboveBlock = logBlock.getRelative(BlockFace.UP); + while(aboveBlock.getType() == Material.LOG) + { + aboveBlock = aboveBlock.getRelative(BlockFace.UP); + } + + //if this is a standard tree, that block MUST be air or leaves + if(aboveBlock.getType() != Material.AIR && aboveBlock.getType() != Material.LEAVES) return null; + + return underBlock.getRelative(BlockFace.UP); + } + + //for sake of identifying trees ONLY, a cheap but not 100% reliable method for identifying player-placed blocks + private boolean isPlayerBlock(Block block) + { + Material material = block.getType(); + + //list of natural blocks which are OK to have next to a log block in a natural tree setting + if( material == Material.AIR || + material == Material.LEAVES || + material == Material.LOG || + material == Material.DIRT || + material == Material.GRASS || + material == Material.STATIONARY_WATER || + material == Material.BROWN_MUSHROOM || + material == Material.RED_MUSHROOM || + material == Material.RED_ROSE || + material == Material.LONG_GRASS || + material == Material.SNOW || + material == Material.STONE || + material == Material.VINE || + material == Material.WATER_LILY || + material == Material.YELLOW_FLOWER || + material == Material.CLAY) + { + return false; + } + else + { + return true; + } + } + + //moves a player from the claim he's in to a nearby wilderness location + public Location ejectPlayer(Player player) + { + //look for a suitable location + Location candidateLocation = player.getLocation(); + while(true) + { + Claim claim = null; + claim = GriefPrevention.instance.dataStore.getClaimAt(candidateLocation, false, null); + + //if there's a claim here, keep looking + if(claim != null) + { + candidateLocation = new Location(claim.lesserBoundaryCorner.getWorld(), claim.lesserBoundaryCorner.getBlockX() - 1, claim.lesserBoundaryCorner.getBlockY(), claim.lesserBoundaryCorner.getBlockZ() - 1); + continue; + } + + //otherwise find a safe place to teleport the player + else + { + //find a safe height, a couple of blocks above the surface + GuaranteeChunkLoaded(candidateLocation); + Block highestBlock = candidateLocation.getWorld().getHighestBlockAt(candidateLocation.getBlockX(), candidateLocation.getBlockZ()); + Location destination = new Location(highestBlock.getWorld(), highestBlock.getX(), highestBlock.getY() + 2, highestBlock.getZ()); + player.teleport(destination); + return destination; + } + } + } + + //ensures a piece of the managed world is loaded into server memory + //(generates the chunk if necessary) + private static void GuaranteeChunkLoaded(Location location) + { + Chunk chunk = location.getChunk(); + while(!chunk.isLoaded() || !chunk.load(true)); + } + + //sends a color-coded message to a player + static void sendMessage(Player player, ChatColor color, String message) + { + player.sendMessage(color + message); + } +} \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerData.java b/src/me/ryanhamshire/GriefPrevention/PlayerData.java new file mode 100644 index 0000000..d67a06e --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/PlayerData.java @@ -0,0 +1,130 @@ +/* + 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.Date; +import java.util.Vector; + +import org.bukkit.Location; + +//holds all of GriefPrevention's player-tied data +public class PlayerData +{ + //the player's claims + public Vector claims = new Vector(); + + //how many claim blocks the player has earned via play time + public int accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks; + + //where this player was the last time we checked on him for earning claim blocks + public Location lastAfkCheckLocation = null; + + //how many claim blocks the player has been gifted by admins, or purchased via economy integration + public int bonusClaimBlocks = 0; + + //what "mode" the shovel is in determines what it will do when it's used + public ShovelMode shovelMode = ShovelMode.Basic; + + //last place the player used the shovel, useful in creating and resizing claims, + //because the player must use the shovel twice in those instances + public Location lastShovelLocation = null; + + //the claim this player is currently resizing + public Claim claimResizing = null; + + //the claim this player is currently subdividing + public Claim claimSubdividing = null; + + //the timestamp for the last time the player used /trapped + public Date lastTrappedUsage; + + //whether or not the player has a pending /trapped rescue + public boolean pendingTrapped = false; + + //last place the player damaged a chest + public Location lastChestDamageLocation = null; + + //spam + public Date lastLogin; //when the player last logged into the server + public String lastMessage = ""; //the player's last chat message, or slash command complete with parameters + public Date lastMessageTimestamp = new Date(); //last time the player sent a chat message or used a monitored slash command + public int spamCount = 0; //number of consecutive "spams" + + //visualization + public Visualization currentVisualization = null; + + //anti-camping pvp protection + public boolean pvpImmune = false; + public long lastSpawn = 0; + + //ignore claims mode + public boolean ignoreClaims = false; + + //the last claim this player was in, that we know of + public Claim lastClaim = null; + + //siege + public SiegeData siegeData = null; + + //pvp + public long lastPvpTimestamp = 0; + public String lastPvpPlayer = ""; + + PlayerData() + { + //default last login date value to a year ago to ensure a brand new player can log in + //see login cooldown feature, PlayerEventHandler.onPlayerLogin() + //if the player successfully logs in, this value will be overwritten with the current date and time + Calendar lastYear = Calendar.getInstance(); + lastYear.add(Calendar.YEAR, -1); + this.lastLogin = lastYear.getTime(); + this.lastTrappedUsage = lastYear.getTime(); + } + + //whether or not this player is "in" pvp combat + public boolean inPvpCombat() + { + if(this.lastPvpTimestamp == 0) return false; + + long now = Calendar.getInstance().getTimeInMillis(); + + long elapsed = now - this.lastPvpTimestamp; + + if(elapsed > 15000) //15 seconds + { + this.lastPvpTimestamp = 0; + return false; + } + + return true; + } + + //the number of claim blocks a player has available for claiming land + public int getRemainingClaimBlocks() + { + int remainingBlocks = this.accruedClaimBlocks + this.bonusClaimBlocks; + for(int i = 0; i < this.claims.size(); i++) + { + Claim claim = this.claims.get(i); + remainingBlocks -= claim.getArea(); + } + + return remainingBlocks; + } +} \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java new file mode 100644 index 0000000..0ef199d --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -0,0 +1,1097 @@ +/* + 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.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +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.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; + +class PlayerEventHandler implements Listener +{ + private DataStore dataStore; + + //typical constructor, yawn + PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) + { + this.dataStore = dataStore; + } + + //when a player chats, monitor for spam + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + void onPlayerChat (PlayerChatEvent event) + { + Player player = event.getPlayer(); + String message = event.getMessage(); + + //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")) + { + GriefPrevention.sendMessage(player, TextMode.Info, "Are you trapped in someone's claim? Consider the /trapped command."); + } + + //FEATURE: monitor for chat and command spam + + if(!GriefPrevention.instance.config_spam_enabled) return; + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + boolean spam = false; + + //filter IP addresses + if(!(event instanceof PlayerCommandPreprocessEvent)) + { + Pattern ipAddressPattern = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+"); + Matcher matcher = ipAddressPattern.matcher(event.getMessage()); + //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() + ": " + event.getMessage()); + + //spam notation + playerData.spamCount++; + spam = true; + + //block message + event.setCancelled(true); + } + } + } + + //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 < 3000) + { + //increment the spam counter + playerData.spamCount++; + spam = true; + } + + //if it's the same as the last message + if(message.equals(playerData.lastMessage)) + { + playerData.spamCount++; + spam = true; + } + + //if the message was mostly non-alpha-numerics, consider it a spam (probably ansi art) + if(message.length() > 5) + { + int symbolsCount = 0; + for(int i = 0; i < message.length(); i++) + { + char character = message.charAt(i); + if(!(Character.isLetterOrDigit(character) || Character.isWhitespace(character))) + { + symbolsCount++; + } + } + + if(symbolsCount > message.length() / 2) + { + spam = true; + playerData.spamCount++; + } + } + + //if the message was determined to be a spam, consider taking action + if(!player.hasPermission("griefprevention.spam") && spam) + { + //at the fifth spam level, auto-ban (if enabled) + if(playerData.spamCount > 4) + { + event.setCancelled(true); + GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); + + 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); + } + } + + //cancel any messages while at or above the third spam level and issue warnings + else if(playerData.spamCount >= 3) + { + GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage); + event.setCancelled(true); + GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + "."); + } + } + + //otherwise if not a spam, reset the spam counter for this player + else + { + playerData.spamCount = 0; + } + + //in any case, record the timestamp of this message and also its content for next time + playerData.lastMessageTimestamp = new Date(); + playerData.lastMessage = message; + } + + //when a player uses a slash command, monitor for spam + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) + { + 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) + String [] args = event.getMessage().split(" "); + if(GriefPrevention.instance.config_spam_monitorSlashCommands.contains(args[0])) this.onPlayerChat(event); + } + + //when a player attempts to join the server... + @EventHandler(ignoreCancelled = true) + void onPlayerLogin (PlayerLoginEvent event) + { + if(!GriefPrevention.instance.config_spam_enabled) return; + + Player player = event.getPlayer(); + + //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()); + } + } + } + + //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) + 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); + + //check inventory, may need pvp protection + GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); + } + + //when a player quits... + @EventHandler + void onPlayerQuit(PlayerQuitEvent event) + { + this.onPlayerDisconnect(event.getPlayer()); + } + + //when a player gets kicked... + @EventHandler(ignoreCancelled = true) + void onPlayerKicked(PlayerKickEvent event) + { + this.onPlayerDisconnect(event.getPlayer()); + } + + //helper for above + private void onPlayerDisconnect(Player player) + { + 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: on logout or kick, give player the any claim blocks he may have earned for this play session + //NOTE: not all kicks are bad, for example an AFK kick or a kick to make room for an admin to log in + //that's why even kicked players get their claim blocks + + //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) + { + player.setHealth(0); + this.dataStore.endSiege(playerData.siegeData, null, player.getName()); + } + + //disable ignore claims mode + playerData.ignoreClaims = false; + } + + //when a player drops an item + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerDropItem(PlayerDropItemEvent event) + { + Player player = event.getPlayer(); + 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(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't drop items while in PvP combat."); + event.setCancelled(true); + } + + //if he's under siege, don't let him drop it + else if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't drop items while involved in a siege."); + event.setCancelled(true); + } + } + + //when a player interacts with an entity... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + 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, "You can't access containers while under siege."); + event.setCancelled(true); + return; + } + + if(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't access containers during PvP combat."); + 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, "That animal belongs to " + claim.getOwnerName() + "."); + event.setCancelled(true); + } + } + } + } + } + + //when a player picks up an item... + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + 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 at least a minute since the last time he spawned, don't pick up the item + long now = Calendar.getInstance().getTimeInMillis(); + long elapsedSinceLastSpawn = now - playerData.lastSpawn; + if(elapsedSinceLastSpawn < 60000) + { + 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, "You're now vulnerable to damage from other players."); + } + } + } + + //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() == Material.GOLD_SPADE) + { + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //reset any work he might have been doing + playerData.lastShovelLocation = null; + playerData.claimResizing = null; + + //always reset to basic claims mode + if(playerData.shovelMode != ShovelMode.Basic) + { + playerData.shovelMode = ShovelMode.Basic; + GriefPrevention.sendMessage(player, TextMode.Info, "Shovel returned to basic claims mode."); + } + + int remainingBlocks = playerData.getRemainingClaimBlocks(); + + //if he doesn't have enough blocks to create a new claim, tell him so and offer advice + if(remainingBlocks < GriefPrevention.instance.config_claims_minSize * GriefPrevention.instance.config_claims_minSize) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have enough available claim blocks to create a new claim (each new claim must be at least " + GriefPrevention.instance.config_claims_minSize + " x " + GriefPrevention.instance.config_claims_minSize + "). Consider /AbandonClaim to delete an existing claim."); + return; + } + + //otherwise instruct him in the steps to create a claim + else + { + GriefPrevention.sendMessage(player, TextMode.Instr, "To start creating a claim, right-click at one corner of the claim area. You may claim up to " + String.valueOf(remainingBlocks) + " more blocks."); + GriefPrevention.sendMessage(player, TextMode.Instr, "Need a demonstration? Watch the \"Grief Prevention Basics\" YouTube video."); + } + } + } + + //block players from entering beds they don't have permission for + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + 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, claim.getOwnerName() + " hasn't given you permission to sleep here."); + } + } + } + + //block use of buckets within other players' claims + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) + { + Player player = bucketEvent.getPlayer(); + Block block = bucketEvent.getBlockClicked(); + int minLavaDistance = 10; + + //if the bucket is being used in a claim + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null) + { + //the player must have build permission to use it + if(claim.allowBuild(player) != null) + { + bucketEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have " + claim.getOwnerName() + "'s permission to use your bucket here."); + return; + } + + //the claim must be at least an hour old + long now = Calendar.getInstance().getTimeInMillis(); + long lastModified = claim.modifiedDate.getTime(); + long elapsed = now - lastModified; + if(bucketEvent.getBucket() == Material.LAVA_BUCKET && !player.hasPermission("griefprevention.lava") && elapsed < 1000 * 60 * 60) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't dump lava here because this claim was recently modified. Try again later."); + bucketEvent.setCancelled(true); + } + + minLavaDistance = 3; + } + + //otherwise it must be underground + else + { + if(bucketEvent.getBucket() == Material.LAVA_BUCKET && block.getY() >= block.getWorld().getSeaLevel() - 5 && !player.hasPermission("griefprevention.lava")) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You may only dump lava inside your claim(s) or underground."); + bucketEvent.setCancelled(true); + } + } + + //lava buckets can't be dumped near other players unless pvp is on + if(!block.getWorld().getPVP()) + { + 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) + { + player.sendMessage(block.getY() + " " + otherPlayer.getLocation().getBlockY()); + + GriefPrevention.sendMessage(player, TextMode.Err, "You can't place lava this close to " + otherPlayer.getName() + "."); + bucketEvent.setCancelled(true); + return; + } + } + } + } + } + + //see above + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) + { + Player player = bucketEvent.getPlayer(); + Block block = bucketEvent.getBlockClicked(); + + Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); + if(claim != null) + { + if(claim.allowBuild(player) != null) + { + bucketEvent.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have permission to use your bucket here."); + } + } + } + + //when a player interacts with the world + @EventHandler(priority = EventPriority.HIGHEST) + 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) + { + //try to find a far away non-air block along line of sight + clickedBlock = player.getTargetBlock(null, 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 buttons and switches + if(GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.LEVER)) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, null); + if(claim != null) + { + String noAccessReason = claim.allowAccess(player); + if(noAccessReason != null) + { + event.setCancelled(true); + GriefPrevention.sendMessage(player, TextMode.Err, noAccessReason); + } + } + } + + //otherwise apply rules for containers and crafting blocks + else if( GriefPrevention.instance.config_claims_preventTheft && ( + event.getAction() == Action.RIGHT_CLICK_BLOCK && ( + clickedBlock.getState() instanceof InventoryHolder || + clickedBlockType == Material.BREWING_STAND || + clickedBlockType == Material.JUKEBOX || + clickedBlockType == Material.ENCHANTMENT_TABLE))) + { + //block container use while under siege, so players can't hide items from attackers + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't access containers while involved in a siege."); + event.setCancelled(true); + return; + } + + //block container use during pvp combat, same reason + if(playerData.inPvpCombat()) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't access containers during PvP combat."); + event.setCancelled(true); + return; + } + + //otherwise check permissions for the claim the player is in + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, null); + if(claim != null) + { + String noContainersReason = claim.allowContainers(player); + if(noContainersReason != null) + { + event.setCancelled(true); + player.sendMessage(noContainersReason); + } + } + } + + //apply rule for players trampling 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); + } + + //otherwise handle right click (shovel, string, bonemeal) + else + { + //ignore all actions except right-click on a block or in the air + Action action = event.getAction(); + if(action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; + + //what's the player holding? + Material materialInHand = player.getItemInHand().getType(); + + //if it's bonemeal, check for build permission (ink sac == bone meal, must be a Bukkit bug?) + if(materialInHand == Material.INK_SACK) + { + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, null); + if(claim == null) return; + + String noBuildReason = claim.allowBuild(player); + if(claim != null && noBuildReason != null) + { + player.sendMessage(noBuildReason); + event.setCancelled(true); + } + + return; + } + + //if it's a string, he's investigating a claim + else if(materialInHand == Material.STRING) + { + //air indicates too far away + if(clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, "That's too far away."); + return; + } + + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false /*ignore height*/, null); + + //no claim case + if(claim == null) + { + GriefPrevention.sendMessage(player, TextMode.Info, "No one has claimed this block."); + Visualization.Revert(player); + } + + //claim case + else + { + GriefPrevention.sendMessage(player, TextMode.Info, "This block has been claimed by " + claim.getOwnerName() + "."); + + //visualize boundary + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim); + Visualization.Apply(player, visualization); + } + + return; + } + + //if it's a golden shovel + else if(materialInHand != Material.GOLD_SPADE) return; + + PlayerData playerData = this.dataStore.getPlayerData(player.getName()); + + //disable golden shovel while under siege + if(playerData.siegeData != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You can't use your shovel tool while involved in a siege."); + event.setCancelled(true); + return; + } + + //can't use the shovel from too far away + if(clickedBlockType == Material.AIR) + { + GriefPrevention.sendMessage(player, TextMode.Err, "That's too far away!"); + 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) + { + //if the clicked block is in a claim, visualize that claim and deliver an error message + Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), true, playerData.lastClaim); + if(claim != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, claim.getOwnerName() + " claimed that block."); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim); + Visualization.Apply(player, visualization); + + return; + } + + //figure out which chunk to regen + Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); + + //check it for players, and cancel if there are any + Entity [] entities = chunk.getEntities(); + for(int i = 0; i < entities.length; i++) + { + if(entities[i] instanceof Player) + { + Player otherPlayer = (Player)entities[i]; + GriefPrevention.sendMessage(player, TextMode.Err, "Unable to restore. " + otherPlayer.getName() + " is in that chunk."); + return; + } + } + + //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 + + //set boundaries for processing + int miny = clickedBlock.getY(); + if(miny > chunk.getWorld().getSeaLevel() - 10) + { + miny = chunk.getWorld().getSeaLevel() - 10; + } + + 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(), chunk.getWorld().getBiome(lesserBoundaryCorner.getBlockX(), lesserBoundaryCorner.getBlockZ()), lesserBoundaryCorner, greaterBoundaryCorner, chunk.getWorld().getSeaLevel(), player); + GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task); + + 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, "You don't have permission to claim land."); + 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, "This new size would be too small. Claims must be at least " + GriefPrevention.instance.config_claims_minSize + " x " + GriefPrevention.instance.config_claims_minSize + "."); + return; + } + + //make sure player has enough blocks to make up the difference + if(!playerData.claimResizing.isAdminClaim()) + { + int newArea = newWidth * newHeight; + int blocksRemainingAfter = playerData.getRemainingClaimBlocks() + playerData.claimResizing.getArea() - newArea; + + if(blocksRemainingAfter < 0) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You don't have enough blocks for this size. You need " + Math.abs(blocksRemainingAfter) + " more."); + return; + } + } + } + + //ask the datastore to try and resize the claim + 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, "Claim resized. You now have " + playerData.getRemainingClaimBlocks() + " available claim blocks."); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim); + Visualization.Apply(player, visualization); + + //clean up + playerData.claimResizing = null; + playerData.lastShovelLocation = null; + } + else + { + //inform player + GriefPrevention.sendMessage(player, TextMode.Err, "Can't resize here because it would overlap another nearby claim."); + + //show the player the conflicting claim + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim); + 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(); + player.sendMessage("Resizing claim. Use your shovel again at the new location for this corner."); + } + + //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, "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."); + } + + //otherwise start a new subdivision + else + { + GriefPrevention.sendMessage(player, TextMode.Instr, "Subdivision corner set! Use your shovel at the location for the opposite corner of this new subdivision."); + 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); + + //if it didn't succeed, tell the player why + if(!result.succeeded) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Your selected area overlaps another subdivision."); + + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim); + Visualization.Apply(player, visualization); + + return; + } + + //otherwise, advise him on the /trust command and show him his new subdivision + else + { + GriefPrevention.sendMessage(player, TextMode.Success, "Subdivision created! Use /trust to share it with friends."); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim); + 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, "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."); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.Claim); + 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, "You can't create a claim here because it would overlap " + claim.getOwnerName() + "'s claim."); + Visualization visualization = Visualization.FromClaim(claim, clickedBlock.getY(), VisualizationType.ErrorClaim); + 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, "Land claims are disabled in this world."); + return; + } + + //remember it, and start him on the new claim + playerData.lastShovelLocation = clickedBlock.getLocation(); + GriefPrevention.sendMessage(player, TextMode.Instr, "Claim corner set! Use the shovel again at the opposite corner to claim a rectangle of land. To cancel, put your shovel away."); + } + + //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, "Stopping your claim here would create a too-small claim. A claim must be at least " + GriefPrevention.instance.config_claims_minSize + " x " + 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, "You don't have enough blocks to claim that entire area. You need " + (newClaimArea - remainingBlocks) + " more blocks."); + GriefPrevention.sendMessage(player, TextMode.Instr, "To delete another claim and free up some blocks, use /abandonclaim."); + 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); + + //if it didn't succeed, tell the player why + if(!result.succeeded) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Your selected area overlaps an existing claim."); + + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.ErrorClaim); + Visualization.Apply(player, visualization); + + return; + } + + //otherwise, advise him on the /trust command and show him his new claim + else + { + GriefPrevention.sendMessage(player, TextMode.Success, "Claim created! Use /trust to share it with friends."); + Visualization visualization = Visualization.FromClaim(result.claim, clickedBlock.getY(), VisualizationType.Claim); + Visualization.Apply(player, visualization); + playerData.lastShovelLocation = null; + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerRescueTask.java b/src/me/ryanhamshire/GriefPrevention/PlayerRescueTask.java new file mode 100644 index 0000000..f5373ec --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/PlayerRescueTask.java @@ -0,0 +1,69 @@ +/* + 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 org.bukkit.Location; +import org.bukkit.entity.Player; + +//tries to rescue a trapped player from a claim where he doesn't have permission to save himself +//related to the /trapped slash command +//this does run in the main thread, so it's okay to make non-thread-safe calls +class PlayerRescueTask implements Runnable +{ + //original location where /trapped was used + private Location location; + + //player data + private Player player; + + public PlayerRescueTask(Player player, Location location) + { + this.player = player; + this.location = location; + } + + @Override + public void run() + { + //if he logged out, don't do anything + if(!player.isOnline()) return; + + //he no longer has a pending /trapped slash command, so he can try to use it again now + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getName()); + playerData.pendingTrapped = false; + + //if the player moved three or more blocks from where he used /trapped, admonish him and don't save him + if(player.getLocation().distance(this.location) > 3) + { + GriefPrevention.sendMessage(player, TextMode.Err, "You moved! Rescue cancelled."); + return; + } + + //otherwise find a place to teleport him + Location destination = GriefPrevention.instance.ejectPlayer(this.player); + + //log entry, in case admins want to investigate the "trap" + GriefPrevention.AddLogEntry("Rescued trapped player " + player.getName() + " from " + this.location.toString() + " to " + destination.toString() + "."); + + //timestamp this successful save so that he can't use /trapped again for a while + playerData.lastTrappedUsage = Calendar.getInstance().getTime(); + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/Public API.txt b/src/me/ryanhamshire/GriefPrevention/Public API.txt new file mode 100644 index 0000000..a038a45 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/Public API.txt @@ -0,0 +1,41 @@ +This document describes the public API, which you can use to create extensions to GriefPrevention which add new features. Before I get into the specifics, let me give you a few examples of often-requested features which, to my knowledge, have not yet been implemented by anyone. If you want to make a big impact with a small project, these are the go-to areas! If you publish one of these extensions on BukkitDev, please contact me and I'll add a link from my project to yours. + +Claim Buy/Sell + +I keep saying no to this because I'm developing an anti grief plugin, not a real estate plugin. But it's a common ask. Lots of people would use an extension that allowed them to use server money to buy and sell claims, or to lease subdivisions. + +More Locks + +Many have asked for wooden doors, trap doors, and fence gates to require /AccessTrust. I've insisted that because players generally expect these to be openable (based on the Vanilla experience), players should just "earn" their privacy by finding some iron and building an iron door. Nonetheless, some folks definitely want this. + +Claim Flags + +Sometimes, folks want to add special flags to their claims like "no monsters spawn here". They can do this today by adding other plugins like WorldGuard, which are compatible with GriefPrevention, but it would be nice if they could just use one plugin (and an extension). I think their flag ideas come mostly from Residence and WorldGuard, so you can look there for ideas. + +Claim Entry/Exit Messages + +I keep telling people NO, I won't do this because it's not anti-grief-related and it's expensive to constantly track player movement. But folks want it, and they keep asking for it. You could build an extension which adds some slash commands for naming claims, and displays enter/exit messages as players walk around. + +Now the specifics! Please note, these are the supported operations. I've done my best to "hide" fields and methods which you shouldn't play with, but if you happen to notice something not discussed here, it's best not to fiddle with it. If in doubt, at the very least look at my source code and comments before using something you're unfamiliar with in an extension. + +Getting the Claim at a Location + +Managing Permissions in a Claim + +Creating a New Claim + +Resizing or Moving a Claim + +Extending a Claim Downward + +Changing a Claim's Owner + +Updating Other Claim Fields + +Uniquely Identifying a Claim + +Starting a Siege + +Ending a Siege + +Getting/Updating Player Data \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java b/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java new file mode 100644 index 0000000..e5d5fa2 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/RestoreNatureExecutionTask.java @@ -0,0 +1,97 @@ +/* + 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.block.Block; +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 + 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.remove(); + } + + //show visualization to player + Claim claim = new Claim(lesserCorner, greaterCorner, "", new String[] {}, new String[] {}, new String[] {}, new String[] {}); + Visualization visualization = Visualization.FromClaim(claim, player.getLocation().getBlockY(), VisualizationType.RestoreNature); + Visualization.Apply(player, visualization); + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java new file mode 100644 index 0000000..9721627 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/RestoreNatureProcessingTask.java @@ -0,0 +1,470 @@ +/* + 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 int seaLevel; + + //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, Player player) + { + this.snapshots = snapshots; + this.miny = miny; + this.environment = environment; + this.lesserBoundaryCorner = lesserBoundaryCorner; + this.greaterBoundaryCorner = greaterBoundaryCorner; + this.biome = biome; + this.seaLevel = seaLevel; + this.player = player; + + this.notAllowedToHang = new ArrayList(); + this.notAllowedToHang.add(Material.DIRT.getId()); + this.notAllowedToHang.add(Material.GRASS.getId()); + this.notAllowedToHang.add(Material.SNOW.getId()); + this.notAllowedToHang.add(Material.LOG.getId()); + + //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 replacements + this.playerBlocks = new ArrayList(); + this.playerBlocks.add(Material.BED_BLOCK.getId()); + this.playerBlocks.add(Material.WOOD.getId()); + this.playerBlocks.add(Material.BOOKSHELF.getId()); + this.playerBlocks.add(Material.BREWING_STAND.getId()); + this.playerBlocks.add(Material.BRICK.getId()); + this.playerBlocks.add(Material.COBBLESTONE.getId()); + this.playerBlocks.add(Material.OBSIDIAN.getId()); + this.playerBlocks.add(Material.GLASS.getId()); + this.playerBlocks.add(Material.LAPIS_BLOCK.getId()); + this.playerBlocks.add(Material.DISPENSER.getId()); + this.playerBlocks.add(Material.NOTE_BLOCK.getId()); + this.playerBlocks.add(Material.POWERED_RAIL.getId()); + this.playerBlocks.add(Material.DETECTOR_RAIL.getId()); + this.playerBlocks.add(Material.PISTON_STICKY_BASE.getId()); + this.playerBlocks.add(Material.PISTON_BASE.getId()); + this.playerBlocks.add(Material.PISTON_EXTENSION.getId()); + this.playerBlocks.add(Material.WOOL.getId()); + this.playerBlocks.add(Material.PISTON_MOVING_PIECE.getId()); + this.playerBlocks.add(Material.GOLD_BLOCK.getId()); + this.playerBlocks.add(Material.IRON_BLOCK.getId()); + this.playerBlocks.add(Material.DOUBLE_STEP.getId()); + this.playerBlocks.add(Material.STEP.getId()); + this.playerBlocks.add(Material.CROPS.getId()); + this.playerBlocks.add(Material.TNT.getId()); + this.playerBlocks.add(Material.MOSSY_COBBLESTONE.getId()); + this.playerBlocks.add(Material.TORCH.getId()); + this.playerBlocks.add(Material.FIRE.getId()); + this.playerBlocks.add(Material.WOOD_STAIRS.getId()); + this.playerBlocks.add(Material.CHEST.getId()); + this.playerBlocks.add(Material.REDSTONE_WIRE.getId()); + this.playerBlocks.add(Material.DIAMOND_BLOCK.getId()); + this.playerBlocks.add(Material.WORKBENCH.getId()); + this.playerBlocks.add(Material.SOIL.getId()); + this.playerBlocks.add(Material.FURNACE.getId()); + this.playerBlocks.add(Material.BURNING_FURNACE.getId()); + this.playerBlocks.add(Material.WOODEN_DOOR.getId()); + this.playerBlocks.add(Material.SIGN_POST.getId()); + this.playerBlocks.add(Material.LADDER.getId()); + this.playerBlocks.add(Material.RAILS.getId()); + this.playerBlocks.add(Material.COBBLESTONE_STAIRS.getId()); + this.playerBlocks.add(Material.WALL_SIGN.getId()); + this.playerBlocks.add(Material.STONE_PLATE.getId()); + this.playerBlocks.add(Material.LEVER.getId()); + this.playerBlocks.add(Material.IRON_DOOR_BLOCK.getId()); + this.playerBlocks.add(Material.WOOD_PLATE.getId()); + this.playerBlocks.add(Material.REDSTONE_TORCH_ON.getId()); + this.playerBlocks.add(Material.REDSTONE_TORCH_OFF.getId()); + this.playerBlocks.add(Material.STONE_BUTTON.getId()); + this.playerBlocks.add(Material.SNOW_BLOCK.getId()); + this.playerBlocks.add(Material.JUKEBOX.getId()); + this.playerBlocks.add(Material.FENCE.getId()); + this.playerBlocks.add(Material.PORTAL.getId()); + this.playerBlocks.add(Material.JACK_O_LANTERN.getId()); + this.playerBlocks.add(Material.CAKE_BLOCK.getId()); + this.playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + this.playerBlocks.add(Material.DIODE_BLOCK_OFF.getId()); + this.playerBlocks.add(Material.TRAP_DOOR.getId()); + this.playerBlocks.add(Material.SMOOTH_BRICK.getId()); + this.playerBlocks.add(Material.HUGE_MUSHROOM_1.getId()); + this.playerBlocks.add(Material.HUGE_MUSHROOM_2.getId()); + this.playerBlocks.add(Material.IRON_FENCE.getId()); + this.playerBlocks.add(Material.THIN_GLASS.getId()); + this.playerBlocks.add(Material.MELON_STEM.getId()); + this.playerBlocks.add(Material.FENCE_GATE.getId()); + this.playerBlocks.add(Material.BRICK_STAIRS.getId()); + this.playerBlocks.add(Material.SMOOTH_STAIRS.getId()); + this.playerBlocks.add(Material.ENCHANTMENT_TABLE.getId()); + this.playerBlocks.add(Material.BREWING_STAND.getId()); + this.playerBlocks.add(Material.CAULDRON.getId()); + this.playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + this.playerBlocks.add(Material.DIODE_BLOCK_ON.getId()); + + //these are unnatural in the standard world, but not in the nether + if(this.environment != Environment.NETHER) + { + this.playerBlocks.add(Material.NETHERRACK.getId()); + this.playerBlocks.add(Material.SOUL_SAND.getId()); + this.playerBlocks.add(Material.GLOWSTONE.getId()); + this.playerBlocks.add(Material.NETHER_BRICK.getId()); + this.playerBlocks.add(Material.NETHER_FENCE.getId()); + this.playerBlocks.add(Material.NETHER_BRICK_STAIRS.getId()); + } + + //these are unnatural in sandy biomes, but not elsewhere + if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH) + { + this.playerBlocks.add(Material.LEAVES.getId()); + this.playerBlocks.add(Material.LOG.getId()); + } + } + + @Override + public void run() + { + //order is important! + + //remove any blocks which are definitely player placed + this.removePlayerBlocks(); + + //remove natural blocks which are unnaturally hanging in the air + this.removeHanging(); + + //remove natural blocks which are unnaturally stacked high + this.removeWallsAndTowers(); + + //cover surface stone and gravel with sand or grass, as the biome requires + this.coverSurfaceStone(); + + //fill unnatural thin trenches and single-block potholes + this.fillHolesAndTrenches(); + + //fill water depressions and fix unnatural surface ripples + this.fixWater(); + + //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 removePlayerBlocks() + { + 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]; + 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.WATER.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); + if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue; + + int righty = this.highestY(x + 1, z); + int lefty = this.highestY(x - 1, z); + while(lefty < thisy && righty < thisy) + { + this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); + changed = true; + } + + int upy = this.highestY(x, z + 1); + int downy = this.highestY(x, z - 1); + 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); + BlockSnapshot block = snapshots[x][y][z]; + + if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.DIRT.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()); + + 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 int highestY(int x, int z) + { + int y; + for(y = snapshots[0].length - 1; y >= 0; y--) + { + BlockSnapshot block = this.snapshots[x][y][z]; + if(block.typeId != Material.AIR.getId() && + !(block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0) && + !(block.typeId == Material.STATIONARY_LAVA.getId() && block.data != 0)) + { + return y; + } + } + + return y; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/SecureClaimTask.java b/src/me/ryanhamshire/GriefPrevention/SecureClaimTask.java new file mode 100644 index 0000000..72d659d --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/SecureClaimTask.java @@ -0,0 +1,56 @@ +/* + 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.entity.Player; + +//secures a claim after a siege looting window has closed +class SecureClaimTask implements Runnable +{ + private SiegeData siegeData; + + public SecureClaimTask(SiegeData siegeData) + { + this.siegeData = siegeData; + } + + @Override + public void run() + { + //for each claim involved in this siege + for(int i = 0; i < this.siegeData.claims.size(); i++) + { + //lock the doors + Claim claim = this.siegeData.claims.get(i); + claim.doorsOpen = false; + + //eject bad guys + Player [] onlinePlayers = GriefPrevention.instance.getServer().getOnlinePlayers(); + for(int j = 0; j < onlinePlayers.length; j++) + { + Player player = onlinePlayers[j]; + if(claim.contains(player.getLocation(), false, false) && claim.allowAccess(player) != null) + { + GriefPrevention.sendMessage(player, TextMode.Err, "Looting time is up! Ejected from the claim."); + GriefPrevention.instance.ejectPlayer(player); + } + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/ShovelMode.java b/src/me/ryanhamshire/GriefPrevention/ShovelMode.java new file mode 100644 index 0000000..44ef29d --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/ShovelMode.java @@ -0,0 +1,28 @@ +/* + 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; + +//enumeration for golden shovel modes +enum ShovelMode +{ + Basic, + Admin, + Subdivide, + RestoreNature +} diff --git a/src/me/ryanhamshire/GriefPrevention/SiegeCheckupTask.java b/src/me/ryanhamshire/GriefPrevention/SiegeCheckupTask.java new file mode 100644 index 0000000..732af23 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/SiegeCheckupTask.java @@ -0,0 +1,110 @@ +/* + 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.entity.Player; + +//checks to see whether or not a siege should end based on the locations of the players +//for example, defender escaped or attacker gave up and left +class SiegeCheckupTask implements Runnable +{ + private SiegeData siegeData; + + public SiegeCheckupTask(SiegeData siegeData) + { + this.siegeData = siegeData; + } + + @Override + public void run() + { + DataStore dataStore = GriefPrevention.instance.dataStore; + Player defender = this.siegeData.defender; + Player attacker = this.siegeData.attacker; + + //where is the defender? + Claim defenderClaim = dataStore.getClaimAt(defender.getLocation(), false, null); + + //if this is a new claim and he has some permission there, extend the siege to include it + if(defenderClaim != null) + { + String noAccessReason = defenderClaim.allowAccess(defender); + if(defenderClaim.canSiege(defender) && noAccessReason == null) + { + this.siegeData.claims.add(defenderClaim); + defenderClaim.siegeData = this.siegeData; + } + } + + //determine who's close enough to the siege area to be considered "still here" + boolean attackerRemains = this.playerRemains(attacker); + boolean defenderRemains = this.playerRemains(defender); + + //if they're both here, just plan to come check again later + if(attackerRemains && defenderRemains) + { + this.scheduleAnotherCheck(); + } + + //otherwise attacker wins if the defender runs away + else if(attackerRemains && !defenderRemains) + { + dataStore.endSiege(this.siegeData, attacker.getName(), defender.getName()); + } + + //or defender wins if the attacker leaves + else if(!attackerRemains && defenderRemains) + { + dataStore.endSiege(this.siegeData, defender.getName(), attacker.getName()); + } + + //if they both left, but are still close together, the battle continues (check again later) + else if(attacker.getLocation().distanceSquared(defender.getLocation()) < 2500) //50-block radius for chasing + { + this.scheduleAnotherCheck(); + } + + //otherwise they both left and aren't close to each other, so call the attacker the winner (defender escaped, possibly after a chase) + else + { + dataStore.endSiege(this.siegeData, attacker.getName(), defender.getName()); + } + } + + //a player has to be within 25 blocks of the edge of a besieged claim to be considered still in the fight + private boolean playerRemains(Player player) + { + for(int i = 0; i < this.siegeData.claims.size(); i++) + { + Claim claim = this.siegeData.claims.get(i); + if(claim.isNear(player.getLocation(), 25)) + { + return true; + } + } + + return false; + } + + //schedules another checkup later + private void scheduleAnotherCheck() + { + this.siegeData.checkupTaskID = GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, this, 20L * 60); + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/SiegeData.java b/src/me/ryanhamshire/GriefPrevention/SiegeData.java new file mode 100644 index 0000000..0c6139d --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/SiegeData.java @@ -0,0 +1,40 @@ +/* + 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.entity.Player; + +//information about an ongoing siege +public class SiegeData +{ + public Player defender; + public Player attacker; + public ArrayList claims; + public int checkupTaskID; + + public SiegeData(Player attacker, Player defender, Claim claim) + { + this.defender = defender; + this.attacker = attacker; + this.claims = new ArrayList(); + this.claims.add(claim); + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/TextMode.java b/src/me/ryanhamshire/GriefPrevention/TextMode.java new file mode 100644 index 0000000..e684f21 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/TextMode.java @@ -0,0 +1,31 @@ +/* + 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.ChatColor; + +//just a few constants for chat color codes +class TextMode +{ + final static ChatColor Info = ChatColor.BLUE; + final static ChatColor Instr = ChatColor.YELLOW; + final static ChatColor Warn = ChatColor.GOLD; + final static ChatColor Err = ChatColor.RED; + final static ChatColor Success = ChatColor.GREEN; +} diff --git a/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java b/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java new file mode 100644 index 0000000..9fe23ef --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/TreeCleanupTask.java @@ -0,0 +1,100 @@ +/* + 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.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; + +//FEATURE: treetops left unnaturally hanging will be automatically cleaned up + +//this main thread task revisits the location of a partially chopped tree from several minutes ago +//if any part of the tree is still there and nothing else has been built in its place, remove the remaining parts +class TreeCleanupTask implements Runnable +{ + private Block originalChoppedBlock; //first block chopped in the tree + private Block originalRootBlock; //where the root of the tree used to be + private ArrayList originalTreeBlocks; //a list of other log blocks determined to be part of this tree + + public TreeCleanupTask(Block originalChoppedBlock, Block originalRootBlock, ArrayList originalTreeBlocks) + { + this.originalChoppedBlock = originalChoppedBlock; + this.originalRootBlock = originalRootBlock; + this.originalTreeBlocks = originalTreeBlocks; + } + + @Override + public void run() + { + //if this chunk is no longer loaded, load it and come back in a few seconds + Chunk chunk = this.originalChoppedBlock.getWorld().getChunkAt(this.originalChoppedBlock); + if(!chunk.isLoaded()) + { + chunk.load(); + GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, this, 100L); + return; + } + + //if the block originally chopped has been replaced with anything but air, something has been built (or has grown here) + //in that case, don't do any cleanup + if(this.originalChoppedBlock.getWorld().getBlockAt(this.originalChoppedBlock.getLocation()).getType() != Material.AIR) return; + + //scan the original tree block locations to see if any of them have been replaced + for(int i = 0; i < this.originalTreeBlocks.size(); i++) + { + Location location = this.originalTreeBlocks.get(i).getLocation(); + Block currentBlock = location.getBlock(); + + //if the block has been replaced, stop here, we won't do any cleanup + if(currentBlock.getType() != Material.LOG && currentBlock.getType() != Material.AIR) + { + return; + } + } + + //otherwise scan again, this time removing any remaining log blocks + boolean logsRemaining = false; + for(int i = 0; i < this.originalTreeBlocks.size(); i++) + { + Location location = this.originalTreeBlocks.get(i).getLocation(); + Block currentBlock = location.getBlock(); + if(currentBlock.getType() == Material.LOG) + { + logsRemaining = true; + currentBlock.setType(Material.AIR); + } + } + + //if any were actually removed and we're set to automatically replant griefed trees, place a sapling where the root block was previously + if(logsRemaining && GriefPrevention.instance.config_trees_regrowGriefedTrees) + { + Block currentBlock = this.originalRootBlock.getLocation().getBlock(); + //make sure there's grass or dirt underneath + if(currentBlock.getType() == Material.AIR && (currentBlock.getRelative(BlockFace.DOWN).getType() == Material.DIRT || currentBlock.getRelative(BlockFace.DOWN).getType() == Material.GRASS)) + { + currentBlock.setType(Material.SAPLING); + currentBlock.setData(this.originalRootBlock.getData()); //makes the sapling type match the original tree type + } + } + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/Visualization.java b/src/me/ryanhamshire/GriefPrevention/Visualization.java new file mode 100644 index 0000000..277e2c7 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/Visualization.java @@ -0,0 +1,214 @@ +/* + 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) + { + //visualize only top level claims + if(claim.parent != null) + { + return FromClaim(claim.parent, height, visualizationType); + } + + 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); + } + + //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); + + 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 + private void addClaimElements(Claim claim, int height, VisualizationType visualizationType) + { + 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.LAVA; + 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)); + + //top line + for(int x = smallx + 10; x < bigx - 10; x += 10) + { + 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) + { + 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) + { + 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) + { + 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 ); + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/VisualizationApplicationTask.java b/src/me/ryanhamshire/GriefPrevention/VisualizationApplicationTask.java new file mode 100644 index 0000000..b3f2451 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/VisualizationApplicationTask.java @@ -0,0 +1,52 @@ +/* + 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.entity.Player; + +//applies a visualization for a player by sending him block change packets +class VisualizationApplicationTask implements Runnable +{ + private Visualization visualization; + private Player player; + private PlayerData playerData; + + public VisualizationApplicationTask(Player player, PlayerData playerData, Visualization visualization) + { + this.visualization = visualization; + this.playerData = playerData; + this.player = player; + } + + @Override + public void run() + { + //for each element (=block) of the visualization + for(int i = 0; i < visualization.elements.size(); i++) + { + VisualizationElement element = visualization.elements.get(i); + + //send the player a fake block change event + player.sendBlockChange(element.location, element.visualizedMaterial, element.visualizedData); + } + + //remember the visualization applied to this player for later (so it can be inexpensively reverted) + playerData.currentVisualization = visualization; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/VisualizationElement.java b/src/me/ryanhamshire/GriefPrevention/VisualizationElement.java new file mode 100644 index 0000000..76f6b6f --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/VisualizationElement.java @@ -0,0 +1,36 @@ +/* + 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.Location; +import org.bukkit.Material; + +//represents a "fake" block sent to a player as part of a visualization +public class VisualizationElement +{ + public Location location; + public Material visualizedMaterial; + public byte visualizedData; + + public VisualizationElement(Location location, Material visualizedMaterial, byte visualizedData) + { + this.location = location; + this.visualizedMaterial= visualizedMaterial; + this.visualizedData = visualizedData; + } +} diff --git a/src/me/ryanhamshire/GriefPrevention/VisualizationType.java b/src/me/ryanhamshire/GriefPrevention/VisualizationType.java new file mode 100644 index 0000000..c68c326 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/VisualizationType.java @@ -0,0 +1,28 @@ +/* + 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; + +//just an enumeration of the visualization types, which determine what materials will be for the fake blocks +public enum VisualizationType +{ + Claim, + Subdivision, + ErrorClaim, + RestoreNature +}