This commit is contained in:
Ryan Hamshire 2012-05-30 19:16:46 -07:00
parent 540746ad4b
commit afe868de2f
13 changed files with 773 additions and 165 deletions

View File

@ -1,7 +1,7 @@
name: GriefPrevention name: GriefPrevention
main: me.ryanhamshire.GriefPrevention.GriefPrevention main: me.ryanhamshire.GriefPrevention.GriefPrevention
softdepend: [Vault, Multiverse-Core] softdepend: [Vault, Multiverse-Core]
version: 3.8 version: 4.2
commands: commands:
abandonclaim: abandonclaim:
description: Deletes a claim. description: Deletes a claim.
@ -60,6 +60,16 @@ commands:
usage: /RestoreNature usage: /RestoreNature
permission: griefprevention.restorenature permission: griefprevention.restorenature
aliases: rn aliases: rn
restorenatureaggressive:
description: Switches the shovel tool to aggressive restoration mode.
usage: /RestoreNatureAggressive
permission: griefprevention.restorenatureaggressive
aliases: rna
restorenaturefill:
description: Switches the shovel tool to fill mode.
usage: /RestoreNatureFill <radius>
permission: griefprevention.restorenatureaggressive
aliases: rnf
basicclaims: basicclaims:
description: Switches the shovel tool back to basic claims mode. description: Switches the shovel tool back to basic claims mode.
usage: /BasicClaims usage: /BasicClaims
@ -102,6 +112,7 @@ permissions:
description: Grants all administrative functionality. description: Grants all administrative functionality.
children: children:
griefprevention.restorenature: true griefprevention.restorenature: true
griefprevention.restorenatureaggressive: true
griefprevention.ignoreclaims: true griefprevention.ignoreclaims: true
griefprevention.adminclaims: true griefprevention.adminclaims: true
griefprevention.adjustclaimblocks: true griefprevention.adjustclaimblocks: true
@ -133,3 +144,6 @@ permissions:
griefprevention.eavesdrop: griefprevention.eavesdrop:
description: Allows a player to see whispered chat messages (/tell). description: Allows a player to see whispered chat messages (/tell).
default: op default: op
griefprevention.restorenatureaggressive:
description: Grants access to /RestoreNatureAggressive and /RestoreNatureFill.
default: op

View File

@ -25,6 +25,7 @@ import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.block.Chest; import org.bukkit.block.Chest;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -41,6 +42,7 @@ import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent; import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.BlockSpreadEvent; import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.world.StructureGrowEvent; import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -139,7 +141,7 @@ public class BlockEventHandler implements Listener
} }
//when a player breaks a block... //when a player breaks a block...
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockBreak(BlockBreakEvent breakEvent) public void onBlockBreak(BlockBreakEvent breakEvent)
{ {
Player player = breakEvent.getPlayer(); Player player = breakEvent.getPlayer();
@ -177,8 +179,36 @@ public class BlockEventHandler implements Listener
} }
} }
//when a player places a sign...
@EventHandler(ignoreCancelled = true)
public void onSignChanged(SignChangeEvent event)
{
Player player = event.getPlayer();
if(player == null) return;
StringBuilder lines = new StringBuilder();
boolean notEmpty = false;
for(int i = 0; i < event.getLines().length; i++)
{
if(event.getLine(i).length() != 0) notEmpty = true;
lines.append(event.getLine(i) + ";");
}
String signMessage = lines.toString();
//if not empty and wasn't the same as the last sign, log it and remember it for later
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
if(notEmpty && playerData.lastMessage != null && !playerData.lastMessage.equals(signMessage))
{
GriefPrevention.AddLogEntry("[Sign Placement] <" + player.getName() + "> " + lines.toString());
GriefPrevention.AddLogEntry("Location: " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation()));
playerData.lastMessage = signMessage;
}
}
//when a player places a block... //when a player places a block...
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockPlace(BlockPlaceEvent placeEvent) public void onBlockPlace(BlockPlaceEvent placeEvent)
{ {
Player player = placeEvent.getPlayer(); Player player = placeEvent.getPlayer();
@ -203,6 +233,20 @@ public class BlockEventHandler implements Listener
} }
} }
//FEATURE: limit tree planting to grass, and dirt with more earth beneath it
if(block.getType() == Material.SAPLING)
{
Block earthBlock = placeEvent.getBlockAgainst();
if(earthBlock.getType() != Material.GRASS)
{
if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR ||
earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR)
{
placeEvent.setCancelled(true);
}
}
}
//make sure the player is allowed to build at the location //make sure the player is allowed to build at the location
String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation()); String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation());
if(noBuildReason != null) if(noBuildReason != null)
@ -292,7 +336,7 @@ public class BlockEventHandler implements Listener
} }
//blocks "pushing" other players' blocks around (pistons) //blocks "pushing" other players' blocks around (pistons)
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockPistonExtend (BlockPistonExtendEvent event) public void onBlockPistonExtend (BlockPistonExtendEvent event)
{ {
//who owns the piston, if anyone? //who owns the piston, if anyone?
@ -316,7 +360,7 @@ public class BlockEventHandler implements Listener
} }
//blocks theft by pulling blocks out of a claim (again pistons) //blocks theft by pulling blocks out of a claim (again pistons)
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockPistonRetract (BlockPistonRetractEvent event) public void onBlockPistonRetract (BlockPistonRetractEvent event)
{ {
//we only care about sticky pistons //we only care about sticky pistons
@ -342,21 +386,21 @@ public class BlockEventHandler implements Listener
} }
//blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise //blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
public void onBlockIgnite (BlockIgniteEvent igniteEvent) public void onBlockIgnite (BlockIgniteEvent igniteEvent)
{ {
if(igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && !GriefPrevention.instance.config_fireSpreads) igniteEvent.setCancelled(true); if(igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && !GriefPrevention.instance.config_fireSpreads) igniteEvent.setCancelled(true);
} }
//fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example) //fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example)
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
public void onBlockSpread (BlockSpreadEvent spreadEvent) public void onBlockSpread (BlockSpreadEvent spreadEvent)
{ {
if(spreadEvent.getSource().getType() == Material.FIRE && !GriefPrevention.instance.config_fireSpreads) spreadEvent.setCancelled(true); if(spreadEvent.getSource().getType() == Material.FIRE && !GriefPrevention.instance.config_fireSpreads) spreadEvent.setCancelled(true);
} }
//blocks are not destroyed by fire, unless configured to do so //blocks are not destroyed by fire, unless configured to do so
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
public void onBlockBurn (BlockBurnEvent burnEvent) public void onBlockBurn (BlockBurnEvent burnEvent)
{ {
if(!GriefPrevention.instance.config_fireDestroys) if(!GriefPrevention.instance.config_fireDestroys)
@ -371,28 +415,28 @@ public class BlockEventHandler implements Listener
} }
} }
//ensures fluids don't flow into claims, unless out of another claim where the owner is trusted to build in the receiving claim //ensures fluids don't flow out of claims, unless into another claim where the owner is trusted to build
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockFromTo (BlockFromToEvent spreadEvent) public void onBlockFromTo (BlockFromToEvent spreadEvent)
{ {
//from where?
Block fromBlock = spreadEvent.getBlock();
Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null);
//where to? //where to?
Block toBlock = spreadEvent.getToBlock(); Block toBlock = spreadEvent.getToBlock();
Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, null); Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim);
//if in a creative world, block any spread into the wilderness //block any spread into the wilderness
if(GriefPrevention.instance.creativeRulesApply(toBlock.getLocation()) && toClaim == null) if(fromClaim != null && toClaim == null)
{ {
spreadEvent.setCancelled(true); spreadEvent.setCancelled(true);
return; return;
} }
//if spreading into a claim //if spreading into a claim
if(toClaim != null) else 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? //who owns the spreading block, if anyone?
OfflinePlayer fromOwner = null; OfflinePlayer fromOwner = null;
if(fromClaim != null) if(fromClaim != null)

View File

