Refactor to maven standard layout (#270)

This commit is contained in:
Narimm
2018-06-11 23:11:37 -07:00
committed by RoboMWM
parent 0c95165af0
commit b38e4aedb6
69 changed files with 7 additions and 4 deletions
@@ -0,0 +1,132 @@
package me.ryanhamshire.GriefPrevention;
import java.util.ArrayList;
import org.bukkit.Bukkit;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
//automatically extends a claim downward based on block types detected
class AutoExtendClaimTask implements Runnable
{
private Claim claim;
private ArrayList<ChunkSnapshot> chunks;
private Environment worldType;
public AutoExtendClaimTask(Claim claim, ArrayList<ChunkSnapshot> chunks, Environment worldType)
{
this.claim = claim;
this.chunks = chunks;
this.worldType = worldType;
}
@Override
public void run()
{
int newY = this.getLowestBuiltY();
if(newY < this.claim.getLesserBoundaryCorner().getBlockY())
{
Bukkit.getScheduler().runTask(GriefPrevention.instance, new ExecuteExtendClaimTask(claim, newY));
}
}
private int getLowestBuiltY()
{
int y = this.claim.getLesserBoundaryCorner().getBlockY();
if(this.yTooSmall(y)) return y;
try
{
for(ChunkSnapshot chunk : this.chunks)
{
Biome biome = chunk.getBiome(0, 0);
ArrayList<Material> playerBlockIDs = RestoreNatureProcessingTask.getPlayerBlocks(this.worldType, biome);
boolean ychanged = true;
while(!this.yTooSmall(y) && ychanged)
{
ychanged = false;
for(int x = 0; x < 16; x++)
{
for(int z = 0; z < 16; z++)
{
Material blockType = chunk.getBlockType(x, y, z);
while(!this.yTooSmall(y) && playerBlockIDs.contains(blockType))
{
ychanged = true;
blockType = chunk.getBlockType(x, --y, z);
}
if(this.yTooSmall(y)) return y;
}
}
}
if(this.yTooSmall(y)) return y;
}
}
catch (NoSuchMethodError e)
{
GriefPrevention.instance.getLogger().severe("You are running an outdated build of Craftbukkit/Spigot/Paper. Please update.");
for(ChunkSnapshot chunk : this.chunks)
{
Biome biome = chunk.getBiome(0, 0);
ArrayList<Material> playerBlockIDs = RestoreNatureProcessingTask.getPlayerBlocks(this.worldType, biome);
boolean ychanged = true;
while(!this.yTooSmall(y) && ychanged)
{
ychanged = false;
for(int x = 0; x < 16; x++)
{
for(int z = 0; z < 16; z++)
{
int blockType = chunk.getBlockTypeId(x, y, z);
while(!this.yTooSmall(y) && playerBlockIDs.contains(Material.getMaterial(blockType)))
{
ychanged = true;
blockType = chunk.getBlockTypeId(x, --y, z);
}
if(this.yTooSmall(y)) return y;
}
}
}
if(this.yTooSmall(y)) return y;
}
}
return y;
}
private boolean yTooSmall(int y)
{
return y == 0 || y <= GriefPrevention.instance.config_claims_maxDepth;
}
//runs in the main execution thread, where it can safely change claims and save those changes
private class ExecuteExtendClaimTask implements Runnable
{
private Claim claim;
private int newY;
public ExecuteExtendClaimTask(Claim claim, int newY)
{
this.claim = claim;
this.newY = newY;
}
@Override
public void run()
{
GriefPrevention.instance.dataStore.extendClaim(claim, newY);
}
}
}
@@ -0,0 +1,856 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Hopper;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.entity.minecart.HopperMinecart;
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.BlockDispenseEvent;
import org.bukkit.event.block.BlockFormEvent;
import org.bukkit.event.block.BlockFromToEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockIgniteEvent.IgniteCause;
import org.bukkit.event.block.BlockMultiPlaceEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.inventory.InventoryPickupItemEvent;
import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.Dispenser;
import org.bukkit.metadata.MetadataValue;
//event handlers related to blocks
public class BlockEventHandler implements Listener
{
//convenience reference to singleton datastore
private DataStore dataStore;
private ArrayList<Material> trashBlocks;
//constructor
public BlockEventHandler(DataStore dataStore)
{
this.dataStore = dataStore;
//create the list of blocks which will not trigger a warning when they're placed outside of land claims
this.trashBlocks = new ArrayList<Material>();
this.trashBlocks.add(Material.COBBLESTONE);
this.trashBlocks.add(Material.TORCH);
this.trashBlocks.add(Material.DIRT);
this.trashBlocks.add(Material.SAPLING);
this.trashBlocks.add(Material.GRAVEL);
this.trashBlocks.add(Material.SAND);
this.trashBlocks.add(Material.TNT);
this.trashBlocks.add(Material.WORKBENCH);
}
//when a player breaks a block...
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockBreak(BlockBreakEvent breakEvent)
{
Player player = breakEvent.getPlayer();
Block block = breakEvent.getBlock();
//make sure the player is allowed to break at the location
String noBuildReason = GriefPrevention.instance.allowBreak(player, block, block.getLocation(), breakEvent);
if(noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
breakEvent.setCancelled(true);
return;
}
}
//when a player places a sign...
@EventHandler(ignoreCancelled = true)
public void onSignChanged(SignChangeEvent event)
{
//send sign content to online administrators
if(!GriefPrevention.instance.config_signNotifications) return;
Player player = event.getPlayer();
if(player == null) return;
StringBuilder lines = new StringBuilder(" placed a sign @ " + GriefPrevention.getfriendlyLocationString(event.getBlock().getLocation()));
boolean notEmpty = false;
for(int i = 0; i < event.getLines().length; i++)
{
String withoutSpaces = event.getLine(i).replace(" ", "");
if(!withoutSpaces.isEmpty())
{
notEmpty = true;
lines.append("\n " + event.getLine(i));
}
}
String signMessage = lines.toString();
//prevent signs with blocked IP addresses
if(!player.hasPermission("griefprevention.spam") && GriefPrevention.instance.containsBlockedIP(signMessage))
{
event.setCancelled(true);
return;
}
PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
//if not empty and wasn't the same as the last sign, log it and remember it for later
//This has been temporarily removed since `signMessage` includes location, not just the message. Waste of memory IMO
//if(notEmpty && (playerData.lastSignMessage == null || !playerData.lastSignMessage.equals(signMessage)))
if (notEmpty)
{
GriefPrevention.AddLogEntry(player.getName() + lines.toString().replace("\n ", ";"), null);
PlayerEventHandler.makeSocialLogEntry(player.getName(), signMessage);
//playerData.lastSignMessage = signMessage;
if(!player.hasPermission("griefprevention.eavesdropsigns"))
{
@SuppressWarnings("unchecked")
Collection<Player> players = (Collection<Player>)GriefPrevention.instance.getServer().getOnlinePlayers();
for(Player otherPlayer : players)
{
if(otherPlayer.hasPermission("griefprevention.eavesdropsigns"))
{
otherPlayer.sendMessage(ChatColor.GRAY + player.getName() + signMessage);
}
}
}
}
}
//when a player places multiple blocks...
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onBlocksPlace(BlockMultiPlaceEvent placeEvent)
{
Player player = placeEvent.getPlayer();
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return;
//make sure the player is allowed to build at the location
for(BlockState block : placeEvent.getReplacedBlockStates())
{
String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType());
if(noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
placeEvent.setCancelled(true);
return;
}
}
}
//when a player places a block...
@SuppressWarnings("null")
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onBlockPlace(BlockPlaceEvent placeEvent)
{
Player player = placeEvent.getPlayer();
Block block = placeEvent.getBlock();
//FEATURE: limit fire placement, to prevent PvP-by-fire
//if placed block is fire and pvp is off, apply rules for proximity to other players
if(block.getType() == Material.FIRE && (!GriefPrevention.instance.pvpRulesApply(block.getWorld()) || !GriefPrevention.instance.config_pvp_allowFireNearPlayers))
{
List<Player> players = block.getWorld().getPlayers();
for(int i = 0; i < players.size(); i++)
{
Player otherPlayer = players.get(i);
Location location = otherPlayer.getLocation();
if(!otherPlayer.equals(player) && location.distanceSquared(block.getLocation()) < 9)
{
GriefPrevention.sendMessage(player, TextMode.Err, Messages.PlayerTooCloseForFire2);
placeEvent.setCancelled(true);
return;
}
}
}
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(placeEvent.getBlock().getWorld())) return;
//make sure the player is allowed to build at the location
String noBuildReason = GriefPrevention.instance.allowBuild(player, block.getLocation(), block.getType());
if(noBuildReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noBuildReason);
placeEvent.setCancelled(true);
return;
}
//if the block is being placed within or under an existing claim
PlayerData playerData = this.dataStore.getPlayerData(player.getUniqueId());
Claim claim = this.dataStore.getClaimAt(block.getLocation(), true, playerData.lastClaim);
if(claim != null)
{
playerData.lastClaim = claim;
//warn about TNT not destroying claimed blocks
if(block.getType() == Material.TNT && !claim.areExplosivesAllowed && playerData.siegeData == null)
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageClaims);
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.ClaimExplosivesAdvertisement);
}
//if the player has permission for the claim and he's placing UNDER the claim
if(block.getY() <= claim.lesserBoundaryCorner.getBlockY() && claim.allowBuild(player, block.getType()) == null)
{
//extend the claim downward
this.dataStore.extendClaim(claim, block.getY() - GriefPrevention.instance.config_claims_claimsExtendIntoGroundDistance);
}
//allow for a build warning in the future
playerData.warnedAboutBuildingOutsideClaims = false;
}
//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(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius > -1 && player.hasPermission("griefprevention.createclaims") && block.getType() == Material.CHEST)
{
//if the chest is too deep underground, don't create the claim and explain why
if(GriefPrevention.instance.config_claims_preventTheft && block.getY() < GriefPrevention.instance.config_claims_maxDepth)
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.TooDeepToClaim);
return;
}
int radius = GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius;
//if the player doesn't have any claims yet, automatically create a claim centered at the chest
if(playerData.getClaims().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.getUniqueId(), null, null, player);
GriefPrevention.sendMessage(player, TextMode.Success, Messages.ChestClaimConfirmation);
}
//otherwise, create a claim in the area around the chest
else
{
//if failure due to insufficient claim blocks available
if(playerData.getRemainingClaimBlocks() < 1)
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoEnoughBlocksForChestClaim);
return;
}
//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
CreateClaimResult result = null;
while(radius >= 0)
{
int area = (radius * 2 + 1) * (radius * 2 + 1);
if(playerData.getRemainingClaimBlocks() >= area)
{
result = 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.getUniqueId(),
null, null,
player);
if(result.succeeded) break;
}
radius--;
}
if(result != null && result.succeeded)
{
//notify and explain to player
GriefPrevention.sendMessage(player, TextMode.Success, Messages.AutomaticClaimNotification);
//show the player the protected area
Visualization visualization = Visualization.FromClaim(result.claim, block.getY(), VisualizationType.Claim, player.getLocation());
Visualization.Apply(player, visualization);
}
}
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SurvivalBasicsVideo2, DataStore.SURVIVAL_VIDEO_URL);
}
//check to see if this chest is in a claim, and warn when it isn't
if(GriefPrevention.instance.config_claims_preventTheft && this.dataStore.getClaimAt(block.getLocation(), false, playerData.lastClaim) == null)
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.UnprotectedChestWarning);
}
}
//FEATURE: limit wilderness tree planting to grass, or dirt with more blocks beneath it
else if(block.getType() == Material.SAPLING && GriefPrevention.instance.config_blockSkyTrees && GriefPrevention.instance.claimsEnabledForWorld(player.getWorld()))
{
Block earthBlock = placeEvent.getBlockAgainst();
if(earthBlock.getType() != Material.GRASS)
{
if(earthBlock.getRelative(BlockFace.DOWN).getType() == Material.AIR ||
earthBlock.getRelative(BlockFace.DOWN).getRelative(BlockFace.DOWN).getType() == Material.AIR)
{
placeEvent.setCancelled(true);
}
}
}
//FEATURE: warn players when they're placing non-trash blocks outside of their claimed areas
else if(!this.trashBlocks.contains(block.getType()) && GriefPrevention.instance.claimsEnabledForWorld(block.getWorld()))
{
if(!playerData.warnedAboutBuildingOutsideClaims && !player.hasPermission("griefprevention.adminclaims")
&& player.hasPermission("griefprevention.createclaims") && ((playerData.lastClaim == null
&& playerData.getClaims().size() == 0) || (playerData.lastClaim != null
&& playerData.lastClaim.isNear(player.getLocation(), 15))))
{
Long now = null;
if(playerData.buildWarningTimestamp == null || (now = System.currentTimeMillis()) - playerData.buildWarningTimestamp > 600000) //10 minute cooldown
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.BuildingOutsideClaims);
playerData.warnedAboutBuildingOutsideClaims = true;
if(now == null) now = System.currentTimeMillis();
playerData.buildWarningTimestamp = now;
if(playerData.getClaims().size() < 2)
{
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SurvivalBasicsVideo2, DataStore.SURVIVAL_VIDEO_URL);
}
if(playerData.lastClaim != null)
{
Visualization visualization = Visualization.FromClaim(playerData.lastClaim, block.getY(), VisualizationType.Claim, player.getLocation());
Visualization.Apply(player, visualization);
}
}
}
}
//warn players when they place TNT above sea level, since it doesn't destroy blocks there
if( GriefPrevention.instance.config_blockSurfaceOtherExplosions && block.getType() == Material.TNT &&
block.getWorld().getEnvironment() != Environment.NETHER &&
block.getY() > GriefPrevention.instance.getSeaLevel(block.getWorld()) - 5 &&
claim == null &&
playerData.siegeData == null)
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoTNTDamageAboveSeaLevel);
}
//warn players about disabled pistons outside of land claims
if( GriefPrevention.instance.config_pistonsInClaimsOnly &&
(block.getType() == Material.PISTON_BASE || block.getType() == Material.PISTON_STICKY_BASE) &&
claim == null )
{
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.NoPistonsOutsideClaims);
}
//limit active blocks in creative mode worlds
if(!player.hasPermission("griefprevention.adminclaims") && GriefPrevention.instance.creativeRulesApply(block.getLocation()) && isActiveBlock(block))
{
String noPlaceReason = claim.allowMoreActiveBlocks();
if(noPlaceReason != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, noPlaceReason);
placeEvent.setCancelled(true);
return;
}
}
}
static boolean isActiveBlock(Block block)
{
return isActiveBlock(block.getType());
}
static boolean isActiveBlock(BlockState state)
{
return isActiveBlock(state.getType());
}
static boolean isActiveBlock(Material type)
{
if(type == Material.HOPPER || type == Material.BEACON || type == Material.MOB_SPAWNER) return true;
return false;
}
//blocks "pushing" other players' blocks around (pistons)
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockPistonExtend (BlockPistonExtendEvent event)
{
//pushing down is ALWAYS safe
if(event.getDirection() == BlockFace.DOWN) return;
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(event.getBlock().getWorld())) return;
Block pistonBlock = event.getBlock();
List<Block> blocks = event.getBlocks();
//if no blocks moving, then only check to make sure we're not pushing into a claim from outside
//this avoids pistons breaking non-solids just inside a claim, like torches, doors, and touchplates
if(blocks.size() == 0)
{
Block invadedBlock = pistonBlock.getRelative(event.getDirection());
//pushing "air" is harmless
if(invadedBlock.getType() == Material.AIR) return;
if( this.dataStore.getClaimAt(pistonBlock.getLocation(), false, null) == null &&
this.dataStore.getClaimAt(invadedBlock.getLocation(), false, null) != null)
{
event.setCancelled(true);
}
return;
}
//who owns the piston, if anyone?
String pistonClaimOwnerName = "_";
Claim claim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null);
if(claim != null) pistonClaimOwnerName = claim.getOwnerName();
//if pistons are limited to same-claim block movement
if(GriefPrevention.instance.config_pistonsInClaimsOnly)
{
//if piston is not in a land claim, cancel event
if(claim == null)
{
event.setCancelled(true);
return;
}
for(Block pushedBlock : event.getBlocks())
{
//if pushing blocks located outside the land claim it lives in, cancel the event
if(!claim.contains(pushedBlock.getLocation(), false, false))
{
event.setCancelled(true);
return;
}
//if pushing a block inside the claim out of the claim, cancel the event
//reason: could push into another land claim, don't want to spend CPU checking for that
//reason: push ice out, place torch, get water outside the claim
if(!claim.contains(pushedBlock.getRelative(event.getDirection()).getLocation(), false, false))
{
event.setCancelled(true);
return;
}
}
}
//otherwise, consider ownership of piston and EACH pushed block
else
{
//which blocks are being pushed?
Claim cachedClaim = claim;
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, cachedClaim);
if(claim != null)
{
cachedClaim = claim;
if(!claim.getOwnerName().equals(pistonClaimOwnerName))
{
event.setCancelled(true);
pistonBlock.getWorld().createExplosion(pistonBlock.getLocation(), 0);
pistonBlock.getWorld().dropItem(pistonBlock.getLocation(), new ItemStack(pistonBlock.getType()));
pistonBlock.setType(Material.AIR);
return;
}
}
}
//if any of the blocks are being pushed into a claim from outside, cancel the event
for(int i = 0; i < blocks.size(); i++)
{
Block block = blocks.get(i);
Claim originalClaim = this.dataStore.getClaimAt(block.getLocation(), false, cachedClaim);
String originalOwnerName = "";
if(originalClaim != null)
{
cachedClaim = originalClaim;
originalOwnerName = originalClaim.getOwnerName();
}
Claim newClaim = this.dataStore.getClaimAt(block.getRelative(event.getDirection()).getLocation(), false, cachedClaim);
String newOwnerName = "";
if(newClaim != null)
{
newOwnerName = newClaim.getOwnerName();
}
//if pushing this block will change ownership, cancel the event and take away the piston (for performance reasons)
if(!newOwnerName.equals(originalOwnerName) && !newOwnerName.isEmpty())
{
event.setCancelled(true);
pistonBlock.getWorld().createExplosion(pistonBlock.getLocation(), 0);
pistonBlock.getWorld().dropItem(pistonBlock.getLocation(), new ItemStack(pistonBlock.getType()));
pistonBlock.setType(Material.AIR);
return;
}
}
}
}
//blocks theft by pulling blocks out of a claim (again pistons)
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockPistonRetract (BlockPistonRetractEvent event)
{
//pulling up is always safe
if(event.getDirection() == BlockFace.UP) return;
try
{
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(event.getBlock().getWorld())) return;
//if pistons limited to only pulling blocks which are in the same claim the piston is in
if(GriefPrevention.instance.config_pistonsInClaimsOnly)
{
//if piston not in a land claim, cancel event
Claim pistonClaim = this.dataStore.getClaimAt(event.getBlock().getLocation(), false, null);
if(pistonClaim == null && !event.getBlocks().isEmpty())
{
event.setCancelled(true);
return;
}
for(Block movedBlock : event.getBlocks())
{
//if pulled block isn't in the same land claim, cancel the event
if(!pistonClaim.contains(movedBlock.getLocation(), false, false))
{
event.setCancelled(true);
return;
}
}
}
//otherwise, consider ownership of both piston and block
else
{
//who owns the piston, if anyone?
String pistonOwnerName = "_";
Block block = event.getBlock();
Location pistonLocation = block.getLocation();
Claim pistonClaim = this.dataStore.getClaimAt(pistonLocation, false, null);
if(pistonClaim != null) pistonOwnerName = pistonClaim.getOwnerName();
String movingBlockOwnerName = "_";
for(Block movedBlock : event.getBlocks())
{
//who owns the moving block, if anyone?
Claim movingBlockClaim = this.dataStore.getClaimAt(movedBlock.getLocation(), false, pistonClaim);
if(movingBlockClaim != null) movingBlockOwnerName = movingBlockClaim.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);
block.getWorld().createExplosion(block.getLocation(), 0);
block.getWorld().dropItem(block.getLocation(), new ItemStack(Material.PISTON_STICKY_BASE));
block.setType(Material.AIR);
return;
}
}
}
}
catch(NoSuchMethodError exception)
{
GriefPrevention.AddLogEntry("Your server is running an outdated version of 1.8 which has a griefing vulnerability. Update your server (reruns buildtools.jar to get an updated server JAR file) to ensure players can't steal claimed blocks using pistons.");
}
}
//blocks are ignited ONLY by flint and steel (not by being near lava, open flames, etc), unless configured otherwise
@EventHandler(priority = EventPriority.LOWEST)
public void onBlockIgnite (BlockIgniteEvent igniteEvent)
{
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(igniteEvent.getBlock().getWorld())) return;
if(!GriefPrevention.instance.config_fireSpreads && igniteEvent.getCause() != IgniteCause.FLINT_AND_STEEL && igniteEvent.getCause() != IgniteCause.LIGHTNING)
{
igniteEvent.setCancelled(true);
}
}
//fire doesn't spread unless configured to, but other blocks still do (mushrooms and vines, for example)
@EventHandler(priority = EventPriority.LOWEST)
public void onBlockSpread (BlockSpreadEvent spreadEvent)
{
if(spreadEvent.getSource().getType() != Material.FIRE) return;
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) return;
if(!GriefPrevention.instance.config_fireSpreads)
{
spreadEvent.setCancelled(true);
Block underBlock = spreadEvent.getSource().getRelative(BlockFace.DOWN);
if(underBlock.getType() != Material.NETHERRACK)
{
spreadEvent.getSource().setType(Material.AIR);
}
return;
}
//never spread into a claimed area, regardless of settings
if(this.dataStore.getClaimAt(spreadEvent.getBlock().getLocation(), false, null) != null)
{
spreadEvent.setCancelled(true);
//if the source of the spread is not fire on netherrack, put out that source fire to save cpu cycles
Block source = spreadEvent.getSource();
if(source.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK)
{
source.setType(Material.AIR);
}
}
}
//blocks are not destroyed by fire, unless configured to do so
@EventHandler(priority = EventPriority.LOWEST)
public void onBlockBurn (BlockBurnEvent burnEvent)
{
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(burnEvent.getBlock().getWorld())) return;
if(!GriefPrevention.instance.config_fireDestroys)
{
burnEvent.setCancelled(true);
Block block = burnEvent.getBlock();
Block [] adjacentBlocks = new Block []
{
block.getRelative(BlockFace.UP),
block.getRelative(BlockFace.DOWN),
block.getRelative(BlockFace.NORTH),
block.getRelative(BlockFace.SOUTH),
block.getRelative(BlockFace.EAST),
block.getRelative(BlockFace.WEST)
};
//pro-actively put out any fires adjacent the burning block, to reduce future processing here
for(int i = 0; i < adjacentBlocks.length; i++)
{
Block adjacentBlock = adjacentBlocks[i];
if(adjacentBlock.getType() == Material.FIRE && adjacentBlock.getRelative(BlockFace.DOWN).getType() != Material.NETHERRACK)
{
adjacentBlock.setType(Material.AIR);
}
}
Block aboveBlock = block.getRelative(BlockFace.UP);
if(aboveBlock.getType() == Material.FIRE)
{
aboveBlock.setType(Material.AIR);
}
return;
}
//never burn claimed blocks, regardless of settings
if(this.dataStore.getClaimAt(burnEvent.getBlock().getLocation(), false, null) != null)
{
burnEvent.setCancelled(true);
}
}
//ensures fluids don't flow into land claims from outside
private Claim lastSpreadClaim = null;
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onBlockFromTo (BlockFromToEvent spreadEvent)
{
//always allow fluids to flow straight down
if(spreadEvent.getFace() == BlockFace.DOWN) return;
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(spreadEvent.getBlock().getWorld())) return;
//where to?
Block toBlock = spreadEvent.getToBlock();
Location toLocation = toBlock.getLocation();
Claim toClaim = this.dataStore.getClaimAt(toLocation, false, lastSpreadClaim);
//if into a land claim, it must be from the same land claim
if(toClaim != null)
{
this.lastSpreadClaim = toClaim;
if(!toClaim.contains(spreadEvent.getBlock().getLocation(), false, true))
{
//exception: from parent into subdivision
if(toClaim.parent == null || !toClaim.parent.contains(spreadEvent.getBlock().getLocation(), false, false))
{
spreadEvent.setCancelled(true);
}
}
}
//otherwise if creative mode world, don't flow
else if(GriefPrevention.instance.creativeRulesApply(toLocation))
{
spreadEvent.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onForm(BlockFormEvent event)
{
Block block = event.getBlock();
Location location = block.getLocation();
if(GriefPrevention.instance.creativeRulesApply(location))
{
Material type = block.getType();
if(type == Material.COBBLESTONE || type == Material.OBSIDIAN || type == Material.STATIONARY_LAVA || type == Material.STATIONARY_WATER)
{
Claim claim = GriefPrevention.instance.dataStore.getClaimAt(location, false, null);
if(claim == null)
{
event.setCancelled(true);
}
}
}
}
//ensures dispensers can't be used to dispense a block(like water or lava) or item across a claim boundary
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onDispense(BlockDispenseEvent dispenseEvent)
{
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(dispenseEvent.getBlock().getWorld())) return;
//from where?
Block fromBlock = dispenseEvent.getBlock();
@SuppressWarnings("deprecation")
Dispenser dispenser = new Dispenser(Material.DISPENSER, fromBlock.getData());
//to where?
Block toBlock = fromBlock.getRelative(dispenser.getFacing());
Claim fromClaim = this.dataStore.getClaimAt(fromBlock.getLocation(), false, null);
Claim toClaim = this.dataStore.getClaimAt(toBlock.getLocation(), false, fromClaim);
//into wilderness is NOT OK in creative mode worlds
Material materialDispensed = dispenseEvent.getItem().getType();
if((materialDispensed == Material.WATER_BUCKET || materialDispensed == Material.LAVA_BUCKET) && GriefPrevention.instance.creativeRulesApply(dispenseEvent.getBlock().getLocation()) && toClaim == null)
{
dispenseEvent.setCancelled(true);
return;
}
//wilderness to wilderness is OK
if(fromClaim == null && toClaim == null) return;
//within claim is OK
if(fromClaim == toClaim) return;
//everything else is NOT OK
dispenseEvent.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onTreeGrow (StructureGrowEvent growEvent)
{
//only take these potentially expensive steps if configured to do so
if(!GriefPrevention.instance.config_limitTreeGrowth) return;
//don't track in worlds where claims are not enabled
if(!GriefPrevention.instance.claimsEnabledForWorld(growEvent.getWorld())) return;
Location rootLocation = growEvent.getLocation();
Claim rootClaim = this.dataStore.getClaimAt(rootLocation, false, null);
String rootOwnerName = null;
//who owns the spreading block, if anyone?
if(rootClaim != null)
{
//tree growth in subdivisions is dependent on who owns the top level claim
if(rootClaim.parent != null) rootClaim = rootClaim.parent;
//if an administrative claim, just let the tree grow where it wants
if(rootClaim.isAdminClaim()) return;
//otherwise, note the owner of the claim
rootOwnerName = rootClaim.getOwnerName();
}
//for each block growing
for(int i = 0; i < growEvent.getBlocks().size(); i++)
{
BlockState block = growEvent.getBlocks().get(i);
Claim blockClaim = this.dataStore.getClaimAt(block.getLocation(), false, rootClaim);
//if it's growing into a claim
if(blockClaim != null)
{
//if there's no owner for the new tree, or the owner for the new tree is different from the owner of the claim
if(rootOwnerName == null || !rootOwnerName.equals(blockClaim.getOwnerName()))
{
growEvent.getBlocks().remove(i--);
}
}
}
}
@EventHandler(ignoreCancelled = true)
public void onInventoryPickupItem (InventoryPickupItemEvent event)
{
//prevent hoppers from picking-up items dropped by players on death
InventoryHolder holder = event.getInventory().getHolder();
if(holder instanceof HopperMinecart || holder instanceof Hopper)
{
Item item = event.getItem();
List<MetadataValue> data = item.getMetadata("GP_ITEMOWNER");
//if this is marked as belonging to a player
if(data != null && data.size() > 0)
{
//don't allow the pickup
event.setCancelled(true);
}
}
}
}
@@ -0,0 +1,38 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.Location;
import org.bukkit.Material;
//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 Material typeId;
public byte data;
public BlockSnapshot(Location location, Material typeId, byte data)
{
this.location = location;
this.typeId = typeId;
this.data = data;
}
}
@@ -0,0 +1,39 @@
/*
GriefPrevention Server Plugin for Minecraft
Copyright (C) 2016 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.Bukkit;
//sends a message to all online players
//used to send delayed messages, for example a quit message after the player has been gone a while
class BroadcastMessageTask implements Runnable
{
private String message;
public BroadcastMessageTask(String message)
{
this.message = message;
}
@Override
public void run()
{
Bukkit.getServer().broadcastMessage(this.message);
}
}
@@ -0,0 +1,60 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.scheduler.BukkitRunnable;
//players can be "trapped" in a portal frame if they don't have permission to break
//solid blocks blocking them from exiting the frame
//if that happens, we detect the problem and send them back through the portal.
class CheckForPortalTrapTask extends BukkitRunnable
{
GriefPrevention instance;
//player who recently teleported via nether portal
private Player player;
//where to send the player back to if he hasn't left the portal frame
private Location returnLocation;
public CheckForPortalTrapTask(Player player, GriefPrevention plugin, Location locationToReturn)
{
this.player = player;
this.instance = plugin;
this.returnLocation = locationToReturn;
player.setMetadata("GP_PORTALRESCUE", new FixedMetadataValue(instance, locationToReturn));
}
@Override
public void run()
{
if(player.isOnline() && player.getPortalCooldown() >= 10)
{
instance.AddLogEntry("Rescued " + player.getName() + " from a nether portal.\nTeleported from " + player.getLocation().toString() + " to " + ((Location)player.getMetadata("GP_PORTALRESCUE").get(0).value()).toString(), CustomLogEntryTypes.Debug);
player.teleport(returnLocation);
player.removeMetadata("GP_PORTALRESCUE", instance);
}
instance.portalReturnTaskMap.remove(player.getUniqueId());
}
}
@@ -0,0 +1,969 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.*;
import org.bukkit.*;
import org.bukkit.World.Environment;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
//represents a player claim
//creating an instance doesn't make an effective claim
//only claims which have been added to the datastore have any effect
public class Claim
{
//two locations, which together define the boundaries of the claim
//note that the upper Y value is always ignored, because claims ALWAYS extend up to the sky
Location lesserBoundaryCorner;
Location greaterBoundaryCorner;
//modification date. this comes from the file timestamp during load, and is updated with runtime changes
public Date modifiedDate;
//id number. unique to this claim, never changes.
Long id = null;
//ownerID. for admin claims, this is NULL
//use getOwnerName() to get a friendly name (will be "an administrator" for admin claims)
public UUID ownerID;
//list of players who (beyond the claim owner) have permission to grant permissions in this claim
public ArrayList<String> managers = new ArrayList<String>();
//permissions for this claim, see ClaimPermission class
private HashMap<String, ClaimPermission> playerIDToClaimPermissionMap = new HashMap<String, ClaimPermission>();
//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;
public boolean areExplosivesAllowed = false;
//parent claim
//only used for claim subdivisions. top level claims have null here
public Claim parent = null;
// intended for subclaims - they inherit no permissions
private boolean inheritNothing = false;
//children (subdivisions)
//note subdivisions themselves never have children
public ArrayList<Claim> children = new ArrayList<Claim>();
//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()
{
if(this.parent != null) return this.parent.isAdminClaim();
return (this.ownerID == null);
}
//accessor for ID
public Long getID()
{
return this.id;
}
//basic constructor, just notes the creation time
//see above declarations for other defaults
Claim()
{
this.modifiedDate = Calendar.getInstance().getTime();
}
//players may only siege someone when he's not in an admin claim
//and when he has some level of permission in the claim
public boolean canSiege(Player defender)
{
if(this.isAdminClaim()) return false;
if(this.allowAccess(defender) != null) return false;
return true;
}
//removes any lava above sea level in a claim
//exclusionClaim is another claim indicating an sub-area to be excluded from this operation
//it may be null
public void removeSurfaceFluids(Claim exclusionClaim)
{
//don't do this for administrative claims
if(this.isAdminClaim()) return;
//don't do it for very large claims
if(this.getArea() > 10000) return;
//only in creative mode worlds
if(!GriefPrevention.instance.creativeRulesApply(this.lesserBoundaryCorner)) return;
Location lesser = this.getLesserBoundaryCorner();
Location greater = this.getGreaterBoundaryCorner();
if(lesser.getWorld().getEnvironment() == Environment.NETHER) return; //don't clean up lava in the nether
int seaLevel = 0; //clean up all fluids in the end
//respect sea level in normal worlds
if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = GriefPrevention.instance.getSeaLevel(lesser.getWorld());
for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++)
{
for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++)
{
for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++)
{
//dodge the exclusion claim
Block block = lesser.getWorld().getBlockAt(x, y, z);
if(exclusionClaim != null && exclusionClaim.contains(block.getLocation(), true, false)) continue;
if(block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.WATER || block.getType() == Material.STATIONARY_WATER || block.getType() == Material.LAVA)
{
block.setType(Material.AIR);
}
}
}
}
}
//determines whether or not a claim has surface lava
//used to warn players when they abandon their claims about automatic fluid cleanup
boolean hasSurfaceFluids()
{
Location lesser = this.getLesserBoundaryCorner();
Location greater = this.getGreaterBoundaryCorner();
//don't bother for very large claims, too expensive
if(this.getArea() > 10000) return false;
int seaLevel = 0; //clean up all fluids in the end
//respect sea level in normal worlds
if(lesser.getWorld().getEnvironment() == Environment.NORMAL) seaLevel = GriefPrevention.instance.getSeaLevel(lesser.getWorld());
for(int x = lesser.getBlockX(); x <= greater.getBlockX(); x++)
{
for(int z = lesser.getBlockZ(); z <= greater.getBlockZ(); z++)
{
for(int y = seaLevel - 1; y <= lesser.getWorld().getMaxHeight(); y++)
{
//dodge the exclusion claim
Block block = lesser.getWorld().getBlockAt(x, y, z);
if(block.getType() == Material.STATIONARY_LAVA || block.getType() == Material.WATER || block.getType() == Material.STATIONARY_WATER || block.getType() == Material.LAVA)
{
return true;
}
}
}
}
return false;
}
//main constructor. note that only creating a claim instance does nothing - a claim must be added to the data store to be effective
Claim(Location lesserBoundaryCorner, Location greaterBoundaryCorner, UUID ownerID, List<String> builderIDs, List<String> containerIDs, List<String> accessorIDs, List<String> managerIDs, boolean inheritNothing, Long id)
{
//modification date
this.modifiedDate = Calendar.getInstance().getTime();
//id
this.id = id;
//store corners
this.lesserBoundaryCorner = lesserBoundaryCorner;
this.greaterBoundaryCorner = greaterBoundaryCorner;
//owner
this.ownerID = ownerID;
//other permissions
for(String builderID : builderIDs)
{
if(builderID != null && !builderID.isEmpty())
{
this.playerIDToClaimPermissionMap.put(builderID, ClaimPermission.Build);
}
}
for(String containerID : containerIDs)
{
if(containerID != null && !containerID.isEmpty())
{
this.playerIDToClaimPermissionMap.put(containerID, ClaimPermission.Inventory);
}
}
for(String accessorID : accessorIDs)
{
if(accessorID != null && !accessorID.isEmpty())
{
this.playerIDToClaimPermissionMap.put(accessorID, ClaimPermission.Access);
}
}
for(String managerID : managerIDs)
{
if(managerID != null && !managerID.isEmpty())
{
this.managers.add(managerID);
}
}
this.inheritNothing = inheritNothing;
}
Claim(Location lesserBoundaryCorner, Location greaterBoundaryCorner, UUID ownerID, List<String> builderIDs, List<String> containerIDs, List<String> accessorIDs, List<String> managerIDs, Long id)
{
this(lesserBoundaryCorner, greaterBoundaryCorner, ownerID, builderIDs, containerIDs, accessorIDs, managerIDs, false, id);
}
//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;
}
public boolean getSubclaimRestrictions()
{
return inheritNothing;
}
public void setSubclaimRestrictions(boolean inheritNothing)
{
this.inheritNothing = inheritNothing;
}
//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),
null, new ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>(), null);
return claim.contains(location, false, true);
}
//permissions. note administrative "public" claims have different rules than other claims
//all of these return NULL when a player has permission, or a String error message when the player doesn't have permission
public String allowEdit(Player player)
{
//if we don't know who's asking, always say no (i've been told some mods can make this happen somehow)
if(player == null) return "";
//special cases...
//admin claims need adminclaims permission only.
if(this.isAdminClaim())
{
if(player.hasPermission("griefprevention.adminclaims")) return null;
}
//anyone with deleteclaims permission can modify non-admin claims at any time
else
{
if(player.hasPermission("griefprevention.deleteclaims")) return null;
}
//no resizing, deleting, and so forth while under siege
if(player.getUniqueId().equals(this.ownerID))
{
if(this.siegeData != null)
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NoModifyDuringSiege);
}
//otherwise, owners can do whatever
return null;
}
//permission inheritance for subdivisions
if(this.parent != null)
{
if (player.getUniqueId().equals(this.parent.ownerID))
return null;
if (!inheritNothing)
return this.parent.allowEdit(player);
}
//error message if all else fails
return GriefPrevention.instance.dataStore.getMessage(Messages.OnlyOwnersModifyClaims, this.getOwnerName());
}
private List<Material> placeableFarmingBlocksList = Arrays.asList(
Material.PUMPKIN_STEM,
Material.CROPS,
Material.MELON_STEM,
Material.CARROT,
Material.POTATO,
Material.NETHER_WARTS,
Material.BEETROOT_BLOCK);
private boolean placeableForFarming(Material material)
{
return this.placeableFarmingBlocksList.contains(material);
}
//build permission check
public String allowBuild(Player player, Material material)
{
//if we don't know who's asking, always say no (i've been told some mods can make this happen somehow)
if(player == null) return "";
//when a player tries to build in a claim, if he's under siege, the siege may extend to include the new claim
GriefPrevention.instance.dataStore.tryExtendSiege(player, this);
//admin claims can always be modified by admins, no exceptions
if(this.isAdminClaim())
{
if(player.hasPermission("griefprevention.adminclaims")) return null;
}
//no building while under siege
if(this.siegeData != null)
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildUnderSiege, this.siegeData.attacker.getName());
}
//no building while in pvp combat
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId());
if(playerData.inPvpCombat())
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPvP);
}
//owners can make changes, or admins with ignore claims mode enabled
if(player.getUniqueId().equals(this.ownerID) || GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()).ignoreClaims) return null;
//anyone with explicit build permission can make changes
if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null;
//also everyone is a member of the "public", so check for public permission
ClaimPermission permissionLevel = this.playerIDToClaimPermissionMap.get("public");
if(ClaimPermission.Build == permissionLevel) return null;
//allow for farming with /containertrust permission
if(this.allowContainers(player) == null)
{
//do allow for farming, if player has /containertrust permission
if(this.placeableForFarming(material))
{
return null;
}
}
//subdivision permission inheritance
if(this.parent != null)
{
if (player.getUniqueId().equals(this.parent.ownerID))
return null;
if (!inheritNothing)
return this.parent.allowBuild(player, material);
}
//failure message for all other cases
String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoBuildPermission, this.getOwnerName());
if(player.hasPermission("griefprevention.ignoreclaims"))
reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
return reason;
}
private boolean hasExplicitPermission(Player player, ClaimPermission level)
{
String playerID = player.getUniqueId().toString();
Set<String> keys = this.playerIDToClaimPermissionMap.keySet();
Iterator<String> iterator = keys.iterator();
while(iterator.hasNext())
{
String identifier = iterator.next();
if(playerID.equalsIgnoreCase(identifier) && this.playerIDToClaimPermissionMap.get(identifier) == level) return true;
else if(identifier.startsWith("[") && identifier.endsWith("]"))
{
//drop the brackets
String permissionIdentifier = identifier.substring(1, identifier.length() - 1);
//defensive coding
if(permissionIdentifier == null || permissionIdentifier.isEmpty()) continue;
//check permission
if(player.hasPermission(permissionIdentifier) && this.playerIDToClaimPermissionMap.get(identifier) == level) return true;
}
}
return false;
}
//break permission check
public String allowBreak(Player player, Material material)
{
//if under siege, some blocks will be breakable
if(this.siegeData != null || this.doorsOpen)
{
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 == material)
{
breakable = true;
break;
}
}
//custom error messages for siege mode
if(!breakable)
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NonSiegeMaterial);
}
else if(player.getUniqueId().equals(this.ownerID))
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NoOwnerBuildUnderSiege);
}
else
{
return null;
}
}
//if not under siege, build rules apply
return this.allowBuild(player, material);
}
//access permission check
public String allowAccess(Player player)
{
//following a siege where the defender lost, the claim will allow everyone access for a time
if(this.doorsOpen) return null;
//admin claims need adminclaims permission only.
if(this.isAdminClaim())
{
if(player.hasPermission("griefprevention.adminclaims")) return null;
}
//claim owner and admins in ignoreclaims mode have access
if(player.getUniqueId().equals(this.ownerID) || GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()).ignoreClaims) return null;
//look for explicit individual access, inventory, or build permission
if(this.hasExplicitPermission(player, ClaimPermission.Access)) return null;
if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null;
if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null;
//also check for public permission
ClaimPermission permissionLevel = this.playerIDToClaimPermissionMap.get("public");
if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel || ClaimPermission.Access == permissionLevel) return null;
//permission inheritance for subdivisions
if(this.parent != null)
{
if (player.getUniqueId().equals(this.parent.ownerID))
return null;
if (!inheritNothing)
return this.parent.allowAccess(player);
}
//catch-all error message for all other cases
String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoAccessPermission, this.getOwnerName());
if(player.hasPermission("griefprevention.ignoreclaims"))
reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
return reason;
}
//inventory permission check
public String allowContainers(Player player)
{
//if we don't know who's asking, always say no (i've been told some mods can make this happen somehow)
if(player == null) return "";
//trying to access inventory in a claim may extend an existing siege to include this claim
GriefPrevention.instance.dataStore.tryExtendSiege(player, this);
//if under siege, nobody accesses containers
if(this.siegeData != null)
{
return GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersSiege, siegeData.attacker.getName());
}
//owner and administrators in ignoreclaims mode have access
if(player.getUniqueId().equals(this.ownerID) || GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()).ignoreClaims) return null;
//admin claims need adminclaims permission only.
if(this.isAdminClaim())
{
if(player.hasPermission("griefprevention.adminclaims")) return null;
}
//check for explicit individual container or build permission
if(this.hasExplicitPermission(player, ClaimPermission.Inventory)) return null;
if(this.hasExplicitPermission(player, ClaimPermission.Build)) return null;
//check for public container or build permission
ClaimPermission permissionLevel = this.playerIDToClaimPermissionMap.get("public");
if(ClaimPermission.Build == permissionLevel || ClaimPermission.Inventory == permissionLevel) return null;
//permission inheritance for subdivisions
if(this.parent != null)
{
if (player.getUniqueId().equals(this.parent.ownerID))
return null;
if (!inheritNothing)
return this.parent.allowContainers(player);
}
//error message for all other cases
String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoContainersPermission, this.getOwnerName());
if(player.hasPermission("griefprevention.ignoreclaims"))
reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
return reason;
}
//grant permission check, relatively simple
public String allowGrantPermission(Player player)
{
//if we don't know who's asking, always say no (i've been told some mods can make this happen somehow)
if(player == null) return "";
//anyone who can modify the claim can do this
if(this.allowEdit(player) == null) return null;
//anyone who's in the managers (/PermissionTrust) list can do this
for(int i = 0; i < this.managers.size(); i++)
{
String managerID = this.managers.get(i);
if(player.getUniqueId().toString().equals(managerID)) return null;
else if(managerID.startsWith("[") && managerID.endsWith("]"))
{
managerID = managerID.substring(1, managerID.length() - 1);
if(managerID == null || managerID.isEmpty()) continue;
if(player.hasPermission(managerID)) return null;
}
}
//permission inheritance for subdivisions
if(this.parent != null)
{
if (player.getUniqueId().equals(this.parent.ownerID))
return null;
if (!inheritNothing)
return this.parent.allowGrantPermission(player);
}
//generic error message
String reason = GriefPrevention.instance.dataStore.getMessage(Messages.NoPermissionTrust, this.getOwnerName());
if(player.hasPermission("griefprevention.ignoreclaims"))
reason += " " + GriefPrevention.instance.dataStore.getMessage(Messages.IgnoreClaimsAdvertisement);
return reason;
}
//grants a permission for a player or the public
public void setPermission(String playerID, ClaimPermission permissionLevel)
{
this.playerIDToClaimPermissionMap.put(playerID.toLowerCase(), permissionLevel);
}
//revokes a permission for a player or the public
public void dropPermission(String playerID)
{
this.playerIDToClaimPermissionMap.remove(playerID.toLowerCase());
for(Claim child : this.children)
{
child.dropPermission(playerID);
}
}
//clears all permissions (except owner of course)
public void clearPermissions()
{
this.playerIDToClaimPermissionMap.clear();
this.managers.clear();
for(Claim child : this.children)
{
child.clearPermissions();
}
}
//gets ALL permissions
//useful for making copies of permissions during a claim resize and listing all permissions in a claim
public void getPermissions(ArrayList<String> builders, ArrayList<String> containers, ArrayList<String> accessors, ArrayList<String> managers)
{
//loop through all the entries in the hash map
Iterator<Map.Entry<String, ClaimPermission>> mappingsIterator = this.playerIDToClaimPermissionMap.entrySet().iterator();
while(mappingsIterator.hasNext())
{
Map.Entry<String, ClaimPermission> 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.ownerID == null)
return GriefPrevention.instance.dataStore.getMessage(Messages.OwnerNameForAdminClaims);
return GriefPrevention.lookupPlayerName(this.ownerID);
}
//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;
double x = location.getX();
double y = location.getY();
double z = location.getZ();
//main check
boolean inClaim = (ignoreHeight || y >= this.lesserBoundaryCorner.getY()) &&
x >= this.lesserBoundaryCorner.getX() &&
x < this.greaterBoundaryCorner.getX() + 1 &&
z >= this.lesserBoundaryCorner.getZ() &&
z < this.greaterBoundaryCorner.getZ() + 1;
if(!inClaim) return false;
//additional check for subdivisions
//you're only in a subdivision when you're also in its parent claim
//NOTE: if a player creates subdivions then resizes the parent claim, it's possible that
//a subdivision can reach outside of its parent's boundaries. so this check is important!
if(this.parent != null)
{
return this.parent.contains(location, ignoreHeight, false);
}
//code to exclude subdivisions in this check
else if(excludeSubdivisions)
{
//search all subdivisions to see if the location is in any of them
for(int i = 0; i < this.children.size(); i++)
{
//if we find such a subdivision, return false
if(this.children.get(i).contains(location, ignoreHeight, true))
{
return false;
}
}
}
//otherwise yes
return true;
}
//whether or not two claims overlap
//used internally to prevent overlaps when creating claims
boolean overlaps(Claim otherClaim)
{
//NOTE: if trying to understand this makes your head hurt, don't feel bad - it hurts mine too.
//try drawing pictures to visualize test cases.
if(!this.lesserBoundaryCorner.getWorld().equals(otherClaim.getLesserBoundaryCorner().getWorld())) return false;
//first, check the corners of this claim aren't inside any existing claims
if(otherClaim.contains(this.lesserBoundaryCorner, true, false)) return true;
if(otherClaim.contains(this.greaterBoundaryCorner, true, false)) return true;
if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.lesserBoundaryCorner.getBlockX(), 0, this.greaterBoundaryCorner.getBlockZ()), true, false)) return true;
if(otherClaim.contains(new Location(this.lesserBoundaryCorner.getWorld(), this.greaterBoundaryCorner.getBlockX(), 0, this.lesserBoundaryCorner.getBlockZ()), true, false)) return true;
//verify that no claim's lesser boundary point is inside this new claim, to cover the "existing claim is entirely inside new claim" case
if(this.contains(otherClaim.getLesserBoundaryCorner(), true, false)) return true;
//verify this claim doesn't band across an existing claim, either horizontally or vertically
if( this.getLesserBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() &&
this.getLesserBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() &&
this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() &&
this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() )
return true;
if( this.getGreaterBoundaryCorner().getBlockZ() <= otherClaim.getGreaterBoundaryCorner().getBlockZ() &&
this.getGreaterBoundaryCorner().getBlockZ() >= otherClaim.getLesserBoundaryCorner().getBlockZ() &&
this.getLesserBoundaryCorner().getBlockX() < otherClaim.getLesserBoundaryCorner().getBlockX() &&
this.getGreaterBoundaryCorner().getBlockX() > otherClaim.getGreaterBoundaryCorner().getBlockX() )
return true;
if( this.getLesserBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() &&
this.getLesserBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() &&
this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() &&
this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() )
return true;
if( this.getGreaterBoundaryCorner().getBlockX() <= otherClaim.getGreaterBoundaryCorner().getBlockX() &&
this.getGreaterBoundaryCorner().getBlockX() >= otherClaim.getLesserBoundaryCorner().getBlockX() &&
this.getLesserBoundaryCorner().getBlockZ() < otherClaim.getLesserBoundaryCorner().getBlockZ() &&
this.getGreaterBoundaryCorner().getBlockZ() > otherClaim.getGreaterBoundaryCorner().getBlockZ() )
return true;
return false;
}
//whether more entities may be added to a claim
public String allowMoreEntities(boolean remove)
{
if(this.parent != null) return this.parent.allowMoreEntities(remove);
//this rule only applies to creative mode worlds
if(!GriefPrevention.instance.creativeRulesApply(this.getLesserBoundaryCorner())) return null;
//admin claims aren't restricted
if(this.isAdminClaim()) return null;
//don't apply this rule to very large claims
if(this.getArea() > 10000) return null;
//determine maximum allowable entity count, based on claim size
int maxEntities = this.getArea() / 50;
if(maxEntities == 0) return GriefPrevention.instance.dataStore.getMessage(Messages.ClaimTooSmallForEntities);
//count current entities (ignoring players)
int totalEntities = 0;
ArrayList<Chunk> chunks = this.getChunks();
for(Chunk chunk : chunks)
{
Entity [] entities = chunk.getEntities();
for(int i = 0; i < entities.length; i++)
{
Entity entity = entities[i];
if(!(entity instanceof Player) && this.contains(entity.getLocation(), false, false))
{
totalEntities++;
if(remove && totalEntities > maxEntities) entity.remove();
}
}
}
if(totalEntities >= maxEntities) return GriefPrevention.instance.dataStore.getMessage(Messages.TooManyEntitiesInClaim);
return null;
}
public String allowMoreActiveBlocks()
{
if(this.parent != null) return this.parent.allowMoreActiveBlocks();
//determine maximum allowable entity count, based on claim size
int maxActives = this.getArea() / 100;
if(maxActives == 0) return GriefPrevention.instance.dataStore.getMessage(Messages.ClaimTooSmallForActiveBlocks);
//count current actives
int totalActives = 0;
ArrayList<Chunk> chunks = this.getChunks();
for(Chunk chunk : chunks)
{
BlockState [] actives = chunk.getTileEntities();
for(int i = 0; i < actives.length; i++)
{
BlockState active = actives[i];
if(BlockEventHandler.isActiveBlock(active))
{
if(this.contains(active.getLocation(), false, false))
{
totalActives++;
}
}
}
}
if(totalActives >= maxActives) return GriefPrevention.instance.dataStore.getMessage(Messages.TooManyActiveBlocksInClaim);
return null;
}
//implements a strict ordering of claims, used to keep the claims collection sorted for faster searching
boolean greaterThan(Claim otherClaim)
{
Location thisCorner = this.getLesserBoundaryCorner();
Location otherCorner = otherClaim.getLesserBoundaryCorner();
if(thisCorner.getBlockX() > otherCorner.getBlockX()) return true;
if(thisCorner.getBlockX() < otherCorner.getBlockX()) return false;
if(thisCorner.getBlockZ() > otherCorner.getBlockZ()) return true;
if(thisCorner.getBlockZ() < otherCorner.getBlockZ()) return false;
return thisCorner.getWorld().getName().compareTo(otherCorner.getWorld().getName()) < 0;
}
@SuppressWarnings("deprecation")
long getPlayerInvestmentScore()
{
//decide which blocks will be considered player placed
Location lesserBoundaryCorner = this.getLesserBoundaryCorner();
ArrayList<Material> playerBlocks = RestoreNatureProcessingTask.getPlayerBlocks(lesserBoundaryCorner.getWorld().getEnvironment(), lesserBoundaryCorner.getBlock().getBiome());
//scan the claim for player placed blocks
double score = 0;
boolean creativeMode = GriefPrevention.instance.creativeRulesApply(lesserBoundaryCorner);
for(int x = this.lesserBoundaryCorner.getBlockX(); x <= this.greaterBoundaryCorner.getBlockX(); x++)
{
for(int z = this.lesserBoundaryCorner.getBlockZ(); z <= this.greaterBoundaryCorner.getBlockZ(); z++)
{
int y = this.lesserBoundaryCorner.getBlockY();
for(; y < GriefPrevention.instance.getSeaLevel(this.lesserBoundaryCorner.getWorld()) - 5; y++)
{
Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z);
if(playerBlocks.contains(block.getType()))
{
if(block.getType() == Material.CHEST && !creativeMode)
{
score += 10;
}
else
{
score += .5;
}
}
}
for(; y < this.lesserBoundaryCorner.getWorld().getMaxHeight(); y++)
{
Block block = this.lesserBoundaryCorner.getWorld().getBlockAt(x, y, z);
if(playerBlocks.contains(block.getType()))
{
if(block.getType() == Material.CHEST && !creativeMode)
{
score += 10;
}
else if(creativeMode && (block.getType() == Material.LAVA || block.getType() == Material.STATIONARY_LAVA))
{
score -= 10;
}
else
{
score += 1;
}
}
}
}
}
return (long)score;
}
public ArrayList<Chunk> getChunks()
{
ArrayList<Chunk> chunks = new ArrayList<Chunk>();
World world = this.getLesserBoundaryCorner().getWorld();
Chunk lesserChunk = this.getLesserBoundaryCorner().getChunk();
Chunk greaterChunk = this.getGreaterBoundaryCorner().getChunk();
for(int x = lesserChunk.getX(); x <= greaterChunk.getX(); x++)
{
for(int z = lesserChunk.getZ(); z <= greaterChunk.getZ(); z++)
{
chunks.add(world.getChunkAt(x, z));
}
}
return chunks;
}
ArrayList<Long> getChunkHashes()
{
ArrayList<Long> hashes = new ArrayList<Long>();
int smallX = this.getLesserBoundaryCorner().getBlockX() >> 4;
int smallZ = this.getLesserBoundaryCorner().getBlockZ() >> 4;
int largeX = this.getGreaterBoundaryCorner().getBlockX() >> 4;
int largeZ = this.getGreaterBoundaryCorner().getBlockZ() >> 4;
for(int x = smallX; x <= largeX; x++)
{
for(int z = smallZ; z <= largeZ; z++)
{
hashes.add(DataStore.getChunkHash(x, z));
}
}
return hashes;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
//basic enum stuff
public enum ClaimPermission
{
Build,
Inventory,
Access
}
@@ -0,0 +1,9 @@
package me.ryanhamshire.GriefPrevention;
public enum ClaimsMode
{
Survival,
Creative,
Disabled,
SurvivalRequiringClaims
}
@@ -0,0 +1,61 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
//asynchronously loads player data without caching it in the datastore, then
//passes those data to a claim cleanup task which might decide to delete a claim for inactivity
class CleanupUnusedClaimPreTask implements Runnable
{
private Claim claim = null;
CleanupUnusedClaimPreTask(Claim claim)
{
this.claim = claim;
}
@Override
public void run()
{
//get the data
PlayerData ownerData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(claim.ownerID);
OfflinePlayer ownerInfo = Bukkit.getServer().getOfflinePlayer(claim.ownerID);
//expiration code uses last logout timestamp to decide whether to expire claims
//don't expire claims for online players
if(ownerInfo.isOnline()) return;
if(ownerInfo.getLastPlayed() <= 0) return;
GriefPrevention.AddLogEntry("Looking for expired claims. Checking data for " + claim.ownerID.toString(), CustomLogEntryTypes.Debug, true);
//skip claims belonging to exempted players based on block totals in config
int bonusBlocks = ownerData.getBonusClaimBlocks();
if(bonusBlocks >= GriefPrevention.instance.config_claims_expirationExemptionBonusBlocks || bonusBlocks + ownerData.getAccruedClaimBlocks() >= GriefPrevention.instance.config_claims_expirationExemptionTotalBlocks)
{
GriefPrevention.AddLogEntry("Player exempt from claim expiration based on claim block counts vs. config file settings.", CustomLogEntryTypes.Debug, true);
return;
}
//pass it back to the main server thread, where it's safe to delete a claim if needed
Bukkit.getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new CleanupUnusedClaimTask(claim, ownerData, ownerInfo), 1L);
}
}
@@ -0,0 +1,137 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.Calendar;
import java.util.Date;
import java.util.Vector;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import me.ryanhamshire.GriefPrevention.events.ClaimExpirationEvent;
class CleanupUnusedClaimTask implements Runnable
{
Claim claim;
PlayerData ownerData;
OfflinePlayer ownerInfo;
CleanupUnusedClaimTask(Claim claim, PlayerData ownerData, OfflinePlayer ownerInfo)
{
this.claim = claim;
this.ownerData = ownerData;
this.ownerInfo = ownerInfo;
}
@Override
public void run()
{
//see if any other plugins don't want this claim deleted
ClaimExpirationEvent event = new ClaimExpirationEvent(this.claim);
Bukkit.getPluginManager().callEvent(event);
if(event.isCancelled()) return;
//determine area of the default chest claim
int areaOfDefaultClaim = 0;
if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius >= 0)
{
areaOfDefaultClaim = (int)Math.pow(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius * 2 + 1, 2);
}
//if this claim is a chest claim and those are set to expire
if(claim.getArea() <= areaOfDefaultClaim && GriefPrevention.instance.config_claims_chestClaimExpirationDays > 0)
{
//if the owner has been gone at least a week, and if he has ONLY the new player claim, it will be removed
Calendar sevenDaysAgo = Calendar.getInstance();
sevenDaysAgo.add(Calendar.DATE, -GriefPrevention.instance.config_claims_chestClaimExpirationDays);
boolean newPlayerClaimsExpired = sevenDaysAgo.getTime().after(new Date(ownerInfo.getLastPlayed()));
if(newPlayerClaimsExpired && ownerData.getClaims().size() == 1)
{
claim.removeSurfaceFluids(null);
GriefPrevention.instance.dataStore.deleteClaim(claim, true, true);
//if configured to do so, restore the land to natural
if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()) || GriefPrevention.instance.config_claims_survivalAutoNatureRestoration)
{
GriefPrevention.instance.restoreClaim(claim, 0);
}
GriefPrevention.AddLogEntry(" " + claim.getOwnerName() + "'s new player claim expired.", CustomLogEntryTypes.AdminActivity);
}
}
//if configured to always remove claims after some inactivity period without exceptions...
else if(GriefPrevention.instance.config_claims_expirationDays > 0)
{
Calendar earliestPermissibleLastLogin = Calendar.getInstance();
earliestPermissibleLastLogin.add(Calendar.DATE, -GriefPrevention.instance.config_claims_expirationDays);
if(earliestPermissibleLastLogin.getTime().after(new Date(ownerInfo.getLastPlayed())))
{
//make a copy of this player's claim list
Vector<Claim> claims = new Vector<Claim>();
for(int i = 0; i < ownerData.getClaims().size(); i++)
{
claims.add(ownerData.getClaims().get(i));
}
//delete them
GriefPrevention.instance.dataStore.deleteClaimsForPlayer(claim.ownerID, true);
GriefPrevention.AddLogEntry(" All of " + claim.getOwnerName() + "'s claims have expired.", CustomLogEntryTypes.AdminActivity);
for(int i = 0; i < claims.size(); i++)
{
//if configured to do so, restore the land to natural
if(GriefPrevention.instance.creativeRulesApply(claims.get(i).getLesserBoundaryCorner()) || GriefPrevention.instance.config_claims_survivalAutoNatureRestoration)
{
GriefPrevention.instance.restoreClaim(claims.get(i), 0);
}
}
}
}
else if(GriefPrevention.instance.config_claims_unusedClaimExpirationDays > 0 && GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))
{
//avoid scanning large claims and administrative claims
if(claim.isAdminClaim() || claim.getWidth() > 25 || claim.getHeight() > 25) return;
//otherwise scan the claim content
int minInvestment = 400;
long investmentScore = claim.getPlayerInvestmentScore();
if(investmentScore < minInvestment)
{
//if the owner has been gone at least a week, and if he has ONLY the new player claim, it will be removed
Calendar sevenDaysAgo = Calendar.getInstance();
sevenDaysAgo.add(Calendar.DATE, -GriefPrevention.instance.config_claims_unusedClaimExpirationDays);
boolean claimExpired = sevenDaysAgo.getTime().after(new Date(ownerInfo.getLastPlayed()));
if(claimExpired)
{
GriefPrevention.instance.dataStore.deleteClaim(claim, true, true);
GriefPrevention.AddLogEntry("Removed " + claim.getOwnerName() + "'s unused claim @ " + GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()), CustomLogEntryTypes.AdminActivity);
//restore the claim area to natural state
GriefPrevention.instance.restoreClaim(claim, 0);
}
}
}
}
}
@@ -0,0 +1,8 @@
package me.ryanhamshire.GriefPrevention;
enum CommandCategory
{
Chat,
Whisper,
None
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
@@ -0,0 +1,29 @@
/*
GriefPrevention Server Plugin for Minecraft
Copyright (C) 2015 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;
public enum CustomLogEntryTypes
{
SocialActivity,
SuspiciousActivity,
AdminActivity,
Debug,
Exception,
MutedChat
}
@@ -0,0 +1,183 @@
/*
GriefPrevention Server Plugin for Minecraft
Copyright (C) 2015 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 java.io.File;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bukkit.scheduler.BukkitScheduler;
import com.google.common.io.Files;
class CustomLogger
{
private final SimpleDateFormat timestampFormat = new SimpleDateFormat("HH:mm");
private final SimpleDateFormat filenameFormat = new SimpleDateFormat("yyyy_MM_dd");
private final String logFolderPath = DataStore.dataLayerFolderPath + File.separator + "Logs";
private final int secondsBetweenWrites = 300;
//stringbuilder is not thread safe, stringbuffer is
private StringBuffer queuedEntries = new StringBuffer();
CustomLogger()
{
//ensure log folder exists
File logFolder = new File(this.logFolderPath);
logFolder.mkdirs();
//delete any outdated log files immediately
this.DeleteExpiredLogs();
//unless disabled, schedule recurring tasks
int daysToKeepLogs = GriefPrevention.instance.config_logs_daysToKeep;
if(daysToKeepLogs > 0)
{
BukkitScheduler scheduler = GriefPrevention.instance.getServer().getScheduler();
final long ticksPerSecond = 20L;
final long ticksPerDay = ticksPerSecond * 60 * 60 * 24;
scheduler.runTaskTimerAsynchronously(GriefPrevention.instance, new EntryWriter(), this.secondsBetweenWrites * ticksPerSecond, this.secondsBetweenWrites * ticksPerSecond);
scheduler.runTaskTimerAsynchronously(GriefPrevention.instance, new ExpiredLogRemover(), ticksPerDay, ticksPerDay);
}
}
private static final Pattern inlineFormatterPattern = Pattern.compile("§.");
void AddEntry(String entry, CustomLogEntryTypes entryType)
{
//if disabled, do nothing
int daysToKeepLogs = GriefPrevention.instance.config_logs_daysToKeep;
if(daysToKeepLogs == 0) return;
//if entry type is not enabled, do nothing
if(!this.isEnabledType(entryType)) return;
//otherwise write to the in-memory buffer, after removing formatters
Matcher matcher = inlineFormatterPattern.matcher(entry);
entry = matcher.replaceAll("");
String timestamp = this.timestampFormat.format(new Date());
this.queuedEntries.append(timestamp + " " + entry + "\n");
}
private boolean isEnabledType(CustomLogEntryTypes entryType)
{
if(entryType == CustomLogEntryTypes.Exception) return true;
if(entryType == CustomLogEntryTypes.SocialActivity && !GriefPrevention.instance.config_logs_socialEnabled) return false;
if(entryType == CustomLogEntryTypes.SuspiciousActivity && !GriefPrevention.instance.config_logs_suspiciousEnabled) return false;
if(entryType == CustomLogEntryTypes.AdminActivity && !GriefPrevention.instance.config_logs_adminEnabled) return false;
if(entryType == CustomLogEntryTypes.Debug && !GriefPrevention.instance.config_logs_debugEnabled) return false;
if(entryType == CustomLogEntryTypes.MutedChat && !GriefPrevention.instance.config_logs_mutedChatEnabled) return false;
return true;
}
void WriteEntries()
{
try
{
//if nothing to write, stop here
if(this.queuedEntries.length() == 0) return;
//determine filename based on date
String filename = this.filenameFormat.format(new Date()) + ".log";
String filepath = this.logFolderPath + File.separator + filename;
File logFile = new File(filepath);
//dump content
Files.append(this.queuedEntries.toString(), logFile, Charset.forName("UTF-8"));
//in case of a failure to write the above due to exception,
//the unwritten entries will remain the buffer for the next write to retry
this.queuedEntries.setLength(0);
}
catch(Exception e)
{
e.printStackTrace();
}
}
private void DeleteExpiredLogs()
{
try
{
//get list of log files
File logFolder = new File(this.logFolderPath);
File [] files = logFolder.listFiles();
//delete any created before x days ago
int daysToKeepLogs = GriefPrevention.instance.config_logs_daysToKeep;
Calendar expirationBoundary = Calendar.getInstance();
expirationBoundary.add(Calendar.DATE, -daysToKeepLogs);
for(int i = 0; i < files.length; i++)
{
File file = files[i];
if(file.isDirectory()) continue; //skip any folders
String filename = file.getName().replace(".log", "");
String [] dateParts = filename.split("_"); //format is yyyy_MM_dd
if(dateParts.length != 3) continue;
try
{
int year = Integer.parseInt(dateParts[0]);
int month = Integer.parseInt(dateParts[1]) - 1;
int day = Integer.parseInt(dateParts[2]);
Calendar filedate = Calendar.getInstance();
filedate.set(year, month, day);
if(filedate.before(expirationBoundary))
{
file.delete();
}
}
catch(NumberFormatException e)
{
//throw this away - effectively ignoring any files without the correct filename format
GriefPrevention.AddLogEntry("Ignoring an unexpected file in the abridged logs folder: " + file.getName(), CustomLogEntryTypes.Debug, true);
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
//transfers the internal buffer to a log file
private class EntryWriter implements Runnable
{
@Override
public void run()
{
WriteEntries();
}
}
private class ExpiredLogRemover implements Runnable
{
@Override
public void run()
{
DeleteExpiredLogs();
}
}
}
@@ -0,0 +1,33 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
public class CustomizableMessage
{
public Messages id;
public String text;
public String notes;
public CustomizableMessage(Messages id, String text, String notes)
{
this.id = id;
this.text = text;
this.notes = notes;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,737 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import org.bukkit.*;
//manages data stored in the file system
public class DatabaseDataStore extends DataStore
{
private Connection databaseConnection = null;
private String databaseUrl;
private String userName;
private String password;
private String updateNameSQL;
private String insertClaimSQL;
private String deleteClaimSQL;
private String getPlayerDataSQL;
private String deletePlayerDataSQL;
private String insertPlayerDataSQL;
private String insertNextClaimIdSQL;
private String deleteGroupBonusSQL;
private String insertSchemaVerSQL;
private String deleteNextClaimIdSQL;
private String deleteSchemaVersionSQL;
private String selectSchemaVersionSQL;
DatabaseDataStore(String url, String userName, String password) throws Exception
{
this.databaseUrl = url;
this.userName = userName;
this.password = password;
this.initialize();
}
@Override
void initialize() throws Exception
{
try
{
//load the java driver for mySQL
Class.forName("com.mysql.jdbc.Driver");
}
catch(Exception e)
{
GriefPrevention.AddLogEntry("ERROR: Unable to load Java's mySQL database driver. Check to make sure you've installed it properly.");
throw e;
}
try
{
this.refreshDataConnection();
}
catch(Exception e2)
{
GriefPrevention.AddLogEntry("ERROR: Unable to connect to database. Check your config file settings.");
throw e2;
}
try
{
//ensure the data tables exist
Statement statement = databaseConnection.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_nextclaimid (nextid INT(15));");
statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_claimdata (id INT(15), owner VARCHAR(50), lessercorner VARCHAR(100), greatercorner VARCHAR(100), builders TEXT, containers TEXT, accessors TEXT, managers TEXT, inheritnothing BOOLEAN, parentid INT(15));");
statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_playerdata (name VARCHAR(50), lastlogin DATETIME, accruedblocks INT(15), bonusblocks INT(15));");
statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_schemaversion (version INT(15));");
statement.execute("ALTER TABLE griefprevention_claimdata MODIFY builders TEXT;");
statement.execute("ALTER TABLE griefprevention_claimdata MODIFY containers TEXT;");
statement.execute("ALTER TABLE griefprevention_claimdata MODIFY accessors TEXT;");
statement.execute("ALTER TABLE griefprevention_claimdata MODIFY managers TEXT;");
//if the next claim id table is empty, this is a brand new database which will write using the latest schema
//otherwise, schema version is determined by schemaversion table (or =0 if table is empty, see getSchemaVersion())
ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_nextclaimid;");
if(!results.next())
{
this.setSchemaVersion(latestSchemaVersion);
}
}
catch(Exception e3)
{
GriefPrevention.AddLogEntry("ERROR: Unable to create the necessary database table. Details:");
GriefPrevention.AddLogEntry(e3.getMessage());
e3.printStackTrace();
throw e3;
}
this.updateNameSQL = "UPDATE griefprevention_playerdata SET name = ? WHERE name = ?;";
this.insertClaimSQL = "INSERT INTO griefprevention_claimdata (id, owner, lessercorner, greatercorner, builders, containers, accessors, managers, inheritnothing, parentid) VALUES(?,?,?,?,?,?,?,?,?,?);";
this.deleteClaimSQL = "DELETE FROM griefprevention_claimdata WHERE id=?;";
this.getPlayerDataSQL = "SELECT * FROM griefprevention_playerdata WHERE name=?;";
this.deletePlayerDataSQL = "DELETE FROM griefprevention_playerdata WHERE name=?;";
this.insertPlayerDataSQL = "INSERT INTO griefprevention_playerdata (name, lastlogin, accruedblocks, bonusblocks) VALUES (?,?,?,?);";
this.insertNextClaimIdSQL = "INSERT INTO griefprevention_nextclaimid VALUES (?);";
this.deleteGroupBonusSQL = "DELETE FROM griefprevention_playerdata WHERE name=?;";
this.insertSchemaVerSQL = "INSERT INTO griefprevention_schemaversion VALUES (?)";
this.deleteNextClaimIdSQL = "DELETE FROM griefprevention_nextclaimid;";
this.deleteSchemaVersionSQL = "DELETE FROM griefprevention_schemaversion;";
this.selectSchemaVersionSQL = "SELECT * FROM griefprevention_schemaversion;";
//load group data into memory
Statement statement = databaseConnection.createStatement();
ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_playerdata;");
while(results.next())
{
String name = results.getString("name");
//ignore non-groups. all group names start with a dollar sign.
if(!name.startsWith("$")) continue;
String groupName = name.substring(1);
if(groupName == null || groupName.isEmpty()) continue; //defensive coding, avoid unlikely cases
int groupBonusBlocks = results.getInt("bonusblocks");
this.permissionToBonusBlocksMap.put(groupName, groupBonusBlocks);
}
//load next claim number into memory
results = statement.executeQuery("SELECT * FROM griefprevention_nextclaimid;");
//if there's nothing yet, add it
if(!results.next())
{
statement.execute("INSERT INTO griefprevention_nextclaimid VALUES(0);");
this.nextClaimID = (long)0;
}
//otherwise load it
else
{
this.nextClaimID = results.getLong("nextid");
}
if(this.getSchemaVersion() == 0)
{
try
{
this.refreshDataConnection();
//pull ALL player data from the database
statement = this.databaseConnection.createStatement();
results = statement.executeQuery("SELECT * FROM griefprevention_playerdata;");
//make a list of changes to be made
HashMap<String, UUID> changes = new HashMap<String, UUID>();
ArrayList<String> namesToConvert = new ArrayList<String>();
while(results.next())
{
//get the id
String playerName = results.getString("name");
//add to list of names to convert to UUID
namesToConvert.add(playerName);
}
//resolve and cache as many as possible through various means
try
{
UUIDFetcher fetcher = new UUIDFetcher(namesToConvert);
fetcher.call();
}
catch(Exception e)
{
GriefPrevention.AddLogEntry("Failed to resolve a batch of names to UUIDs. Details:" + e.getMessage());
e.printStackTrace();
}
//reset results cursor
results.beforeFirst();
//for each result
while(results.next())
{
//get the id
String playerName = results.getString("name");
//try to convert player name to UUID
try
{
UUID playerID = UUIDFetcher.getUUIDOf(playerName);
//if successful, update the playerdata row by replacing the player's name with the player's UUID
if(playerID != null)
{
changes.put(playerName, playerID);
}
}
//otherwise leave it as-is. no harm done - it won't be requested by name, and this update only happens once.
catch(Exception ex){ }
}
//refresh data connection in case data migration took a long time
this.refreshDataConnection();
for(String name : changes.keySet())
{
try (PreparedStatement updateStmnt = this.databaseConnection.prepareStatement(this.getUpdateNameSQL())) {
updateStmnt.setString(1, changes.get(name).toString());
updateStmnt.setString(2, name);
updateStmnt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to convert player data for " + name + ". Skipping.");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to convert player data. Details:");
GriefPrevention.AddLogEntry(e.getMessage());
e.printStackTrace();
}
}
if(this.getSchemaVersion() <= 2)
{
statement = this.databaseConnection.createStatement();
statement.execute("ALTER TABLE griefprevention_claimdata ADD inheritNothing BOOLEAN DEFAULT 0 AFTER managers;");
}
//load claims data into memory
results = statement.executeQuery("SELECT * FROM griefprevention_claimdata;");
ArrayList<Claim> claimsToRemove = new ArrayList<Claim>();
ArrayList<Claim> subdivisionsToLoad = new ArrayList<Claim>();
List<World> validWorlds = Bukkit.getServer().getWorlds();
Long claimID = null;
while(results.next())
{
try
{
//problematic claims will be removed from secondary storage, and never added to in-memory data store
boolean removeClaim = false;
long parentId = results.getLong("parentid");
claimID = results.getLong("id");
boolean inheritNothing = results.getBoolean("inheritNothing");
Location lesserBoundaryCorner = null;
Location greaterBoundaryCorner = null;
String lesserCornerString = "(location not available)";
try
{
lesserCornerString = results.getString("lessercorner");
lesserBoundaryCorner = this.locationFromString(lesserCornerString, validWorlds);
String greaterCornerString = results.getString("greatercorner");
greaterBoundaryCorner = this.locationFromString(greaterCornerString, validWorlds);
}
catch(Exception e)
{
if(e.getMessage() != null && e.getMessage().contains("World not found"))
{
GriefPrevention.AddLogEntry("Failed to load a claim (ID:" + claimID.toString() + ") because its world isn't loaded (yet?). Please delete the claim or contact the GriefPrevention developer with information about which plugin(s) you're using to load or create worlds. " + lesserCornerString);
continue;
}
else
{
throw e;
}
}
String ownerName = results.getString("owner");
UUID ownerID = null;
if(ownerName.isEmpty() || ownerName.startsWith("--"))
{
ownerID = null; //administrative land claim or subdivision
}
else if(this.getSchemaVersion() < 1)
{
try
{
ownerID = UUIDFetcher.getUUIDOf(ownerName);
}
catch(Exception ex)
{
GriefPrevention.AddLogEntry("This owner name did not convert to a UUID: " + ownerName + ".");
GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString());
}
}
else
{
try
{
ownerID = UUID.fromString(ownerName);
}
catch(Exception ex)
{
GriefPrevention.AddLogEntry("This owner entry is not a UUID: " + ownerName + ".");
GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString());
}
}
String buildersString = results.getString("builders");
List<String> builderNames = Arrays.asList(buildersString.split(";"));
builderNames = this.convertNameListToUUIDList(builderNames);
String containersString = results.getString("containers");
List<String> containerNames = Arrays.asList(containersString.split(";"));
containerNames = this.convertNameListToUUIDList(containerNames);
String accessorsString = results.getString("accessors");
List<String> accessorNames = Arrays.asList(accessorsString.split(";"));
accessorNames = this.convertNameListToUUIDList(accessorNames);
String managersString = results.getString("managers");
List<String> managerNames = Arrays.asList(managersString.split(";"));
managerNames = this.convertNameListToUUIDList(managerNames);
Claim claim = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, ownerID, builderNames, containerNames, accessorNames, managerNames, inheritNothing, claimID);
if(removeClaim)
{
claimsToRemove.add(claim);
}
else if(parentId == -1)
{
//top level claim
this.addClaim(claim, false);
}
else
{
//subdivision
subdivisionsToLoad.add(claim);
}
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to load a claim. Details: " + e.getMessage() + " ... " + results.toString());
e.printStackTrace();
}
}
//add subdivisions to their parent claims
for(Claim childClaim : subdivisionsToLoad)
{
//find top level claim parent
Claim topLevelClaim = this.getClaimAt(childClaim.getLesserBoundaryCorner(), true, null);
if(topLevelClaim == null)
{
claimsToRemove.add(childClaim);
GriefPrevention.AddLogEntry("Removing orphaned claim subdivision: " + childClaim.getLesserBoundaryCorner().toString());
continue;
}
//add this claim to the list of children of the current top level claim
childClaim.parent = topLevelClaim;
topLevelClaim.children.add(childClaim);
childClaim.inDataStore = true;
}
for(Claim claim : claimsToRemove)
{
this.deleteClaimFromSecondaryStorage(claim);
}
if(this.getSchemaVersion() <= 2)
{
this.refreshDataConnection();
statement = this.databaseConnection.createStatement();
statement.execute("DELETE FROM griefprevention_claimdata WHERE id='-1';");
}
super.initialize();
}
@Override
synchronized void writeClaimToStorage(Claim claim) //see datastore.cs. this will ALWAYS be a top level claim
{
try
{
this.refreshDataConnection();
//wipe out any existing data about this claim
this.deleteClaimFromSecondaryStorage(claim);
//write claim data to the database
this.writeClaimData(claim);
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to save data for claim at " + this.locationToString(claim.lesserBoundaryCorner) + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
//actually writes claim data to the database
synchronized private void writeClaimData(Claim claim) throws SQLException
{
String lesserCornerString = this.locationToString(claim.getLesserBoundaryCorner());
String greaterCornerString = this.locationToString(claim.getGreaterBoundaryCorner());
String owner = "";
if(claim.ownerID != null) owner = claim.ownerID.toString();
ArrayList<String> builders = new ArrayList<String>();
ArrayList<String> containers = new ArrayList<String>();
ArrayList<String> accessors = new ArrayList<String>();
ArrayList<String> managers = new ArrayList<String>();
claim.getPermissions(builders, containers, accessors, managers);
String buildersString = this.storageStringBuilder(builders);
String containersString = this.storageStringBuilder(containers);
String accessorsString = this.storageStringBuilder(accessors);
String managersString = this.storageStringBuilder(managers);
boolean inheritNothing = claim.getSubclaimRestrictions();
long parentId = claim.parent == null ? -1 : claim.parent.id;
try (PreparedStatement insertStmt = this.databaseConnection.prepareStatement(this.getInsertClaimSQL())) {
insertStmt.setLong(1, claim.id);
insertStmt.setString(2, owner);
insertStmt.setString(3, lesserCornerString);
insertStmt.setString(4, greaterCornerString);
insertStmt.setString(5, buildersString);
insertStmt.setString(6, containersString);
insertStmt.setString(7, accessorsString);
insertStmt.setString(8, managersString);
insertStmt.setBoolean(9, inheritNothing);
insertStmt.setLong(10, parentId);
insertStmt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to save data for claim at " + this.locationToString(claim.lesserBoundaryCorner) + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
//deletes a claim from the database
@Override
synchronized void deleteClaimFromSecondaryStorage(Claim claim)
{
try(PreparedStatement deleteStmnt = this.databaseConnection.prepareStatement(this.getDeleteClaimSQL())) {
deleteStmnt.setLong(1, claim.id);
deleteStmnt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to delete data for claim " + claim.id + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
e.printStackTrace();
}
}
@Override
synchronized PlayerData getPlayerDataFromStorage(UUID playerID)
{
PlayerData playerData = new PlayerData();
playerData.playerID = playerID;
try (PreparedStatement selectStmnt = this.databaseConnection.prepareStatement( this.getGetPlayerDataSQL()))
{
selectStmnt.setString(1, playerID.toString());
ResultSet results = selectStmnt.executeQuery();
//if data for this player exists, use it
if(results.next())
{
playerData.setAccruedClaimBlocks(results.getInt("accruedblocks"));
playerData.setBonusClaimBlocks(results.getInt("bonusblocks"));
}
}
catch(SQLException e)
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(playerID + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
return playerData;
}
//saves changes to player data. MUST be called after you're done making changes, otherwise a reload will lose them
@Override
public void overrideSavePlayerData(UUID playerID, PlayerData playerData)
{
//never save data for the "administrative" account. an empty string for player name indicates administrative account
if(playerID == null) return;
this.savePlayerData(playerID.toString(), playerData);
}
private void savePlayerData(String playerID, PlayerData playerData)
{
try (PreparedStatement deleteStmnt = this.databaseConnection.prepareStatement(this.getDeletePlayerDataSQL());
PreparedStatement insertStmnt = this.databaseConnection.prepareStatement(this.getInsertPlayerDataSQL())) {
OfflinePlayer player = Bukkit.getOfflinePlayer(UUID.fromString(playerID));
SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = sqlFormat.format(new Date(player.getLastPlayed()));
deleteStmnt.setString(1, playerID);
deleteStmnt.executeUpdate();
insertStmnt.setString(1, playerID);
insertStmnt.setString(2, dateString);
insertStmnt.setInt(3, playerData.getAccruedClaimBlocks());
insertStmnt.setInt(4, playerData.getBonusClaimBlocks());
insertStmnt.executeUpdate();
}
catch(SQLException e)
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(playerID + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
}
@Override
synchronized void incrementNextClaimID()
{
this.setNextClaimID(this.nextClaimID + 1);
}
//sets the next claim ID. used by incrementNextClaimID() above, and also while migrating data from a flat file data store
synchronized void setNextClaimID(long nextID)
{
this.nextClaimID = nextID;
try (PreparedStatement deleteStmnt = this.databaseConnection.prepareStatement(this.getDeleteNextClaimIdSQL());
PreparedStatement insertStmnt = this.databaseConnection.prepareStatement(this.getInsertNextClaimIdSQL())) {
deleteStmnt.execute();
insertStmnt.setLong(1, nextID);
insertStmnt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to set next claim ID to " + nextID + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
//updates the database with a group's bonus blocks
@Override
synchronized void saveGroupBonusBlocks(String groupName, int currentValue)
{
//group bonus blocks are stored in the player data table, with player name = $groupName
try (PreparedStatement deleteStmnt = this.databaseConnection.prepareStatement(this.getDeleteGroupBonusSQL());
PreparedStatement insertStmnt = this.databaseConnection.prepareStatement(this.getInsertPlayerDataSQL())) {
SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = sqlFormat.format(new Date());
deleteStmnt.setString(1, '$' + groupName);
deleteStmnt.executeUpdate();
insertStmnt.setString(1, '$' + groupName);
insertStmnt.setString(2, dateString);
insertStmnt.setInt(3, 0);
insertStmnt.setInt(4, currentValue);
insertStmnt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to save data for group " + groupName + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
@Override
synchronized void close()
{
if(this.databaseConnection != null)
{
try
{
if(!this.databaseConnection.isClosed())
{
this.databaseConnection.close();
}
}
catch(SQLException e){};
}
this.databaseConnection = null;
}
private synchronized void refreshDataConnection() throws SQLException
{
if(this.databaseConnection == null || !this.databaseConnection.isValid(3))
{
if(this.databaseConnection != null && !this.databaseConnection.isClosed())
{
this.databaseConnection.close();
}
//set username/pass properties
Properties connectionProps = new Properties();
connectionProps.put("user", this.userName);
connectionProps.put("password", this.password);
connectionProps.put("autoReconnect", "true");
connectionProps.put("maxReconnects", String.valueOf(Integer.MAX_VALUE));
//establish connection
this.databaseConnection = DriverManager.getConnection(this.databaseUrl, connectionProps);
}
}
@Override
protected int getSchemaVersionFromStorage()
{
try (PreparedStatement selectStmnt = this.databaseConnection.prepareStatement(this.getSelectSchemaVersionSQL())) {
ResultSet results = selectStmnt.executeQuery();
//if there's nothing yet, assume 0 and add it
if(!results.next())
{
this.setSchemaVersion(0);
return 0;
}
//otherwise return the value that's in the table
else
{
return results.getInt("version");
}
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to retrieve schema version from database. Details:");
GriefPrevention.AddLogEntry(e.getMessage());
e.printStackTrace();
return 0;
}
}
@Override
protected void updateSchemaVersionInStorage(int versionToSet)
{
try (PreparedStatement deleteStmnt = this.databaseConnection.prepareStatement(this.getDeleteSchemaVersionSQL());
PreparedStatement insertStmnt = this.databaseConnection.prepareStatement(this.getInsertSchemaVerSQL())) {
deleteStmnt.execute();
insertStmnt.setInt(1, versionToSet);
insertStmnt.executeUpdate();
}
catch(SQLException e)
{
GriefPrevention.AddLogEntry("Unable to set next schema version to " + versionToSet + ". Details:");
GriefPrevention.AddLogEntry(e.getMessage());
}
}
/**
* Concats an array to a string divided with the ; sign
* @param input Arraylist with strings to concat
* @return String with all values from input array
*/
private String storageStringBuilder(ArrayList<String> input) {
String output = "";
for(String string : input) {
output += string + ";";
}
return output;
}
public String getUpdateNameSQL() {
return updateNameSQL;
}
public String getInsertClaimSQL() {
return insertClaimSQL;
}
public String getDeleteClaimSQL() {
return deleteClaimSQL;
}
public String getGetPlayerDataSQL() {
return getPlayerDataSQL;
}
public String getDeletePlayerDataSQL() {
return deletePlayerDataSQL;
}
public String getInsertPlayerDataSQL() {
return insertPlayerDataSQL;
}
public String getInsertNextClaimIdSQL() {
return insertNextClaimIdSQL;
}
public String getDeleteGroupBonusSQL() {
return deleteGroupBonusSQL;
}
public String getInsertSchemaVerSQL() {
return insertSchemaVerSQL;
}
public String getDeleteNextClaimIdSQL() {
return deleteNextClaimIdSQL;
}
public String getDeleteSchemaVersionSQL() {
return deleteSchemaVersionSQL;
}
public String getSelectSchemaVersionSQL() {
return selectSchemaVersionSQL;
}
}
@@ -0,0 +1,128 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.Collection;
import me.ryanhamshire.GriefPrevention.events.AccrueClaimBlocksEvent;
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
{
private Player player;
private GriefPrevention instance;
private int idleThresholdSquared;
public DeliverClaimBlocksTask(Player player, GriefPrevention instance)
{
this.player = player;
this.instance = instance;
this.idleThresholdSquared = instance.config_claims_accruedIdleThreshold * instance.config_claims_accruedIdleThreshold;
}
@Override
public void run()
{
//if no player specified, this task will create a player-specific task for each online player, scheduled one tick apart
if(this.player == null)
{
@SuppressWarnings("unchecked")
Collection<Player> players = (Collection<Player>)GriefPrevention.instance.getServer().getOnlinePlayers();
long i = 0;
for(Player onlinePlayer : players)
{
DeliverClaimBlocksTask newTask = new DeliverClaimBlocksTask(onlinePlayer, instance);
instance.getServer().getScheduler().scheduleSyncDelayedTask(instance, newTask, i++);
}
return; //tasks started for each player
}
//deliver claim blocks to the specified player
if(!this.player.isOnline())
{
return; //player is not online to receive claim blocks
}
DataStore dataStore = instance.dataStore;
PlayerData playerData = dataStore.getPlayerData(player.getUniqueId());
// check if player is idle. considered idle if
// in vehicle or is in water (pushed by water)
// or has not moved at least defined blocks since last check
boolean isIdle = false;
try
{
isIdle = player.isInsideVehicle() || player.getLocation().getBlock().isLiquid() ||
!(playerData.lastAfkCheckLocation == null || playerData.lastAfkCheckLocation.distanceSquared(player.getLocation()) > idleThresholdSquared);
}
catch(IllegalArgumentException ignore) //can't measure distance when to/from are different worlds
{
}
//remember current location for next time
playerData.lastAfkCheckLocation = player.getLocation();
try
{
//determine how fast blocks accrue for this player //RoboMWM: addons determine this instead
int accrualRate = instance.config_claims_blocksAccruedPerHour_default;
//determine idle accrual rate when idle
if (isIdle)
{
if (instance.config_claims_accruedIdlePercent <= 0)
{
GriefPrevention.AddLogEntry(player.getName() + " wasn't active enough to accrue claim blocks this round.", CustomLogEntryTypes.Debug, true);
return; //idle accrual percentage is disabled
}
accrualRate = (int) (accrualRate * (instance.config_claims_accruedIdlePercent / 100.0D));
}
//fire event for addons
AccrueClaimBlocksEvent event = new AccrueClaimBlocksEvent(player, accrualRate, isIdle);
instance.getServer().getPluginManager().callEvent(event);
if (event.isCancelled())
{
GriefPrevention.AddLogEntry(player.getName() + " claim block delivery was canceled by another plugin.", CustomLogEntryTypes.Debug, true);
return; //event was cancelled
}
//set actual accrual
accrualRate = event.getBlocksToAccrue();
if (accrualRate < 0) accrualRate = 0;
playerData.accrueBlocks(accrualRate);
GriefPrevention.AddLogEntry("Delivering " + event.getBlocksToAccrue() + " blocks to " + player.getName(), CustomLogEntryTypes.Debug, true);
//intentionally NOT saving data here to reduce overall secondary storage access frequency
//many other operations will cause this player's data to save, including his eventual logout
//dataStore.savePlayerData(player.getUniqueIdentifier(), playerData);
}
catch(Exception e)
{
GriefPrevention.AddLogEntry("Problem delivering claim blocks to player " + player.getName() + ":");
e.printStackTrace();
}
}
}
@@ -0,0 +1,149 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Boat;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Vehicle;
//FEATURE: creative mode worlds get a regular entity cleanup
//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 EntityCleanupTask implements Runnable
{
//where to start cleaning in the list of entities
private double percentageStart;
public EntityCleanupTask(double percentageStart)
{
this.percentageStart = percentageStart;
}
@Override
public void run()
{
ArrayList<World> worlds = new ArrayList<World>();
for(World world : GriefPrevention.instance.getServer().getWorlds())
{
if(GriefPrevention.instance.config_claims_worldModes.get(world) == ClaimsMode.Creative)
{
worlds.add(world);
}
}
for(int i = 0; i < worlds.size(); i++)
{
World world = worlds.get(i);
List<Entity> entities = world.getEntities();
//starting and stopping point. each execution of the task scans 10% of the server's (loaded) entities
int j = (int)(entities.size() * this.percentageStart);
int k = (int)(entities.size() * (this.percentageStart + .1));
Claim cachedClaim = null;
for(; j < entities.size() && j < k; j++)
{
Entity entity = entities.get(j);
boolean remove = false;
if(entity instanceof Boat) //boats must be occupied
{
Boat boat = (Boat)entity;
if(boat.isEmpty()) remove = true;
}
else if(entity instanceof Vehicle)
{
Vehicle vehicle = (Vehicle)entity;
//minecarts in motion must be occupied by a player
if(vehicle.getVelocity().lengthSquared() != 0)
{
if(vehicle.isEmpty() || !(vehicle.getPassenger() instanceof Player))
{
remove = true;
}
}
//stationary carts must be on rails
else
{
Material material = world.getBlockAt(vehicle.getLocation()).getType();
if(material != Material.RAILS && material != Material.POWERED_RAIL && material != Material.DETECTOR_RAIL)
{
remove = true;
}
}
}
//all non-player entities must be in claims
else if(!(entity instanceof Player))
{
Claim claim = GriefPrevention.instance.dataStore.getClaimAt(entity.getLocation(), false, cachedClaim);
if(claim != null)
{
cachedClaim = claim;
}
else
{
remove = true;
}
}
if(remove)
{
entity.remove();
}
}
}
//starting and stopping point. each execution of the task scans 5% of the server's claims
List<Claim> claims = GriefPrevention.instance.dataStore.claims;
int j = (int)(claims.size() * this.percentageStart);
int k = (int)(claims.size() * (this.percentageStart + .05));
for(; j < claims.size() && j < k; j++)
{
Claim claim = claims.get(j);
//if it's a creative mode claim
if(GriefPrevention.instance.creativeRulesApply(claim.getLesserBoundaryCorner()))
{
//check its entity count and remove any extras
claim.allowMoreEntities(true);
}
}
//schedule the next run of this task, in 3 minutes (20L is approximately 1 second)
double nextRunPercentageStart = this.percentageStart + .05;
if(nextRunPercentageStart > .99)
{
nextRunPercentageStart = 0;
}
EntityCleanupTask task = new EntityCleanupTask(nextRunPercentageStart);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task, 20L * 60 * 1);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
//tells a player about how many claim blocks he has, etc
//implemented as a task so that it can be delayed
//otherwise, it's spammy when players mouse-wheel past the shovel in their hot bars
class EquipShovelProcessingTask implements Runnable
{
//player data
private Player player;
public EquipShovelProcessingTask(Player player)
{
this.player = player;
}
@Override
public void run()
{
//if he's not holding the golden shovel anymore, do nothing
if(GriefPrevention.instance.getItemInHand(player, EquipmentSlot.HAND).getType() != GriefPrevention.instance.config_claims_modificationTool) return;
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId());
//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, Messages.ShovelBasicClaimMode);
}
//tell him how many claim blocks he has available
int remainingBlocks = playerData.getRemainingClaimBlocks();
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.RemainingBlocks, String.valueOf(remainingBlocks));
//link to a video demo of land claiming, based on world type
if(GriefPrevention.instance.creativeRulesApply(player.getLocation()))
{
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.CreativeBasicsVideo2, DataStore.CREATIVE_VIDEO_URL);
}
else if(GriefPrevention.instance.claimsEnabledForWorld(player.getLocation().getWorld()))
{
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SurvivalBasicsVideo2, DataStore.SURVIVAL_VIDEO_URL);
}
//if standing in a claim owned by the player, visualize it
Claim claim = GriefPrevention.instance.dataStore.getClaimAt(player.getLocation(), true, playerData.lastClaim);
if(claim != null && claim.allowEdit(player) == null)
{
playerData.lastClaim = claim;
Visualization.Apply(player, Visualization.FromClaim(claim, player.getEyeLocation().getBlockY(), VisualizationType.Claim, player.getLocation()));
}
}
}
@@ -0,0 +1,66 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.Random;
import org.bukkit.Bukkit;
//FEATURE: automatically remove claims owned by inactive players which:
//...aren't protecting much OR
//...are a free new player claim (and the player has no other claims) OR
//...because the player has been gone a REALLY long time, and that expiration has been configured in config.yml
//runs every 1 minute in the main thread
class FindUnusedClaimsTask implements Runnable
{
int nextClaimIndex;
FindUnusedClaimsTask()
{
//start scanning in a random spot
if(GriefPrevention.instance.dataStore.claims.size() == 0)
{
this.nextClaimIndex = 0;
}
else
{
Random randomNumberGenerator = new Random();
this.nextClaimIndex = randomNumberGenerator.nextInt(GriefPrevention.instance.dataStore.claims.size());
}
}
@Override
public void run()
{
//don't do anything when there are no claims
if(GriefPrevention.instance.dataStore.claims.size() == 0) return;
//wrap search around to beginning
if(this.nextClaimIndex >= GriefPrevention.instance.dataStore.claims.size()) this.nextClaimIndex = 0;
//decide which claim to check next
Claim claim = GriefPrevention.instance.dataStore.claims.get(this.nextClaimIndex++);
//skip administrative claims
if(claim.isAdminClaim()) return;
Bukkit.getScheduler().runTaskAsynchronously(GriefPrevention.instance, new CleanupUnusedClaimPreTask(claim));
}
}
@@ -0,0 +1,931 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.io.*;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import org.bukkit.*;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import com.google.common.io.Files;
//manages data stored in the file system
public class FlatFileDataStore extends DataStore
{
private final static String claimDataFolderPath = dataLayerFolderPath + File.separator + "ClaimData";
private final static String nextClaimIdFilePath = claimDataFolderPath + File.separator + "_nextClaimID";
private final static String schemaVersionFilePath = dataLayerFolderPath + File.separator + "_schemaVersion";
static boolean hasData()
{
File claimsDataFolder = new File(claimDataFolderPath);
return claimsDataFolder.exists();
}
//initialization!
FlatFileDataStore() throws Exception
{
this.initialize();
}
@Override
void initialize() throws Exception
{
//ensure data folders exist
boolean newDataStore = false;
File playerDataFolder = new File(playerDataFolderPath);
File claimDataFolder = new File(claimDataFolderPath);
if(!playerDataFolder.exists() || !claimDataFolder.exists())
{
newDataStore = true;
playerDataFolder.mkdirs();
claimDataFolder.mkdirs();
}
//if there's no data yet, then anything written will use the schema implemented by this code
if(newDataStore)
{
this.setSchemaVersion(DataStore.latestSchemaVersion);
}
//load group data into memory
File [] files = playerDataFolder.listFiles();
for(int i = 0; i < files.length; i++)
{
File file = files[i];
if(!file.isFile()) continue; //avoids folders
//all group data files start with a dollar sign. ignoring the rest, which are player data files.
if(!file.getName().startsWith("$")) continue;
String groupName = file.getName().substring(1);
if(groupName == null || groupName.isEmpty()) continue; //defensive coding, avoid unlikely cases
BufferedReader inStream = null;
try
{
inStream = new BufferedReader(new FileReader(file.getAbsolutePath()));
String line = inStream.readLine();
int groupBonusBlocks = Integer.parseInt(line);
this.permissionToBonusBlocksMap.put(groupName, groupBonusBlocks);
}
catch(Exception e)
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(errors.toString(), CustomLogEntryTypes.Exception);
}
try
{
if(inStream != null) inStream.close();
}
catch(IOException exception) {}
}
//load next claim number from file
File nextClaimIdFile = new File(nextClaimIdFilePath);
if(nextClaimIdFile.exists())
{
BufferedReader inStream = null;
try
{
inStream = new BufferedReader(new FileReader(nextClaimIdFile.getAbsolutePath()));
//read the id
String line = inStream.readLine();
//try to parse into a long value
this.nextClaimID = Long.parseLong(line);
}
catch(Exception e){ }
try
{
if(inStream != null) inStream.close();
}
catch(IOException exception) {}
}
//if converting up from schema version 0, rename player data files using UUIDs instead of player names
//get a list of all the files in the claims data folder
if(this.getSchemaVersion() == 0)
{
files = playerDataFolder.listFiles();
ArrayList<String> namesToConvert = new ArrayList<String>();
for(File playerFile : files)
{
namesToConvert.add(playerFile.getName());
}
//resolve and cache as many as possible through various means
try
{
UUIDFetcher fetcher = new UUIDFetcher(namesToConvert);
fetcher.call();
}
catch(Exception e)
{
GriefPrevention.AddLogEntry("Failed to resolve a batch of names to UUIDs. Details:" + e.getMessage());
e.printStackTrace();
}
//rename files
for(File playerFile : files)
{
String currentFilename = playerFile.getName();
//if corrected casing and a record already exists using the correct casing, skip this one
String correctedCasing = UUIDFetcher.correctedNames.get(currentFilename);
if(correctedCasing != null && !currentFilename.equals(correctedCasing))
{
File correctedCasingFile = new File(playerDataFolder.getPath() + File.separator + correctedCasing);
if(correctedCasingFile.exists())
{
continue;
}
}
//try to convert player name to UUID
UUID playerID = null;
try
{
playerID = UUIDFetcher.getUUIDOf(currentFilename);
//if successful, rename the file using the UUID
if(playerID != null)
{
playerFile.renameTo(new File(playerDataFolder, playerID.toString()));
}
}
catch(Exception ex){ }
}
}
//load claims data into memory
//get a list of all the files in the claims data folder
files = claimDataFolder.listFiles();
if(this.getSchemaVersion() <= 1)
{
this.loadClaimData_Legacy(files);
}
else
{
this.loadClaimData(files);
}
super.initialize();
}
void loadClaimData_Legacy(File [] files) throws Exception
{
List<World> validWorlds = Bukkit.getServer().getWorlds();
for(int i = 0; i < files.length; i++)
{
if(files[i].isFile()) //avoids folders
{
//skip any file starting with an underscore, to avoid special files not representing land claims
if(files[i].getName().startsWith("_")) continue;
//the filename is the claim ID. try to parse it
long claimID;
try
{
claimID = Long.parseLong(files[i].getName());
}
//because some older versions used a different file name pattern before claim IDs were introduced,
//those files need to be "converted" by renaming them to a unique ID
catch(Exception e)
{
claimID = this.nextClaimID;
this.incrementNextClaimID();
File newFile = new File(claimDataFolderPath + File.separator + String.valueOf(this.nextClaimID));
files[i].renameTo(newFile);
files[i] = newFile;
}
BufferedReader inStream = null;
String lesserCornerString = "";
try
{
Claim topLevelClaim = null;
inStream = new BufferedReader(new FileReader(files[i].getAbsolutePath()));
String line = inStream.readLine();
while(line != null)
{
//skip any SUB:### lines from previous versions
if(line.toLowerCase().startsWith("sub:"))
{
line = inStream.readLine();
}
//skip any UUID lines from previous versions
Matcher match = uuidpattern.matcher(line.trim());
if(match.find())
{
line = inStream.readLine();
}
//first line is lesser boundary corner location
lesserCornerString = line;
Location lesserBoundaryCorner = this.locationFromString(lesserCornerString, validWorlds);
//second line is greater boundary corner location
line = inStream.readLine();
Location greaterBoundaryCorner = this.locationFromString(line, validWorlds);
//third line is owner name
line = inStream.readLine();
String ownerName = line;
UUID ownerID = null;
if(ownerName.isEmpty() || ownerName.startsWith("--"))
{
ownerID = null; //administrative land claim or subdivision
}
else if(this.getSchemaVersion() == 0)
{
try
{
ownerID = UUIDFetcher.getUUIDOf(ownerName);
}
catch(Exception ex)
{
GriefPrevention.AddLogEntry("Couldn't resolve this name to a UUID: " + ownerName + ".");
GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString());
}
}
else
{
try
{
ownerID = UUID.fromString(ownerName);
}
catch(Exception ex)
{
GriefPrevention.AddLogEntry("Error - this is not a valid UUID: " + ownerName + ".");
GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString());
}
}
//fourth line is list of builders
line = inStream.readLine();
List<String> builderNames = Arrays.asList(line.split(";"));
builderNames = this.convertNameListToUUIDList(builderNames);
//fifth line is list of players who can access containers
line = inStream.readLine();
List<String> containerNames = Arrays.asList(line.split(";"));
containerNames = this.convertNameListToUUIDList(containerNames);
//sixth line is list of players who can use buttons and switches
line = inStream.readLine();
List<String> accessorNames = Arrays.asList(line.split(";"));
accessorNames = this.convertNameListToUUIDList(accessorNames);
//seventh line is list of players who can grant permissions
line = inStream.readLine();
if(line == null) line = "";
List<String> managerNames = Arrays.asList(line.split(";"));
managerNames = this.convertNameListToUUIDList(managerNames);
//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, ownerID, builderNames, containerNames, accessorNames, managerNames, claimID);
topLevelClaim.modifiedDate = new Date(files[i].lastModified());
this.addClaim(topLevelClaim, false);
}
//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, null, builderNames, containerNames, accessorNames, managerNames, null);
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();
}
//if there's any problem with the file's content, log an error message and skip it
catch(Exception e)
{
if(e.getMessage() != null && e.getMessage().contains("World not found"))
{
GriefPrevention.AddLogEntry("Failed to load a claim " + files[i].getName() + " because its world isn't loaded (yet?). Please delete the claim file or contact the GriefPrevention developer with information about which plugin(s) you're using to load or create worlds. " + lesserCornerString);
inStream.close();
}
else
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(files[i].getName() + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
}
try
{
if(inStream != null) inStream.close();
}
catch(IOException exception) {}
}
}
}
void loadClaimData(File [] files) throws Exception
{
ConcurrentHashMap<Claim, Long> orphans = new ConcurrentHashMap<Claim, Long>();
for(int i = 0; i < files.length; i++)
{
if(files[i].isFile()) //avoids folders
{
//skip any file starting with an underscore, to avoid special files not representing land claims
if(files[i].getName().startsWith("_")) continue;
//delete any which don't end in .yml
if(!files[i].getName().endsWith(".yml"))
{
files[i].delete();
continue;
}
//the filename is the claim ID. try to parse it
long claimID;
try
{
claimID = Long.parseLong(files[i].getName().split("\\.")[0]);
}
//because some older versions used a different file name pattern before claim IDs were introduced,
//those files need to be "converted" by renaming them to a unique ID
catch(Exception e)
{
claimID = this.nextClaimID;
this.incrementNextClaimID();
File newFile = new File(claimDataFolderPath + File.separator + String.valueOf(this.nextClaimID) + ".yml");
files[i].renameTo(newFile);
files[i] = newFile;
}
try
{
ArrayList<Long> out_parentID = new ArrayList<Long>(); //hacky output parameter
Claim claim = this.loadClaim(files[i], out_parentID, claimID);
if(out_parentID.size() == 0 || out_parentID.get(0) == -1)
{
this.addClaim(claim, false);
}
else
{
orphans.put(claim, out_parentID.get(0));
}
}
//if there's any problem with the file's content, log an error message and skip it
catch(Exception e)
{
if(e.getMessage() != null && e.getMessage().contains("World not found"))
{
files[i].delete();
}
else
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(files[i].getName() + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
}
}
}
//link children to parents
for(Claim child : orphans.keySet())
{
Claim parent = this.getClaim(orphans.get(child));
if(parent != null)
{
child.parent = parent;
this.addClaim(child, false);
}
}
}
Claim loadClaim(File file, ArrayList<Long> out_parentID, long claimID) throws IOException, InvalidConfigurationException, Exception
{
List<String> lines = Files.readLines(file, Charset.forName("UTF-8"));
StringBuilder builder = new StringBuilder();
for(String line : lines)
{
builder.append(line).append('\n');
}
return this.loadClaim(builder.toString(), out_parentID, file.lastModified(), claimID, Bukkit.getServer().getWorlds());
}
Claim loadClaim(String input, ArrayList<Long> out_parentID, long lastModifiedDate, long claimID, List<World> validWorlds) throws InvalidConfigurationException, Exception
{
Claim claim = null;
YamlConfiguration yaml = new YamlConfiguration();
yaml.loadFromString(input);
//boundaries
Location lesserBoundaryCorner = this.locationFromString(yaml.getString("Lesser Boundary Corner"), validWorlds);
Location greaterBoundaryCorner = this.locationFromString(yaml.getString("Greater Boundary Corner"), validWorlds);
//owner
String ownerIdentifier = yaml.getString("Owner");
UUID ownerID = null;
if(!ownerIdentifier.isEmpty())
{
try
{
ownerID = UUID.fromString(ownerIdentifier);
}
catch(Exception ex)
{
GriefPrevention.AddLogEntry("Error - this is not a valid UUID: " + ownerIdentifier + ".");
GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString());
}
}
List<String> builders = yaml.getStringList("Builders");
List<String> containers = yaml.getStringList("Containers");
List<String> accessors = yaml.getStringList("Accessors");
List<String> managers = yaml.getStringList("Managers");
boolean inheritNothing = yaml.getBoolean("inheritNothing");
out_parentID.add(yaml.getLong("Parent Claim ID", -1L));
//instantiate
claim = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, ownerID, builders, containers, accessors, managers, inheritNothing, claimID);
claim.modifiedDate = new Date(lastModifiedDate);
claim.id = claimID;
return claim;
}
String getYamlForClaim(Claim claim)
{
YamlConfiguration yaml = new YamlConfiguration();
//boundaries
yaml.set("Lesser Boundary Corner", this.locationToString(claim.lesserBoundaryCorner));
yaml.set("Greater Boundary Corner", this.locationToString(claim.greaterBoundaryCorner));
//owner
String ownerID = "";
if(claim.ownerID != null) ownerID = claim.ownerID.toString();
yaml.set("Owner", ownerID);
ArrayList<String> builders = new ArrayList<String>();
ArrayList<String> containers = new ArrayList<String>();
ArrayList<String> accessors = new ArrayList<String>();
ArrayList<String> managers = new ArrayList<String>();
claim.getPermissions(builders, containers, accessors, managers);
yaml.set("Builders", builders);
yaml.set("Containers", containers);
yaml.set("Accessors", accessors);
yaml.set("Managers", managers);
Long parentID = -1L;
if(claim.parent != null)
{
parentID = claim.parent.id;
}
yaml.set("Parent Claim ID", parentID);
yaml.set("inheritNothing", claim.getSubclaimRestrictions());
return yaml.saveToString();
}
@Override
synchronized void writeClaimToStorage(Claim claim)
{
String claimID = String.valueOf(claim.id);
String yaml = this.getYamlForClaim(claim);
try
{
//open the claim's file
File claimFile = new File(claimDataFolderPath + File.separator + claimID + ".yml");
claimFile.createNewFile();
Files.write(yaml.getBytes("UTF-8"), claimFile);
}
//if any problem, log it
catch(Exception e)
{
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(claimID + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
}
//deletes a claim from the file system
@Override
synchronized void deleteClaimFromSecondaryStorage(Claim claim)
{
String claimID = String.valueOf(claim.id);
//remove from disk
File claimFile = new File(claimDataFolderPath + File.separator + claimID + ".yml");
if(claimFile.exists() && !claimFile.delete())
{
GriefPrevention.AddLogEntry("Error: Unable to delete claim file \"" + claimFile.getAbsolutePath() + "\".");
}
}
@Override
synchronized PlayerData getPlayerDataFromStorage(UUID playerID)
{
File playerFile = new File(playerDataFolderPath + File.separator + playerID.toString());
PlayerData playerData = new PlayerData();
playerData.playerID = playerID;
//if it exists as a file, read the file
if(playerFile.exists())
{
boolean needRetry = false;
int retriesRemaining = 5;
Exception latestException = null;
do
{
try
{
needRetry = false;
//read the file content and immediately close it
List<String> lines = Files.readLines(playerFile, Charset.forName("UTF-8"));
Iterator<String> iterator = lines.iterator();
iterator.next();
//first line is last login timestamp //RoboMWM - not using this anymore
//
// //convert that to a date and store it
// DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss");
// try
// {
// playerData.setLastLogin(dateFormat.parse(lastLoginTimestampString));
// }
// catch(ParseException parseException)
// {
// GriefPrevention.AddLogEntry("Unable to load last login for \"" + playerFile.getName() + "\".");
// playerData.setLastLogin(null);
// }
//second line is accrued claim blocks
String accruedBlocksString = iterator.next();
//convert that to a number and store it
playerData.setAccruedClaimBlocks(Integer.parseInt(accruedBlocksString));
//third line is any bonus claim blocks granted by administrators
String bonusBlocksString = iterator.next();
//convert that to a number and store it
playerData.setBonusClaimBlocks(Integer.parseInt(bonusBlocksString));
//fourth line is a double-semicolon-delimited list of claims, which is currently ignored
//String claimsString = inStream.readLine();
//iterator.next();
}
//if there's any problem with the file's content, retry up to 5 times with 5 milliseconds between
catch(Exception e)
{
latestException = e;
needRetry = true;
retriesRemaining--;
}
try
{
if(needRetry) Thread.sleep(5);
}
catch(InterruptedException exception) {}
}while(needRetry && retriesRemaining >= 0);
//if last attempt failed, log information about the problem
if(needRetry)
{
StringWriter errors = new StringWriter();
latestException.printStackTrace(new PrintWriter(errors));
GriefPrevention.AddLogEntry(playerID + " " + errors.toString(), CustomLogEntryTypes.Exception);
}
}
return playerData;
}
//saves changes to player data. MUST be called after you're done making changes, otherwise a reload will lose them
@Override
public void overrideSavePlayerData(UUID playerID, PlayerData playerData)
{
//never save data for the "administrative" account. null for claim owner ID indicates administrative account
if(playerID == null) return;
StringBuilder fileContent = new StringBuilder();
try
{
//first line is last login timestamp //RoboMWM - no longer storing/using
//if(playerData.getLastLogin() == null) playerData.setLastLogin(new Date());
//DateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss");
//fileContent.append(dateFormat.format(playerData.getLastLogin()));
fileContent.append("\n");
//second line is accrued claim blocks
fileContent.append(String.valueOf(playerData.getAccruedClaimBlocks()));
fileContent.append("\n");
//third line is bonus claim blocks
fileContent.append(String.valueOf(playerData.getBonusClaimBlocks()));
fileContent.append("\n");
//fourth line is blank
fileContent.append("\n");
//write data to file
File playerDataFile = new File(playerDataFolderPath + File.separator + playerID.toString());
Files.write(fileContent.toString().getBytes("UTF-8"), playerDataFile);
}
//if any problem, log it
catch(Exception e)
{
GriefPrevention.AddLogEntry("GriefPrevention: Unexpected exception saving data for player \"" + playerID.toString() + "\": " + e.getMessage());
e.printStackTrace();
}
}
@Override
synchronized void incrementNextClaimID()
{
//increment in memory
this.nextClaimID++;
BufferedWriter outStream = null;
try
{
//open the file and write the new value
File nextClaimIdFile = new File(nextClaimIdFilePath);
nextClaimIdFile.createNewFile();
outStream = new BufferedWriter(new FileWriter(nextClaimIdFile));
outStream.write(String.valueOf(this.nextClaimID));
}
//if any problem, log it
catch(Exception e)
{
GriefPrevention.AddLogEntry("Unexpected exception saving next claim ID: " + e.getMessage());
e.printStackTrace();
}
//close the file
try
{
if(outStream != null) outStream.close();
}
catch(IOException exception) {}
}
//grants a group (players with a specific permission) bonus claim blocks as long as they're still members of the group
@Override
synchronized void saveGroupBonusBlocks(String groupName, int currentValue)
{
//write changes to file to ensure they don't get lost
BufferedWriter outStream = null;
try
{
//open the group's file
File groupDataFile = new File(playerDataFolderPath + File.separator + "$" + groupName);
groupDataFile.createNewFile();
outStream = new BufferedWriter(new FileWriter(groupDataFile));
//first line is number of bonus blocks
outStream.write(String.valueOf(currentValue));
outStream.newLine();
}
//if any problem, log it
catch(Exception e)
{
GriefPrevention.AddLogEntry("Unexpected exception saving data for group \"" + groupName + "\": " + e.getMessage());
}
try
{
//close the file
if(outStream != null)
{
outStream.close();
}
}
catch(IOException exception){}
}
synchronized void migrateData(DatabaseDataStore databaseStore)
{
//migrate claims
for(int i = 0; i < this.claims.size(); i++)
{
Claim claim = this.claims.get(i);
databaseStore.addClaim(claim, true);
for(Claim child : claim.children)
{
databaseStore.addClaim(child, true);
}
}
//migrate groups
Iterator<String> groupNamesEnumerator = this.permissionToBonusBlocksMap.keySet().iterator();
while(groupNamesEnumerator.hasNext())
{
String groupName = groupNamesEnumerator.next();
databaseStore.saveGroupBonusBlocks(groupName, this.permissionToBonusBlocksMap.get(groupName));
}
//migrate players
File playerDataFolder = new File(playerDataFolderPath);
File [] files = playerDataFolder.listFiles();
for(int i = 0; i < files.length; i++)
{
File file = files[i];
if(!file.isFile()) continue; //avoids folders
if(file.isHidden()) continue; //avoid hidden files, which are likely not created by GriefPrevention
//all group data files start with a dollar sign. ignoring those, already handled above
if(file.getName().startsWith("$")) continue;
//ignore special files
if(file.getName().startsWith("_")) continue;
if(file.getName().endsWith(".ignore")) continue;
UUID playerID = UUID.fromString(file.getName());
databaseStore.savePlayerData(playerID, this.getPlayerData(playerID));
this.clearCachedPlayerData(playerID);
}
//migrate next claim ID
if(this.nextClaimID > databaseStore.nextClaimID)
{
databaseStore.setNextClaimID(this.nextClaimID);
}
//rename player and claim data folders so the migration won't run again
int i = 0;
File claimsBackupFolder;
File playersBackupFolder;
do
{
String claimsFolderBackupPath = claimDataFolderPath;
if(i > 0) claimsFolderBackupPath += String.valueOf(i);
claimsBackupFolder = new File(claimsFolderBackupPath);
String playersFolderBackupPath = playerDataFolderPath;
if(i > 0) playersFolderBackupPath += String.valueOf(i);
playersBackupFolder = new File(playersFolderBackupPath);
i++;
} while(claimsBackupFolder.exists() || playersBackupFolder.exists());
File claimsFolder = new File(claimDataFolderPath);
File playersFolder = new File(playerDataFolderPath);
claimsFolder.renameTo(claimsBackupFolder);
playersFolder.renameTo(playersBackupFolder);
GriefPrevention.AddLogEntry("Backed your file system data up to " + claimsBackupFolder.getName() + " and " + playersBackupFolder.getName() + ".");
GriefPrevention.AddLogEntry("If your migration encountered any problems, you can restore those data with a quick copy/paste.");
GriefPrevention.AddLogEntry("When you're satisfied that all your data have been safely migrated, consider deleting those folders.");
}
@Override
synchronized void close() { }
@Override
int getSchemaVersionFromStorage()
{
File schemaVersionFile = new File(schemaVersionFilePath);
if(schemaVersionFile.exists())
{
BufferedReader inStream = null;
int schemaVersion = 0;
try
{
inStream = new BufferedReader(new FileReader(schemaVersionFile.getAbsolutePath()));
//read the version number
String line = inStream.readLine();
//try to parse into an int value
schemaVersion = Integer.parseInt(line);
}
catch(Exception e){ }
try
{
if(inStream != null) inStream.close();
}
catch(IOException exception) {}
return schemaVersion;
}
else
{
this.updateSchemaVersionInStorage(0);
return 0;
}
}
@Override
void updateSchemaVersionInStorage(int versionToSet)
{
BufferedWriter outStream = null;
try
{
//open the file and write the new value
File schemaVersionFile = new File(schemaVersionFilePath);
schemaVersionFile.createNewFile();
outStream = new BufferedWriter(new FileWriter(schemaVersionFile));
outStream.write(String.valueOf(versionToSet));
}
//if any problem, log it
catch(Exception e)
{
GriefPrevention.AddLogEntry("Unexpected exception saving schema version: " + e.getMessage());
}
//close the file
try
{
if(outStream != null) outStream.close();
}
catch(IOException exception) {}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
package me.ryanhamshire.GriefPrevention;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.google.common.io.Files;
//loads ignore data from file into a hash map
class IgnoreLoaderThread extends Thread
{
private UUID playerToLoad;
private ConcurrentHashMap<UUID, Boolean> destinationMap;
IgnoreLoaderThread(UUID playerToLoad, ConcurrentHashMap<UUID, Boolean> destinationMap)
{
this.playerToLoad = playerToLoad;
this.destinationMap = destinationMap;
this.setPriority(MIN_PRIORITY);
}
@Override
public void run()
{
File ignoreFile = new File(DataStore.playerDataFolderPath + File.separator + this.playerToLoad + ".ignore");
//if the file doesn't exist, there's nothing to do here
if(!ignoreFile.exists()) return;
boolean needRetry = false;
int retriesRemaining = 5;
Exception latestException = null;
do
{
try
{
needRetry = false;
//read the file content and immediately close it
List<String> lines = Files.readLines(ignoreFile, Charset.forName("UTF-8"));
//each line is one ignore. asterisks indicate administrative ignores
for(String line : lines)
{
boolean adminIgnore = false;
if(line.startsWith("*"))
{
adminIgnore = true;
line = line.substring(1);
}
try
{
UUID ignoredUUID = UUID.fromString(line);
this.destinationMap.put(ignoredUUID, adminIgnore);
}
catch(IllegalArgumentException e){} //if a bad UUID, ignore the line
}
}
//if there's any problem with the file's content, retry up to 5 times with 5 milliseconds between
catch(Exception e)
{
latestException = e;
needRetry = true;
retriesRemaining--;
}
try
{
if(needRetry) Thread.sleep(5);
}
catch(InterruptedException exception) {}
}while(needRetry && retriesRemaining >= 0);
//if last attempt failed, log information about the problem
if(needRetry)
{
GriefPrevention.AddLogEntry("Retry attempts exhausted. Unable to load ignore data for player \"" + playerToLoad.toString() + "\": " + latestException.toString());
latestException.printStackTrace();
}
}
}
@@ -0,0 +1,35 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
@@ -0,0 +1,54 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.HashSet;
import java.util.Set;
//ordered list of material info objects, for fast searching
public class MaterialCollection
{
Set<MaterialInfo> materials = new HashSet<MaterialInfo>();
void Add(MaterialInfo material)
{
this.materials.add(material);
}
boolean Contains(MaterialInfo material)
{
return this.materials.contains(material);
}
@Override
public String toString()
{
return materials.toString();
}
public int size()
{
return this.materials.size();
}
public void clear()
{
this.materials.clear();
}
}
@@ -0,0 +1,96 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
//represents a material or collection of materials
import org.bukkit.Material;
public class MaterialInfo
{
Material typeID;
byte data;
boolean allDataValues;
String description;
public MaterialInfo(Material typeID, byte data, String description)
{
this.typeID = typeID;
this.data = data;
this.allDataValues = false;
this.description = description;
}
public MaterialInfo(Material typeID, String description)
{
this.typeID = typeID;
this.data = 0;
this.allDataValues = true;
this.description = description;
}
private MaterialInfo(Material typeID, byte data, boolean allDataValues, String description)
{
this.typeID = typeID;
this.data = data;
this.allDataValues = allDataValues;
this.description = description;
}
@Override
public String toString()
{
String returnValue = String.valueOf(this.typeID) + ":" + (this.allDataValues?"*":String.valueOf(this.data));
if(this.description != null) returnValue += ":" + this.description;
return returnValue;
}
public static MaterialInfo fromString(String string)
{
if(string == null || string.isEmpty()) return null;
String [] parts = string.split(":");
if(parts.length < 3) return null;
try
{
Material typeID = Material.matchMaterial(parts[0]);
byte data;
boolean allDataValues;
if(parts[1].equals("*"))
{
allDataValues = true;
data = 0;
}
else
{
allDataValues = false;
data = Byte.parseByte(parts[1]);
}
return new MaterialInfo(typeID, data, allDataValues, parts[2]);
}
catch(NumberFormatException exception)
{
return null;
}
}
}
@@ -0,0 +1,256 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
public enum Messages
{
RespectingClaims,
IgnoringClaims,
SuccessfulAbandon,
RestoreNatureActivate,
RestoreNatureAggressiveActivate,
FillModeActive,
TransferClaimPermission,
TransferClaimMissing,
TransferClaimAdminOnly,
PlayerNotFound2,
TransferTopLevel,
TransferSuccess,
TrustListNoClaim,
ClearPermsOwnerOnly,
UntrustIndividualAllClaims,
UntrustEveryoneAllClaims,
NoPermissionTrust,
ClearPermissionsOneClaim,
UntrustIndividualSingleClaim,
OnlySellBlocks,
BlockPurchaseCost,
ClaimBlockLimit,
InsufficientFunds,
PurchaseConfirmation,
OnlyPurchaseBlocks,
BlockSaleValue,
NotEnoughBlocksForSale,
BlockSaleConfirmation,
AdminClaimsMode,
BasicClaimsMode,
SubdivisionMode,
SubdivisionVideo2,
DeleteClaimMissing,
DeletionSubdivisionWarning,
DeleteSuccess,
CantDeleteAdminClaim,
DeleteAllSuccess,
NoDeletePermission,
AllAdminDeleted,
AdjustBlocksSuccess,
NotTrappedHere,
RescuePending,
NonSiegeWorld,
AlreadySieging,
NotSiegableThere,
SiegeTooFarAway,
NoSiegeYourself,
NoSiegeDefenseless,
AlreadyUnderSiegePlayer,
AlreadyUnderSiegeArea,
NoSiegeAdminClaim,
SiegeOnCooldown,
SiegeAlert,
SiegeConfirmed,
AbandonClaimMissing,
NotYourClaim,
DeleteTopLevelClaim,
AbandonSuccess,
CantGrantThatPermission,
GrantPermissionNoClaim,
GrantPermissionConfirmation,
ManageUniversalPermissionsInstruction,
ManageOneClaimPermissionsInstruction,
CollectivePublic,
BuildPermission,
ContainersPermission,
AccessPermission,
PermissionsPermission,
LocationCurrentClaim,
LocationAllClaims,
PvPImmunityStart,
SiegeNoDrop,
DonateItemsInstruction,
ChestFull,
DonationSuccess,
PlayerTooCloseForFire2,
TooDeepToClaim,
ChestClaimConfirmation,
AutomaticClaimNotification,
UnprotectedChestWarning,
ThatPlayerPvPImmune,
CantFightWhileImmune,
NoDamageClaimedEntity,
ShovelBasicClaimMode,
RemainingBlocks,
CreativeBasicsVideo2,
SurvivalBasicsVideo2,
TrappedChatKeyword,
TrappedInstructions,
PvPNoDrop,
SiegeNoTeleport,
BesiegedNoTeleport,
SiegeNoContainers,
PvPNoContainers,
PvPImmunityEnd,
NoBedPermission,
NoWildernessBuckets,
NoLavaNearOtherPlayer,
TooFarAway,
BlockNotClaimed,
BlockClaimed,
SiegeNoShovel,
RestoreNaturePlayerInChunk,
NoCreateClaimPermission,
ResizeNeedMoreBlocks,
NoCreativeUnClaim,
ClaimResizeSuccess,
ResizeFailOverlap,
ResizeStart,
ResizeFailOverlapSubdivision,
SubdivisionStart,
CreateSubdivisionOverlap,
SubdivisionSuccess,
CreateClaimFailOverlap,
CreateClaimFailOverlapOtherPlayer,
ClaimsDisabledWorld,
ClaimStart,
NewClaimTooNarrow,
CreateClaimInsufficientBlocks,
AbandonClaimAdvertisement,
CreateClaimFailOverlapShort,
CreateClaimSuccess,
SiegeWinDoorsOpen,
RescueAbortedMoved,
SiegeDoorsLockedEjection,
NoModifyDuringSiege,
OnlyOwnersModifyClaims,
NoBuildUnderSiege,
NoBuildPvP,
NoBuildPermission,
NonSiegeMaterial,
NoOwnerBuildUnderSiege,
NoAccessPermission,
NoContainersSiege,
NoContainersPermission,
OwnerNameForAdminClaims,
ClaimTooSmallForEntities,
TooManyEntitiesInClaim,
YouHaveNoClaims,
ConfirmFluidRemoval,
AutoBanNotify,
AdjustGroupBlocksSuccess,
InvalidPermissionID,
HowToClaimRegex,
NoBuildOutsideClaims,
PlayerOfflineTime,
BuildingOutsideClaims,
TrappedWontWorkHere,
CommandBannedInPvP,
UnclaimCleanupWarning,
BuySellNotConfigured,
NoTeleportPvPCombat,
NoTNTDamageAboveSeaLevel,
NoTNTDamageClaims,
IgnoreClaimsAdvertisement,
NoPermissionForCommand,
ClaimsListNoPermission,
ExplosivesDisabled,
ExplosivesEnabled,
ClaimExplosivesAdvertisement,
PlayerInPvPSafeZone,
NoPistonsOutsideClaims,
SoftMuted,
UnSoftMuted,
DropUnlockAdvertisement,
PickupBlockedExplanation,
DropUnlockConfirmation,
DropUnlockOthersConfirmation,
AdvertiseACandACB,
AdvertiseAdminClaims,
AdvertiseACB,
NotYourPet,
PetGiveawayConfirmation,
PetTransferCancellation,
ReadyToTransferPet,
AvoidGriefClaimLand,
BecomeMayor,
ClaimCreationFailedOverClaimCountLimit,
CreateClaimFailOverlapRegion,
ResizeFailOverlapRegion,
NoBuildPortalPermission,
ShowNearbyClaims,
NoChatUntilMove,
SiegeImmune,
SetClaimBlocksSuccess,
IgnoreConfirmation,
NotIgnoringPlayer,
UnIgnoreConfirmation,
SeparateConfirmation,
UnSeparateConfirmation,
NotIgnoringAnyone,
TrustListHeader,
Manage,
Build,
Containers,
Access,
HasSubclaimRestriction,
StartBlockMath,
ClaimsListHeader,
ContinueBlockMath,
EndBlockMath,
NoClaimDuringPvP,
UntrustAllOwnerOnly,
ManagersDontUntrustManagers,
BookAuthor,
BookTitle,
BookIntro,
BookDisabledChestClaims,
BookUsefulCommands,
BookLink,
BookTools,
ResizeClaimTooNarrow,
ResizeClaimInsufficientArea,
NoProfanity,
PlayerNotIgnorable,
NoEnoughBlocksForChestClaim,
IsIgnoringYou,
MustHoldModificationToolForThat,
StandInClaimToResize,
ClaimsExtendToSky,
ClaimsAutoExtendDownward,
MinimumRadius,
RadiusRequiresGoldenShovel,
ClaimTooSmallForActiveBlocks,
TooManyActiveBlocksInClaim,
ConsoleOnlyCommand,
WorldNotFound,
AdjustBlocksAllSuccess,
TooMuchIpOverlap,
StandInSubclaim,
SubclaimRestricted,
SubclaimUnrestricted,
NetherPortalTrapDetectionMessage
}
@@ -0,0 +1,21 @@
package me.ryanhamshire.GriefPrevention;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
class PendingItemProtection
{
public Location location;
public UUID owner;
long expirationTimestamp;
ItemStack itemStack;
public PendingItemProtection(Location location, UUID owner, long expirationTimestamp, ItemStack itemStack)
{
this.location = location;
this.owner = owner;
this.expirationTimestamp = expirationTimestamp;
this.itemStack = itemStack;
}
}
@@ -0,0 +1,352 @@
/*
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 java.net.InetAddress;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import me.ryanhamshire.GriefPrevention.Claim;
import me.ryanhamshire.GriefPrevention.GriefPrevention;
import me.ryanhamshire.GriefPrevention.ShovelMode;
import me.ryanhamshire.GriefPrevention.SiegeData;
import me.ryanhamshire.GriefPrevention.Visualization;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
//holds all of GriefPrevention's player-tied data
public class PlayerData
{
//the player's ID
public UUID playerID;
//the player's claims
private Vector<Claim> claims = null;
//how many claim blocks the player has earned via play time
private Integer accruedClaimBlocks = null;
//temporary holding area to avoid opening data files too early
private int newlyAccruedClaimBlocks = 0;
//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
private Integer bonusClaimBlocks = null;
//what "mode" the shovel is in determines what it will do when it's used
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,
//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;
//whether or not the player has a pending /trapped rescue
public boolean pendingTrapped = false;
//whether this player was recently warned about building outside land claims
boolean warnedAboutBuildingOutsideClaims = false;
//timestamp when last siege ended (where this player was the defender)
long lastSiegeEndTimeStamp = 0;
//whether the player was kicked (set and used during logout)
boolean wasKicked = false;
//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 = "";
//safety confirmation for deleting multi-subdivision claims
public boolean warnedAboutMajorDeletion = false;
public InetAddress ipAddress;
//for addons to set per-player claim limits. Any negative value will use config's value
private int AccruedClaimBlocksLimit = -1;
//whether or not this player has received a message about unlocking death drops since his last death
boolean receivedDropUnlockAdvertisement = false;
//whether or not this player's dropped items (on death) are unlocked for other players to pick up
boolean dropsAreUnlocked = false;
//message to send to player after he respawns
String messageOnRespawn = null;
//player which a pet will be given to when it's right-clicked
OfflinePlayer petGiveawayRecipient = null;
//timestamp for last "you're building outside your land claims" message
Long buildWarningTimestamp = null;
//spot where a player can't talk, used to mute new players until they've moved a little
//this is an anti-bot strategy.
Location noChatLocation = null;
//ignore list
//true means invisible (admin-forced ignore), false means player-created ignore
public ConcurrentHashMap<UUID, Boolean> ignoredPlayers = new ConcurrentHashMap<UUID, Boolean>();
public boolean ignoreListChanged = false;
//profanity warning, once per play session
boolean profanityWarned = false;
//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 > GriefPrevention.instance.config_pvp_combatTimeoutSeconds * 1000) //X 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.getAccruedClaimBlocks() + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID);
for(int i = 0; i < this.getClaims().size(); i++)
{
Claim claim = this.getClaims().get(i);
remainingBlocks -= claim.getArea();
}
return remainingBlocks;
}
//don't load data from secondary storage until it's needed
public int getAccruedClaimBlocks()
{
if(this.accruedClaimBlocks == null) this.loadDataFromSecondaryStorage();
//update claim blocks with any he has accrued during his current play session
if(this.newlyAccruedClaimBlocks > 0)
{
int accruedLimit = this.getAccruedClaimBlocksLimit();
//if over the limit before adding blocks, leave it as-is, because the limit may have changed AFTER he accrued the blocks
if(this.accruedClaimBlocks < accruedLimit)
{
//move any in the holding area
int newTotal = this.accruedClaimBlocks + this.newlyAccruedClaimBlocks;
//respect limits
this.accruedClaimBlocks = Math.min(newTotal, accruedLimit);
}
this.newlyAccruedClaimBlocks = 0;
return this.accruedClaimBlocks;
}
return accruedClaimBlocks;
}
public void setAccruedClaimBlocks(Integer accruedClaimBlocks)
{
this.accruedClaimBlocks = accruedClaimBlocks;
this.newlyAccruedClaimBlocks = 0;
}
public int getBonusClaimBlocks()
{
if(this.bonusClaimBlocks == null) this.loadDataFromSecondaryStorage();
return bonusClaimBlocks;
}
public void setBonusClaimBlocks(Integer bonusClaimBlocks)
{
this.bonusClaimBlocks = bonusClaimBlocks;
}
private void loadDataFromSecondaryStorage()
{
//reach out to secondary storage to get any data there
PlayerData storageData = GriefPrevention.instance.dataStore.getPlayerDataFromStorage(this.playerID);
if(this.accruedClaimBlocks == null)
{
if(storageData.accruedClaimBlocks != null)
{
this.accruedClaimBlocks = storageData.accruedClaimBlocks;
//ensure at least minimum accrued are accrued (in case of settings changes to increase initial amount)
if(GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && (this.accruedClaimBlocks < GriefPrevention.instance.config_claims_initialBlocks))
{
this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks;
}
}
else
{
this.accruedClaimBlocks = GriefPrevention.instance.config_claims_initialBlocks;
}
}
if(this.bonusClaimBlocks == null)
{
if(storageData.bonusClaimBlocks != null)
{
this.bonusClaimBlocks = storageData.bonusClaimBlocks;
}
else
{
this.bonusClaimBlocks = 0;
}
}
}
public Vector<Claim> getClaims()
{
if(this.claims == null)
{
this.claims = new Vector<Claim>();
//find all the claims belonging to this player and note them for future reference
DataStore dataStore = GriefPrevention.instance.dataStore;
int totalClaimsArea = 0;
for(int i = 0; i < dataStore.claims.size(); i++)
{
Claim claim = dataStore.claims.get(i);
if(!claim.inDataStore)
{
dataStore.claims.remove(i--);
continue;
}
if(playerID.equals(claim.ownerID))
{
this.claims.add(claim);
totalClaimsArea += claim.getArea();
}
}
//ensure player has claim blocks for his claims, and at least the minimum accrued
this.loadDataFromSecondaryStorage();
//if total claimed area is more than total blocks available
int totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID);
if(GriefPrevention.instance.config_advanced_fixNegativeClaimblockAmounts && totalBlocks < totalClaimsArea)
{
OfflinePlayer player = GriefPrevention.instance.getServer().getOfflinePlayer(this.playerID);
GriefPrevention.AddLogEntry(player.getName() + " has more claimed land than blocks available. Adding blocks to fix.", CustomLogEntryTypes.Debug, true);
GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks(), CustomLogEntryTypes.Debug, true);
GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true);
for(Claim claim : this.claims)
{
if(!claim.inDataStore) continue;
GriefPrevention.AddLogEntry(
GriefPrevention.getfriendlyLocationString(claim.getLesserBoundaryCorner()) + " // "
+ GriefPrevention.getfriendlyLocationString(claim.getGreaterBoundaryCorner()) + " = "
+ claim.getArea()
, CustomLogEntryTypes.Debug, true);
}
//try to fix it by adding to accrued blocks
this.accruedClaimBlocks = totalClaimsArea; //Set accrued blocks to equal total claims
int accruedLimit = this.getAccruedClaimBlocksLimit();
this.accruedClaimBlocks = Math.min(accruedLimit, this.accruedClaimBlocks); //set accrued blocks to maximum limit, if it's smaller
GriefPrevention.AddLogEntry("New accrued blocks: " + this.accruedClaimBlocks, CustomLogEntryTypes.Debug, true);
//Recalculate total blocks (accrued + bonus + permission group bonus)
totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID);
GriefPrevention.AddLogEntry("New total blocks: " + totalBlocks, CustomLogEntryTypes.Debug, true);
//if that didn't fix it, then make up the difference with bonus blocks
if(totalBlocks < totalClaimsArea)
{
int bonusBlocksToAdd = totalClaimsArea - totalBlocks;
this.bonusClaimBlocks += bonusBlocksToAdd;
GriefPrevention.AddLogEntry("Accrued blocks weren't enough. Adding " + bonusBlocksToAdd + " bonus blocks.", CustomLogEntryTypes.Debug, true);
}
GriefPrevention.AddLogEntry(player.getName() + " Accrued blocks: " + this.getAccruedClaimBlocks() + " Bonus blocks: " + this.getBonusClaimBlocks() + " Group Bonus Blocks: " + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID), CustomLogEntryTypes.Debug, true);
//Recalculate total blocks (accrued + bonus + permission group bonus)
totalBlocks = this.accruedClaimBlocks + this.getBonusClaimBlocks() + GriefPrevention.instance.dataStore.getGroupBonusBlocks(this.playerID);
GriefPrevention.AddLogEntry("Total blocks: " + totalBlocks + " Total claimed area: " + totalClaimsArea, CustomLogEntryTypes.Debug, true);
GriefPrevention.AddLogEntry("Remaining claim blocks to use: " + this.getRemainingClaimBlocks() + " (should be 0)", CustomLogEntryTypes.Debug, true);
}
}
for(int i = 0; i < this.claims.size(); i++)
{
if(!claims.get(i).inDataStore)
{
claims.remove(i--);
}
}
return claims;
}
//Limit can be changed by addons
public int getAccruedClaimBlocksLimit()
{
if (this.AccruedClaimBlocksLimit < 0)
return GriefPrevention.instance.config_claims_maxAccruedBlocks_default;
return this.AccruedClaimBlocksLimit;
}
public void setAccruedClaimBlocksLimit(int limit)
{
this.AccruedClaimBlocksLimit = limit;
}
public void accrueBlocks(int howMany)
{
this.newlyAccruedClaimBlocks += howMany;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import me.ryanhamshire.GriefPrevention.events.PlayerKickBanEvent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
//kicks or bans a player
//need a task for this because async threads (like the chat event handlers) can't kick or ban.
//but they CAN schedule a task to run in the main thread to do that job
class PlayerKickBanTask implements Runnable
{
//player to kick or ban
private Player player;
//message to send player.
private String reason;
//source of ban
private String source;
//whether to ban
private boolean ban;
public PlayerKickBanTask(Player player, String reason, String source, boolean ban)
{
this.player = player;
this.reason = reason;
this.source = source;
this.ban = ban;
}
@Override
public void run()
{
PlayerKickBanEvent kickBanEvent = new PlayerKickBanEvent(player, reason, source, ban);
Bukkit.getPluginManager().callEvent(kickBanEvent);
if (kickBanEvent.isCancelled())
{
return; // cancelled by a plugin
}
if(this.ban)
{
//ban
GriefPrevention.banPlayer(this.player, this.reason, this.source);
}
else if(this.player.isOnline())
{
this.player.kickPlayer(this.reason);
}
}
}
@@ -0,0 +1,75 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
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;
//rescue destination, may be decided at instantiation or at execution
private Location destination;
//player data
private Player player;
public PlayerRescueTask(Player player, Location location, Location destination)
{
this.player = player;
this.location = location;
this.destination = destination;
}
@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.getUniqueId());
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, Messages.RescueAbortedMoved);
return;
}
//otherwise find a place to teleport him
if(this.destination == null)
{
this.destination = GriefPrevention.instance.ejectPlayer(this.player);
}
else
{
player.teleport(this.destination);
}
//log entry, in case admins want to investigate the "trap"
GriefPrevention.AddLogEntry("Rescued trapped player " + player.getName() + " from " + GriefPrevention.getfriendlyLocationString(this.location) + " to " + GriefPrevention.getfriendlyLocationString(this.destination) + ".");
}
}
@@ -0,0 +1,55 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.entity.Player;
//sends a message to a player
//used to send delayed messages, for example help text triggered by a player's chat
class PvPImmunityValidationTask implements Runnable
{
private Player player;
public PvPImmunityValidationTask(Player player)
{
this.player = player;
}
@Override
public void run()
{
if(!player.isOnline()) return;
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId());
if(!playerData.pvpImmune) return;
//check the player's inventory for anything
if(!GriefPrevention.isInventoryEmpty(player))
{
//if found, cancel invulnerability and notify
playerData.pvpImmune = false;
GriefPrevention.sendMessage(player, TextMode.Warn, Messages.PvPImmunityEnd);
}
else
{
//otherwise check again in one minute
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, this, 1200L);
}
}
}
@@ -0,0 +1,130 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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;
import org.bukkit.entity.Animals;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Hanging;
import org.bukkit.entity.Player;
//this main thread task takes the output from the RestoreNatureProcessingTask\
//and updates the world accordingly
class RestoreNatureExecutionTask implements Runnable
{
//results from processing thread
//will be applied to the world
private BlockSnapshot[][][] snapshots;
//boundaries for changes
private int miny;
private Location lesserCorner;
private Location greaterCorner;
//player who should be notified about the result (will see a visualization when the restoration is complete)
private Player player;
public RestoreNatureExecutionTask(BlockSnapshot[][][] snapshots, int miny, Location lesserCorner, Location greaterCorner, Player player)
{
this.snapshots = snapshots;
this.miny = miny;
this.lesserCorner = lesserCorner;
this.greaterCorner = greaterCorner;
this.player = player;
}
@SuppressWarnings("deprecation")
@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 - 1; 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.getType()|| blockUpdate.data != currentBlock.getData())
{
Claim claim = GriefPrevention.instance.dataStore.getClaimAt(blockUpdate.location, false, cachedClaim);
if(claim != null)
{
cachedClaim = claim;
break;
}
try
{
currentBlock.setType(blockUpdate.typeId, false);
currentBlock.setData(blockUpdate.data, false);
}
catch(IllegalArgumentException e)
{
//just don't update this block and continue trying to update other blocks
}
}
}
}
}
//clean up any entities in the chunk, ensure no players are suffocated
Chunk chunk = this.lesserCorner.getChunk();
Entity [] entities = chunk.getEntities();
for(int i = 0; i < entities.length; i++)
{
Entity entity = entities[i];
if(!(entity instanceof Player || entity instanceof Animals))
{
//hanging entities (paintings, item frames) are protected when they're in land claims
if(!(entity instanceof Hanging) || GriefPrevention.instance.dataStore.getClaimAt(entity.getLocation(), false, null) == null)
{
//everything else is removed
entity.remove();
}
}
//for players, always ensure there's air where the player is standing
else
{
Block feetBlock = entity.getLocation().getBlock();
feetBlock.setType(Material.AIR);
feetBlock.getRelative(BlockFace.UP).setType(Material.AIR);
}
}
//show visualization to player who started the restoration
if(player != null)
{
Claim claim = new Claim(lesserCorner, greaterCorner, null, new ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>(), null);
Visualization visualization = Visualization.FromClaim(claim, player.getLocation().getBlockY(), VisualizationType.RestoreNature, player.getLocation());
Visualization.Apply(player, visualization);
}
}
}
@@ -0,0 +1,810 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.ArrayList;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.entity.Player;
//non-main-thread task which processes world data to repair the unnatural
//after processing is complete, creates a main thread task to make the necessary changes to the world
class RestoreNatureProcessingTask implements Runnable
{
//world information captured from the main thread
//will be updated and sent back to main thread to be applied to the world
private BlockSnapshot[][][] snapshots;
//other information collected from the main thread.
//not to be updated, only to be passed back to main thread to provide some context about the operation
private int miny;
private Environment environment;
private Location lesserBoundaryCorner;
private Location greaterBoundaryCorner;
private Player player; //absolutely must not be accessed. not thread safe.
private Biome biome;
private boolean creativeMode;
private int seaLevel;
private boolean aggressiveMode;
//two lists of materials
private ArrayList<Material> notAllowedToHang; //natural blocks which don't naturally hang in their air
private ArrayList<Material> playerBlocks; //a "complete" list of player-placed blocks. MUST BE MAINTAINED as patches introduce more
@SuppressWarnings("deprecation")
public RestoreNatureProcessingTask(BlockSnapshot[][][] snapshots, int miny, Environment environment, Biome biome, Location lesserBoundaryCorner, Location greaterBoundaryCorner, int seaLevel, boolean aggressiveMode, boolean creativeMode, Player player)
{
this.snapshots = snapshots;
this.miny = miny;
if(this.miny < 0) this.miny = 0;
this.environment = environment;
this.lesserBoundaryCorner = lesserBoundaryCorner;
this.greaterBoundaryCorner = greaterBoundaryCorner;
this.biome = biome;
this.seaLevel = seaLevel;
this.aggressiveMode = aggressiveMode;
this.player = player;
this.creativeMode = creativeMode;
this.notAllowedToHang = new ArrayList<Material>();
this.notAllowedToHang.add(Material.DIRT);
this.notAllowedToHang.add(Material.LONG_GRASS);
this.notAllowedToHang.add(Material.SNOW);
this.notAllowedToHang.add(Material.LOG);
if(this.aggressiveMode)
{
this.notAllowedToHang.add(Material.GRASS);
this.notAllowedToHang.add(Material.STONE);
}
this.playerBlocks = new ArrayList<Material>();
this.playerBlocks.addAll(RestoreNatureProcessingTask.getPlayerBlocks(this.environment, this.biome));
//in aggressive or creative world mode, also treat these blocks as user placed, to be removed
//this is helpful in the few cases where griefers intentionally use natural blocks to grief,
//like a single-block tower of iron ore or a giant penis constructed with melons
if(this.aggressiveMode || this.creativeMode)
{
this.playerBlocks.add(Material.IRON_ORE);
this.playerBlocks.add(Material.GOLD_ORE);
this.playerBlocks.add(Material.DIAMOND_ORE);
this.playerBlocks.add(Material.MELON_BLOCK);
this.playerBlocks.add(Material.MELON_STEM);
this.playerBlocks.add(Material.BEDROCK);
this.playerBlocks.add(Material.COAL_ORE);
this.playerBlocks.add(Material.PUMPKIN);
this.playerBlocks.add(Material.PUMPKIN_STEM);
this.playerBlocks.add(Material.MELON);
}
if(this.aggressiveMode)
{
this.playerBlocks.add(Material.LEAVES);
this.playerBlocks.add(Material.LEAVES_2);
this.playerBlocks.add(Material.LOG);
this.playerBlocks.add(Material.LOG_2);
this.playerBlocks.add(Material.VINE);
}
}
@Override
public void run()
{
//order is important!
//remove sandstone which appears to be unnatural
this.removeSandstone();
//remove any blocks which are definitely player placed
this.removePlayerBlocks();
//reduce large outcroppings of stone, sandstone
this.reduceStone();
//reduce logs, except in jungle biomes
this.reduceLogs();
//remove natural blocks which are unnaturally hanging in the air
this.removeHanging();
//remove natural blocks which are unnaturally stacked high
this.removeWallsAndTowers();
//fill unnatural thin trenches and single-block potholes
this.fillHolesAndTrenches();
//fill water depressions and fix unnatural surface ripples
this.fixWater();
//remove water/lava above sea level
this.removeDumpedFluids();
//cover surface stone and gravel with sand or grass, as the biome requires
this.coverSurfaceStone();
//remove any player-placed leaves
this.removePlayerLeaves();
//schedule main thread task to apply the result to the world
RestoreNatureExecutionTask task = new RestoreNatureExecutionTask(this.snapshots, this.miny, this.lesserBoundaryCorner, this.greaterBoundaryCorner, this.player);
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, task);
}
@SuppressWarnings("deprecation")
private void removePlayerLeaves()
{
if(this.seaLevel < 1) 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; y++)
{
//note: see minecraft wiki data values for leaves
BlockSnapshot block = snapshots[x][y][z];
if(block.typeId == Material.LEAVES && (block.data & 0x4) != 0)
{
block.typeId = Material.AIR;
}
}
}
}
}
//converts sandstone adjacent to sand to sand, and any other sandstone to air
@SuppressWarnings("deprecation")
private void removeSandstone()
{
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
for(int y = snapshots[0].length - 2; y > miny; y--)
{
if(snapshots[x][y][z].typeId != Material.SANDSTONE) continue;
BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
BlockSnapshot underBlock = this.snapshots[x][y - 1][z];
BlockSnapshot aboveBlock = this.snapshots[x][y + 1][z];
//skip blocks which may cause a cave-in
if(aboveBlock.typeId == Material.SAND && underBlock.typeId == Material.AIR) continue;
//count adjacent non-air/non-leaf blocks
if( leftBlock.typeId == Material.SAND ||
rightBlock.typeId == Material.SAND ||
upBlock.typeId == Material.SAND ||
downBlock.typeId == Material.SAND ||
aboveBlock.typeId == Material.SAND ||
underBlock.typeId == Material.SAND)
{
snapshots[x][y][z].typeId = Material.SAND;
}
else
{
snapshots[x][y][z].typeId = Material.AIR;
}
}
}
}
}
@SuppressWarnings("deprecation")
private void reduceStone()
{
if(this.seaLevel < 1) return;
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
int thisy = this.highestY(x, z, true);
while(thisy > this.seaLevel - 1 && (this.snapshots[x][thisy][z].typeId == Material.STONE || this.snapshots[x][thisy][z].typeId == Material.SANDSTONE))
{
BlockSnapshot leftBlock = this.snapshots[x + 1][thisy][z];
BlockSnapshot rightBlock = this.snapshots[x - 1][thisy][z];
BlockSnapshot upBlock = this.snapshots[x][thisy][z + 1];
BlockSnapshot downBlock = this.snapshots[x][thisy][z - 1];
//count adjacent non-air/non-leaf blocks
byte adjacentBlockCount = 0;
if(leftBlock.typeId != Material.AIR && leftBlock.typeId != Material.LEAVES && leftBlock.typeId != Material.VINE)
{
adjacentBlockCount++;
}
if(rightBlock.typeId != Material.AIR && rightBlock.typeId != Material.LEAVES && rightBlock.typeId != Material.VINE)
{
adjacentBlockCount++;
}
if(downBlock.typeId != Material.AIR && downBlock.typeId != Material.LEAVES && downBlock.typeId != Material.VINE)
{
adjacentBlockCount++;
}
if(upBlock.typeId != Material.AIR && upBlock.typeId != Material.LEAVES && upBlock.typeId != Material.VINE)
{
adjacentBlockCount++;
}
if(adjacentBlockCount < 3)
{
this.snapshots[x][thisy][z].typeId = Material.AIR;
}
thisy--;
}
}
}
}
@SuppressWarnings("deprecation")
private void reduceLogs()
{
if(this.seaLevel < 1) return;
boolean jungleBiome = this.biome == Biome.JUNGLE || this.biome == Biome.JUNGLE_HILLS;
//scan all blocks above sea level
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
for(int y = this.seaLevel - 1; y < snapshots[0].length; y++)
{
BlockSnapshot block = snapshots[x][y][z];
//skip non-logs
if(block.typeId != Material.LOG) continue;
if(block.typeId != Material.LOG_2) continue;
//if in jungle biome, skip jungle logs
if(jungleBiome && block.data == 3) continue;
//examine adjacent blocks for logs
BlockSnapshot leftBlock = this.snapshots[x + 1][y][z];
BlockSnapshot rightBlock = this.snapshots[x - 1][y][z];
BlockSnapshot upBlock = this.snapshots[x][y][z + 1];
BlockSnapshot downBlock = this.snapshots[x][y][z - 1];
//if any, remove the log
if(leftBlock.typeId == Material.LOG || rightBlock.typeId == Material.LOG || upBlock.typeId == Material.LOG || downBlock.typeId == Material.LOG)
{
this.snapshots[x][y][z].typeId = Material.AIR;
}
}
}
}
}
@SuppressWarnings("deprecation")
private void removePlayerBlocks()
{
int miny = this.miny;
if(miny < 1) miny = 1;
//remove all player blocks
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
for(int y = miny; y < snapshots[0].length - 1; y++)
{
BlockSnapshot block = snapshots[x][y][z];
if(this.playerBlocks.contains(block.typeId))
{
block.typeId = Material.AIR;
}
}
}
}
}
@SuppressWarnings("deprecation")
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 || underBlock.typeId == Material.STATIONARY_WATER || underBlock.typeId == Material.STATIONARY_LAVA || underBlock.typeId == Material.LEAVES)
{
if(this.notAllowedToHang.contains(block.typeId))
{
block.typeId = Material.AIR;
}
}
}
}
}
}
@SuppressWarnings("deprecation")
private void removeWallsAndTowers()
{
Material [] excludedBlocksArray = new Material []
{
Material.CACTUS,
Material.LONG_GRASS,
Material.RED_MUSHROOM,
Material.BROWN_MUSHROOM,
Material.DEAD_BUSH,
Material.SAPLING,
Material.YELLOW_FLOWER,
Material.RED_ROSE,
Material.SUGAR_CANE_BLOCK,
Material.VINE,
Material.PUMPKIN,
Material.WATER_LILY,
Material.LEAVES
};
ArrayList<Material> excludedBlocks = new ArrayList<Material>();
for(int i = 0; i < excludedBlocksArray.length; i++) excludedBlocks.add(excludedBlocksArray[i]);
boolean changed;
do
{
changed = false;
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
int thisy = this.highestY(x, z, false);
if(excludedBlocks.contains(this.snapshots[x][thisy][z].typeId)) continue;
int righty = this.highestY(x + 1, z, false);
int lefty = this.highestY(x - 1, z, false);
while(lefty < thisy && righty < thisy)
{
this.snapshots[x][thisy--][z].typeId = Material.AIR;
changed = true;
}
int upy = this.highestY(x, z + 1, false);
int downy = this.highestY(x, z - 1, false);
while(upy < thisy && downy < thisy)
{
this.snapshots[x][thisy--][z].typeId = Material.AIR;
changed = true;
}
}
}
}while(changed);
}
@SuppressWarnings("deprecation")
private void coverSurfaceStone()
{
for(int x = 1; x < snapshots.length - 1; x++)
{
for(int z = 1; z < snapshots[0][0].length - 1; z++)
{
int y = this.highestY(x, z, true);
BlockSnapshot block = snapshots[x][y][z];
if(block.typeId == Material.STONE || block.typeId == Material.GRAVEL || block.typeId == Material.SOIL || block.typeId == Material.DIRT || block.typeId == Material.SANDSTONE)
{
if(this.biome == Biome.DESERT || this.biome == Biome.DESERT_HILLS || this.biome == Biome.BEACHES)
{
this.snapshots[x][y][z].typeId = Material.SAND;
}
else
{
this.snapshots[x][y][z].typeId = Material.GRASS;
}
}
}
}
}
@SuppressWarnings("deprecation")
private void fillHolesAndTrenches()
{
ArrayList<Material> fillableBlocks = new ArrayList<Material>();
fillableBlocks.add(Material.AIR);
fillableBlocks.add(Material.STATIONARY_WATER);
fillableBlocks.add(Material.STATIONARY_LAVA);
fillableBlocks.add(Material.LONG_GRASS);
ArrayList<Material> notSuitableForFillBlocks = new ArrayList<Material>();
notSuitableForFillBlocks.add(Material.LONG_GRASS);
notSuitableForFillBlocks.add(Material.CACTUS);
notSuitableForFillBlocks.add(Material.STATIONARY_WATER);
notSuitableForFillBlocks.add(Material.STATIONARY_LAVA);
notSuitableForFillBlocks.add(Material.LOG);
notSuitableForFillBlocks.add(Material.LOG_2);
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);
}
@SuppressWarnings("deprecation")
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 || block.typeId == Material.STATIONARY_LAVA)
{
if(underBlock.typeId == Material.AIR || (underBlock.data != 0))
{
block.typeId = Material.AIR;
}
}
}
}
}
//fill water depressions
do
{
changed = false;
for(int y = Math.max(this.seaLevel - 10, 0); 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 || (block.typeId == Material.STATIONARY_WATER && 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 || underBlock.data != 0) continue;
//count adjacent source water blocks
byte adjacentSourceWaterCount = 0;
if(leftBlock.typeId == Material.STATIONARY_WATER && leftBlock.data == 0)
{
adjacentSourceWaterCount++;
}
if(rightBlock.typeId == Material.STATIONARY_WATER && rightBlock.data == 0)
{
adjacentSourceWaterCount++;
}
if(upBlock.typeId == Material.STATIONARY_WATER && upBlock.data == 0)
{
adjacentSourceWaterCount++;
}
if(downBlock.typeId == Material.STATIONARY_WATER && downBlock.data == 0)
{
adjacentSourceWaterCount++;
}
//at least two adjacent blocks must be source water
if(adjacentSourceWaterCount >= 2)
{
block.typeId = Material.STATIONARY_WATER;
block.data = 0;
changed = true;
}
}
}
}
}
}while(changed);
}
@SuppressWarnings("deprecation")
private void removeDumpedFluids()
{
if(this.seaLevel < 1) return;
//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 || block.typeId == Material.STATIONARY_LAVA ||
block.typeId == Material.WATER || block.typeId == Material.LAVA)
{
block.typeId = Material.AIR;
}
}
}
}
}
@SuppressWarnings("deprecation")
private int highestY(int x, int z, boolean ignoreLeaves)
{
int y;
for(y = snapshots[0].length - 1; y > 0; y--)
{
BlockSnapshot block = this.snapshots[x][y][z];
if(block.typeId != Material.AIR &&
!(ignoreLeaves && block.typeId == Material.SNOW) &&
!(ignoreLeaves && block.typeId == Material.LEAVES) &&
!(ignoreLeaves && block.typeId == Material.LEAVES_2) &&
!(block.typeId == Material.STATIONARY_WATER) &&
!(block.typeId == Material.WATER) &&
!(block.typeId == Material.LAVA) &&
!(block.typeId == Material.STATIONARY_LAVA))
{
return y;
}
}
return y;
}
@SuppressWarnings("deprecation")
static ArrayList<Material> getPlayerBlocks(Environment environment, Biome biome)
{
//NOTE on this list. why not make a list of natural blocks?
//answer: better to leave a few player blocks than to remove too many natural blocks. remember we're "restoring nature"
//a few extra player blocks can be manually removed, but it will be impossible to guess exactly which natural materials to use in manual repair of an overzealous block removal
ArrayList<Material> playerBlocks = new ArrayList<Material>();
playerBlocks.add(Material.FIRE);
playerBlocks.add(Material.BED_BLOCK);
playerBlocks.add(Material.WOOD);
playerBlocks.add(Material.BOOKSHELF);
playerBlocks.add(Material.BREWING_STAND);
playerBlocks.add(Material.BRICK);
playerBlocks.add(Material.COBBLESTONE);
playerBlocks.add(Material.GLASS);
playerBlocks.add(Material.LAPIS_BLOCK);
playerBlocks.add(Material.DISPENSER);
playerBlocks.add(Material.NOTE_BLOCK);
playerBlocks.add(Material.POWERED_RAIL);
playerBlocks.add(Material.DETECTOR_RAIL);
playerBlocks.add(Material.PISTON_STICKY_BASE);
playerBlocks.add(Material.PISTON_BASE);
playerBlocks.add(Material.PISTON_EXTENSION);
playerBlocks.add(Material.WOOL);
playerBlocks.add(Material.PISTON_MOVING_PIECE);
playerBlocks.add(Material.GOLD_BLOCK);
playerBlocks.add(Material.IRON_BLOCK);
playerBlocks.add(Material.DOUBLE_STEP);
playerBlocks.add(Material.STEP);
playerBlocks.add(Material.CROPS);
playerBlocks.add(Material.TNT);
playerBlocks.add(Material.MOSSY_COBBLESTONE);
playerBlocks.add(Material.TORCH);
playerBlocks.add(Material.FIRE);
playerBlocks.add(Material.WOOD_STAIRS);
playerBlocks.add(Material.CHEST);
playerBlocks.add(Material.REDSTONE_WIRE);
playerBlocks.add(Material.DIAMOND_BLOCK);
playerBlocks.add(Material.WORKBENCH);
playerBlocks.add(Material.FURNACE);
playerBlocks.add(Material.BURNING_FURNACE);
playerBlocks.add(Material.WOODEN_DOOR);
playerBlocks.add(Material.SIGN_POST);
playerBlocks.add(Material.LADDER);
playerBlocks.add(Material.RAILS);
playerBlocks.add(Material.COBBLESTONE_STAIRS);
playerBlocks.add(Material.WALL_SIGN);
playerBlocks.add(Material.STONE_PLATE);
playerBlocks.add(Material.LEVER);
playerBlocks.add(Material.IRON_DOOR_BLOCK);
playerBlocks.add(Material.WOOD_PLATE);
playerBlocks.add(Material.REDSTONE_TORCH_ON);
playerBlocks.add(Material.REDSTONE_TORCH_OFF);
playerBlocks.add(Material.STONE_BUTTON);
playerBlocks.add(Material.SNOW_BLOCK);
playerBlocks.add(Material.JUKEBOX);
playerBlocks.add(Material.FENCE);
playerBlocks.add(Material.PORTAL);
playerBlocks.add(Material.JACK_O_LANTERN);
playerBlocks.add(Material.CAKE_BLOCK);
playerBlocks.add(Material.DIODE_BLOCK_ON);
playerBlocks.add(Material.DIODE_BLOCK_OFF);
playerBlocks.add(Material.TRAP_DOOR);
playerBlocks.add(Material.SMOOTH_BRICK);
playerBlocks.add(Material.HUGE_MUSHROOM_1);
playerBlocks.add(Material.HUGE_MUSHROOM_2);
playerBlocks.add(Material.IRON_FENCE);
playerBlocks.add(Material.THIN_GLASS);
playerBlocks.add(Material.MELON_STEM);
playerBlocks.add(Material.FENCE_GATE);
playerBlocks.add(Material.BRICK_STAIRS);
playerBlocks.add(Material.SMOOTH_STAIRS);
playerBlocks.add(Material.ENCHANTMENT_TABLE);
playerBlocks.add(Material.BREWING_STAND);
playerBlocks.add(Material.CAULDRON);
playerBlocks.add(Material.DIODE_BLOCK_ON);
playerBlocks.add(Material.DIODE_BLOCK_ON);
playerBlocks.add(Material.WEB);
playerBlocks.add(Material.SPONGE);
playerBlocks.add(Material.GRAVEL);
playerBlocks.add(Material.EMERALD_BLOCK);
playerBlocks.add(Material.SANDSTONE);
playerBlocks.add(Material.WOOD_STEP);
playerBlocks.add(Material.WOOD_DOUBLE_STEP);
playerBlocks.add(Material.ENDER_CHEST);
playerBlocks.add(Material.SANDSTONE_STAIRS);
playerBlocks.add(Material.SPRUCE_WOOD_STAIRS);
playerBlocks.add(Material.JUNGLE_WOOD_STAIRS);
playerBlocks.add(Material.COMMAND);
playerBlocks.add(Material.BEACON);
playerBlocks.add(Material.COBBLE_WALL);
playerBlocks.add(Material.FLOWER_POT);
playerBlocks.add(Material.CARROT);
playerBlocks.add(Material.POTATO);
playerBlocks.add(Material.WOOD_BUTTON);
playerBlocks.add(Material.SKULL);
playerBlocks.add(Material.ANVIL);
playerBlocks.add(Material.SPONGE);
playerBlocks.add(Material.DOUBLE_STONE_SLAB2);
playerBlocks.add(Material.STAINED_GLASS);
playerBlocks.add(Material.STAINED_GLASS_PANE);
playerBlocks.add(Material.BANNER);
playerBlocks.add(Material.STANDING_BANNER);
playerBlocks.add(Material.ACACIA_STAIRS);
playerBlocks.add(Material.BIRCH_WOOD_STAIRS);
playerBlocks.add(Material.DARK_OAK_STAIRS);
playerBlocks.add(Material.TRAPPED_CHEST);
playerBlocks.add(Material.GOLD_PLATE);
playerBlocks.add(Material.IRON_PLATE);
playerBlocks.add(Material.REDSTONE_COMPARATOR_OFF);
playerBlocks.add(Material.REDSTONE_COMPARATOR_ON);
playerBlocks.add(Material.DAYLIGHT_DETECTOR);
playerBlocks.add(Material.DAYLIGHT_DETECTOR_INVERTED);
playerBlocks.add(Material.REDSTONE_BLOCK);
playerBlocks.add(Material.HOPPER);
playerBlocks.add(Material.QUARTZ_BLOCK);
playerBlocks.add(Material.QUARTZ_STAIRS);
playerBlocks.add(Material.DROPPER);
playerBlocks.add(Material.SLIME_BLOCK);
playerBlocks.add(Material.IRON_TRAPDOOR);
playerBlocks.add(Material.PRISMARINE);
playerBlocks.add(Material.HAY_BLOCK);
playerBlocks.add(Material.CARPET);
playerBlocks.add(Material.SEA_LANTERN);
playerBlocks.add(Material.RED_SANDSTONE_STAIRS);
playerBlocks.add(Material.STONE_SLAB2);
playerBlocks.add(Material.ACACIA_FENCE);
playerBlocks.add(Material.ACACIA_FENCE_GATE);
playerBlocks.add(Material.BIRCH_FENCE);
playerBlocks.add(Material.BIRCH_FENCE_GATE);
playerBlocks.add(Material.DARK_OAK_FENCE);
playerBlocks.add(Material.DARK_OAK_FENCE_GATE);
playerBlocks.add(Material.JUNGLE_FENCE);
playerBlocks.add(Material.JUNGLE_FENCE_GATE);
playerBlocks.add(Material.SPRUCE_FENCE);
playerBlocks.add(Material.SPRUCE_FENCE_GATE);
playerBlocks.add(Material.ACACIA_DOOR);
playerBlocks.add(Material.SPRUCE_DOOR);
playerBlocks.add(Material.DARK_OAK_DOOR);
playerBlocks.add(Material.JUNGLE_DOOR);
playerBlocks.add(Material.BIRCH_DOOR);
playerBlocks.add(Material.COAL_BLOCK);
playerBlocks.add(Material.REDSTONE_LAMP_OFF);
playerBlocks.add(Material.REDSTONE_LAMP_ON);
playerBlocks.add(Material.PURPUR_BLOCK);
playerBlocks.add(Material.PURPUR_SLAB);
playerBlocks.add(Material.PURPUR_DOUBLE_SLAB);
playerBlocks.add(Material.PURPUR_PILLAR);
playerBlocks.add(Material.PURPUR_STAIRS);
playerBlocks.add(Material.NETHER_WART_BLOCK);
playerBlocks.add(Material.RED_NETHER_BRICK);
playerBlocks.add(Material.BONE_BLOCK);
//these are unnatural in the standard world, but not in the nether
if(environment != Environment.NETHER)
{
playerBlocks.add(Material.NETHERRACK);
playerBlocks.add(Material.SOUL_SAND);
playerBlocks.add(Material.GLOWSTONE);
playerBlocks.add(Material.NETHER_BRICK);
playerBlocks.add(Material.NETHER_FENCE);
playerBlocks.add(Material.NETHER_BRICK_STAIRS);
playerBlocks.add(Material.MAGMA);
}
//these are unnatural in the standard and nether worlds, but not in the end
if(environment != Environment.THE_END)
{
playerBlocks.add(Material.OBSIDIAN);
playerBlocks.add(Material.ENDER_STONE);
playerBlocks.add(Material.ENDER_PORTAL_FRAME);
playerBlocks.add(Material.CHORUS_PLANT);
playerBlocks.add(Material.CHORUS_FLOWER);
}
//these are unnatural in sandy biomes, but not elsewhere
if(biome == Biome.DESERT || biome == Biome.DESERT_HILLS || biome == Biome.BEACHES || environment != Environment.NORMAL)
{
playerBlocks.add(Material.LEAVES);
playerBlocks.add(Material.LEAVES_2);
playerBlocks.add(Material.LOG);
playerBlocks.add(Material.LOG_2);
}
return playerBlocks;
}
}
@@ -0,0 +1,58 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import java.util.Collection;
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
@SuppressWarnings("unchecked")
Collection<Player> onlinePlayers = (Collection<Player>)GriefPrevention.instance.getServer().getOnlinePlayers();
for(Player player : onlinePlayers)
{
if(claim.contains(player.getLocation(), false, false) && claim.allowAccess(player) != null)
{
GriefPrevention.sendMessage(player, TextMode.Err, Messages.SiegeDoorsLockedEjection);
GriefPrevention.instance.ejectPlayer(player);
}
}
}
}
}
@@ -0,0 +1,61 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
//sends a message to a player
//used to send delayed messages, for example help text triggered by a player's chat
class SendPlayerMessageTask implements Runnable
{
private Player player;
private ChatColor color;
private String message;
public SendPlayerMessageTask(Player player, ChatColor color, String message)
{
this.player = player;
this.color = color;
this.message = message;
}
@Override
public void run()
{
if(player == null)
{
GriefPrevention.AddLogEntry(color + message);
return;
}
//if the player is dead, save it for after his respawn
if(this.player.isDead())
{
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(this.player.getUniqueId());
playerData.messageOnRespawn = this.color + this.message;
}
//otherwise send it immediately
else
{
GriefPrevention.sendMessage(this.player, this.color, this.message);
}
}
}
@@ -0,0 +1,30 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
//enumeration for golden shovel modes
public enum ShovelMode
{
Basic,
Admin,
Subdivide,
RestoreNature,
RestoreNatureAggressive,
RestoreNatureFill
}
@@ -0,0 +1,110 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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(), null);
}
//or defender wins if the attacker leaves
else if(!attackerRemains && defenderRemains)
{
dataStore.endSiege(this.siegeData, defender.getName(), attacker.getName(), null);
}
//if they both left, but are still close together, the battle continues (check again later)
else if(attacker.getWorld().equals(defender.getWorld()) && 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(), null);
}
}
//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 * 30);
}
}
@@ -0,0 +1,40 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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<Claim> claims;
public int checkupTaskID;
public SiegeData(Player attacker, Player defender, Claim claim)
{
this.defender = defender;
this.attacker = attacker;
this.claims = new ArrayList<Claim>();
this.claims.add(claim);
}
}
@@ -0,0 +1,269 @@
package me.ryanhamshire.GriefPrevention;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
class SpamDetector
{
//last chat message shown and its timestamp, regardless of who sent it
private String lastChatMessage = "";
private long lastChatMessageTimestamp = 0;
//number of identical chat messages in a row
private int duplicateMessageCount = 0;
//data for individual chatters
ConcurrentHashMap<UUID, ChatterData> dataStore = new ConcurrentHashMap<UUID, ChatterData>();
private ChatterData getChatterData(UUID chatterID)
{
ChatterData data = this.dataStore.get(chatterID);
if(data == null)
{
data = new ChatterData();
this.dataStore.put(chatterID, data);
}
return data;
}
SpamAnalysisResult AnalyzeMessage(UUID chatterID, String message, long timestamp)
{
SpamAnalysisResult result = new SpamAnalysisResult();
result.finalMessage = message;
//remedy any CAPS SPAM, exception for very short messages which could be emoticons like =D or XD
if(message.length() > 4 && this.stringsAreSimilar(message.toUpperCase(), message))
{
message = message.toLowerCase();
result.finalMessage = message;
}
boolean spam = false;
ChatterData chatterData = this.getChatterData(chatterID);
//mute if total volume of text from this player is too high
if(message.length() > 50 && chatterData.getTotalRecentLength(timestamp) > 200)
{
spam = true;
result.muteReason = "too much chat sent in 10 seconds";
chatterData.spamLevel++;
}
//always mute an exact match to the last chat message
if(result.finalMessage.equals(this.lastChatMessage) && timestamp - this.lastChatMessageTimestamp < 2000)
{
chatterData.spamLevel += ++this.duplicateMessageCount;
spam = true;
result.muteReason = "repeat message";
}
else
{
this.lastChatMessage = message;
this.lastChatMessageTimestamp = timestamp;
this.duplicateMessageCount = 0;
}
//check message content and timing
long millisecondsSinceLastMessage = timestamp - chatterData.lastMessageTimestamp;
//if the message came too close to the last one
if(millisecondsSinceLastMessage < 1500)
{
//increment the spam counter
chatterData.spamLevel++;
spam = true;
}
//if it's exactly the same as the last message from the same player and within 30 seconds
if(result.muteReason == null && millisecondsSinceLastMessage < 30000 && result.finalMessage.equalsIgnoreCase(chatterData.lastMessage))
{
chatterData.spamLevel++;
spam = true;
result.muteReason = "repeat message";
}
//if it's very similar to the last message from the same player and within 10 seconds of that message
if(result.muteReason == null && millisecondsSinceLastMessage < 10000 && this.stringsAreSimilar(message.toLowerCase(), chatterData.lastMessage.toLowerCase()))
{
chatterData.spamLevel++;
spam = true;
if(chatterData.spamLevel > 2)
{
result.muteReason = "similar message";
}
}
//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(result.muteReason == null && message.length() > 5)
{
int symbolsCount = 0;
int whitespaceCount = 0;
for(int i = 0; i < message.length(); i++)
{
char character = message.charAt(i);
if(!(Character.isLetterOrDigit(character)))
{
symbolsCount++;
}
if(Character.isWhitespace(character))
{
whitespaceCount++;
}
}
if(symbolsCount > message.length() / 2 || (message.length() > 15 && whitespaceCount < message.length() / 10))
{
spam = true;
if(chatterData.spamLevel > 0) result.muteReason = "gibberish";
chatterData.spamLevel++;
}
}
//very short messages close together are spam
if(result.muteReason == null && message.length() < 5 && millisecondsSinceLastMessage < 3000)
{
spam = true;
chatterData.spamLevel++;
}
//if the message was determined to be a spam, consider taking action
if(spam)
{
//anything above level 8 for a player which has received a warning... kick or if enabled, ban
if(chatterData.spamLevel > 8 && chatterData.spamWarned)
{
result.shouldBanChatter = true;
}
else if(chatterData.spamLevel >= 4)
{
if(!chatterData.spamWarned)
{
chatterData.spamWarned = true;
result.shouldWarnChatter = true;
}
if(result.muteReason == null)
{
result.muteReason = "too-frequent text";
}
}
}
//otherwise if not a spam, reduce the spam level for this player
else
{
chatterData.spamLevel = 0;
chatterData.spamWarned = false;
}
chatterData.AddMessage(message, timestamp);
return result;
}
//if two strings are 75% identical, they're too close to follow each other in the chat
private boolean stringsAreSimilar(String message, String lastMessage)
{
//ignore differences in only punctuation and whitespace
message = message.replaceAll("[^\\p{Alpha}]", "");
lastMessage = lastMessage.replaceAll("[^\\p{Alpha}]", "");
//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;
int i;
for(i = 0; i < shorterString.length(); i++)
{
if(shorterString.charAt(i) == longerString.charAt(i)) identicalCount++;
if(identicalCount > maxIdenticalCharacters) return true;
}
//compare backward
int j;
for(j = 0; j < shorterString.length() - i; j++)
{
if(shorterString.charAt(shorterString.length() - j - 1) == longerString.charAt(longerString.length() - j - 1)) identicalCount++;
if(identicalCount > maxIdenticalCharacters) return true;
}
return false;
}
}
class SpamAnalysisResult
{
String finalMessage;
boolean shouldWarnChatter = false;
boolean shouldBanChatter = false;
String muteReason;
}
class ChatterData
{
public String lastMessage = ""; //the player's last chat message, or slash command complete with parameters
public long lastMessageTimestamp; //last time the player sent a chat message or used a monitored slash command
public int spamLevel = 0; //number of consecutive "spams"
public boolean spamWarned = false; //whether the player has received a warning recently
//all recent message lengths and their total
private ConcurrentLinkedQueue<LengthTimestampPair> recentMessageLengths = new ConcurrentLinkedQueue<LengthTimestampPair>();
private int recentTotalLength = 0;
public void AddMessage(String message, long timestamp)
{
int length = message.length();
this.recentMessageLengths.add(new LengthTimestampPair(length, timestamp));
this.recentTotalLength += length;
this.lastMessage = message;
this.lastMessageTimestamp = timestamp;
}
public int getTotalRecentLength(long timestamp)
{
LengthTimestampPair oldestPair = this.recentMessageLengths.peek();
while(oldestPair != null && timestamp - oldestPair.timestamp > 10000)
{
this.recentMessageLengths.poll();
this.recentTotalLength -= oldestPair.length;
oldestPair = this.recentMessageLengths.peek();
}
return this.recentTotalLength;
}
}
class LengthTimestampPair
{
public long timestamp;
public int length;
public LengthTimestampPair(int length, long timestamp)
{
this.length = length;
this.timestamp = timestamp;
}
}
@@ -0,0 +1,31 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import org.bukkit.ChatColor;
//just a few constants for chat color codes
public class TextMode
{
final static ChatColor Info = ChatColor.AQUA;
final static ChatColor Instr = ChatColor.YELLOW;
final static ChatColor Warn = ChatColor.GOLD;
final static ChatColor Err = ChatColor.RED;
final static ChatColor Success = ChatColor.GREEN;
}
@@ -0,0 +1,224 @@
//BIG THANKS to EvilMidget38 for providing this handy UUID lookup tool to the Bukkit community! :)
package me.ryanhamshire.GriefPrevention;
import com.google.common.base.Charsets;
import org.bukkit.OfflinePlayer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
class UUIDFetcher {
private static int PROFILES_PER_REQUEST = 100;
private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft";
private final JSONParser jsonParser = new JSONParser();
private final List<String> names;
private final boolean rateLimiting;
//cache for username -> uuid lookups
static HashMap<String, UUID> lookupCache;
//record of username -> proper casing updates
static HashMap<String, String> correctedNames;
public UUIDFetcher(List<String> names, boolean rateLimiting) {
this.names = names;
this.rateLimiting = rateLimiting;
}
public UUIDFetcher(List<String> names) {
this(names, true);
}
public void call() throws Exception
{
if(lookupCache == null)
{
lookupCache = new HashMap<String, UUID>();
}
if(correctedNames == null)
{
correctedNames = new HashMap<String, String>();
}
GriefPrevention.AddLogEntry("UUID conversion process started. Please be patient - this may take a while.");
GriefPrevention.AddLogEntry("Mining your local world data to save calls to Mojang...");
OfflinePlayer [] players = GriefPrevention.instance.getServer().getOfflinePlayers();
for(OfflinePlayer player : players)
{
if(player.getName() != null && player.getUniqueId() != null)
{
lookupCache.put(player.getName(), player.getUniqueId());
lookupCache.put(player.getName().toLowerCase(), player.getUniqueId());
correctedNames.put(player.getName().toLowerCase(), player.getName());
}
}
//try to get correct casing from local data
GriefPrevention.AddLogEntry("Checking local server data to get correct casing for player names...");
for(int i = 0; i < names.size(); i++)
{
String name = names.get(i);
String correctCasingName = correctedNames.get(name);
if(correctCasingName != null && !name.equals(correctCasingName))
{
GriefPrevention.AddLogEntry(name + " --> " + correctCasingName);
names.set(i, correctCasingName);
}
}
//look for local uuid's first
GriefPrevention.AddLogEntry("Checking local server data for UUIDs already seen...");
for(int i = 0; i < names.size(); i++)
{
String name = names.get(i);
UUID uuid = lookupCache.get(name);
if(uuid != null)
{
GriefPrevention.AddLogEntry(name + " --> " + uuid.toString());
names.remove(i--);
}
}
//for online mode, call Mojang to resolve the rest
if(GriefPrevention.instance.getServer().getOnlineMode())
{
GriefPrevention.AddLogEntry("Calling Mojang to get UUIDs for remaining unresolved players (this is the slowest step)...");
for (int i = 0; i * PROFILES_PER_REQUEST < names.size(); i++)
{
boolean retry = false;
JSONArray array = null;
do
{
HttpURLConnection connection = createConnection();
String body = JSONArray.toJSONString(names.subList(i * PROFILES_PER_REQUEST, Math.min((i + 1) * PROFILES_PER_REQUEST, names.size())));
writeBody(connection, body);
retry = false;
array = null;
try
{
array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream()));
}
catch(Exception e)
{
//in case of error 429 too many requests, pause and then retry later
if(e.getMessage().contains("429"))
{
retry = true;
//if this is the first time we're sending anything, the batch size must be too big
//try reducing it
if(i == 0 && PROFILES_PER_REQUEST > 1)
{
GriefPrevention.AddLogEntry("Batch size " + PROFILES_PER_REQUEST + " seems too large. Looking for a workable batch size...");
PROFILES_PER_REQUEST = Math.max(PROFILES_PER_REQUEST - 5, 1);
}
//otherwise, keep the batch size which has worked for previous iterations
//but wait a little while before trying again.
else
{
GriefPrevention.AddLogEntry("Mojang says we're sending requests too fast. Will retry every 30 seconds until we succeed...");
Thread.sleep(30000);
}
}
else
{
throw e;
}
}
}while(retry);
for (Object profile : array) {
JSONObject jsonProfile = (JSONObject) profile;
String id = (String) jsonProfile.get("id");
String name = (String) jsonProfile.get("name");
UUID uuid = UUIDFetcher.getUUID(id);
GriefPrevention.AddLogEntry(name + " --> " + uuid.toString());
lookupCache.put(name, uuid);
lookupCache.put(name.toLowerCase(), uuid);
}
if (rateLimiting) {
Thread.sleep(200L);
}
}
}
//for offline mode, generate UUIDs for the rest
else
{
GriefPrevention.AddLogEntry("Generating offline mode UUIDs for remaining unresolved players...");
for(int i = 0; i < names.size(); i++)
{
String name = names.get(i);
UUID uuid = java.util.UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8));
GriefPrevention.AddLogEntry(name + " --> " + uuid.toString());
lookupCache.put(name, uuid);
lookupCache.put(name.toLowerCase(), uuid);
}
}
}
private static void writeBody(HttpURLConnection connection, String body) throws Exception {
OutputStream stream = connection.getOutputStream();
stream.write(body.getBytes());
stream.flush();
stream.close();
}
private static HttpURLConnection createConnection() throws Exception {
URL url = new URL(PROFILE_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
return connection;
}
private static UUID getUUID(String id) {
return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" +id.substring(20, 32));
}
public static byte[] toBytes(UUID uuid) {
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
return byteBuffer.array();
}
public static UUID fromBytes(byte[] array) {
if (array.length != 16) {
throw new IllegalArgumentException("Illegal byte array length: " + array.length);
}
ByteBuffer byteBuffer = ByteBuffer.wrap(array);
long mostSignificant = byteBuffer.getLong();
long leastSignificant = byteBuffer.getLong();
return new UUID(mostSignificant, leastSignificant);
}
public static UUID getUUIDOf(String name) throws Exception
{
UUID result = lookupCache.get(name);
if(result == null)
{
//throw up our hands and report the problem in the logs
//this player will lose his land claim blocks, but claims will stay in place as admin claims
throw new IllegalArgumentException(name);
}
return result;
}
}
@@ -0,0 +1,333 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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<VisualizationElement> elements = new ArrayList<VisualizationElement>();
//sends a visualization to a player
public static void Apply(Player player, Visualization visualization)
{
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId());
//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
if(player.isOnline() && visualization.elements.size() > 0 && visualization.elements.get(0).location.getWorld().equals(player.getWorld()))
{
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(GriefPrevention.instance, new VisualizationApplicationTask(player, playerData, visualization), 1L);
}
}
//reverts a visualization by sending another block change list, this time with the real world block values
@SuppressWarnings("deprecation")
public static void Revert(Player player)
{
if(!player.isOnline()) return;
PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId());
Visualization visualization = playerData.currentVisualization;
if(playerData.currentVisualization != null)
{
//locality
int minx = player.getLocation().getBlockX() - 100;
int minz = player.getLocation().getBlockZ() - 100;
int maxx = player.getLocation().getBlockX() + 100;
int maxz = player.getLocation().getBlockZ() + 100;
//remove any elements which are too far away
visualization.removeElementsOutOfRange(visualization.elements, minx, minz, maxx, maxz);
//send real block information for any remaining elements
for(int i = 0; i < visualization.elements.size(); i++)
{
VisualizationElement element = visualization.elements.get(i);
//check player still in world where visualization exists
if(i == 0)
{
if(!player.getWorld().equals(element.location.getWorld())) return;
}
player.sendBlockChange(element.location, element.realMaterial, element.realData);
}
playerData.currentVisualization = null;
}
}
//convenience method to build a visualization from a claim
//visualizationType determines the style (gold blocks, silver, red, diamond, etc)
public static Visualization FromClaim(Claim claim, int height, VisualizationType visualizationType, Location locality)
{
//visualize only top level claims
if(claim.parent != null)
{
return FromClaim(claim.parent, height, visualizationType, locality);
}
Visualization visualization = new Visualization();
//add subdivisions first
for(int i = 0; i < claim.children.size(); i++)
{
Claim child = claim.children.get(i);
if(!child.inDataStore) continue;
visualization.addClaimElements(child, height, VisualizationType.Subdivision, locality);
}
//special visualization for administrative land claims
if(claim.isAdminClaim() && visualizationType == VisualizationType.Claim)
{
visualizationType = VisualizationType.AdminClaim;
}
//add top level last so that it takes precedence (it shows on top when the child claim boundaries overlap with its boundaries)
visualization.addClaimElements(claim, height, visualizationType, locality);
return visualization;
}
//adds a claim's visualization to the current visualization
//handy for combining several visualizations together, as when visualization a top level claim with several subdivisions inside
//locality is a performance consideration. only create visualization blocks for around 100 blocks of the locality
@SuppressWarnings("deprecation")
private void addClaimElements(Claim claim, int height, VisualizationType visualizationType, Location locality)
{
Location smallXsmallZ = claim.getLesserBoundaryCorner();
Location bigXbigZ = claim.getGreaterBoundaryCorner();
World world = smallXsmallZ.getWorld();
boolean waterIsTransparent = locality.getBlock().getType() == Material.STATIONARY_WATER;
int smallx = smallXsmallZ.getBlockX();
int smallz = smallXsmallZ.getBlockZ();
int bigx = bigXbigZ.getBlockX();
int bigz = bigXbigZ.getBlockZ();
Material cornerMaterial;
Material accentMaterial;
ArrayList<VisualizationElement> newElements = new ArrayList<VisualizationElement>();
if(visualizationType == VisualizationType.Claim)
{
cornerMaterial = Material.GLOWSTONE;
accentMaterial = Material.GOLD_BLOCK;
}
else if(visualizationType == VisualizationType.AdminClaim)
{
cornerMaterial = Material.GLOWSTONE;
accentMaterial = Material.PUMPKIN;
}
else if(visualizationType == VisualizationType.Subdivision)
{
cornerMaterial = Material.IRON_BLOCK;
accentMaterial = Material.WOOL;
}
else if(visualizationType == VisualizationType.RestoreNature)
{
cornerMaterial = Material.DIAMOND_BLOCK;
accentMaterial = Material.DIAMOND_BLOCK;
}
else
{
cornerMaterial = Material.GLOWING_REDSTONE_ORE;
accentMaterial = Material.NETHERRACK;
}
//initialize visualization elements without Y values and real data
//that will be added later for only the visualization elements within visualization range
//locality
int minx = locality.getBlockX() - 75;
int minz = locality.getBlockZ() - 75;
int maxx = locality.getBlockX() + 75;
int maxz = locality.getBlockZ() + 75;
final int STEP = 10;
//top line
newElements.add(new VisualizationElement(new Location(world, smallx, 0, bigz), cornerMaterial, (byte)0, Material.AIR, (byte)0));
newElements.add(new VisualizationElement(new Location(world, smallx + 1, 0, bigz), accentMaterial, (byte)0, Material.AIR, (byte)0));
for(int x = smallx + STEP; x < bigx - STEP / 2; x += STEP)
{
if(x > minx && x < maxx)
newElements.add(new VisualizationElement(new Location(world, x, 0, bigz), accentMaterial, (byte)0, Material.AIR, (byte)0));
}
newElements.add(new VisualizationElement(new Location(world, bigx - 1, 0, bigz), accentMaterial, (byte)0, Material.AIR, (byte)0));
//bottom line
newElements.add(new VisualizationElement(new Location(world, smallx + 1, 0, smallz), accentMaterial, (byte)0, Material.AIR, (byte)0));
for(int x = smallx + STEP; x < bigx - STEP / 2; x += STEP)
{
if(x > minx && x < maxx)
newElements.add(new VisualizationElement(new Location(world, x, 0, smallz), accentMaterial, (byte)0, Material.AIR, (byte)0));
}
newElements.add(new VisualizationElement(new Location(world, bigx - 1, 0, smallz), accentMaterial, (byte)0, Material.AIR, (byte)0));
//left line
newElements.add(new VisualizationElement(new Location(world, smallx, 0, smallz), cornerMaterial, (byte)0, Material.AIR, (byte)0));
newElements.add(new VisualizationElement(new Location(world, smallx, 0, smallz + 1), accentMaterial, (byte)0, Material.AIR, (byte)0));
for(int z = smallz + STEP; z < bigz - STEP / 2; z += STEP)
{
if(z > minz && z < maxz)
newElements.add(new VisualizationElement(new Location(world, smallx, 0, z), accentMaterial, (byte)0, Material.AIR, (byte)0));
}
newElements.add(new VisualizationElement(new Location(world, smallx, 0, bigz - 1), accentMaterial, (byte)0, Material.AIR, (byte)0));
//right line
newElements.add(new VisualizationElement(new Location(world, bigx, 0, smallz), cornerMaterial, (byte)0, Material.AIR, (byte)0));
newElements.add(new VisualizationElement(new Location(world, bigx, 0, smallz + 1), accentMaterial, (byte)0, Material.AIR, (byte)0));
for(int z = smallz + STEP; z < bigz - STEP / 2; z += STEP)
{
if(z > minz && z < maxz)
newElements.add(new VisualizationElement(new Location(world, bigx, 0, z), accentMaterial, (byte)0, Material.AIR, (byte)0));
}
newElements.add(new VisualizationElement(new Location(world, bigx, 0, bigz - 1), accentMaterial, (byte)0, Material.AIR, (byte)0));
newElements.add(new VisualizationElement(new Location(world, bigx, 0, bigz), cornerMaterial, (byte)0, Material.AIR, (byte)0));
//remove any out of range elements
this.removeElementsOutOfRange(newElements, minx, minz, maxx, maxz);
//remove any elements outside the claim
for(int i = 0; i < newElements.size(); i++)
{
VisualizationElement element = newElements.get(i);
if(!claim.contains(element.location, true, false))
{
newElements.remove(i--);
}
}
//set Y values and real block information for any remaining visualization blocks
for(VisualizationElement element : newElements)
{
Location tempLocation = element.location;
element.location = getVisibleLocation(tempLocation.getWorld(), tempLocation.getBlockX(), height, tempLocation.getBlockZ(), waterIsTransparent);
height = element.location.getBlockY();
element.realMaterial = element.location.getBlock().getType();
element.realData = element.location.getBlock().getData();
}
this.elements.addAll(newElements);
}
//removes any elements which are out of visualization range
private void removeElementsOutOfRange(ArrayList<VisualizationElement> elements, int minx, int minz, int maxx, int maxz)
{
for(int i = 0; i < elements.size(); i++)
{
Location location = elements.get(i).location;
if(location.getX() < minx || location.getX() > maxx || location.getZ() < minz || location.getZ() > maxz)
{
elements.remove(i--);
}
}
}
//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, boolean waterIsTransparent)
{
Block block = world.getBlockAt(x, y, z);
BlockFace direction = (isTransparent(block, waterIsTransparent)) ? BlockFace.DOWN : BlockFace.UP;
while( block.getY() >= 1 &&
block.getY() < world.getMaxHeight() - 1 &&
(!isTransparent(block.getRelative(BlockFace.UP), waterIsTransparent) || isTransparent(block, waterIsTransparent)))
{
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, boolean waterIsTransparent)
{
//Blacklist
switch (block.getType())
{
case SNOW:
return false;
}
//Whitelist TODO: some of this might already be included in isTransparent()
switch (block.getType())
{
case AIR:
case FENCE:
case ACACIA_FENCE:
case BIRCH_FENCE:
case DARK_OAK_FENCE:
case JUNGLE_FENCE:
case NETHER_FENCE:
case SPRUCE_FENCE:
case FENCE_GATE:
case ACACIA_FENCE_GATE:
case BIRCH_FENCE_GATE:
case DARK_OAK_FENCE_GATE:
case SPRUCE_FENCE_GATE:
case JUNGLE_FENCE_GATE:
case SIGN:
case SIGN_POST:
case WALL_SIGN:
return true;
}
if ((waterIsTransparent && block.getType() == Material.STATIONARY_WATER) ||
block.getType().isTransparent())
return true;
return false;
}
public static Visualization fromClaims(Iterable<Claim> claims, int height, VisualizationType type, Location locality)
{
Visualization visualization = new Visualization();
for(Claim claim : claims)
{
visualization.addClaimElements(claim, height, type, locality);
}
return visualization;
}
}
@@ -0,0 +1,60 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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;
}
@SuppressWarnings("deprecation")
@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
if(!element.location.getChunk().isLoaded()) continue; //cheap distance check
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;
//schedule automatic visualization reversion in 60 seconds.
GriefPrevention.instance.getServer().getScheduler().scheduleSyncDelayedTask(
GriefPrevention.instance,
new VisualizationReversionTask(player, playerData, visualization),
20L * 60); //60 seconds
}
}
@@ -0,0 +1,40 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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 Material realMaterial;
public byte realData;
public VisualizationElement(Location location, Material visualizedMaterial, byte visualizedData, Material realMaterial, byte realData)
{
this.location = location;
this.visualizedMaterial= visualizedMaterial;
this.visualizedData = visualizedData;
this.realData = realData;
this.realMaterial = realMaterial;
}
}
@@ -0,0 +1,52 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package me.ryanhamshire.GriefPrevention;
import me.ryanhamshire.GriefPrevention.events.VisualizationEvent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.Collections;
//applies a visualization for a player by sending him block change packets
class VisualizationReversionTask implements Runnable
{
private Visualization visualization;
private Player player;
private PlayerData playerData;
public VisualizationReversionTask(Player player, PlayerData playerData, Visualization visualization)
{
this.visualization = visualization;
this.playerData = playerData;
this.player = player;
}
@Override
public void run()
{
//don't do anything if the player's current visualization is different from the one scheduled to revert
if(playerData.currentVisualization != visualization) return;
// alert plugins of a visualization
Bukkit.getPluginManager().callEvent(new VisualizationEvent(player, Collections.<Claim>emptySet()));
Visualization.Revert(player);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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,
AdminClaim
}
@@ -0,0 +1,77 @@
package me.ryanhamshire.GriefPrevention;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFactory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
public class WelcomeTask implements Runnable
{
private Player player;
public WelcomeTask(Player player)
{
this.player = player;
}
@Override
public void run()
{
//abort if player has logged out since this task was scheduled
if(!this.player.isOnline()) return;
//offer advice and a helpful link
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.AvoidGriefClaimLand);
GriefPrevention.sendMessage(player, TextMode.Instr, Messages.SurvivalBasicsVideo2, DataStore.SURVIVAL_VIDEO_URL);
//give the player a reference book for later
if(GriefPrevention.instance.config_claims_supplyPlayerManual)
{
ItemFactory factory = Bukkit.getItemFactory();
BookMeta meta = (BookMeta) factory.getItemMeta(Material.WRITTEN_BOOK);
DataStore datastore = GriefPrevention.instance.dataStore;
meta.setAuthor(datastore.getMessage(Messages.BookAuthor));
meta.setTitle(datastore.getMessage(Messages.BookTitle));
StringBuilder page1 = new StringBuilder();
String URL = datastore.getMessage(Messages.BookLink, DataStore.SURVIVAL_VIDEO_URL);
String intro = datastore.getMessage(Messages.BookIntro);
page1.append(URL).append("\n\n");
page1.append(intro).append("\n\n");
String editToolName = GriefPrevention.instance.config_claims_modificationTool.name().replace('_', ' ').toLowerCase();
String infoToolName = GriefPrevention.instance.config_claims_investigationTool.name().replace('_', ' ').toLowerCase();
String configClaimTools = datastore.getMessage(Messages.BookTools, editToolName, infoToolName);
page1.append(configClaimTools);
if(GriefPrevention.instance.config_claims_automaticClaimsForNewPlayersRadius < 0)
{
page1.append(datastore.getMessage(Messages.BookDisabledChestClaims));
}
StringBuilder page2 = new StringBuilder(datastore.getMessage(Messages.BookUsefulCommands)).append("\n\n");
page2.append("/Trust /UnTrust /TrustList\n");
page2.append("/ClaimsList\n");
page2.append("/AbandonClaim\n\n");
page2.append("/Claim /ExtendClaim\n");
page2.append("/IgnorePlayer\n\n");
page2.append("/SubdivideClaims\n");
page2.append("/AccessTrust\n");
page2.append("/ContainerTrust\n");
page2.append("/PermissionTrust");
meta.setPages(page1.toString(), page2.toString());
ItemStack item = new ItemStack(Material.WRITTEN_BOOK);
item.setItemMeta(meta);
player.getInventory().addItem(item);
}
}
}
@@ -0,0 +1,41 @@
package me.ryanhamshire.GriefPrevention;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class WordFinder
{
private Pattern pattern;
WordFinder(List<String> wordsToFind)
{
if(wordsToFind.size() == 0) return;
StringBuilder patternBuilder = new StringBuilder();
for(String word : wordsToFind)
{
if(!word.isEmpty() && !word.trim().isEmpty())
{
patternBuilder.append("|(([^\\w]|^)" + Pattern.quote(word) + "([^\\w]|$))");
}
}
String patternString = patternBuilder.toString();
if(patternString.length() > 1)
{
//trim extraneous leading pipe (|)
patternString = patternString.substring(1);
}
this.pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
}
boolean hasMatch(String input)
{
if(this.pattern == null) return false;
Matcher matcher = this.pattern.matcher(input);
return matcher.find();
}
}
@@ -0,0 +1,58 @@
package me.ryanhamshire.GriefPrevention;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldguard.LocalPlayer;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.bukkit.permission.RegionPermissionModel;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.flags.DefaultFlag;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
class WorldGuardWrapper
{
private WorldGuardPlugin worldGuard = null;
public WorldGuardWrapper() throws ClassNotFoundException
{
this.worldGuard = (WorldGuardPlugin)GriefPrevention.instance.getServer().getPluginManager().getPlugin("WorldGuard");
}
public boolean canBuild(Location lesserCorner, Location greaterCorner, Player creatingPlayer)
{
World world = lesserCorner.getWorld();
if (worldGuard == null)
{
GriefPrevention.AddLogEntry("WorldGuard is out of date and not enabled. Please update or remove WorldGuard.", CustomLogEntryTypes.Debug, false);
return true;
}
if(new RegionPermissionModel(this.worldGuard, creatingPlayer).mayIgnoreRegionProtection(world)) return true;
RegionManager manager = this.worldGuard.getRegionManager(world);
if(manager != null)
{
ProtectedCuboidRegion tempRegion = new ProtectedCuboidRegion(
"GP_TEMP",
new BlockVector(lesserCorner.getX(), 0, lesserCorner.getZ()),
new BlockVector(greaterCorner.getX(), world.getMaxHeight(), greaterCorner.getZ()));
ApplicableRegionSet overlaps = manager.getApplicableRegions(tempRegion);
LocalPlayer localPlayer = worldGuard.wrapPlayer(creatingPlayer);
for (ProtectedRegion r : overlaps.getRegions()) {
if (!manager.getApplicableRegions(r).testState(localPlayer, DefaultFlag.BUILD)) {
return false;
}
}
return true;
}
return true;
}
}
@@ -0,0 +1,102 @@
package me.ryanhamshire.GriefPrevention.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called when GP is about to deliver claim blocks to a player (~every 10 minutes)
*
* @author RoboMWM
* 11/15/2016.
*/
public class AccrueClaimBlocksEvent extends Event
{
// Custom Event Requirements
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
private Player player;
private int blocksToAccrue;
private boolean isIdle = false;
private boolean cancelled = false;
/**
* @param player Player receiving accruals
* @param blocksToAccrue Blocks to accrue
*
* @deprecated Use {@link #AccrueClaimBlocksEvent(Player, int, boolean)} instead
*/
public AccrueClaimBlocksEvent(Player player, int blocksToAccrue)
{
this.player = player;
this.blocksToAccrue = blocksToAccrue / 6;
}
/**
* @param player Player receiving accruals
* @param blocksToAccrue Blocks to accrue
* @param isIdle Whether player is detected as idle
*/
public AccrueClaimBlocksEvent(Player player, int blocksToAccrue, boolean isIdle)
{
this.player = player;
this.blocksToAccrue = blocksToAccrue / 6;
this.isIdle = isIdle;
}
public Player getPlayer()
{
return this.player;
}
/**
* @return amount of claim blocks GP will deliver to the player for this 10 minute interval
*/
public int getBlocksToAccrue()
{
return this.blocksToAccrue;
}
/**
* @return whether the player was detected as idle (used for idle accrual percentage)
*/
public boolean isIdle() {
return this.isIdle;
}
public boolean isCancelled()
{
return this.cancelled;
}
/**
* Modify the amount of claim blocks to deliver to the player for this 10 minute interval
* @param blocksToAccrue blocks to deliver
*/
public void setBlocksToAccrue(int blocksToAccrue)
{
this.blocksToAccrue = blocksToAccrue;
}
/**
* Similar to setBlocksToAccrue(int), but automatically converting from a per-hour rate value to a 10-minute rate value
* @param blocksToAccruePerHour the per-hour rate of blocks to deliver
*/
public void setBlocksToAccruePerHour(int blocksToAccruePerHour)
{
this.blocksToAccrue = blocksToAccruePerHour / 6;
}
public void setCancelled(boolean cancel)
{
this.cancelled = cancel;
}
}
@@ -0,0 +1,43 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* This event gets called whenever a claim is going to be deleted. This event is
* not called when a claim is resized.
*
* @author Tux2
*
*/
public class ClaimDeletedEvent extends Event{
// Custom Event Requirements
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
private Claim claim;
public ClaimDeletedEvent(Claim claim) {
this.claim = claim;
}
/**
* Gets the claim to be deleted.
*
* @return
*/
public Claim getClaim() {
return claim;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}
@@ -0,0 +1,49 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
//if cancelled, the claim will not be deleted
public class ClaimExpirationEvent extends Event implements Cancellable
{
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
public static HandlerList getHandlerList()
{
return handlers;
}
Claim claim;
public ClaimExpirationEvent(Claim claim)
{
this.claim = claim;
}
public Claim getClaim()
{
return this.claim;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
@Override
public boolean isCancelled()
{
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,55 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Messages;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import javax.annotation.Nonnull;
/**
* Called when GP is retrieving the denial message to send to the player when canceling an action
*
* @author RoboMWM
* Created 1/4/2017.
*/
public class DeniedMessageEvent extends Event
{
// Custom Event Requirements
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
private String message;
private Messages messageID;
public DeniedMessageEvent(Messages messageID, String message)
{
this.message = message;
this.messageID = messageID;
}
public Messages getMessageID()
{
return this.messageID;
}
public String getMessage()
{
return this.message;
}
/**
* Sets the message to print to the player.
* @param message Cannot be null. Set to an empty string if you wish for no message to be printed.
*/
public void setMessage(@Nonnull String message)
{
this.message = message;
}
}
@@ -0,0 +1,89 @@
package me.ryanhamshire.GriefPrevention.events;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called when GP is about to kick or ban a player
*
* @author BillyGalbreath
* 03/10/2017.
*/
public class PlayerKickBanEvent extends Event
{
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList()
{
return handlers;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
private Player player;
private String reason;
private String source;
private boolean ban;
private boolean cancelled = false;
/**
* @param player Player getting kicked and/or banned
* @param reason Reason message for kick/ban
* @param source What caused the kick/ban
* @param ban True if player is getting banned
*/
public PlayerKickBanEvent(Player player, String reason, String source, boolean ban)
{
this.player = player;
this.reason = reason;
this.source = source;
this.ban = ban;
}
/**
* @return player getting kicked/banned
*/
public Player getPlayer()
{
return this.player;
}
/**
* @return reason player is getting kicked/banned
*/
public String getReason()
{
return this.reason;
}
/**
* @return source that is kicking/banning the player
*/
public String getSource()
{
return this.source;
}
/**
* @return is player getting banned
*/
public boolean getBan()
{
return this.ban;
}
public boolean isCancelled()
{
return this.cancelled;
}
public void setCancelled(boolean cancel)
{
this.cancelled = cancel;
}
}
@@ -0,0 +1,47 @@
package me.ryanhamshire.GriefPrevention.events;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.block.BlockBreakEvent;
//if cancelled, GriefPrevention will allow a block to be broken which it would not have otherwise
public class PreventBlockBreakEvent extends Event implements Cancellable
{
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
private BlockBreakEvent innerEvent;
public static HandlerList getHandlerList()
{
return handlers;
}
public PreventBlockBreakEvent(BlockBreakEvent innerEvent)
{
this.innerEvent = innerEvent;
}
public BlockBreakEvent getInnerEvent()
{
return this.innerEvent;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
@Override
public boolean isCancelled()
{
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,49 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
//if cancelled, GriefPrevention will not cancel the PvP event it's processing.
public class PreventPvPEvent extends Event implements Cancellable
{
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
public static HandlerList getHandlerList()
{
return handlers;
}
Claim claim;
public PreventPvPEvent(Claim claim)
{
this.claim = claim;
}
public Claim getClaim()
{
return this.claim;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
@Override
public boolean isCancelled()
{
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,49 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
//if cancelled, GriefPrevention will not protect items dropped by a player on death
public class ProtectDeathDropsEvent extends Event implements Cancellable
{
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
public static HandlerList getHandlerList()
{
return handlers;
}
Claim claim;
public ProtectDeathDropsEvent(Claim claim)
{
this.claim = claim;
}
public Claim getClaim()
{
return this.claim;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
@Override
public boolean isCancelled()
{
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,61 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.Location;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
//if destination field is set, then GriefPrevention will send the player to that location instead of searching for one
public class SaveTrappedPlayerEvent extends Event implements Cancellable
{
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
private Location destination = null;
public static HandlerList getHandlerList()
{
return handlers;
}
Claim claim;
public SaveTrappedPlayerEvent(Claim claim)
{
this.claim = claim;
}
public Location getDestination()
{
return destination;
}
public void setDestination(Location destination)
{
this.destination = destination;
}
public Claim getClaim()
{
return this.claim;
}
@Override
public HandlerList getHandlers()
{
return handlers;
}
@Override
public boolean isCancelled()
{
return this.cancelled;
}
@Override
public void setCancelled(boolean cancelled)
{
this.cancelled = cancelled;
}
}
@@ -0,0 +1,69 @@
package me.ryanhamshire.GriefPrevention.events;
import me.ryanhamshire.GriefPrevention.Claim;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import java.util.Collection;
import java.util.Collections;
/**
* Called when GriefPrevention is sending claim visuals to a player
*/
public class VisualizationEvent extends PlayerEvent {
private static final HandlerList handlers = new HandlerList();
private final Collection<Claim> claims;
private final boolean showSubdivides;
/**
* New visualization being sent to player
*
* @param player Player receiving visuals
* @param claim The claim being visualized (with subdivides), or null if visuals being removed
*/
public VisualizationEvent(Player player, Claim claim) {
super(player);
this.claims = Collections.singleton(claim);
this.showSubdivides = true;
}
/**
* New visualization being sent to player
*
* @param player Player receiving visuals
* @param claims Claims being visualized (without subdivides)
*/
public VisualizationEvent(Player player, Collection<Claim> claims) {
super(player);
this.claims = claims;
this.showSubdivides = false;
}
/**
* Get the claims being visualized, or null if visualization being removed
*
* @return Claims being visualized
*/
public Collection<Claim> getClaims() {
return claims;
}
/**
* Check if subdivide claims are being shown
*
* @return True if subdivide claims are being shown
*/
public boolean showSubdivides() {
return showSubdivides;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
@@ -0,0 +1,8 @@
/**
*
*/
/**
* @author Ryan
*
*/
package me.ryanhamshire.GriefPrevention.events;
+41
View File
@@ -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
+371
View File
@@ -0,0 +1,371 @@
name: GriefPrevention
main: me.ryanhamshire.GriefPrevention.GriefPrevention
softdepend: [Vault, Multiverse-Core, My Worlds, MystCraft, Transporter, TheUnderground, WorldGuard, WorldEdit, RoyalCommands, MultiWorld, Denizen]
dev-url: http://dev.bukkit.org/server-mods/grief-prevention
loadbefore: [TheUnderground]
version: ${project.version}
commands:
abandonclaim:
description: Deletes a claim.
usage: /AbandonClaim
aliases: [unclaim, declaim, removeclaim, disclaim]
permission: griefprevention.claims
abandontoplevelclaim:
description: Deletes a claim and all its subdivisions.
usage: /AbandonTopLevelClaim
permission: griefprevention.claims
abandonallclaims:
description: Deletes ALL your claims.
usage: /AbandonAllClaims
permission: griefprevention.claims
trust:
description: Grants a player full access to your claim(s).
usage: /Trust <player> Grants a player permission to build. See also /UnTrust, /ContainerTrust, /AccessTrust, and /PermissionTrust.
aliases: tr
permission: griefprevention.claims
untrust:
description: Revokes a player's access to your claim(s).
usage: /UnTrust <player>
aliases: ut
permission: griefprevention.claims
containertrust:
description: Grants a player access to your claim's containers, crops, animals, bed, buttons, and levers.
usage: /ContainerTrust <player>. Grants a player access to your inventory, crops, animals, bed, and buttons/levers.
aliases: ct
permission: griefprevention.claims
accesstrust:
description: Grants a player entry to your claim(s) and use of your bed.
usage: /AccessTrust <player>. Grants a player access to your bed, buttons, and levers.
aliases: at
permission: griefprevention.claims
permissiontrust:
description: Grants a player permission to grant his level of permission to others.
usage: /PermissionTrust <player>. Permits a player to share his permission level with others.
aliases: pt
permission: griefprevention.claims
subdivideclaims:
description: Switches the shovel tool to subdivision mode, used to subdivide your claims.
usage: /SubdivideClaims
aliases: [sc, subdivideclaim]
permission: griefprevention.claims
restrictsubclaim:
description: Restricts a subclaim, so that it inherits no permissions from the parent claim
usage: /restrictsubclaim
aliases: rsc
permission: griefprevention.claims
adjustbonusclaimblocks:
description: Adds or subtracts bonus claim blocks for a player.
usage: /AdjustBonusClaimBlocks <player> <amount>
permission: griefprevention.adjustclaimblocks
aliases: acb
adjustbonusclaimblocksall:
description: Adds or subtracts bonus claim blocks for all online players.
usage: /AdjustBonusClaimBlocksAll <player> <amount>
permission: griefprevention.adjustclaimblocks
aliases: acball
setaccruedclaimblocks:
description: Updates a player's accrued claim block total.
usage: /SetAccruedClaimBlocks <player> <amount>
permission: griefprevention.adjustclaimblocks
aliases: scb
deleteclaim:
description: Deletes the claim you're standing in, even if it's not your claim.
usage: /DeleteClaim
permission: griefprevention.deleteclaims
deleteallclaims:
description: Deletes all of another player's claims.
usage: /DeleteAllClaims <player>
permission: griefprevention.deleteclaims
deleteclaimsinworld:
description: Deletes all the claims in a world. Only usable at the server console.
usage: /DeleteClaimsInWorld <world>
aliases: [deleteallclaimsinworld, clearclaimsinworld, clearallclaimsinworld]
permission: griefprevention.deleteclaimsinworld
deleteuserclaimsinworld:
description: Deletes all the non-admin claims in a world. Only usable at the server console.
usage: /DeleteUserClaimsInWorld <world>
aliases: [deletealluserclaimsinworld, clearuserclaimsinworld, clearalluserclaimsinworld]
permission: griefprevention.deleteclaimsinworld
adminclaims:
description: Switches the shovel tool to administrative claims mode.
usage: /AdminClaims
permission: griefprevention.adminclaims
aliases: ac
restorenature:
description: Switches the shovel tool to restoration mode.
usage: /RestoreNature
permission: griefprevention.restorenature
aliases: rn
restorenatureaggressive:
description: Switches the shovel tool to aggressive restoration mode.
usage: /RestoreNatureAggressive
permission: griefprevention.restorenatureaggressive
aliases: rna
restorenaturefill:
description: Switches the shovel tool to fill mode.
usage: /RestoreNatureFill <radius>
permission: griefprevention.restorenatureaggressive
aliases: rnf
basicclaims:
description: Switches the shovel tool back to basic claims mode.
usage: /BasicClaims
aliases: bc
permission: griefprevention.claims
extendclaim:
description: Resizes the land claim you're standing in by pushing or pulling its boundary in the direction you're facing.
usage: /ExtendClaim <numberOfBlocks>
aliases: [expandclaim, resizeclaim]
permission: griefprevention.claims
claim:
description: Creates a land claim centered at your current location.
usage: /Claim [optional radius]
aliases: [createclaim, makeclaim, newclaim]
permission: griefprevention.claims
buyclaimblocks:
description: Purchases additional claim blocks with server money. Doesn't work on servers without a Vault-compatible economy plugin.
usage: /BuyClaimBlocks <numberOfBlocks>
aliases: buyclaim
permission: griefprevention.buysellclaimblocks
sellclaimblocks:
description: Sells your claim blocks for server money. Doesn't work on servers without a Vault-compatible economy plugin.
usage: /SellClaimBlocks <numberOfBlocks>
aliases: sellclaim
permission: griefprevention.buysellclaimblocks
trapped:
description: Ejects you to nearby unclaimed land. Has a substantial cooldown period.
usage: /Trapped
permission: griefprevention.trapped
trustlist:
description: Lists permissions for the claim you're standing in.
usage: /TrustList
permission: griefprevention.claims
siege:
description: Initiates a siege versus another player.
usage: /Siege <playerName>
permission: griefprevention.siege
ignoreclaims:
description: Toggles ignore claims mode.
usage: /IgnoreClaims
permission: griefprevention.ignoreclaims
aliases: ic
deletealladminclaims:
description: Deletes all administrative claims.
usage: /DeleteAllAdminClaims
permission: griefprevention.adminclaims
adminclaimslist:
description: Lists all administrative claims.
usage: /AdminClaimsList
permission: griefprevention.adminclaims
transferclaim:
description: Converts an administrative claim to a private claim.
usage: /TransferClaim <player>
aliases: giveclaim
permission: griefprevention.transferclaim
unlockdrops:
description: Allows other players to pick up the items you dropped when you died.
usage: /UnlockDrops
permission: griefprevention.unlockdrops
claimslist:
description: Lists information about a player's claim blocks and claims.
usage: /ClaimsList or /ClaimsList <player>
aliases: [claimlist, listclaims]
permission: griefprevention.claims
claimexplosions:
description: Toggles whether explosives may be used in a specific land claim.
usage: /ClaimExplosions
permission: griefprevention.claims
aliases: claimexplosion
softmute:
description: Toggles whether a player's messages will only reach other soft-muted players.
usage: /SoftMute <player>
permission: griefprevention.softmute
gpreload:
description: Reloads Grief Prevention's configuration settings. Does NOT totally reload the entire plugin.
usage: /gpreload
permission: griefprevention.reload
givepet:
description: Allows a player to give away a pet he or she tamed.
usage: /GivePet <player>
permission: griefprevention.givepet
gpblockinfo:
description: Allows an administrator to get technical information about blocks in the world and items in hand.
usage: /GPBlockInfo
permission: griefprevention.gpblockinfo
ignoreplayer:
description: Ignores another player's chat messages.
usage: /IgnorePlayer <player name>
aliases: [ignore]
permission: griefprevention.ignore
unignoreplayer:
description: Unignores another player's chat messages.
usage: /UnIgnorePlayer <player name>
aliases: [unignore]
permission: griefprevention.ignore
ignoredplayerlist:
description: Lists the players you're ignoring in chat.
usage: /IgnoredPlayerList
aliases: [ignores, ignored, ignorelist, ignoredlist, listignores, listignored, ignoring]
permission: griefprevention.ignore
separate:
description: Forces two players to ignore each other in chat.
usage: /Separate <player1> <player2>
permission: griefprevention.separate
unseparate:
description: Reverses /separate.
usage: /UnSeparate <player1> <player2>
permission: griefprevention.separate
claimbook:
description: Gives a player a manual about claiming land.
usage: /ClaimBook <player>
permission: griefprevention.claimbook
permissions:
griefprevention.createclaims:
description: Grants permission to create claims.
default: true
griefprevention.admin.*:
description: Grants all administrative functionality.
children:
griefprevention.restorenature: true
griefprevention.restorenatureaggressive: true
griefprevention.ignoreclaims: true
griefprevention.adminclaims: true
griefprevention.adjustclaimblocks: true
griefprevention.deleteclaims: true
griefprevention.spam: true
griefprevention.lava: true
griefprevention.eavesdrop: true
griefprevention.deathblow: true
griefprevention.softmute: true
griefprevention.reload: true
griefprevention.visualizenearbyclaims: true
griefprevention.overrideclaimcountlimit: true
griefprevention.transferclaim: true
griefprevention.claimslistother: true
griefprevention.siegeimmune: true
griefprevention.separate: true
griefprevention.eavesdropsigns: true
griefprevention.claimbook: true
griefprevention.notignorable: true
griefprevention.seeinactivity: true
griefprevention.eavesdropimmune: true
griefprevention.deleteclaimsinworld: true
griefprevention.siegeteleport: true
griefprevention.unlockothersdrops: true
griefprevention.seeclaimsize: true
griefprevention.siegeimmune:
description: Makes a player immune to /Siege.
default: op
griefprevention.givepet:
description: Grants permission to use /GivePet.
default: true
griefprevention.siege:
description: Grants permission to use /Siege.
default: true
griefprevention.unlockdrops:
description: Grants permission to use /UnlockDrops.
default: true
griefprevention.unlockothersdrops:
description: Grants permission to use /UnlockDrops for other players.
default: op
griefprevention.trapped:
description: Grants permission to use /Trapped.
default: true
griefprevention.claimslistother:
description: Grants permission to use /ClaimsList to get another player's information.
default: op
griefprevention.restorenature:
description: Grants permission to use /RestoreNature.
default: op
griefprevention.transferclaim:
description: Grants permission to use /TransferClaim.
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.deleteclaimsinworld:
description: Not used. DeleteClaimsInWorld must be executed at the server console.
default: op
griefprevention.adjustclaimblocks:
description: Grants permission to add or remove bonus blocks from a player's account.
default: op
griefprevention.spam:
description: Grants permission to log in, send messages, and send commands rapidly.
default: op
griefprevention.lava:
description: Grants permission to place lava near the surface and outside of claims.
default: op
griefprevention.eavesdrop:
description: Allows a player to see whispered chat messages (/tell) and softmuted messages.
default: op
griefprevention.eavesdropsigns:
description: Allows a player to see sign placements as chat messages.
default: op
griefprevention.restorenatureaggressive:
description: Grants access to /RestoreNatureAggressive and /RestoreNatureFill.
default: op
griefprevention.deathblow:
description: Grants access to /DeathBlow.
default: op
griefprevention.reload:
description: Grants access to /gpreload.
default: op
griefprevention.softmute:
description: Grants access to /SoftMute.
default: op
griefprevention.claims:
description: Grants access to claim-related slash commands.
default: true
griefprevention.buysellclaimblocks:
description: Grants access to claim block buy/sell commands.
default: true
griefprevention.visualizenearbyclaims:
description: Allows a player to see all nearby claims at once.
default: op
griefprevention.seeclaimsize:
description: Allows a player to see claim size for other players claims when right clicking with investigation tool
default: op
griefprevention.gpblockinfo:
description: Grants access to /GPBlockInfo.
default: op
griefprevention.overrideclaimcountlimit:
description: Allows players to create more claims than the limit specified by the config.
default: op
griefprevention.separate:
description: Grants access to /Separate and /UnSeparate.
default: op
griefprevention.ignore:
description: Grants access to /Ignore, /Unignore, and /IgnoreList
default: true
griefprevention.claimbook:
description: Grants access to /ClaimBook.
default: op
griefprevention.notignorable:
description: Players with this permission can't be ignored.
default: op
griefprevention.seeinactivity:
description: Players with this permission can see how long a claim owner has been offline.
default: op
griefprevention.eavesdropimmune:
description: Players with this permission can't have their private messages eavesdropped.
default: op
griefprevention.fasteraccrual:
description: Players with this permission accrue claim blocks at the faster rate specified in the config file.
default: false
griefprevention.fastestaccrual:
description: Players with this permission accrue claim blocks at the fastest rate specified in the config file.
default: false
griefprevention.moreaccrued:
description: Players with this permission can accrue more claim blocks (limit specified in the config file).
default: false
griefprevention.mostaccrued:
description: Players with this permission can accrue more claim blocks (limit specified in the config file).
default: false
griefprevention.siegeteleport:
description: Players with this permission can teleport into and out of besieged areas.
default: op