@ -26,6 +26,8 @@ import java.util.Iterator;
import java.util.Map; import java.util.Map;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.World.Environment;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -98,6 +100,44 @@ public class Claim
return true; return true;
} }
//removes any fluids above sea level in a claim
//exclusionClaim is another claim indicating an sub-area to be excluded from this operation
//it may be null
public void removeSurfaceFluids(Claim exclusionClaim)
{
//don't do this automatically for administrative claims
if(this.isAdminClaim()) return;
Location lesser = this.getLesserBoundaryCorner();
Location greater = this.getGreaterBoundaryCorner();
if(lesser.getWorld().getEnvironment() == Environment.NETHER) return; //don't clean up lava in the nether
int seaLevel = 0; //clean up all fluids in the end
//respect sea level in normal worlds
if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = lesser.getWorld().getSeaLevel();
for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++)
{
for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++)
{
for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++)
{
//dodge the exclusion claim
Block block = lesser.getWorld().getBlockAt(x, y, z);
if(exclusionClaim != null && exclusionClaim.contains(block.getLocation(), true, false)) continue;
if(block.getType() == Material.STATIONARY_WATER || block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.LAVA || block.getType() == Material.WATER)
{
block.setType(Material.AIR);
}
}
}
}
}
//main constructor. note that only creating a claim instance does nothing - a claim must be added to the data store to be effective //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) Claim(Location lesserBoundaryCorner, Location greaterBoundaryCorner, String ownerName, String [] builderNames, String [] containerNames, String [] accessorNames, String [] managerNames)
{ {

View File

@ -217,11 +217,24 @@ public class DataStore
//if that's a chest claim, delete it //if that's a chest claim, delete it
if(claim.getArea() <= areaOfDefaultClaim) if(claim.getArea() <= areaOfDefaultClaim)
{ {
claim.removeSurfaceFluids(null);
this.deleteClaim(claim); this.deleteClaim(claim);
GriefPrevention.AddLogEntry(" " + playerName + "'s new player claim expired."); GriefPrevention.AddLogEntry(" " + playerName + "'s new player claim expired.");
} }
} }
if(GriefPrevention.instance.config_claims_expirationDays > 0)
{
Calendar earliestPermissibleLastLogin = Calendar.getInstance();
earliestPermissibleLastLogin.add(Calendar.DATE, -GriefPrevention.instance.config_claims_expirationDays);
if(earliestPermissibleLastLogin.getTime().after(playerData.lastLogin))
{
this.deleteClaimsForPlayer(playerName, true);
GriefPrevention.AddLogEntry(" All of " + playerName + "'s claims have expired.");
}
}
//toss that player data out of the cache, it's not needed in memory right now //toss that player data out of the cache, it's not needed in memory right now
this.clearCachedPlayerData(playerName); this.clearCachedPlayerData(playerName);
} }
@ -1074,6 +1087,7 @@ public class DataStore
//delete them one by one //delete them one by one
for(int i = 0; i < claimsToDelete.size(); i++) for(int i = 0; i < claimsToDelete.size(); i++)
{ {
claimsToDelete.get(i).removeSurfaceFluids(null);
this.deleteClaim(claimsToDelete.get(i)); this.deleteClaim(claimsToDelete.get(i));
} }
} }

View File

@ -21,11 +21,12 @@ package me.ryanhamshire.GriefPrevention;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Animals; import org.bukkit.entity.Animals;
import org.bukkit.entity.Arrow; import org.bukkit.entity.Arrow;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Enderman; import org.bukkit.entity.Enderman;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity; import org.bukkit.entity.LivingEntity;
@ -34,6 +35,7 @@ import org.bukkit.entity.ThrownPotion;
import org.bukkit.entity.Vehicle; import org.bukkit.entity.Vehicle;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
@ -59,17 +61,17 @@ class EntityEventHandler implements Listener
} }
//when an entity explodes... //when an entity explodes...
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onEntityExplode(EntityExplodeEvent explodeEvent) public void onEntityExplode(EntityExplodeEvent explodeEvent)
{ {
List<Block> blocks = explodeEvent.blockList(); List<Block> blocks = explodeEvent.blockList();
Entity entity = explodeEvent.getEntity(); Location location = explodeEvent.getLocation();
//FEATURE: creepers don't destroy blocks when they explode near or above sea level //FEATURE: explosions don't destroy blocks when they explode near or above sea level in standard worlds
if(GriefPrevention.instance.config_creepersDontDestroySurface && entity instanceof Creeper) if(GriefPrevention.instance.config_blockSurfaceExplosions && location.getWorld().getEnvironment() == Environment.NORMAL)
{ {
if(entity.getLocation().getBlockY() > entity.getLocation().getWorld().getSeaLevel() - 7) if(location.getBlockY() >location.getWorld().getSeaLevel() - 7)
{ {
blocks.clear(); //explosion still happens, can damage creatures/players, but no blocks will be destroyed blocks.clear(); //explosion still happens, can damage creatures/players, but no blocks will be destroyed
return; return;
@ -77,7 +79,7 @@ class EntityEventHandler implements Listener
} }
//special rule for creative worlds: explosions don't destroy anything //special rule for creative worlds: explosions don't destroy anything
if(GriefPrevention.instance.creativeRulesApply(entity.getLocation())) if(GriefPrevention.instance.creativeRulesApply(explodeEvent.getLocation()))
{ {
blocks.clear(); blocks.clear();
} }
@ -106,7 +108,7 @@ class EntityEventHandler implements Listener
} }
//when an item spawns... //when an item spawns...
@EventHandler @EventHandler(priority = EventPriority.LOWEST)
public void onItemSpawn(ItemSpawnEvent event) public void onItemSpawn(ItemSpawnEvent event)
{ {
//if in a creative world, cancel the event (don't drop items on the ground) //if in a creative world, cancel the event (don't drop items on the ground)
@ -117,7 +119,7 @@ class EntityEventHandler implements Listener
} }
//when a creature spawns... //when a creature spawns...
@EventHandler @EventHandler(priority = EventPriority.LOWEST)
public void onEntitySpawn(CreatureSpawnEvent event) public void onEntitySpawn(CreatureSpawnEvent event)
{ {
LivingEntity entity = event.getEntity(); LivingEntity entity = event.getEntity();
@ -175,7 +177,7 @@ class EntityEventHandler implements Listener
} }
//when an entity picks up an item //when an entity picks up an item
@EventHandler @EventHandler(priority = EventPriority.LOWEST)
public void onEntityPickup(EntityChangeBlockEvent event) public void onEntityPickup(EntityChangeBlockEvent event)
{ {
//FEATURE: endermen don't steal claimed blocks //FEATURE: endermen don't steal claimed blocks
@ -193,7 +195,7 @@ class EntityEventHandler implements Listener
} }
//when a painting is broken //when a painting is broken
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPaintingBreak(PaintingBreakEvent event) public void onPaintingBreak(PaintingBreakEvent event)
{ {
//FEATURE: claimed paintings are protected from breakage //FEATURE: claimed paintings are protected from breakage
@ -217,10 +219,6 @@ class EntityEventHandler implements Listener
return; return;
} }
//make sure the player has build permission here
Claim claim = this.dataStore.getClaimAt(event.getPainting().getLocation(), false, null);
if(claim == null) return;
//if the player doesn't have build permission, don't allow the breakage //if the player doesn't have build permission, don't allow the breakage
Player playerRemover = (Player)entityEvent.getRemover(); Player playerRemover = (Player)entityEvent.getRemover();
String noBuildReason = GriefPrevention.instance.allowBuild(playerRemover, event.getPainting().getLocation()); String noBuildReason = GriefPrevention.instance.allowBuild(playerRemover, event.getPainting().getLocation());
@ -232,7 +230,7 @@ class EntityEventHandler implements Listener
} }
//when a painting is placed... //when a painting is placed...
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPaintingPlace(PaintingPlaceEvent event) public void onPaintingPlace(PaintingPlaceEvent event)
{ {
//FEATURE: similar to above, placing a painting requires build permission in the claim //FEATURE: similar to above, placing a painting requires build permission in the claim
@ -247,7 +245,7 @@ class EntityEventHandler implements Listener
} }
//when an entity is damaged //when an entity is damaged
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onEntityDamage (EntityDamageEvent event) public void onEntityDamage (EntityDamageEvent event)
{ {
//only actually interested in entities damaging entities (ignoring environmental damage) //only actually interested in entities damaging entities (ignoring environmental damage)

View File

@ -69,6 +69,7 @@ public class GriefPrevention extends JavaPlugin
public int config_claims_blocksAccruedPerHour; //how many additional blocks players get each hour of play (can be zero) 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_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_maxDepth; //limit on how deep claims can go
public int config_claims_expirationDays; //how many days of inactivity before a player loses his claims
public int config_claims_automaticClaimsForNewPlayersRadius; //how big automatic new player claims (when they place a chest) should be. 0 to disable public 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 boolean config_claims_creationRequiresPermission; //whether creating claims with the shovel requires a permission
@ -99,7 +100,7 @@ public class GriefPrevention extends JavaPlugin
public double config_economy_claimBlocksPurchaseCost; //cost to purchase a claim block. set to zero to disable purchase. 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 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 public boolean config_blockSurfaceExplosions; //whether creeper/TNT explosions near or above the surface destroy blocks
public boolean config_fireSpreads; //whether fire spreads outside of claims public boolean config_fireSpreads; //whether fire spreads outside of claims
public boolean config_fireDestroys; //whether fire destroys blocks outside of claims public boolean config_fireDestroys; //whether fire destroys blocks outside of claims
@ -107,12 +108,17 @@ public class GriefPrevention extends JavaPlugin
public boolean config_addItemsToClaimedChests; //whether players may add items to claimed chests by left-clicking them public boolean config_addItemsToClaimedChests; //whether players may add items to claimed chests by left-clicking them
public boolean config_eavesdrop; //whether whispered messages will be visible to administrators public boolean config_eavesdrop; //whether whispered messages will be visible to administrators
public boolean config_smartBan; //whether to ban accounts which very likely owned by a banned player
//reference to the economy plugin, if economy integration is enabled //reference to the economy plugin, if economy integration is enabled
public static Economy economy = null; public static Economy economy = null;
//how far away to search from a tree trunk for its branch blocks //how far away to search from a tree trunk for its branch blocks
public static final int TREE_RADIUS = 5; public static final int TREE_RADIUS = 5;
//how long to wait before deciding a player is staying online or staying offline, for notication messages
public static final int NOTIFICATION_SECONDS = 20;
//adds a server log entry //adds a server log entry
public static void AddLogEntry(String entry) public static void AddLogEntry(String entry)
{ {
@ -207,11 +213,12 @@ public class GriefPrevention extends JavaPlugin
this.config_claims_creationRequiresPermission = config.getBoolean("GriefPrevention.Claims.CreationRequiresPermission", false); this.config_claims_creationRequiresPermission = config.getBoolean("GriefPrevention.Claims.CreationRequiresPermission", false);
this.config_claims_minSize = config.getInt("GriefPrevention.Claims.MinimumSize", 10); this.config_claims_minSize = config.getInt("GriefPrevention.Claims.MinimumSize", 10);
this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", 0); this.config_claims_maxDepth = config.getInt("GriefPrevention.Claims.MaximumDepth", 0);
this.config_claims_expirationDays = config.getInt("GriefPrevention.Claims.IdleLimitDays", 0);
this.config_claims_trappedCooldownHours = config.getInt("GriefPrevention.Claims.TrappedCommandCooldownHours", 8); this.config_claims_trappedCooldownHours = config.getInt("GriefPrevention.Claims.TrappedCommandCooldownHours", 8);
this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true); this.config_spam_enabled = config.getBoolean("GriefPrevention.Spam.Enabled", true);
this.config_spam_loginCooldownMinutes = config.getInt("GriefPrevention.Spam.LoginCooldownMinutes", 5); this.config_spam_loginCooldownMinutes = config.getInt("GriefPrevention.Spam.LoginCooldownMinutes", 2);
this.config_spam_warningMessage = config.getString("GriefPrevention.Spam.WarningMessage", "Please reduce your message speed. Spammers will be banned."); this.config_spam_warningMessage = config.getString("GriefPrevention.Spam.WarningMessage", "Please reduce your noise level. Spammers will be banned.");
this.config_spam_allowedIpAddresses = config.getString("GriefPrevention.Spam.AllowedIpAddresses", "1.2.3.4; 5.6.7.8"); this.config_spam_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_banOffenders = config.getBoolean("GriefPrevention.Spam.BanOffenders", true);
this.config_spam_banMessage = config.getString("GriefPrevention.Spam.BanMessage", "Banned for spam."); this.config_spam_banMessage = config.getString("GriefPrevention.Spam.BanMessage", "Banned for spam.");
@ -228,7 +235,7 @@ public class GriefPrevention extends JavaPlugin
this.config_economy_claimBlocksPurchaseCost = config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0); this.config_economy_claimBlocksPurchaseCost = config.getDouble("GriefPrevention.Economy.ClaimBlocksPurchaseCost", 0);
this.config_economy_claimBlocksSellValue = config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0); this.config_economy_claimBlocksSellValue = config.getDouble("GriefPrevention.Economy.ClaimBlocksSellValue", 0);
this.config_creepersDontDestroySurface = config.getBoolean("GriefPrevention.CreepersDontDestroySurface", true); this.config_blockSurfaceExplosions = config.getBoolean("GriefPrevention.BlockSurfaceExplosions", true);
this.config_fireSpreads = config.getBoolean("GriefPrevention.FireSpreads", false); this.config_fireSpreads = config.getBoolean("GriefPrevention.FireSpreads", false);
this.config_fireDestroys = config.getBoolean("GriefPrevention.FireDestroys", false); this.config_fireDestroys = config.getBoolean("GriefPrevention.FireDestroys", false);
@ -236,6 +243,8 @@ public class GriefPrevention extends JavaPlugin
this.config_addItemsToClaimedChests = config.getBoolean("GriefPrevention.AddItemsToClaimedChests", true); this.config_addItemsToClaimedChests = config.getBoolean("GriefPrevention.AddItemsToClaimedChests", true);
this.config_eavesdrop = config.getBoolean("GriefPrevention.EavesdropEnabled", false); this.config_eavesdrop = config.getBoolean("GriefPrevention.EavesdropEnabled", false);
this.config_smartBan = config.getBoolean("GriefPrevention.SmartBan", true);
//default for siege worlds list //default for siege worlds list
ArrayList<String> defaultSiegeWorldNames = new ArrayList<String>(); ArrayList<String> defaultSiegeWorldNames = new ArrayList<String>();
@ -320,6 +329,7 @@ public class GriefPrevention extends JavaPlugin
config.set("GriefPrevention.Claims.CreationRequiresPermission", this.config_claims_creationRequiresPermission); config.set("GriefPrevention.Claims.CreationRequiresPermission", this.config_claims_creationRequiresPermission);
config.set("GriefPrevention.Claims.MinimumSize", this.config_claims_minSize); config.set("GriefPrevention.Claims.MinimumSize", this.config_claims_minSize);
config.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth); config.set("GriefPrevention.Claims.MaximumDepth", this.config_claims_maxDepth);
config.set("GriefPrevention.Claims.IdleLimitDays", this.config_claims_expirationDays);
config.set("GriefPrevention.Claims.TrappedCommandCooldownHours", this.config_claims_trappedCooldownHours); config.set("GriefPrevention.Claims.TrappedCommandCooldownHours", this.config_claims_trappedCooldownHours);
config.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled); config.set("GriefPrevention.Spam.Enabled", this.config_spam_enabled);
@ -341,7 +351,7 @@ public class GriefPrevention extends JavaPlugin
config.set("GriefPrevention.Economy.ClaimBlocksPurchaseCost", this.config_economy_claimBlocksPurchaseCost); config.set("GriefPrevention.Economy.ClaimBlocksPurchaseCost", this.config_economy_claimBlocksPurchaseCost);
config.set("GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue); config.set("GriefPrevention.Economy.ClaimBlocksSellValue", this.config_economy_claimBlocksSellValue);
config.set("GriefPrevention.CreepersDontDestroySurface", this.config_creepersDontDestroySurface); config.set("GriefPrevention.BlockSurfaceExplosions", this.config_blockSurfaceExplosions);
config.set("GriefPrevention.FireSpreads", this.config_fireSpreads); config.set("GriefPrevention.FireSpreads", this.config_fireSpreads);
config.set("GriefPrevention.FireDestroys", this.config_fireDestroys); config.set("GriefPrevention.FireDestroys", this.config_fireDestroys);
@ -349,6 +359,8 @@ public class GriefPrevention extends JavaPlugin
config.set("GriefPrevention.AddItemsToClaimedChests", this.config_addItemsToClaimedChests); config.set("GriefPrevention.AddItemsToClaimedChests", this.config_addItemsToClaimedChests);
config.set("GriefPrevention.EavesdropEnabled", this.config_eavesdrop); config.set("GriefPrevention.EavesdropEnabled", this.config_eavesdrop);
config.set("GriefPrevention.SmartBan", this.config_smartBan);
config.set("GriefPrevention.Siege.Worlds", siegeEnabledWorldNames); config.set("GriefPrevention.Siege.Worlds", siegeEnabledWorldNames);
config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList); config.set("GriefPrevention.Siege.BreakableBlocks", breakableBlocksList);
@ -518,6 +530,40 @@ public class GriefPrevention extends JavaPlugin
return true; return true;
} }
//restore nature aggressive mode
else if(cmd.getName().equalsIgnoreCase("restorenatureaggressive") && player != null)
{
//change shovel mode
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
playerData.shovelMode = ShovelMode.RestoreNatureAggressive;
GriefPrevention.sendMessage(player, TextMode.Warn, "Aggressive mode activated. Do NOT use this underneath anything you want to keep! Right click to aggressively restore nature, and use /BasicClaims to stop.");
return true;
}
//restore nature fill mode
else if(cmd.getName().equalsIgnoreCase("restorenaturefill") && player != null)
{
//change shovel mode
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
playerData.shovelMode = ShovelMode.RestoreNatureFill;
//set radius based on arguments
playerData.fillRadius = 2;
if(args.length > 0)
{
try
{
playerData.fillRadius = Integer.parseInt(args[0]);
}
catch(Exception exception){ }
}
if(playerData.fillRadius < 0) playerData.fillRadius = 2;
GriefPrevention.sendMessage(player, TextMode.Success, "Fill mode activated with radius " + playerData.fillRadius + ". Right-click an area to fill.");
return true;
}
//trust <player> //trust <player>
else if(cmd.getName().equalsIgnoreCase("trust") && player != null) else if(cmd.getName().equalsIgnoreCase("trust") && player != null)
{ {
@ -833,6 +879,17 @@ public class GriefPrevention extends JavaPlugin
else else
{ {
//determine max purchasable blocks
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
int maxPurchasable = GriefPrevention.instance.config_claims_maxAccruedBlocks - playerData.accruedClaimBlocks;
//if the player is at his max, tell him so
if(maxPurchasable <= 0)
{
GriefPrevention.sendMessage(player, TextMode.Err, "You've reached your claim block limit. You can't purchase more.");
return true;
}
//try to parse number of blocks //try to parse number of blocks
int blockCount; int blockCount;
try try
@ -844,6 +901,12 @@ public class GriefPrevention extends JavaPlugin
return false; //causes usage to be displayed return false; //causes usage to be displayed
} }
//correct block count to max allowed
if(blockCount > maxPurchasable)
{
blockCount = maxPurchasable;
}
//if the player can't afford his purchase, send error message //if the player can't afford his purchase, send error message
double balance = economy.getBalance(player.getName()); double balance = economy.getBalance(player.getName());
double totalCost = blockCount * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost; double totalCost = blockCount * GriefPrevention.instance.config_economy_claimBlocksPurchaseCost;
@ -859,8 +922,7 @@ public class GriefPrevention extends JavaPlugin
economy.withdrawPlayer(player.getName(), totalCost); economy.withdrawPlayer(player.getName(), totalCost);
//add blocks //add blocks
PlayerData playerData = this.dataStore.getPlayerData(player.getName()); playerData.accruedClaimBlocks += blockCount;
playerData.bonusClaimBlocks += blockCount;
this.dataStore.savePlayerData(player.getName(), playerData); this.dataStore.savePlayerData(player.getName(), playerData);
//inform player //inform player
@ -920,7 +982,7 @@ public class GriefPrevention extends JavaPlugin
economy.depositPlayer(player.getName(), totalValue); economy.depositPlayer(player.getName(), totalValue);
//subtract blocks //subtract blocks
playerData.bonusClaimBlocks -= blockCount; playerData.accruedClaimBlocks -= blockCount;
this.dataStore.savePlayerData(player.getName(), playerData); this.dataStore.savePlayerData(player.getName(), playerData);
//inform player //inform player
@ -978,12 +1040,24 @@ public class GriefPrevention extends JavaPlugin
//deleting an admin claim additionally requires the adminclaims permission //deleting an admin claim additionally requires the adminclaims permission
if(!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims")) if(!claim.isAdminClaim() || player.hasPermission("griefprevention.adminclaims"))
{ {
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
if(claim.children.size() > 0 && !playerData.warnedAboutMajorDeletion)
{
GriefPrevention.sendMessage(player, TextMode.Warn, "This claim includes subdivisions. If you're sure you want to delete it, use /DeleteClaim again.");
playerData.warnedAboutMajorDeletion = true;
}
else
{
claim.removeSurfaceFluids(null);
this.dataStore.deleteClaim(claim); this.dataStore.deleteClaim(claim);
GriefPrevention.sendMessage(player, TextMode.Success, "Claim deleted."); GriefPrevention.sendMessage(player, TextMode.Success, "Claim deleted.");
GriefPrevention.AddLogEntry(player.getName() + " deleted " + claim.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner())); GriefPrevention.AddLogEntry(player.getName() + " deleted " + claim.getOwnerName() + "'s claim at " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()));
//revert any current visualization //revert any current visualization
Visualization.Revert(player); Visualization.Revert(player);
playerData.warnedAboutMajorDeletion = false;
}
} }
else else
{ {
@ -1266,6 +1340,7 @@ public class GriefPrevention extends JavaPlugin
else else
{ {
//delete it //delete it
claim.removeSurfaceFluids(null);
this.dataStore.deleteClaim(claim); this.dataStore.deleteClaim(claim);
//tell the player how many claim blocks he has left //tell the player how many claim blocks he has left
@ -1619,8 +1694,8 @@ public class GriefPrevention extends JavaPlugin
//schedule a cleanup task for later, in case the player leaves part of this tree hanging in the air //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); TreeCleanupTask cleanupTask = new TreeCleanupTask(block, rootBlock, treeBlocks);
//20L ~ 1 second, so 5 mins = 300 seconds ~ 6000L //20L ~ 1 second, so 2 mins = 120 seconds ~ 2400L
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, cleanupTask, 6000L); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, cleanupTask, 2400L);
} }
} }

View File

@ -0,0 +1,17 @@
package me.ryanhamshire.GriefPrevention;
import java.net.InetAddress;
public class IpBanInfo
{
InetAddress address;
long expirationTimestamp;
String bannedAccountName;
IpBanInfo(InetAddress address, long expirationTimestamp, String bannedAccountName)
{
this.address = address;
this.expirationTimestamp = expirationTimestamp;
this.bannedAccountName = bannedAccountName;
}
}

View File

@ -0,0 +1,66 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.entity.Player;
//this main thread task takes the output from the RestoreNatureProcessingTask\
//and updates the world accordingly
class JoinLeaveAnnouncementTask implements Runnable
{
//player joining or leaving the server
private Player player;
//message to be displayed
private String message;
//whether joining or leaving
private boolean joining;
public JoinLeaveAnnouncementTask(Player player, String message, boolean joining)
{
this.player = player;
this.message = message;
this.joining = joining;
}
@Override
public void run()
{
//verify the player still has the same online/offline status
if((this.joining && this.player.isOnline() || (!this.joining && !this.player.isOnline())))
{
Player players [] = GriefPrevention.instance.getServer().getOnlinePlayers();
for(int i = 0; i < players.length; i++)
{
if(!players[i].equals(this.player))
{
players[i].sendMessage(this.message);
}
}
//if left
if(!joining)
{
//drop player data from memory
GriefPrevention.instance.dataStore.clearCachedPlayerData(this.player.getName());
}
}
}
}

View File

@ -17,6 +17,7 @@
*/ */
package me.ryanhamshire.GriefPrevention; package me.ryanhamshire.GriefPrevention;
import java.net.InetAddress;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.Vector; import java.util.Vector;
@ -41,6 +42,9 @@ public class PlayerData
//what "mode" the shovel is in determines what it will do when it's used //what "mode" the shovel is in determines what it will do when it's used
public ShovelMode shovelMode = ShovelMode.Basic; public ShovelMode shovelMode = ShovelMode.Basic;
//radius for restore nature fill mode
int fillRadius = 0;
//last place the player used the shovel, useful in creating and resizing claims, //last place the player used the shovel, useful in creating and resizing claims,
//because the player must use the shovel twice in those instances //because the player must use the shovel twice in those instances
public Location lastShovelLocation = null; public Location lastShovelLocation = null;
@ -65,6 +69,10 @@ public class PlayerData
public String lastMessage = ""; //the player's last chat message, or slash command complete with parameters 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 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" public int spamCount = 0; //number of consecutive "spams"
public boolean spamWarned = false; //whether the player recently received a warning
//last logout timestamp, default to long enough to trigger a join message, see player join event
public long lastLogout = Calendar.getInstance().getTimeInMillis() - GriefPrevention.NOTIFICATION_SECONDS * 2000;
//visualization //visualization
public Visualization currentVisualization = null; public Visualization currentVisualization = null;
@ -86,6 +94,11 @@ public class PlayerData
public long lastPvpTimestamp = 0; public long lastPvpTimestamp = 0;
public String lastPvpPlayer = ""; public String lastPvpPlayer = "";
//safety confirmation for deleting multi-subdivision claims
public boolean warnedAboutMajorDeletion = false;
public InetAddress ipAddress;
PlayerData() PlayerData()
{ {
//default last login date value to a year ago to ensure a brand new player can log in //default last login date value to a year ago to ensure a brand new player can log in

View File

@ -17,6 +17,7 @@
*/ */
package me.ryanhamshire.GriefPrevention; package me.ryanhamshire.GriefPrevention;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -27,7 +28,10 @@ import org.bukkit.ChatColor;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.World.Environment;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Animals; import org.bukkit.entity.Animals;
import org.bukkit.entity.Boat; import org.bukkit.entity.Boat;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@ -49,6 +53,12 @@ class PlayerEventHandler implements Listener
{ {
private DataStore dataStore; private DataStore dataStore;
//list of temporarily banned ip's
private ArrayList<IpBanInfo> tempBannedIps = new ArrayList<IpBanInfo>();
//number of milliseconds in a day
private final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;
//typical constructor, yawn //typical constructor, yawn
PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
{ {
@ -56,7 +66,7 @@ class PlayerEventHandler implements Listener
} }
//when a player chats, monitor for spam //when a player chats, monitor for spam
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
void onPlayerChat (PlayerChatEvent event) void onPlayerChat (PlayerChatEvent event)
{ {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -74,21 +84,29 @@ class PlayerEventHandler implements Listener
if(!GriefPrevention.instance.config_spam_enabled) return; if(!GriefPrevention.instance.config_spam_enabled) return;
//if the player has permission to spam, don't bother even examining the message
if(player.hasPermission("griefprevention.spam")) return;
//remedy any CAPS SPAM without bothering to fault the player for it //remedy any CAPS SPAM without bothering to fault the player for it
if(message.length() > 4 && !player.hasPermission("griefprevention.spam") && message.toUpperCase().equals(message)) if(message.length() > 4 && message.toUpperCase().equals(message))
{ {
event.setMessage(message.toLowerCase()); event.setMessage(message.toLowerCase());
} }
//where spam is concerned, casing isn't significant
message = message.toLowerCase();
PlayerData playerData = this.dataStore.getPlayerData(player.getName()); PlayerData playerData = this.dataStore.getPlayerData(player.getName());
boolean spam = false; boolean spam = false;
boolean muted = false;
//filter IP addresses //filter IP addresses
if(!(event instanceof PlayerCommandPreprocessEvent)) if(!(event instanceof PlayerCommandPreprocessEvent))
{ {
Pattern ipAddressPattern = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+"); Pattern ipAddressPattern = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+");
Matcher matcher = ipAddressPattern.matcher(event.getMessage()); Matcher matcher = ipAddressPattern.matcher(event.getMessage());
//if it looks like an IP address //if it looks like an IP address
while(matcher.find()) while(matcher.find())
{ {
@ -103,7 +121,7 @@ class PlayerEventHandler implements Listener
spam = true; spam = true;
//block message //block message
event.setCancelled(true); muted = true;
} }
} }
} }
@ -112,35 +130,41 @@ class PlayerEventHandler implements Listener
long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime(); long millisecondsSinceLastMessage = (new Date()).getTime() - playerData.lastMessageTimestamp.getTime();
//if the message came too close to the last one //if the message came too close to the last one
if(millisecondsSinceLastMessage < 2000) if(millisecondsSinceLastMessage < 3000)
{ {
//increment the spam counter //increment the spam counter
playerData.spamCount++; playerData.spamCount++;
spam = true; spam = true;
} }
//if it's the same as the last message //if it's very similar to the last message
if(message.equals(playerData.lastMessage)) if(this.stringsAreSimilar(message, playerData.lastMessage))
{ {
playerData.spamCount++; playerData.spamCount++;
event.setCancelled(true);
spam = true; spam = true;
muted = true;
} }
//if the message was mostly non-alpha-numerics, consider it a spam (probably ansi art) //if the message was mostly non-alpha-numerics or doesn't include much whitespace, consider it a spam (probably ansi art or random text gibberish)
if(message.length() > 5) if(message.length() > 5)
{ {
int symbolsCount = 0; int symbolsCount = 0;
int whitespaceCount = 0;
for(int i = 0; i < message.length(); i++) for(int i = 0; i < message.length(); i++)
{ {
char character = message.charAt(i); char character = message.charAt(i);
if(!(Character.isLetterOrDigit(character) || Character.isWhitespace(character))) if(!(Character.isLetterOrDigit(character)))
{ {
symbolsCount++; symbolsCount++;
} }
if(Character.isWhitespace(character))
{
whitespaceCount++;
}
} }
if(symbolsCount > message.length() / 2) if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10))
{ {
spam = true; spam = true;
playerData.spamCount++; playerData.spamCount++;
@ -150,12 +174,9 @@ class PlayerEventHandler implements Listener
//if the message was determined to be a spam, consider taking action //if the message was determined to be a spam, consider taking action
if(!player.hasPermission("griefprevention.spam") && spam) if(!player.hasPermission("griefprevention.spam") && spam)
{ {
//at the fifth spam level, auto-ban (if enabled) //anything above level 4 for a player which has received a warning... kick or if enabled, ban
if(playerData.spamCount > 4) if(playerData.spamCount > 4 && playerData.spamWarned)
{ {
event.setCancelled(true);
GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message);
if(GriefPrevention.instance.config_spam_banOffenders) if(GriefPrevention.instance.config_spam_banOffenders)
{ {
//log entry //log entry
@ -174,12 +195,28 @@ class PlayerEventHandler implements Listener
} }
//cancel any messages while at or above the third spam level and issue warnings //cancel any messages while at or above the third spam level and issue warnings
else if(playerData.spamCount >= 3) //anything above level 2, mute and warn
if(playerData.spamCount >= 3)
{
muted = true;
if(!playerData.spamWarned)
{ {
GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage); GriefPrevention.sendMessage(player, TextMode.Warn, GriefPrevention.instance.config_spam_warningMessage);
event.setCancelled(true);
GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties."); GriefPrevention.AddLogEntry("Warned " + player.getName() + " about spam penalties.");
playerData.spamWarned = true;
}
}
if(muted)
{
//cancel the event and make a log entry
//cancelling the event guarantees players don't receive the message
event.setCancelled(true);
GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message); GriefPrevention.AddLogEntry("Muted spam from " + player.getName() + ": " + message);
//send a fake message so the player doesn't realize he's muted
//less information for spammers = less effective spam filter dodging
player.sendMessage("<" + player.getName() + "> " + event.getMessage());
} }
} }
@ -187,6 +224,7 @@ class PlayerEventHandler implements Listener
else else
{ {
playerData.spamCount = 0; playerData.spamCount = 0;
playerData.spamWarned = false;
} }
//in any case, record the timestamp of this message and also its content for next time //in any case, record the timestamp of this message and also its content for next time
@ -194,8 +232,50 @@ class PlayerEventHandler implements Listener
playerData.lastMessage = message; playerData.lastMessage = message;
} }
//if two strings are 75% identical, they're too close to follow each other in the chat
private boolean stringsAreSimilar(String message, String lastMessage)
{
//determine which is shorter
String shorterString, longerString;
if(lastMessage.length() < message.length())
{
shorterString = lastMessage;
longerString = message;
}
else
{
shorterString = message;
longerString = lastMessage;
}
if(shorterString.length() <= 5) return shorterString.equals(longerString);
//set similarity tolerance
int maxIdenticalCharacters = longerString.length() - longerString.length() / 4;
//trivial check on length
if(shorterString.length() < maxIdenticalCharacters) return false;
//compare forward
int identicalCount = 0;
for(int i = 0; i < shorterString.length(); i++)
{
if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++;
if(identicalCount > maxIdenticalCharacters) return true;
}
//compare backward
for(int i = 0; i < shorterString.length(); i++)
{
if(shorterString.charAt(shorterString.length() - i - 1) == longerString.charAt(longerString.length() - i - 1)) identicalCount++;
if(identicalCount > maxIdenticalCharacters) return true;
}
return false;
}
//when a player uses a slash command, monitor for spam //when a player uses a slash command, monitor for spam
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event) void onPlayerCommandPreprocess (PlayerCommandPreprocessEvent event)
{ {
if(!GriefPrevention.instance.config_spam_enabled) return; if(!GriefPrevention.instance.config_spam_enabled) return;
@ -231,13 +311,14 @@ class PlayerEventHandler implements Listener
} }
//when a player attempts to join the server... //when a player attempts to join the server...
@EventHandler(ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST)
void onPlayerLogin (PlayerLoginEvent event) void onPlayerLogin (PlayerLoginEvent event)
{ {
if(!GriefPrevention.instance.config_spam_enabled) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
//all this is anti-spam code
if(GriefPrevention.instance.config_spam_enabled)
{
//FEATURE: login cooldown to prevent login/logout spam with custom clients //FEATURE: login cooldown to prevent login/logout spam with custom clients
//if allowed to join and login cooldown enabled //if allowed to join and login cooldown enabled
@ -256,6 +337,71 @@ class PlayerEventHandler implements Listener
event.setResult(Result.KICK_OTHER); event.setResult(Result.KICK_OTHER);
event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again."); event.setKickMessage("You must wait " + cooldownRemaining + " more minutes before logging-in again.");
event.disallow(event.getResult(), event.getKickMessage()); event.disallow(event.getResult(), event.getKickMessage());
return;
}
}
}
//remember the player's ip address
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
playerData.ipAddress = event.getAddress();
//FEATURE: auto-ban accounts who use an IP address which was very recently used by another banned account
if(GriefPrevention.instance.config_smartBan)
{
//if logging-in account is banned, remember IP address for later
long now = Calendar.getInstance().getTimeInMillis();
if(event.getResult() == Result.KICK_BANNED)
{
this.tempBannedIps.add(new IpBanInfo(event.getAddress(), now + this.MILLISECONDS_IN_DAY, player.getName()));
}
//otherwise if not banned
else
{
//search temporarily banned IP addresses for this one
for(int i = 0; i < this.tempBannedIps.size(); i++)
{
IpBanInfo info = this.tempBannedIps.get(i);
String address = info.address.toString();
//eliminate any expired entries
if(now > info.expirationTimestamp)
{
this.tempBannedIps.remove(i--);
}
//if we find a match
else if(address.equals(playerData.ipAddress.toString()))
{
//if the account associated with the IP ban has been pardoned, remove all ip bans for that ip and we're done
OfflinePlayer bannedPlayer = GriefPrevention.instance.getServer().getOfflinePlayer(info.bannedAccountName);
if(!bannedPlayer.isBanned())
{
for(int j = 0; j < this.tempBannedIps.size(); j++)
{
IpBanInfo info2 = this.tempBannedIps.get(j);
if(info2.address.toString().equals(address))
{
OfflinePlayer bannedAccount = GriefPrevention.instance.getServer().getOfflinePlayer(info2.bannedAccountName);
bannedAccount.setBanned(false);
this.tempBannedIps.remove(j--);
}
}
break;
}
//otherwise if that account is still banned, ban this account, too
else
{
player.setBanned(true);
event.setResult(Result.KICK_BANNED);
event.disallow(event.getResult(), "");
GriefPrevention.AddLogEntry("Auto-banned " + player.getName() + " because that account is using an IP address very recently used by banned player " + info.bannedAccountName + " (" + info.address.toString() + ").");
}
}
}
} }
} }
} }
@ -270,7 +416,7 @@ class PlayerEventHandler implements Listener
} }
//when a player successfully joins the server... //when a player successfully joins the server...
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onPlayerJoin(PlayerJoinEvent event) void onPlayerJoin(PlayerJoinEvent event)
{ {
String playerName = event.getPlayer().getName(); String playerName = event.getPlayer().getName();
@ -283,24 +429,47 @@ class PlayerEventHandler implements Listener
//check inventory, may need pvp protection //check inventory, may need pvp protection
GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer()); GriefPrevention.instance.checkPvpProtectionNeeded(event.getPlayer());
//how long since the last logout?
long elapsed = Calendar.getInstance().getTimeInMillis() - playerData.lastLogout;
//remember message, then silence it. may broadcast it later
String message = event.getJoinMessage();
event.setJoinMessage(null);
if(message != null && elapsed >= GriefPrevention.NOTIFICATION_SECONDS * 1000)
{
//start a timer for a delayed join notification message (will only show if player is still online in 30 seconds)
JoinLeaveAnnouncementTask task = new JoinLeaveAnnouncementTask(event.getPlayer(), message, true);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * GriefPrevention.NOTIFICATION_SECONDS);
}
} }
//when a player quits... //when a player quits...
@EventHandler @EventHandler(priority = EventPriority.HIGHEST)
void onPlayerQuit(PlayerQuitEvent event)
{
this.onPlayerDisconnect(event.getPlayer());
}
//when a player gets kicked...
@EventHandler(ignoreCancelled = true)
void onPlayerKicked(PlayerKickEvent event) void onPlayerKicked(PlayerKickEvent event)
{ {
this.onPlayerDisconnect(event.getPlayer()); Player player = event.getPlayer();
PlayerData playerData = this.dataStore.getPlayerData(player.getName());
if(player.isBanned())
{
long now = Calendar.getInstance().getTimeInMillis();
this.tempBannedIps.add(new IpBanInfo(playerData.ipAddress, now + this.MILLISECONDS_IN_DAY, player.getName()));
}
}
//when a player quits...
@EventHandler(priority = EventPriority.HIGHEST)
void onPlayerQuit(PlayerQuitEvent event)
{
this.onPlayerDisconnect(event.getPlayer(), event.getQuitMessage());
//silence the leave message (may be broadcast later, if the player stays offline)
event.setQuitMessage(null);
} }
//helper for above //helper for above
private void onPlayerDisconnect(Player player) private void onPlayerDisconnect(Player player, String notificationMessage)
{ {
String playerName = player.getName(); String playerName = player.getName();
PlayerData playerData = this.dataStore.getPlayerData(playerName); PlayerData playerData = this.dataStore.getPlayerData(playerName);
@ -319,15 +488,24 @@ class PlayerEventHandler implements Listener
if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message if(player.getHealth() > 0) player.setHealth(0); //might already be zero from above, this avoids a double death message
} }
//disable ignore claims mode //how long was the player online?
playerData.ignoreClaims = false; long now = Calendar.getInstance().getTimeInMillis();
long elapsed = now - playerData.lastLogin.getTime();
//drop player data from memory //remember logout time
this.dataStore.clearCachedPlayerData(playerName); playerData.lastLogout = Calendar.getInstance().getTimeInMillis();
//if notification message isn't null and the player has been online for at least 30 seconds...
if(notificationMessage != null && elapsed >= 1000 * GriefPrevention.NOTIFICATION_SECONDS)
{
//start a timer for a delayed leave notification message (will only show if player is still offline in 30 seconds)
JoinLeaveAnnouncementTask task = new JoinLeaveAnnouncementTask(player, notificationMessage, false);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * GriefPrevention.NOTIFICATION_SECONDS);
}
} }
//when a player drops an item //when a player drops an item
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerDropItem(PlayerDropItemEvent event) public void onPlayerDropItem(PlayerDropItemEvent event)
{ {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -360,7 +538,7 @@ class PlayerEventHandler implements Listener
} }
//when a player teleports //when a player teleports
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerTeleport(PlayerTeleportEvent event) public void onPlayerTeleport(PlayerTeleportEvent event)
{ {
//FEATURE: prevent teleport abuse to win sieges //FEATURE: prevent teleport abuse to win sieges
@ -390,7 +568,7 @@ class PlayerEventHandler implements Listener
} }
//when a player interacts with an entity... //when a player interacts with an entity...
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) public void onPlayerInteractEntity(PlayerInteractEntityEvent event)
{ {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -458,7 +636,7 @@ class PlayerEventHandler implements Listener
} }
//when a player picks up an item... //when a player picks up an item...
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerPickupItem(PlayerPickupItemEvent event) public void onPlayerPickupItem(PlayerPickupItemEvent event)
{ {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -504,7 +682,7 @@ class PlayerEventHandler implements Listener
} }
//block players from entering beds they don't have permission for //block players from entering beds they don't have permission for
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent) public void onPlayerBedEnter (PlayerBedEnterEvent bedEvent)
{ {
if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return; if(!GriefPrevention.instance.config_claims_preventButtonsSwitches) return;
@ -526,7 +704,7 @@ class PlayerEventHandler implements Listener
} }
//block use of buckets within other players' claims //block use of buckets within other players' claims
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent) public void onPlayerBucketEmpty (PlayerBucketEmptyEvent bucketEvent)
{ {
Player player = bucketEvent.getPlayer(); Player player = bucketEvent.getPlayer();
@ -542,29 +720,19 @@ class PlayerEventHandler implements Listener
return; return;
} }
//if the bucket is being used in a claim //if the bucket is being used in a claim, allow for dumping lava closer to other players
Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null); Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, null);
if(claim != null) if(claim != null)
{ {
//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; minLavaDistance = 3;
} }
//otherwise it must be underground //otherwise no dumping anything unless underground
else else
{ {
if(bucketEvent.getBucket() == Material.LAVA_BUCKET && block.getY() >= block.getWorld().getSeaLevel() - 5 && !player.hasPermission("griefprevention.lava")) if(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."); GriefPrevention.sendMessage(player, TextMode.Err, "You may only dump buckets inside your claim(s) or underground.");
bucketEvent.setCancelled(true); bucketEvent.setCancelled(true);
return; return;
} }
@ -592,7 +760,7 @@ class PlayerEventHandler implements Listener
} }
//see above //see above
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent) public void onPlayerBucketFill (PlayerBucketFillEvent bucketEvent)
{ {
Player player = bucketEvent.getPlayer(); Player player = bucketEvent.getPlayer();
@ -609,7 +777,7 @@ class PlayerEventHandler implements Listener
} }
//when a player interacts with the world //when a player interacts with the world
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.LOWEST)
void onPlayerInteract(PlayerInteractEvent event) void onPlayerInteract(PlayerInteractEvent event)
{ {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -640,7 +808,7 @@ class PlayerEventHandler implements Listener
Material clickedBlockType = clickedBlock.getType(); Material clickedBlockType = clickedBlock.getType();
//apply rules for buttons and switches //apply rules for buttons and switches
if(GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.LEVER)) if(GriefPrevention.instance.config_claims_preventButtonsSwitches && (clickedBlockType == null || clickedBlockType == Material.STONE_BUTTON || clickedBlockType == Material.LEVER))
{ {
Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, null); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, null);
if(claim != null) if(claim != null)
@ -814,7 +982,7 @@ class PlayerEventHandler implements Listener
//if the player is in restore nature mode, do only that //if the player is in restore nature mode, do only that
String playerName = player.getName(); String playerName = player.getName();
playerData = this.dataStore.getPlayerData(player.getName()); playerData = this.dataStore.getPlayerData(player.getName());
if(playerData.shovelMode == ShovelMode.RestoreNature) if(playerData.shovelMode == ShovelMode.RestoreNature || playerData.shovelMode == ShovelMode.RestoreNatureAggressive)
{ {
//if the clicked block is in a claim, visualize that claim and deliver an error message //if the clicked block is in a claim, visualize that claim and deliver an error message
Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim); Claim claim = this.dataStore.getClaimAt(clickedBlock.getLocation(), false, playerData.lastClaim);
@ -827,7 +995,7 @@ class PlayerEventHandler implements Listener
return; return;
} }
//figure out which chunk to regen //figure out which chunk to repair
Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation()); Chunk chunk = player.getWorld().getChunkAt(clickedBlock.getLocation());
//check it for players, and cancel if there are any //check it for players, and cancel if there are any
@ -863,22 +1031,127 @@ class PlayerEventHandler implements Listener
//set boundaries for processing //set boundaries for processing
int miny = clickedBlock.getY(); int miny = clickedBlock.getY();
//if not in aggressive mode, extend the selection down to a little below sea level
if(!(playerData.shovelMode == ShovelMode.RestoreNatureAggressive))
{
if(miny > chunk.getWorld().getSeaLevel() - 10) if(miny > chunk.getWorld().getSeaLevel() - 10)
{ {
miny = chunk.getWorld().getSeaLevel() - 10; miny = chunk.getWorld().getSeaLevel() - 10;
} }
}
Location lesserBoundaryCorner = chunk.getBlock(0, 0, 0).getLocation(); Location lesserBoundaryCorner = chunk.getBlock(0, 0, 0).getLocation();
Location greaterBoundaryCorner = chunk.getBlock(15, 0, 15).getLocation(); Location greaterBoundaryCorner = chunk.getBlock(15, 0, 15).getLocation();
//create task //create task
//when done processing, this task will create a main thread task to actually update the world with processing results //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); RestoreNatureProcessingTask task = new RestoreNatureProcessingTask(snapshots, miny, chunk.getWorld().getEnvironment(), chunk.getWorld().getBiome(lesserBoundaryCorner.getBlockX(), lesserBoundaryCorner.getBlockZ()), lesserBoundaryCorner, greaterBoundaryCorner, chunk.getWorld().getSeaLevel(), playerData.shovelMode == ShovelMode.RestoreNatureAggressive, player);
GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task); GriefPrevention.instance.getServer().getScheduler().scheduleAsyncDelayedTask(GriefPrevention.instance, task);
return; return;
} }
//if in restore nature fill mode
if(playerData.shovelMode == ShovelMode.RestoreNatureFill)
{
ArrayList<Material> allowedFillBlocks = new ArrayList<Material>();
Environment environment = clickedBlock.getWorld().getEnvironment();
if(environment == Environment.NETHER)
{
allowedFillBlocks.add(Material.NETHERRACK);
}
else if(environment == Environment.THE_END)
{
allowedFillBlocks.add(Material.ENDER_STONE);
}
else
{
allowedFillBlocks.add(Material.STONE);
allowedFillBlocks.add(Material.SAND);
allowedFillBlocks.add(Material.SANDSTONE);
allowedFillBlocks.add(Material.DIRT);
allowedFillBlocks.add(Material.GRASS);
}
Block centerBlock = clickedBlock;
int maxHeight = centerBlock.getY();
int minx = centerBlock.getX() - playerData.fillRadius;
int maxx = centerBlock.getX() + playerData.fillRadius;
int minz = centerBlock.getZ() - playerData.fillRadius;
int maxz = centerBlock.getZ() + playerData.fillRadius;
int minHeight = maxHeight - 10;
if(minHeight < 0) minHeight = 0;
Claim cachedClaim = null;
for(int x = minx; x <= maxx; x++)
{
for(int z = minz; z <= maxz; z++)
{
//circular brush
Location location = new Location(centerBlock.getWorld(), x, centerBlock.getY(), z);
if(location.distance(centerBlock.getLocation()) > playerData.fillRadius) continue;
//fill bottom to top
for(int y = minHeight; y <= maxHeight; y++)
{
Block block = centerBlock.getWorld().getBlockAt(x, y, z);
//respect claims
Claim claim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim);
if(claim != null)
{
cachedClaim = claim;
break;
}
//only replace air and spilling water
if(block.getType() == Material.AIR || block.getType() == Material.STATIONARY_WATER && block.getData() != 0)
{
//look to neighbors for an appropriate fill block
Block eastBlock = block.getRelative(BlockFace.EAST);
Block westBlock = block.getRelative(BlockFace.WEST);
Block northBlock = block.getRelative(BlockFace.NORTH);
Block southBlock = block.getRelative(BlockFace.SOUTH);
Block underBlock = block.getRelative(BlockFace.DOWN);
//first, check lateral neighbors (ideally, want to keep natural layers)
if(allowedFillBlocks.contains(eastBlock.getType()))
{
block.setType(eastBlock.getType());
}
else if(allowedFillBlocks.contains(westBlock.getType()))
{
block.setType(westBlock.getType());
}
else if(allowedFillBlocks.contains(northBlock.getType()))
{
block.setType(northBlock.getType());
}
else if(allowedFillBlocks.contains(southBlock.getType()))
{
block.setType(southBlock.getType());
}
//then check underneath
else if(allowedFillBlocks.contains(underBlock.getType()))
{
block.setType(underBlock.getType());
}
//if all else fails, use the first material listed in the acceptable fill blocks above
else
{
block.setType(allowedFillBlocks.get(0));
}
}
}
}
}
return;
}
//if the player doesn't have claims permission, don't do anything //if the player doesn't have claims permission, don't do anything
if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims")) if(GriefPrevention.instance.config_claims_creationRequiresPermission && !player.hasPermission("griefprevention.createclaims"))
{ {
@ -959,25 +1232,31 @@ class PlayerEventHandler implements Listener
} }
} }
//in creative mode, top-level claims can't be moved or resized smaller. //special rules for making a top-level claim smaller. to check this, verifying the old claim's corners are inside the new claim's boundaries.
//to check this, verifying the old claim's corners are inside the new claim's boundaries. //rule1: in creative mode, top-level claims can't be moved or resized smaller.
if(!player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation()) && playerData.claimResizing.parent == null) //rule2: in any mode, shrinking a claim removes any surface fluids
{
Claim oldClaim = playerData.claimResizing; Claim oldClaim = playerData.claimResizing;
if(oldClaim.parent == null)
{
//temporary claim instance, just for checking contains() //temporary claim instance, just for checking contains()
Claim newClaim = new Claim( Claim newClaim = new Claim(
new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1), new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx1, newy1, newz1),
new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2), new Location(oldClaim.getLesserBoundaryCorner().getWorld(), newx2, newy2, newz2),
"", new String[]{}, new String[]{}, new String[]{}, new String[]{}); "", new String[]{}, new String[]{}, new String[]{}, new String[]{});
//both greater and lesser boundary corners of the old claim must be inside the new claim //if the new claim is smaller
if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false)) if(!newClaim.contains(oldClaim.getLesserBoundaryCorner(), true, false) || !newClaim.contains(oldClaim.getGreaterBoundaryCorner(), true, false))
{ {
//otherwise, show an error message and stop here //enforce creative mode rule
if(!player.hasPermission("griefprevention.deleteclaims") && GriefPrevention.instance.creativeRulesApply(player.getLocation()))
{
GriefPrevention.sendMessage(player, TextMode.Err, "You can't un-claim creative mode land. You can only make this claim larger or create additional claims."); GriefPrevention.sendMessage(player, TextMode.Err, "You can't un-claim creative mode land. You can only make this claim larger or create additional claims.");
return; return;
} }
//remove surface fluids about to be unclaimed
oldClaim.removeSurfaceFluids(newClaim);
}
} }
//ask the datastore to try and resize the claim, this checks for conflicts with other claims //ask the datastore to try and resize the claim, this checks for conflicts with other claims

View File

@ -61,7 +61,7 @@ class PlayerRescueTask implements Runnable
Location destination = GriefPrevention.instance.ejectPlayer(this.player); Location destination = GriefPrevention.instance.ejectPlayer(this.player);
//log entry, in case admins want to investigate the "trap" //log entry, in case admins want to investigate the "trap"
GriefPrevention.AddLogEntry("Rescued trapped player " + player.getName() + " from " + this.location.toString() + " to " + destination.toString() + "."); GriefPrevention.AddLogEntry("Rescued trapped player " + player.getName() + " from " + GriefPrevention.getfriendlyLocationString(this.location) + " to " + GriefPrevention.getfriendlyLocationString(destination) + ".");
//timestamp this successful save so that he can't use /trapped again for a while //timestamp this successful save so that he can't use /trapped again for a while
playerData.lastTrappedUsage = Calendar.getInstance().getTime(); playerData.lastTrappedUsage = Calendar.getInstance().getTime();

View File

@ -43,12 +43,13 @@ class RestoreNatureProcessingTask implements Runnable
private Player player; //absolutely must not be accessed. not thread safe. private Player player; //absolutely must not be accessed. not thread safe.
private Biome biome; private Biome biome;
private int seaLevel; private int seaLevel;
private boolean aggressiveMode;
//two lists of materials //two lists of materials
private ArrayList<Integer> notAllowedToHang; //natural blocks which don't naturally hang in their air private ArrayList<Integer> notAllowedToHang; //natural blocks which don't naturally hang in their air
private ArrayList<Integer> playerBlocks; //a "complete" list of player-placed blocks. MUST BE MAINTAINED as patches introduce more private ArrayList<Integer> 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) public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, Player player)
{ {
this.snapshots = snapshots; this.snapshots = snapshots;
this.miny = miny; this.miny = miny;
@ -57,18 +58,24 @@ class RestoreNatureProcessingTask implements Runnable
this.greaterBoundaryCorner = greaterBoundaryCorner; this.greaterBoundaryCorner = greaterBoundaryCorner;
this.biome = biome; this.biome = biome;
this.seaLevel = seaLevel; this.seaLevel = seaLevel;
this.aggressiveMode = aggressiveMode;
this.player = player; this.player = player;
this.notAllowedToHang = new ArrayList<Integer>(); this.notAllowedToHang = new ArrayList<Integer>();
this.notAllowedToHang.add(Material.DIRT.getId()); this.notAllowedToHang.add(Material.DIRT.getId());
this.notAllowedToHang.add(Material.GRASS.getId());
this.notAllowedToHang.add(Material.LONG_GRASS.getId()); this.notAllowedToHang.add(Material.LONG_GRASS.getId());
this.notAllowedToHang.add(Material.SNOW.getId()); this.notAllowedToHang.add(Material.SNOW.getId());
this.notAllowedToHang.add(Material.LOG.getId()); this.notAllowedToHang.add(Material.LOG.getId());
if(this.aggressiveMode)
{
this.notAllowedToHang.add(Material.GRASS.getId());
this.notAllowedToHang.add(Material.STONE.getId());
}
//NOTE on this list. why not make a list of natural blocks? //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" //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 //a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal
this.playerBlocks = new ArrayList<Integer>(); this.playerBlocks = new ArrayList<Integer>();
this.playerBlocks.add(Material.FIRE.getId()); this.playerBlocks.add(Material.FIRE.getId());
this.playerBlocks.add(Material.BED_BLOCK.getId()); this.playerBlocks.add(Material.BED_BLOCK.getId());
@ -155,11 +162,23 @@ class RestoreNatureProcessingTask implements Runnable
} }
//these are unnatural in sandy biomes, but not elsewhere //these are unnatural in sandy biomes, but not elsewhere
if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH) if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACH || this.aggressiveMode)
{ {
this.playerBlocks.add(Material.LEAVES.getId()); this.playerBlocks.add(Material.LEAVES.getId());
this.playerBlocks.add(Material.LOG.getId()); this.playerBlocks.add(Material.LOG.getId());
} }
//in aggressive mode, also treat these blocks as user placed, to be removed
//this is helpful in the few cases where griefers intentionally use natural blocks to grief,
//like a single-block tower of iron ore or a giant penis constructed with logs
if(this.aggressiveMode)
{
this.playerBlocks.add(Material.IRON_ORE.getId());
this.playerBlocks.add(Material.PUMPKIN.getId());
this.playerBlocks.add(Material.PUMPKIN_STEM.getId());
this.playerBlocks.add(Material.MELON_BLOCK.getId());
this.playerBlocks.add(Material.MELON_STEM.getId());
}
} }
@Override @Override
@ -185,6 +204,9 @@ class RestoreNatureProcessingTask implements Runnable
//fill water depressions and fix unnatural surface ripples //fill water depressions and fix unnatural surface ripples
this.fixWater(); this.fixWater();
//remove water/lava above sea level
this.removeDumpedFluids();
//schedule main thread task to apply the result to the world //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); RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task); GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task);
@ -195,6 +217,7 @@ class RestoreNatureProcessingTask implements Runnable
int miny = this.miny; int miny = this.miny;
if(miny < 1) miny = 1; if(miny < 1) miny = 1;
//remove all player blocks
for(int x = 1; x < snapshots.length - 1; x++) for(int x = 1; x < snapshots.length - 1; x++)
{ {
for(int z = 1; z < snapshots[0][0].length - 1; z++) for(int z = 1; z < snapshots[0][0].length - 1; z++)
@ -225,7 +248,7 @@ class RestoreNatureProcessingTask implements Runnable
BlockSnapshot block = snapshots[x][y][z]; BlockSnapshot block = snapshots[x][y][z];
BlockSnapshot underBlock = snapshots[x][y - 1][z]; BlockSnapshot underBlock = snapshots[x][y - 1][z];
if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.WATER.getId()) if(underBlock.typeId == Material.AIR.getId() || underBlock.typeId == Material.STATIONARY_WATER.getId() || underBlock.typeId == Material.STATIONARY_LAVA.getId() || underBlock.typeId == Material.LEAVES.getId())
{ {
if(this.notAllowedToHang.contains(block.typeId)) if(this.notAllowedToHang.contains(block.typeId))
{ {
@ -267,19 +290,19 @@ class RestoreNatureProcessingTask implements Runnable
{ {
for(int z = 1; z < snapshots[0][0].length - 1; z++) for(int z = 1; z < snapshots[0][0].length - 1; z++)
{ {
int thisy = this.highestY(x, z); int thisy = this.highestY(x, z, false);
if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue; if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue;
int righty = this.highestY(x + 1, z); int righty = this.highestY(x + 1, z, false);
int lefty = this.highestY(x - 1, z); int lefty = this.highestY(x - 1, z, false);
while(lefty < thisy && righty < thisy) while(lefty < thisy && righty < thisy)
{ {
this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
changed = true; changed = true;
} }
int upy = this.highestY(x, z + 1); int upy = this.highestY(x, z + 1, false);
int downy = this.highestY(x, z - 1); int downy = this.highestY(x, z - 1, false);
while(upy < thisy && downy < thisy) while(upy < thisy && downy < thisy)
{ {
this.snapshots[x][thisy--][z].typeId = Material.AIR.getId(); this.snapshots[x][thisy--][z].typeId = Material.AIR.getId();
@ -296,7 +319,7 @@ class RestoreNatureProcessingTask implements Runnable
{ {
for(int z = 1; z < snapshots[0][0].length - 1; z++) for(int z = 1; z < snapshots[0][0].length - 1; z++)
{ {
int y = this.highestY(x, z); int y = this.highestY(x, z, true);
BlockSnapshot block = snapshots[x][y][z]; BlockSnapshot block = snapshots[x][y][z];
if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.DIRT.getId()) if(block.typeId == Material.STONE.getId() || block.typeId == Material.GRAVEL.getId() || block.typeId == Material.DIRT.getId())
@ -454,13 +477,36 @@ class RestoreNatureProcessingTask implements Runnable
}while(changed); }while(changed);
} }
private int highestY(int x, int z) private void removeDumpedFluids()
{
//remove any surface water or lava above sea level, presumed to be placed by players
//sometimes, this is naturally generated. but replacing it is very easy with a bucket, so overall this is a good plan
if(this.environment == Environment.NETHER) return;
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
for(int y = this.seaLevel - 1; y < snapshots[0].length - 1; y++)
{
BlockSnapshot block = snapshots[x][y][z];
if(block.typeId == Material.STATIONARY_WATER.getId() || block.typeId == Material.STATIONARY_LAVA.getId() ||
block.typeId == Material.WATER.getId() || block.typeId == Material.LAVA.getId())
{
block.typeId = Material.AIR.getId();
}
}
}
}
}
private int highestY(int x, int z, boolean ignoreLeaves)
{ {
int y; int y;
for(y = snapshots[0].length - 1; y > 0; y--) for(y = snapshots[0].length - 1; y > 0; y--)
{ {
BlockSnapshot block = this.snapshots[x][y][z]; BlockSnapshot block = this.snapshots[x][y][z];
if(block.typeId != Material.AIR.getId() && if(block.typeId != Material.AIR.getId() &&
!(ignoreLeaves && block.typeId == Material.LEAVES.getId()) &&
!(block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0) && !(block.typeId == Material.STATIONARY_WATER.getId() && block.data != 0) &&
!(block.typeId == Material.STATIONARY_LAVA.getId() && block.data != 0)) !(block.typeId == Material.STATIONARY_LAVA.getId() && block.data != 0))
{ {

View File

@ -24,5 +24,7 @@ enum ShovelMode
Basic, Basic,
Admin, Admin,
Subdivide, Subdivide,
RestoreNature RestoreNature,
RestoreNatureAggressive,
RestoreNatureFill
} }