public interface BorderHandler extends ExilePearlRunnable, Listener {

} public interface BrewHandler {

	/**
	 * Base handler description for checking if something is not a vanilla potion, but
	 * rather a plugin brew.
	 * @param item The item to check
	 * @return true if a plugin brew, false otherwise.
	 */
	public boolean isBrew(ItemStack item);
} public interface DamageLogger extends Configurable, Listener, ExilePearlRunnable {

	/**
	 * Records damage that a player deals to another player
	 * @param playerId The player ID being harmed
	 * @param damager The damage dealer
	 * @param amount The damage amount
	 */
	void recordDamage(UUID playerId, Player damager, double amount); 

	/**
	 * Records damage that a player deals to another player
	 * @param player The player being harmed
	 * @param damager The damage dealer
	 * @param amount The damage amount
	 */
	void recordDamage(Player player, Player damager, double amount); 

	/**
	 * Gets a sorted list of damaging players.
	 * 
	 * The player at the front of the collection is the one that should be 
	 * awarded the pearl according to the configured algorithm.
	 *  
	 * @param playerId The player ID being harmed
	 * @return The damaging players
	 */
	List<Player> getSortedDamagers(UUID playerId);

	/**
	 * Gets a sorted list of damaging players.
	 * 
	 * The player at the front of the collection is the one that should be 
	 * awarded the pearl according to the configured algorithm.
	 *  
	 * @param player The player being harmed
	 * @return The damaging players
	 */
	List<Player> getSortedDamagers(Player player);

} public interface ExilePearl {

	/**
	 * Gets the pearl item name
	 * @return The item name
	 */
	String getItemName();

	/**
	 * Gets the exiled player ID
	 * @return The player ID
	 */
	UUID getPlayerId();

	/**
	 * Gets the unique pearl ID
	 * @return The pearl ID
	 */
	int getPearlId();

	/**
	 * Gets the exiled player instance
	 * @return The exiled player 
	 */
	Player getPlayer();

	/**
	 * Gets the pearl type
	 * @return The pearl type
	 */
	PearlType getPearlType();

	/**
	 * Sets the pearl type
	 * @param type The pearl type
	 */
	void setPearlType(PearlType type);

	/**
	 * Gets when the player was pearled
	 * @return The time the player was pearled
	 */
	Date getPearledOn();

	/**
	 * Sets when the player was pearled
	 * @param pearledOn The time the player was pearled
	 */
	void setPearledOn(Date pearledOn);

	/**
	 * Gets the exiled player's name
	 * @return The exiled player's name
	 */
	String getPlayerName();

	/**
	 * Gets the pearl holder
	 * @return The pearl holder
	 */
	PearlHolder getHolder();

	/**
	 * Sets the pearl holder to a player
	 * @param player The new pearl holder
	 */
	void setHolder(Player player);

	/**
	 * Sets the pearl holder to a block
	 * @param block The new pearl block
	 */
	void setHolder(Block block);

	/**
	 * Sets the pearl holder to a location
	 * @param item The new pearl item
	 */
	void setHolder(Item item);

	/**
	 * Sets the pearl holder to an entity
	 * @param entity The new pearl entity
	 */
	void setHolder(Entity entity);

    /**
     * Gets the pearl health value
     * @return The health value
     */
    int getHealth();

    /**
     * Gets the pearl health percent value
     * @return The health percent value
     */
    Integer getHealthPercent();
    
    /**
     * Sets the pearl health value
     * @param health The health value
     */
    void setHealth(int health);
    
    /**
     * Gets the location of the pearl
     * @return The location of the pearl
     */
	Location getLocation();

	/**
	 * Sets the killer ID
	 * @param killerId The killer ID
	 */
	void setKillerId(UUID killerId);

	/**
	 * Gets the ID of the killing player
	 * @return The ID of the killing player
	 */
	UUID getKillerId();

	/**
	 * Gets the name of the killing player
	 * @return The name of the killing player
	 */
	String getKillerName();

	/**
	 * Gets the string describing the pearl current location
	 * @return The description of the current location
	 */
	String getLocationDescription();

	/**
	 * Gets whether the player was freed offline
	 * @return true if the player was freed offline
	 */
	boolean getFreedOffline();

	/**
	 * Gets whether the player was freed offline
	 * @param freedOffline The freed offline value
	 */
	void setFreedOffline(boolean freedOffline);

	/**
	 * Creates an item stack for the pearl
	 * @return The new item stack
	 */
	ItemStack createItemStack();

	/**
	 * Verifies the pearl location
	 * @return true if the location is verified
	 */
	boolean verifyLocation();

	/**
	 * Validates that an item stack is the exile pearl
	 * @param is The item stack
	 * @return true if validation passes
	 */
	boolean validateItemStack(ItemStack is);

	/**
	 * Enables storage updates when writing values
	 */
	void enableStorage();

	/**
	 * Broadcasts the pearl location to all the listeners
	 */
	void performBroadcast();

	/**
	 * Adds a broadcast target
	 * @param bcast The broadcast target
	 */
	void addBroadcastListener(BroadcastListener bcast);

	/**
	 * Removes a broadcast target
	 * @param bcast The broadcast target
	 */
	void removeBroadcastListener(Object bcast);

	/**
	 * Checks if the pearl is broadcasting to an object
	 * @param o The object to check
	 * @return true if it is broadcasting
	 */
	boolean isBroadcastingTo(Object o);

	/**
	 * Gets last recorded appearance online for the player
	 * held by this pearl
	 * 
	 * @return last online Date
	 */
	Date getLastOnline();

	/**
	 * Resets the recorded appearance online for a player
	 * held by this pearl
	 * 
	 * @param online The new latest Seen Date.
	 */
	void setLastOnline(Date online);

	/**
	 * Gets whether or not the pearled player is summoned
	 * This is always false for exiled players, summoning only works for upgraded PrisonPearls
	 * @return true if the player is summoned
	 */
	boolean isSummoned();

	/**
	 * Sets the summoned state of a player
	 * Can only be true if pearl is of prison type
	 * @param summoned The summoned value
	 */
	void setSummoned(boolean summoned);

	/**
	 * Gets the location where player should be returned from summon
	 * @return The location
	 */
	Location getReturnLocation();

	/**
	 * Sets the location where a player should be returned from summon
	 * @param loc The location
	 */
	void setReturnLocation(Location loc);
	
	/**
	 * Gets the multiplier to apply to the amount of repair materials needed to repair the pearl 
	 * based on how long the player has been imprisoned
	 * @return Repair cost multiplier
	 */
	double getLongTimeMultiplier();
} public interface ExilePearlApi extends Plugin, PearlAccess, PearlLogger, PlayerProvider {

	/**
	 * Gets the exile pearl configuration
	 * @return The exile pearl configuration
	 */
	PearlConfig getPearlConfig();

	/**
	 * Gets the pearl lore provider
	 * @return The lore provider
	 */
	LoreProvider getLoreProvider();

	/**
	 * Gets the storage provider
	 * @return The storage provider
	 */
	StorageProvider getStorageProvider();

	/**
	 * Gets the suicide handler
	 * @return The suicide handler
	 */
	SuicideHandler getSuicideHandler();

	/**
	 * Gets the pearl manager
	 * @return The pearl manager
	 */
	PearlManager getPearlManager();

	/**
	 * Gets the damage logger
	 * @return The damage logger
	 */
	DamageLogger getDamageLogger();

	/**
	 * Gets the auto-help command
	 * @return The auto-help
	 */
	PearlCommand getAutoHelp();

	/**
	 * Gets whether a player is combat tagged
	 * @param uid The player UUID
	 * @return true if the player is tagged
	 */
	boolean isPlayerTagged(UUID uid);

	/**
	 * Gets a player as a tagged NPC entity.
	 * <p>
	 * This will return null for a normal player.
	 * @param player The player being referenced
	 * @return The NPC entity instance
	 */
	NpcIdentity getPlayerAsTaggedNpc(Player player);

	/**
	 * Gets whether Artemis hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isArtemisEnabled();
	
	/**
	 * Gets whether NameLayer hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isNameLayerEnabled();

	/**
	 * Gets whether Citadel hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isCitadelEnabled();

	/**
	 * Gets whether Bastion hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isBastionEnabled();

	/**
	 * Gets whether JukeAlert hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isJukeAlertEnabled();

	/**
	 * Gets whether BanStick hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isBanStickEnabled();

	/**
	 * Gets whether RandomSpawn hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isRandomSpawnEnabled();

	/**
	 * Gets whether WorldBorder hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isWorldBorderEnabled();

	/**
	 * Gets whether CombatTag hooks are enabled
	 * @return True if it is enabled
	 */
	boolean isCombatTagEnabled();

	/**
	 * Checks if a location is inside the world border
	 * @param location the location to check
	 * @return true if the location is inside the border
	 */
	boolean isLocationInsideBorder(Location location);

	/**
	 * Gets whether a player is in a non-permission bastion
	 * @param player The player to check
	 * @return true if the player is inside a non-permission bastion
	 */
	boolean isPlayerInUnpermittedBastion(Player player);

	/**
	 * Gets bastions the player is in, that they shouldn't be in.
	 * @param player The player to check
	 * @return list of bastions the player is inside that they shouldn't be
	 */
	List<BastionWrapper> getPlayerInUnpermittedBastion(Player player);

	/**
	 * Gets the clock instance
	 * @return the clock instance
	 */
	Clock getClock();

	/**
	 * Gets the Brew handler for brew checking, if any.
	 *
	 * @return a Brew Handler appropriate for the server.
	 */
	BrewHandler getBrewHandler();

	/**
	 * Gets NameLayer PermissionTracker
	 *
	 * @return NameLayerPermissions class
	 */
	NameLayerPermissions getNameLayerPermissions();
} public interface LoreProvider {

	/**
	 * Generates the lore for the pearl
	 * @param pearl The pearl instance
	 * @return The pearl lore
	 */
	List<String> generateLore(ExilePearl pearl);

	/**
	 * Generates the lore for a pearl with a modified health value
	 * @return The pearl lore
	 */
	List<String> generateLoreWithModifiedHealth(ExilePearl pearl, int healthValue);

	/**
	 * Generates the lore for a pearl with a modified type
	 * @return The pearl lore
	 */
	List<String> generateLoreWithModifiedType(ExilePearl pearl, PearlType type);

	/**
	 * Gets the pearl ID from a pearl item stack
	 * @param is The item stack to parse
	 * @return The pearl ID, or 0 if can't parse
	 */
	int getPearlIdFromItemStack(ItemStack is);

	/**
	 * Gets a player ID from a legacy prison pearl
	 * @return The player ID if it exists
	 */
	UUID getPlayerIdFromLegacyPearl(ItemStack is);

	/**
	 * Generates the info for a pearl
	 * @param pearl The pearl instance
	 * @return The returned lore
	 */
	List<String> generatePearlInfo(ExilePearl pearl);
} public enum PearlFreeReason
{
	FREED_BY_PLAYER,
	PEARL_THROWN,
	FREED_BY_ADMIN,
	FORCE_FREED_BY_ADMIN,
	HEALTH_DECAY,
	PEARL_DESTROYED,
	VALIDATION_FAILED,
	OUTSIDE_WORLD_BORDER,
	FREED_OFFLINE,
	CHUNK_UNLOADED
	;
} public interface PearlManager extends PearlAccess {

	/**
	 * Loads all pearls from storage
	 */
	void loadPearls();

	/**
	 * Performs a health decay operation on all pearls
	 */
	void decayPearls();

	/**
	 * Adds a pearl broadcast request for a player
	 * @param player The player requested
	 * @param pearl The pearl instance
	 */
	void addBroadcastRequest(Player player, ExilePearl pearl);

	/**
	 * Gets a pending broadcast request for a player
	 * @param player The player to check
	 * @return The pearl that requested a broadcast if it exists
	 */
	ExilePearl getBroadcastRequest(Player player);

	/**
	 * Removes a broadcast request for a player
	 * @param player The player to remove
	 */
	void removeBroadcastRequest(Player player);
} public interface StorageProvider {

	/**
	 * Gets the plugin storage instance
	 * @return The plugin storage instance
	 */
	PluginStorage getStorage();
} public interface SuicideHandler extends ExilePearlRunnable, Listener {

	/**
	 * Adds a player to the suicide queue
	 * @param player The player to add
	 */
	void addPlayer(Player player);

	/**
	 * Gets whether a player is added
	 * @param uid The player to check
	 * @return true if the player is added
	 */
	boolean isAdded(UUID uid);
} public abstract class BaseCommand<T extends Plugin> {

	protected final T plugin;

	private final Map<String, String> permissionDescriptions = new HashMap<String, String>();
	private final List<BaseCommand<? extends Plugin>> subCommands = new ArrayList<BaseCommand<? extends Plugin>>();

	// The different names this commands will react to  
	protected final StringListIgnoresCase aliases = new StringListIgnoresCase();

	// Information on the args
	protected final List<CommandArg> commandArgs = new ArrayList<CommandArg>();
	protected boolean errorOnToManyArgs = true;

	// FIELD: Help Short
	// This field may be left blank and will in such case be loaded from the permissions node instead.
	// Thus make sure the permissions node description is an action description like "eat hamburgers" or "do admin stuff".
	protected String helpShort = "";

	protected final List<String> helpLong = new ArrayList<String>();
	protected CommandVisibility visibility = CommandVisibility.VISIBLE;

	public List<String> getLongHelp() {
		return this.helpLong;
	}

	public CommandVisibility getVisibility() {
		return this.visibility;
	}

	// Some information on permissions
	protected boolean senderMustBePlayer;
	protected String permission;
	protected boolean senderMustConfirm;

	// Information available on execution of the command
	protected CommandSender sender; // Will always be set
	protected boolean senderIsConsole;
	protected List<String> args; // Will contain the arguments, or and empty list if there are none.
	protected List<BaseCommand<? extends Plugin>> commandChain = new ArrayList<BaseCommand<? extends Plugin>>(); // The command chain used to execute this command

	// Will only be set when the sender is a player
	private Player me;

	/**
	 * Creates a new SabreCommand instance
	 * @param plugin The plugin instance
	 */
	public BaseCommand(T plugin) {
		this.plugin = plugin;
	}


	/**
	 * Performs the command
	 */
	protected abstract void perform();


	/**
	 * Performs the command tab list completion
	 */
	protected List<String> performTabList() {
		return null;
	}


	/**
	 * Executes the command
	 * @param sender The command sender
	 * @param args The command arguments
	 * @param commandChain The command chain
	 */
	public final void execute(CommandSender sender, List<String> args, List<BaseCommand<? extends Plugin>> commandChain) {
		if (prepareCommand(sender, args, commandChain)) {
			perform();
		}
	}

	/**
	 * Executes the command
	 * @param sender The command sender
	 * @param args The command arguments
	 */
	public final void execute(CommandSender sender, List<String> args) {
		execute(sender, args, new ArrayList<BaseCommand<? extends Plugin>>());
	}


	/**
	 * Gets the tab list for the command
	 * @param sender The command sender
	 * @param args The command arguments
	 * @return The tab list
	 */
	public final List<String> getTabList(CommandSender sender, List<String> args, List<BaseCommand<? extends Plugin>> commandChain) {
		return prepareTab(sender, args, commandChain);
	}


	/**
	 * Prepares a command for execution
	 * @param sender The command sender
	 * @param args The command arguments
	 * @param commandChain The command chain
	 * @return true if the command should execute
	 */
	private boolean prepareCommand(CommandSender sender, List<String> args, List<BaseCommand<? extends Plugin>> commandChain) {
		// Set the execution-time specific variables
		this.sender = sender;
		if (sender instanceof Player) {
			Player p = (Player)sender;

			this.me = plugin.getServer().getPlayer(p.getUniqueId());
			this.senderIsConsole = false;
		} else {
			this.me = null;
			this.senderIsConsole = true;
		}
		this.args = args;
		this.commandChain = commandChain;

		// Sender must have permission for the root node of this command
		if ( !validSenderPermissions(sender, true)) {
			return false;
		}

		// Find a matching sub-command
		if (args.size() > 0 ) {
			for (BaseCommand<? extends Plugin> subCommand: this.subCommands) {
				if (subCommand.aliases.contains(args.get(0))) {
					// Pretend like this command doesn't exist
					if (!subCommand.validSenderPermissions(sender, false) && subCommand.visibility != CommandVisibility.VISIBLE) {
						sendTooManyArgs();
						return false;
					}				

					args.remove(0);
					commandChain.add(this);
					subCommand.execute(sender, args, commandChain);
					return false;
				}
			}
		}

		if ( !validCall(this.sender, this.args)) {
			return false;
		}

		return true;
	}

	/**
	 * Prepares a tab command for execution
	 * @param sender The command sender
	 * @param args The command arguments
	 * @param commandChain The command chain
	 * @return the tab results
	 */
	private List<String> prepareTab(CommandSender sender, List<String> args, List<BaseCommand<? extends Plugin>> commandChain) {
		// Set the execution-time specific variables
		this.sender = sender;
		if (sender instanceof Player) {
			Player p = (Player)sender;

			this.me = plugin.getServer().getPlayer(p.getUniqueId());
			this.senderIsConsole = false;
		} else {
			this.me = null;
			this.senderIsConsole = true;
		}
		this.args = args;
		this.commandChain = commandChain;

		// Sender must have permission for the root node of this command
		if ( !validSenderPermissions(sender, true)) {
			return null;
		}

		// Find a matching sub-command
		if (args.size() > 0 ) {
			for (BaseCommand<? extends Plugin> subCommand: this.subCommands) {
				if (subCommand.aliases.contains(args.get(0))) {
					args.remove(0);
					commandChain.add(this);
					return subCommand.getTabList(sender, args, commandChain);
				}
			}
		}

		List<String> subCommands = new ArrayList<String>();

		// Get the tab list of any matching sub-commands
		if (args.size() > 0 ) {
			for (BaseCommand<? extends Plugin> subCommand: this.subCommands) {
				if (!(subCommand.validSenderPermissions(sender, false))) {
					continue;
				}

				if (subCommand.senderMustBePlayer && senderIsConsole) {
					continue;
				}

				for(String alias : subCommand.aliases) {
					if (alias.startsWith(args.get(0))) {
						subCommands.add(alias);
						break;
					}
				}
			}
		}

		// Return the matching tab commands if there are any
		if (this.subCommands.size() > 0) {
			if (subCommands.size() > 0) {
				return subCommands;
			} else {
				return null;
			}
		}

		// This is the case when someone tabs at the end of a command before pressing space
		if (args.size() == 0 || (args.size() == 1 && args.get(0) == "")) {
			return null;
		}

		// Check if the arg is an auto-tab type
	   if(args.size() > commandArgs.size()) {
		   msg("<i>No more arguments are needed.");
		   return null;
	   }

		CommandArg tabArg = commandArgs.get(args.size() - 1);
		if (tabArg == null) {
			return null;
		}

		List<String> tabList = new ArrayList<String>();

		if (tabArg.isAutoTab()) {
			tabList = getAutoTab(tabArg.getAutoTab().getName(), args.get(args.size() - 1));
			if (tabList != null) {
				return tabList;
			}
		}

		// Otherwise perform the tab routine for the command itself
		tabList = performTabList();
		if (tabList != null) {
			return tabList;
		}

		// If still nothing found, send the help message
		if(tabArg.isAutoTab() && tabArg.getAutoTab().getHelp() != null) {
			msg("<i>" + tabArg.getAutoTab().getHelp());
		}
		return null;
	}

	protected List<String> getAutoTab(String name, String pattern) {		
		List<String> tabList = new ArrayList<String>();

		switch(name) {
		case "player":
			for (Player p: Bukkit.getOnlinePlayers()) {
				if (p.getName().toLowerCase().startsWith(pattern.toLowerCase())) {
					tabList.add(p.getName());
				}
			}
			break;
		case "uuid":
			for (Player p: Bukkit.getOnlinePlayers()) {
				if (p.getUniqueId().toString().toLowerCase().startsWith(pattern.toLowerCase())) {
					tabList.add(p.getUniqueId().toString());
				}
			}
			break;
		case "player_uuid":
			List<String> players = getAutoTab("player", pattern);
			List<String> ids = getAutoTab("uuid", pattern);

			if (players != null) {
				tabList.addAll(players);
			}

			if (ids != null) {
				tabList.addAll(ids);
			}
			break;
		default:
			List<String> customTab = getCustomAutoTab(name, pattern);
			if (customTab != null) {
				tabList.addAll(customTab);
			}
			break;
		}

		if (tabList.size() == 0) {
			return null;
		}

		return tabList;
	}

	/**
	 * Method that can be overridden for getting custom auto-tabs
	 * @param tabName The tab argument
	 * @param pattern The search pattern
	 * @return The tab list if any results
	 */
	protected List<String> getCustomAutoTab(String tabName, String pattern) {
		return null;
	}


	/**
	 * Gets the tab list for the command
	 * @param sender The command sender
	 * @param args The command arguments
	 * @return The tab list
	 */
	public final List<String> getTabList(CommandSender sender, List<String> args) {
		return getTabList(sender, args, new ArrayList<BaseCommand<? extends Plugin>>());
	}


	/**
	 * Gets the command aliases
	 * @return The command aliases
	 */
	public ArrayList<String> getAliases() {
		return this.aliases;
	}

	/**
	 * Gets the sender instance
	 * @return The sender instance
	 */
	protected Player player() {
		return me;
	}


	/**
	 * Adds a sub-command
	 * @param subCommand the sub-command to add
	 */
	public void addSubCommand(BaseCommand<? extends Plugin> subCommand) {
		subCommand.getCommandChain().addAll(this.commandChain);
		subCommand.getCommandChain().add(this);
		this.subCommands.add(subCommand);
	}


	/**
	 * Gets the command chain
	 * @return The command chain
	 */
	public List<BaseCommand<? extends Plugin>> getCommandChain() {
		return commandChain;
	}

	/**
	 * Gets the sub-commands
	 * @return The sub-commands
	 */
	public List<BaseCommand<? extends Plugin>> getSubCommands() {
		return this.subCommands;
	}

	/**
	 * Sets the short help string
	 * @param str The short help value
	 */
	protected void setHelpShort(String str) { 
		this.helpShort = str;
	}

	/**
	 * Gets the short help string
	 * @return The short help string
	 */
	protected String getHelpShort() {
		if (this.helpShort == null) {
			String pdesc = getPermissionDescription(this.permission);
			if (pdesc != null) {
				return pdesc;
			}
			return "*info unavailable*";
		}
		return this.helpShort;
	}


	/**
	 * This method validates that all prerequisites to perform this command has been met.
	 * @param sender The command sender
	 * @param args The command args
	 * @return true if the call if valid and can proceed
	 */
	protected boolean validCall(CommandSender sender, List<String> args) {
		if (!validSenderType(sender, true)) {
			return false;
		}

		if (!validSenderPermissions(sender, true)) {
			return false;
		}

		if (!validArgs(args, sender)) {
			return false;
		}
		return true;
	}

	/**
	 * Checks if the sender is the correct type (console/player)
	 * @param sender The sender to check
	 * @param informSenderIfNot Whether to inform the sender if not valid
	 * @return true if the sender is valid
	 */
	protected boolean validSenderType(CommandSender sender, boolean informSenderIfNot) {		
		if (senderMustBePlayer && (!(sender instanceof Player))) {
			if (informSenderIfNot) {
				msg(Lang.commandSenderMustBePlayer);
			}
			return false;
		}
		return true;
	}

	/**
	 * Checks if the sender has valid permissions
	 * @param sender The sender to check
	 * @param informSenderIfNot Whether to inform the sender if no permission
	 * @return true if the sender has permission
	 */
	public boolean validSenderPermissions(CommandSender sender, boolean informSenderIfNot) {
		if (this.permission == null) {
			return true;
		}
		return senderHasPerm(sender, this.permission, this.visibility, informSenderIfNot);
	}

	/**
	 * Checks if the valid arguments were sent for the command
	 * @param args The args to check
	 * @param sender The command sender
	 * @return true if the commands are valid
	 */
	protected boolean validArgs(List<String> args, CommandSender sender) {
		if (args.size() < numRequiredArgs()) {
			msg(Lang.commandToFewArgs);
			msg(getUsageTemplate());
			return false;
		}

		if (args.size() > this.commandArgs.size() && this.errorOnToManyArgs) {
			sendTooManyArgs();
			return false;
		}
		return true;
	}

	private void sendTooManyArgs() {
		if (args.size() > this.commandArgs.size() && this.errorOnToManyArgs) {
			// Get the to many string slice
			List<String> theToMany = args.subList(this.commandArgs.size(), args.size());
			msg(Lang.commandToManyArgs, TextUtil.implode(theToMany, " "));
			msg(this.getUsageTemplate());
		}
	}

	/**
	 * Gets the number of required arguments
	 * @return The number of required arguments
	 */
	protected int numRequiredArgs() {
		int i = 0;
		for (CommandArg a : commandArgs) {
			if (a.isRequired()) {
				i++;
			}
		}
		return i;
	}

	/**
	 * Checks if the valid arguments were sent for the command
	 * @param args The args to check
	 * @return true if the args are valid
	 */
	protected boolean validArgs(List<String> args) {
		return this.validArgs(args, null);
	}

	/**
	 * Gets the complete usage template for a command chain
	 * @param commandChain The command chain
	 * @param addShortHelp Whether to add a short help description
	 * @return The full command help
	 */
	public String getUseageTemplate(List<BaseCommand<? extends Plugin>> commandChain, boolean addShortHelp) {
		StringBuilder ret = new StringBuilder();
		ret.append(TextUtil.parseTags("<c>"));
		ret.append('/');

		for (BaseCommand<? extends Plugin> mc : commandChain) {
			ret.append(TextUtil.implode(mc.aliases, ","));
			ret.append(' ');
		}

		ret.append(TextUtil.implode(this.aliases, ","));

		if (commandArgs.size() > 0) {
			for (CommandArg a : this.commandArgs) {
				ret.append("<p> ");
				ret.append(a.toString());
			}
		}

		if (addShortHelp) {
			ret.append(TextUtil.parseTags(" <i>"));
			ret.append(this.getHelpShort());
		}

		return ret.toString();
	}

	/**
	 * Gets the usage template for the command
	 * @param addShortHelp whether to add a short help description
	 * @return The usage template
	 */
	public String getUsageTemplate(boolean addShortHelp) {
		return getUseageTemplate(this.commandChain, addShortHelp);
	}

	/**
	 * Gets the usage template for the command
	 * @return The usage template
	 */
	public String getUsageTemplate() {
		return getUsageTemplate(false);
	}

	protected void msg(Player player, String str) {
		str = parse(str);
		player.sendMessage(str);
	}

	/**
	 * Formats and sends a message to the player
	 * @param str The message to send 
	 * @param args The message arguments
	 */
	protected void msg(Player player, String str, Object... args) {
		str = parse(str, args);
		player.sendMessage(str);
	}

	protected void msg(String str) {
		str = parse(str);
		if (senderIsConsole) {
			sender.sendMessage(str);
		} else {
			me.sendMessage(str);
		}
	}

	/**
	 * Formats and sends a message to the player
	 * @param str The message to send 
	 * @param args The message arguments
	 */
	protected void msg(String str, Object... args) {
		str = parse(str, args);
		if (senderIsConsole) {
			sender.sendMessage(str);
		} else {
			me.sendMessage(str);
		}
	}

	/**
	 * Sends a list of messages to the player
	 * @param msgs The messages to send
	 */
	protected void msg(List<String> msgs) {
		for(String msg : msgs) {
			this.msg(msg);
		}
	}

	/**
	 * Gets whether an argument is set
	 * @param index The index to check
	 * @return true if the argument has a value
	 */
	protected boolean argIsSet(int index) {
		if (this.args.size() < index + 1) {
			return false;
		}
		return true;
	}

	/**
	 * Gets an argument as a string
	 * @param index The index
	 * @param def The default value
	 * @return The string argument
	 */
	protected String argAsString(int index, String def) {
		if (this.args.size() < index + 1) {
			return def;
		}
		return this.args.get(index);
	}

	/**
	 * Gets an argument as a string
	 * @param index The index
	 * @return The string argument
	 */
	protected String argAsString(int index) {
		return this.argAsString(index, null);
	}

	/**
	 * Gets an integer value from a string
	 * @param str The string value
	 * @param def The default value
	 * @return The integer argument
	 */
	protected Integer strAsInt(String str, Integer def) {
		if (str == null) return def;
		try {
			Integer ret = Integer.parseInt(str);
			return ret;
		} catch (Exception e) {
			return def;
		}
	}

	/**
	 * Gets an argument as an integer
	 * @param index The index
	 * @param def The default value
	 * @return The integer argument
	 */
	protected Integer argAsInt(int index, Integer def) {
		return strAsInt(this.argAsString(index), def);
	}

	/**
	 * Gets an argument as a integer
	 * @param index The index
	 * @return The integer argument
	 */
	protected Integer argAsInt(int index) {
		return this.argAsInt(index, null);
	}

	/**
	 * Gets an default value from a string
	 * @param str The string value
	 * @param def The default value
	 * @return The double argument
	 */
	protected Double strAsDouble(String str, Double def) {
		if (str == null) return def;
		try {
			Double ret = Double.parseDouble(str);
			return ret;
		} catch (Exception e) {
			return def;
		}
	}

	/**
	 * Gets an argument as a double
	 * @param index The index
	 * @param def The default value
	 * @return The double argument
	 */
	protected Double argAsDouble(int index, Double def) {
		return strAsDouble(this.argAsString(index), def);
	}

	/**
	 * Gets an argument as a double
	 * @param index The index
	 * @return The double argument
	 */
	protected Double argAsDouble(int index) {
		return this.argAsDouble(index, null);
	}

	/**
	 * Gets an boolean value from a string
	 * Values that qualify as a 'true' value are [y, t, on, +, 1]
	 * @param str The string value
	 * @return The boolean argument
	 */
	protected Boolean strAsBool(String str) {
		str = str.toLowerCase();
		if (str.startsWith("y") || str.startsWith("t") || str.startsWith("on") || str.startsWith("1")) {
			return true;
		}
		return false;
	}

	/**
	 * Gets an argument as a boolean
	 * @param index The index
	 * @param def The default value
	 * @return The boolean argument
	 */
	protected Boolean argAsBool(int index, Boolean def) {
		String str = this.argAsString(index);
		if (str == null) {
			return def;
		}

		return strAsBool(str);
	}

	/**
	 * Gets an argument as a boolean
	 * @param index The index
	 * @return The boolean argument
	 */
	protected Boolean argAsBool(int index) {
		return this.argAsBool(index, false);
	}

	/**
	 * Gets an argument as a UUID
	 * @param index The index
	 * @return The UUID argument
	 */
	protected UUID argAsUUID(int index) {
		try {
			UUID ret = UUID.fromString(argAsString(index));
			return ret;
		} catch (Exception e) {
			return null;
		}
	}


	/**
	 * Gets the forbidden message for a command.
	 * @param perm The permission to check
	 * @return The permission forbidden message
	 */
	protected String getForbiddenMessage(String perm) {
		return parse(Lang.permForbidden, getPermissionDescription(perm));
	}


	/**
	 * Gets the permission description for the command
	 * @param perm The permission to check
	 * @return The permission description
	 */
	private String getPermissionDescription(String perm) {
		String desc = permissionDescriptions.get(perm);
		if (desc == null) {
			return Lang.permDoThat;
		}
		return desc;
	}


	/**
	 * Checks if a sender has a certain permission
	 * @param me The command sender
	 * @param perm The permission to check
	 * @return true if the sender has the permission
	 */
	private boolean senderHasPerm(CommandSender me, String perm) {
		if (me == null) {
			return false;
		}

		if (!senderIsConsole) {
			return me.hasPermission(perm);
		}

		return me.hasPermission(perm);
	}


	/**
	 * Checks if a sender has a certain permission
	 * @param me The command sender
	 * @param perm The permission to check
	 * @param visiblity The command visibility
	 * @param informSenderIfNot Whether to inform the sender of invalid perms
	 * @return true if the sender has the permission
	 */
	private boolean senderHasPerm(CommandSender me, String perm, CommandVisibility visiblity, boolean informSenderIfNot) {
		if (senderHasPerm(me, perm)) {
			return true;
		}
		else if (visiblity != CommandVisibility.VISIBLE) {
			if (informSenderIfNot) {
				me.sendMessage(Lang.unknownCommand);
			}
		}
		else if (informSenderIfNot) {
			msg(getForbiddenMessage(perm));
		}
		return false;
	}


	/**
	 * Formats a string through the text tag parser
	 * @param str The string to format
	 * @param args The string arguments
	 * @return The formatted string
	 */
	protected static String parse(String str, Object... args) {
		return TextUtil.parse(str, args);
	}

	protected static String parse(String str) {
		return TextUtil.parse(str);
	}


	/*
	 * The following are utility methods for easily generating 
	 * command and auto-tab instances
	 */


	protected final static AutoTab autoTab(String name, String help) {
		return new AutoTab(name, help);
	}

	protected final static AutoTab autoTab(String name) {
		return new AutoTab(name, null);
	}

	protected final static CommandArg required(String name, AutoTab autoTab) {
		return new RequiredCommandArg(name, autoTab);
	}

	protected final static CommandArg required(String name) {
		return required(name, null);
	}

	protected final static CommandArg requiredPlayer(String name) {
		return required(name, autoTab("player", "No matching player found."));
	}

	protected final static CommandArg requiredPlayerOrUUID(String name) {
		return required(name, autoTab("player_uuid", "No matching player found."));
	}

	protected final static CommandArg optional(String name, String defValue, AutoTab autoTab) {
		return new OptionalCommandArg(name, defValue, autoTab);
	}

	protected final static CommandArg optional(String name, String defValue) {
		return optional(name, defValue, null);
	}

	protected final static CommandArg optional(String name, AutoTab autoTab) {
		return optional(name, null, autoTab);
	}

	protected final static CommandArg optional(String name) {
		return optional(name, null, null);
	}

	protected final static CommandArg optionalPlayer(String name) {
		return optional(name, autoTab("player", "No matching player found."));
	}

	protected final static CommandArg optionalPlayerOrUUID(String name) {
		return optional(name, autoTab("player_uuid", "No matching player found."));
	}
} public class CmdAutoHelp extends PearlCommand
{
	public CmdAutoHelp(ExilePearlApi pearlApi) {
		super(pearlApi);

		this.aliases.add("help");

		this.setHelpShort("");

		this.commandArgs.add(optional("page", "1"));
	}

	@Override
	public void perform() {
		if (this.commandChain.size() == 0) return;
		BaseCommand<?> cmd = this.commandChain.get(this.commandChain.size()-1);

		ArrayList<String> lines = new ArrayList<String>();

		lines.addAll(cmd.getLongHelp());

		for(BaseCommand<?> c : cmd.getSubCommands()) {
			if (!c.validSenderType(sender, false)) {
				continue;
			}

			// Only list help for commands that are visible or the sender has permission for
			if (c.getVisibility() == CommandVisibility.VISIBLE || (c.getVisibility() == CommandVisibility.SECRET && c.validSenderPermissions(sender, false))) {
				lines.add(c.getUseageTemplate(this.commandChain, true));
			}
		}

		int page = this.argAsInt(0, 1);

		msg(TextUtil.getPage(lines, page, "Help for command \""+cmd.getAliases().get(0)+"\""));
	}
} public class CmdExilePearl extends PearlCommand {

	private static CmdExilePearl instance;

	public static CmdExilePearl instance() {
		return instance;
	}

	public final PearlCommand cmdLocate;
	public final PearlCommand cmdFree;
	public final PearlCommand cmdBcast;
	public final PearlCommand cmdBcastConfirm;
	public final PearlCommand cmdBcastSilence;
	public final PearlCommand cmdDowngrade;

	public CmdExilePearl(ExilePearlApi pearlApi) {
		super(pearlApi);
		this.aliases.add("ep");

		this.setHelpShort("The ExilePearl command");
		this.getLongHelp().add("<n>Use <c>/ep help <n>for command help.");

		cmdLocate = new CmdPearlLocate(plugin);
		cmdFree = new CmdPearlFree(plugin);
		cmdBcast = new CmdPearlBroadcast(plugin);
		cmdBcastConfirm = new CmdPearlBroadcastAccept(plugin);
		cmdBcastSilence = new CmdPearlBroadcastSilence(plugin);
		cmdDowngrade = new CmdDowngrade(plugin);

		addSubCommand(plugin.getAutoHelp());

		addSubCommand(cmdLocate);
		addSubCommand(cmdFree);
		addSubCommand(cmdBcast);
		addSubCommand(cmdBcastConfirm);
		addSubCommand(cmdBcastSilence);
		addSubCommand(cmdDowngrade);
		addSubCommand(new CmdSummon(plugin));
		addSubCommand(new CmdReturn(plugin));
		addSubCommand(new CmdSummonConfirm(plugin));
		addSubCommand(new CmdUpgrade(plugin));

		// Admin commands
		addSubCommand(new CmdConfig(plugin));
		addSubCommand(new CmdAdminDecay(plugin));
		addSubCommand(new CmdAdminExileAny(plugin));
		addSubCommand(new CmdAdminFreeAny(plugin));
		addSubCommand(new CmdAdminSetHealth(plugin));
		addSubCommand(new CmdAdminSetType(plugin));
		addSubCommand(new CmdAdminSetKiller(plugin));
		addSubCommand(new CmdAdminCheckExiled(plugin));
		addSubCommand(new CmdAdminListExiled(plugin));
		addSubCommand(new CmdAdminReload(plugin));

		instance = this;
	}

	@Override
	public void perform() {
		this.commandChain.add(this);
		plugin.getAutoHelp().execute(this.sender, this.args, this.commandChain);
	}
} public class CmdLegacy extends PearlCommand {

	public CmdLegacy(ExilePearlApi pearlApi) {
		super(pearlApi);
		this.aliases.add("pp");

		this.visibility = CommandVisibility.INVISIBLE;
	}

	@Override
	public void perform() {
		msg("<i>Use <c>/ep <i>instead.");
	}
} public class CmdSuicide extends PearlCommand {

	public CmdSuicide(ExilePearlApi pearlApi) {
		super(pearlApi);
		this.aliases.add("suicide");

		this.senderMustBePlayer = true;
		this.errorOnToManyArgs = false;
		this.visibility = CommandVisibility.INVISIBLE;
	}

	@Override
	public void perform() {
		if(!plugin.getPearlConfig().canPerform(ExileRule.SUICIDE)) {
			msg(Lang.unknownCommand);
			return;
		}

		if (!plugin.isPlayerExiled(player().getUniqueId())) {
			msg(Lang.onlyExiledPlayers);
			return;
		}

		plugin.getSuicideHandler().addPlayer(player());
	}
} public abstract class PearlCommand extends BaseCommand<ExilePearlApi> {

	public PearlCommand(ExilePearlApi pearlApi) {
		super(pearlApi);
	}



	/**
	 * Gets an argument as a UUID
	 * @param index The index
	 * @return The UUID argument
	 */
	protected UUID argAsPlayerOrUUID(int index) {
		final String arg = argAsString(index);
		try {
			UUID ret = UUID.fromString(arg);
			return ret;
		} catch (Exception e) {
			return plugin.getUniqueId(arg);
		}
	}

	@Override
	protected List<String> getCustomAutoTab(String tabName, String pattern) {
		List<String> tabList = new ArrayList<String>();

		switch(tabName) {
		case "exile_rule":
			for(ExileRule v : new ArrayList<ExileRule>(Arrays.asList(ExileRule.values()))) {
				if (v.toString().toLowerCase().startsWith(pattern.toLowerCase())) {
					tabList.add(v.toString());
				}
			}
			break;
		case "pearled":
			for(ExilePearl pearl : plugin.getPearls()) {
				if (pearl.getPlayerName().toLowerCase().startsWith(pattern.toLowerCase())) {
					tabList.add(pearl.getPlayerName());
				}
				if (pearl.getPlayerId().toString().toLowerCase().startsWith(pattern.toLowerCase())) {
					tabList.add(pearl.getPlayerId().toString());
				}
			}
			break;

		case "group":
			tabList.addAll(NameLayerTabCompletion.completeGroupName(pattern, player()));
			break;

		case "group_or_player":
			List<String> groups = getAutoTab("group", pattern);
			List<String> players = getAutoTab("player", pattern);

			if (groups != null) {
				tabList.addAll(groups);
			}

			if (players != null) {
				tabList.addAll(players);
			}
			break;

		default:
			break;

		}
		return tabList;
	}

	protected final static CommandArg requiredPearlPlayer() {
		return required("player", autoTab("pearled", "No matching pearled player found."));
	}

	protected final static CommandArg requiredGroup() {
		return required("group", autoTab("group", "No matching group found."));
	}
} public interface PearlConfig extends MySqlConfig, DocumentConfig {

	/**
	 * Gets the storage type to use
	 * @return The storage type
	 */
	StorageType getStorageType();

	/**
	 * Gets a set of worlds that pearls can not be stored in
	 * @return a set of names of worlds pearls can't be stored in
	 */
	Set<String> getDisallowedWorlds();

	/**
	 * Gets the name of the interval humans measure pearl decay in
	 */
	String getPearlHealthDecayHumanInterval();

	/**
	 * Gets the length of the interval humans measure pearl decay in (minutes)
	 */
	int getPearlHealthDecayHumanIntervalMin();

	/**
	 * Gets the pearl decay minute interval
	 * @return the pearl decay minute interval
	 */
	int getPearlHealthDecayIntervalMin();

	/**
	 * Gets the pearl decay amount
	 * @return the pearl decay amount
	 */
	int getPearlHealthDecayAmount();

	/**
	 * Gets the pearl health decay shutoff timeout (time since last login
	 *  that decay is no longer applied)
	 * @return the # of minutes absent at which decay is skipped.
	 */
	int getPearlHealthDecayTimeout();

	/**
	 * Gets the pearl health start value
	 * @return the pearl health start value
	 */
	int getPearlHealthStartValue();

	/**
	 * Gets the pearl health max value
	 * @return the pearl max start value
	 */
	int getPearlHealthMaxValue();

	/**
	 * Gets whether pearls outside world border should be freed
	 * @return the pearl max start value
	 */
	boolean getShouldAutoFreeWorldBorder();

	/**
	 * Gets whether the pearl must be in the hot bar
	 * @return the pearl max start value
	 */
	boolean getMustPrisonPearlHotBar();

	/**
	 * Gets whether pearls can be freed by throwing them
	 * @return true if they can be freed by throwing
	 */
	boolean getFreeByThrowing();

	/**
	 * Gets whether freeing a prison pearl should teleport the player
	 * @return true if they should be teleported
	 */
	boolean getShouldFreeTeleport();

	/**
	 * Gets whether killing exiles should announce their location
	 * @return true if it should be announced
	 */
	boolean getShouldAnnounceExileLocation();

	/**
	 * Gets how much to damage the players when they are inside a bastion field
	 * @return The damage amount
	 */
	double getBastionDamage();

	/**
	 * Gets the pearl repair materials
	 * @param type The type for repairs
	 * @return The pearl repair materials
	 */
	Set<RepairMaterial> getRepairMaterials(PearlType type);

	/**
	 * Gets the default pearl type between EXILE or PRISON
	 * @return The default pearl type
	 */
	PearlType getDefaultPearlType();

	/**
	 * Gets the names of animals that are protected
	 * @return The protected animal names
	 */
	List<String> getProtectedAnimals();

	/**
	 * Gets the suicide timeout in seconds
	 * @return The suicide timeout
	 */
	int getSuicideTimeoutSeconds();

	/**
	 * Gets whether the damage log is enabled
	 * @return true if the damage log is enabled
	 */
	boolean getDamageLogEnabled();

	/**
	 * Gets the damage log algorithm type
	 * @return The algorithm type
	 */
	int getDamageLogAlgorithm();

	/**
	 * Gets the damage log tick interval
	 * @return The damage log tick interval
	 */
	int getDamageLogInterval();

	/**
	 * Gets the damage log decay amount
	 * @return The decay amount
	 */
	double getDamageLogDecayAmount();

	/**
	 * Gets the max tracked damage
	 * @return The max tracked damage
	 */
	double getDamageLogMaxDamage();

	/**
	 * Gets the potion damage amount
	 * @return The potion damage amount
	 */
	double getDamageLogPotionDamage();

	int getRulePearlRadius();

	void setRulePearlRadius(int value);

	boolean canPerform(ExileRule rule);

	void setRule(ExileRule rule, boolean value);

	boolean getUseHelpItem();

	String getHelpItemName();

	List<String> getHelpItemText();

	World getPrisonWorld();

	World getMainWorld();

	Set<RepairMaterial> getUpgradeMaterials();

	boolean allowPearlStealing();

	boolean allowSummoning();

	/**
	 * @return Maximum ban stick alts allowed to be pearled
	 */
	int maxAltsPearled();

	/**
	 * @return Message displayed when attempting to login when having to many pearled alts
	 */
	String altBanMessage();
	
	int pearlCostMultiplicationTimerDays();
} public interface PearlHolder {

	/**
	 * Gets the holder name
	 * @return The holder name
	 */
	public String getName();

	/**
	 * Gets the holder location
	 * @return The holder location
	 */
	public Location getLocation();

	/**
	 * Validate that the given pearl still exists in this holder instance
	 * @param pearl The exile pearl
	 * @return the validation result
	 */
	public HolderVerifyResult validate(final ExilePearl pearl);

	/**
	 * Gets whether the holder is a block
	 * @return true if the holder is a block
	 */
	public boolean isBlock();

	/**
	 * Gets whether the holder is in a loaded chunk
	 * @return true if the holder is in a loaded chunk
	 */
	public default boolean inLoadedChunk() {
		Location loc = getLocation();
		World world = loc.getWorld();

		if (world == null) {
			return false;
		}

		return world.isChunkLoaded(BlockBasedChunkMeta.toChunkCoord(loc.getBlockX()), BlockBasedChunkMeta.toChunkCoord(loc.getBlockZ()));
	}
} public class ArtemisListener extends RuleListener {
	/**
	 * Creates a new ExileListener instance
	 *
	 * @param pearlApi The PearlApi instance
	 */
	public ArtemisListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}
	
	@EventHandler
	public void onPlayerShardTransfer(PlayerAttemptLeaveShard event) {
		if (pearlApi.isPlayerExiled(event.getPlayer())) {
			event.setCancelled(true);
			event.getPlayer().sendMessage(ChatColor.RED + "Exiled players cannot travel between shards.");
			return;
		}

		for (ItemStack itemStack : event.getPlayer().getInventory()) {
			if (pearlApi.getPearlFromItemStack(itemStack) != null) {
				event.setCancelled(true);
				event.getPlayer().sendMessage(ChatColor.RED + "You cannot carry pearls across shards! Store the pearl before retrying.");
				return;
			}
		}
	}
} public class BanStickListener extends RuleListener {

    public BanStickListener(ExilePearlApi pearlApi) {
        super(pearlApi);
    }

    /**
     * Prevents alts from logging in if the limit of pearled accounts is reached
     *
     * @param e
     */
    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void onLoginEvent(AsyncPlayerPreLoginEvent e) {
        if (pearlApi.getPearl(e.getUniqueId()) != null) {
            // dont lock out pearled account
            return;
        }
        if (pearlApi.getExiledAlts(e.getUniqueId(), false) >= config.maxAltsPearled()) {
            e.setLoginResult(Result.KICK_OTHER);
            e.setKickMessage(config.altBanMessage());
        }
    }

    /**
     * Kicks online alts if the limit of pearled accounts is reached
     *
     * @param e
     */
	@EventHandler
    public void playerPearl(PlayerPearledEvent e) {
        UUID uuid = e.getPearl().getPlayerId();
        if (pearlApi.getExiledAlts(uuid, false) < config.maxAltsPearled()) {
            return;
        }
/*        BSPlayer player = BSPlayer.byUUID(uuid);
        for (BSPlayer alt : player.getTransitiveSharedPlayers(true)) {
            ExilePearl altPearl = pearlApi.getPearl(alt.getUUID());
            if (altPearl == null && !alt.getUUID().equals(uuid)) {
                Player p = Bukkit.getPlayer(alt.getUUID());
                if (p != null) {
                    p.kickPlayer(config.altBanMessage());
                }
            }
        }*/
    }

} public class BastionListener extends RuleListener {


	public BastionListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}

	/**
	 * Prevents exiled players from creating bastions
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onBastionCreate(BastionCreateEvent e) {
		checkAndCancelRule(ExileRule.CREATE_BASTION, e, e.getPlayer());
	}

	/**
	 * Prevents exiled players from damaging bastions
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onBastionDamage(BastionDamageEvent e) {
		checkAndCancelRule(ExileRule.DAMAGE_BASTION, e, e.getPlayer());
	}
} public class ChatListener extends RuleListener {


	public ChatListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}

	/**
	 * Prevents exiled players from chatting globally
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onChatEvent(AsyncPlayerChatEvent e) {
		checkAndCancelRule(ExileRule.CHAT, e, e.getPlayer());
	}
} public class CitadelListener extends RuleListener {


	public CitadelListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}

	/**
	 * Prevents exiled players from damaging reinforcements
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onReinforcementDamage(ReinforcementDamageEvent e) {
		checkAndCancelRule(ExileRule.DAMAGE_REINFORCEMENT, e, e.getPlayer());
	}

	/**
	 * Prevents exiled players from using acid blocks
	 * Uses the same rule as damaging reinforcements
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onAcidBlockEvent(ReinforcementAcidBlockedEvent e) {
		checkAndCancelRule(ExileRule.DAMAGE_REINFORCEMENT, e, e.getPlayer());
	}
} public class ExileListener extends RuleListener implements Configurable {

	private Set<String> protectedAnimals = new HashSet<String>();

	/**
	 * Creates a new ExileListener instance
	 * @param pearlApi The PearlApi instance
	 */
	public ExileListener(final ExilePearlApi pearlApi) {
		super(pearlApi);
	}


	/**
	 * Clears the bed of newly exiled players
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerPearled(PlayerPearledEvent e) {
		if (config.canPerform(ExileRule.USE_BED)) {
			return;
		}
		Player p = e.getPearl().getPlayer();

		if (p != null && p.isOnline()) {
			e.getPearl().getPlayer().setBedSpawnLocation(null, true);
		}
	}


	/**
	 * Clears the bed of exiled players when they log in
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerJoin(PlayerJoinEvent e) {
		if (config.canPerform(ExileRule.USE_BED)) {
			return;
		}
		if (pearlApi.isPlayerExiled(e.getPlayer())) {
			//if it's a prison pearl set their spawn to the prison world, probably the end
			boolean prison = pearlApi.getPearl(e.getPlayer().getUniqueId()).getPearlType() == PearlType.PRISON;
			e.getPlayer().setBedSpawnLocation(prison ? pearlApi.getPearlConfig().getPrisonWorld().getSpawnLocation().add(0, 0.5, 0) : null, true);
		}
	}


	/**
	 * Prevent exiled players from using a bed
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onPlayerEnterBed(PlayerBedEnterEvent e) {
		checkAndCancelRule(ExileRule.USE_BED, e, e.getPlayer());
	}

	/**
	 * Prevent exiled players from throwing ender pearls
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPearlThrow(ProjectileLaunchEvent e) {
		if (e.getEntity() instanceof EnderPearl) {
			checkAndCancelRule(ExileRule.THROW_PEARL, e, (Player)e.getEntity().getShooter());
		}
	}

	/**
	 * Prevent exiled players from using buckets
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerFillBucket(PlayerBucketFillEvent e) {
		if (e.getItemStack() != null && Material.MILK_BUCKET.equals(e.getItemStack().getType())) {
			checkAndCancelRule(ExileRule.MILK_COWS, e, e.getPlayer());
		} else {
			checkAndCancelRule(ExileRule.FILL_BUCKET, e, e.getPlayer());
		}
	}

	/**
	 * Prevent exiled players from using buckets
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerEmptyBucket(PlayerBucketEmptyEvent e) {
		if (e.getBlockClicked() != null && Material.CAULDRON.equals(e.getBlockClicked().getType())) {
			checkAndCancelRule(ExileRule.FILL_CAULDRON, e, e.getPlayer());
		} else {
			checkAndCancelRule(ExileRule.USE_BUCKET, e, e.getPlayer());
		}
	}

	/**
	 * Prevent exiled players from using certain blocks
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerInteract(PlayerInteractEvent e) {
		if (e.hasBlock()) {
			Material type = e.getClickedBlock().getType();
			if (type == Material.BREWING_STAND) {
				checkAndCancelRule(ExileRule.BREW, e, e.getPlayer());
			} else if (type == Material.ANVIL) {
				checkAndCancelRule(ExileRule.USE_ANVIL, e, e.getPlayer());
			}
		}
	}

	/**
	 * Prevent exiled players from enchanting
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerEnchant(EnchantItemEvent e) {
		checkAndCancelRule(ExileRule.ENCHANT, e, e.getEnchanter());
	}

	/**
	 * Prevent exiled players from pvping and killing pets and protected mobs
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerDamage(EntityDamageByEntityEvent e) {
		Player player = new EntityDamageEventWrapper(e).getPlayerDamager();
		if (player == null) {
			return;
		}

		if (e.getEntity() instanceof Player) {
			checkAndCancelRule(ExileRule.PVP, e, player);
			return;
		}

		if (!(e.getEntity() instanceof LivingEntity)) {
			return;
		}

		LivingEntity living = (LivingEntity)e.getEntity();
		String name = living.getCustomName();

		if (name != null && name != "") {
			checkAndCancelRule(ExileRule.KILL_PETS, e, player);
			return;
		}

		if (!isRuleActive(ExileRule.KILL_MOBS, player.getUniqueId())) {
			return;
		}

		for(String animal : protectedAnimals) {
			Class<?> clazz = null;

			try {
				clazz = Class.forName("org.bukkit.entity." + animal);
			} catch (Exception ex) {
				continue;
			}

			if (clazz != null && clazz.isInstance(living)) {
				checkAndCancelRule(ExileRule.KILL_MOBS, e, player);
				return;
			}
		}
	}

	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerCombust(EntityCombustByEntityEvent e) {
		Player player = new EntityCombustEventWrapper(e).getPlayerDamager();
		if (player == null) {
			return;
		}

		if (e.getEntity() instanceof Player) {
			checkAndCancelRule(ExileRule.PVP, e, player);
			return;
		}

		if (!(e.getEntity() instanceof LivingEntity)) {
			return;
		}

		LivingEntity living = (LivingEntity)e.getEntity();
		String name = living.getCustomName();

		if (name != null && name != "") {
			checkAndCancelRule(ExileRule.KILL_PETS, e, player);
			return;
		}

		if (!isRuleActive(ExileRule.KILL_MOBS, player.getUniqueId())) {
			return;
		}

		for(String animal : protectedAnimals) {
			Class<?> clazz = null;

			try {
				clazz = Class.forName("org.bukkit.entity." + animal);
			} catch (Exception ex) {
				continue;
			}

			if (clazz != null && clazz.isInstance(living)) {
				checkAndCancelRule(ExileRule.KILL_MOBS, e, player);
				return;
			}
		}
	}



	/**
	 * Prevent exiled players from drinking potions
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerDrinkPotion(PlayerItemConsumeEvent e) {
		if(e.getItem().getType() == Material.POTION) {
			if (pearlApi.getBrewHandler().isBrew(e.getItem())) {
				checkAndCancelRule(ExileRule.DRINK_BREWS, e, e.getPlayer());
			} else {
				checkAndCancelRule(ExileRule.USE_POTIONS, e, e.getPlayer());
			}
		}
	}

	/**
	 * Prevent exiled players from using splash potions
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerThrowPotion(PotionSplashEvent e) {
		if(e.getEntity() != null && e.getEntity().getShooter() instanceof Player) {
			checkAndCancelRule(ExileRule.USE_POTIONS, e, (Player)e.getEntity().getShooter());
		}
	}

	/**
	 * Prevent exiled players from using lingering splash potions
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPlayerThrowLingeringPotion(LingeringPotionSplashEvent e) {
		if(e.getEntity() != null && e.getEntity().getShooter() instanceof Player) {
			checkAndCancelRule(ExileRule.USE_POTIONS, e, (Player)e.getEntity().getShooter());
		}
	}

	/**
	 * Prevents exiled players from breaking blocks
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onPlayerIgnite(BlockIgniteEvent e) {
		if (e.getCause() == IgniteCause.FLINT_AND_STEEL) {
			checkAndCancelRule(ExileRule.IGNITE, e, e.getPlayer());
		}
	}

	/**
	 * Prevents exiled players from breaking blocks
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
	public void onBlockBreak(BlockBreakEvent e) {
		checkAndCancelRule(ExileRule.MINE, e, e.getPlayer());
	}

	/**
	 * Prevents exiled players from collecting xp
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onCollectXp(PlayerExpChangeEvent e) {
		if (isRuleActive(ExileRule.COLLECT_XP, e.getPlayer().getUniqueId())) {
			e.setAmount(0);
			msg(e.getPlayer(), Lang.ruleCantDoThat, ExileRule.COLLECT_XP.getActionString());
		}
	}

	/**
	 * Prevents exiled players from placing certain blocks
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onBlockPlaced(BlockPlaceEvent e) {
		Material m = e.getBlockPlaced().getType();
		if (m == Material.TNT) {
			checkAndCancelRule(ExileRule.PLACE_TNT, e, e.getPlayer());
		}
	}

	/**
	 * Prevents exiled players from moving restricted items like lava and TNT
	 * into dispensers, hoppers, and droppers
	 * @param e The event
	 */
	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
	public void onInventoryClick(InventoryClickEvent e) {
		Player player = (Player) e.getWhoClicked();

		InventoryAction action = e.getAction();
		ItemStack item;
		InventoryHolder holder = null; 

		if(action == InventoryAction.PLACE_ALL
				|| action == InventoryAction.PLACE_SOME
				|| action == InventoryAction.PLACE_ONE) {
			item = e.getCursor();
			boolean clickedTop = e.getRawSlot() < e.getView().getTopInventory().getSize(); // bukkit API has bad advice
			if (item == null || !clickedTop) {
				return;
			}
			holder = e.getView().getTopInventory().getHolder();

		} else if(action == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
			item = e.getCurrentItem();
			boolean clickedTop = e.getRawSlot() < e.getView().getTopInventory().getSize();
			if (item == null || clickedTop) { // allow remove from top but not shift INTO top
				return;
			}

			holder = e.getView().getTopInventory().getHolder(); // we're canceling bottom
			// inventory click but only if _top_ inventory is specific type.
		} else {
			return;
		}

		if (item == null || holder == null) {
			return;
		}

		if (!(holder instanceof Dispenser || holder instanceof Dropper || holder instanceof Hopper)) {
			return;
		}

		Material m = item.getType();
		if (m == Material.TNT) {
			checkAndCancelRule(ExileRule.PLACE_TNT, e, player, false);
		}
		else if (m == Material.LAVA_BUCKET || m == Material.WATER_BUCKET) {
			checkAndCancelRule(ExileRule.USE_BUCKET, e, player, false);
		}

		if (e.isCancelled()) {
			msg(player, Lang.ruleCantDoThat, "do that");
		}
	}

	@Override
	public void loadConfig(PearlConfig config) {
		protectedAnimals.addAll(config.getProtectedAnimals());
	}
} public class JukeAlertListener extends RuleListener {


	public JukeAlertListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}


	/**
	 * Prevents exiled players from placing snitches
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onSnitchPlaced(BlockPlaceEvent e) {
		Material m = e.getBlockPlaced().getType();
		if (m == Material.JUKEBOX || m == Material.NOTE_BLOCK) {
			checkAndCancelRule(ExileRule.SNITCH, e, e.getPlayer());
		}
	}
} public class PlayerListener implements Listener, Configurable {

	private final ExilePearlApi pearlApi;

	private Map<PearlType, Set<RepairMaterial>> repairMaterials = new HashMap<>();
	private Set<RepairMaterial> upgradeMaterials = new HashSet<>();


	private boolean useHelpItem = false;
	private String helpItemName = "";
	private List<String> helpItemText = new ArrayList<>();

	/**
	 * Creates a new PlayerListener instance
	 * @param pearlApi The pearlApi instance
	 */
	public PlayerListener(final ExilePearlApi pearlApi) {
		Guard.ArgumentNotNull(pearlApi, "pearlApi");

		this.pearlApi = pearlApi;
	}


	/**
	 * Announce the person in a pearl when a player holds it
	 * @param event
	 */
	@EventHandler(priority = EventPriority.MONITOR)
	public void onItemHeldChange(PlayerItemHeldEvent event) {

		Inventory inv = event.getPlayer().getInventory();
		ItemStack item = inv.getItem(event.getNewSlot());
		ItemStack newitem = validatePearl(item);
		if (newitem != null) {
			inv.setItem(event.getNewSlot(), newitem);
		}
	}


	/**
	 * Updates all pearls in an inventory when opened
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onInventoryOpen(InventoryOpenEvent e) {
		Inventory inv = e.getInventory();
		HashMap<Integer, ItemStack> potentialPearls = new HashMap<>();
		potentialPearls.putAll(inv.all(Material.ENDER_PEARL));
		potentialPearls.putAll(inv.all(Material.PLAYER_HEAD));
		for (Entry<Integer, ? extends ItemStack> entry : potentialPearls.entrySet()) {
			ItemStack newitem = validatePearl(entry.getValue());
			if (newitem != null) {
				inv.setItem(entry.getKey(), newitem);
			}
		}
	}


	/**
	 * Validates an ender pearl item
	 * @param item The item to check
	 * @return the updated item
	 */
	private ItemStack validatePearl(ItemStack item) {
		if (item == null) {
			return null;
		}

		if ((item.getType() == Material.PLAYER_HEAD || item.getType() == Material.ENDER_PEARL)
				&& item.getEnchantmentLevel(Enchantment.DURABILITY) != 0) {
			ExilePearl pearl = pearlApi.getPearlFromItemStack(item);
			if (pearl == null || pearl.getFreedOffline()) {
				return new ItemStack(Material.ENDER_PEARL, 1);
			}
			return pearl.createItemStack();
		}

		return null;
	}


	/**
	 * Track the location of a pearl if it spawns as an item for any reason
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onItemSpawn(ItemSpawnEvent e) {
		Item item = e.getEntity();

		ExilePearl pearl = pearlApi.getPearlFromItemStack(item.getItemStack());
		if (pearl == null) {
			return;
		}

		pearl.setHolder(item);
	}

	// Prevent dropped pearls from going through portals
	@EventHandler
	public void onItemSpawn(EntityPortalEvent e) {
		if(!(e.getEntity() instanceof Item)){
			return;
		}
		Item item = (Item) e.getEntity();
		ExilePearl pearl = pearlApi.getPearlFromItemStack(item.getItemStack());
		if(pearl != null){
			e.setCancelled(true);
		}
	}

	// Listen for hoppers picking up pearls
	@EventHandler
	public void onInventoryPickupItem(InventoryPickupItemEvent e) {
		ExilePearl pearl = pearlApi.getPearlFromItemStack(e.getItem().getItemStack());
		if(pearl != null){
			if (e.getInventory().getHolder() instanceof HopperMinecart) {
				e.setCancelled(true);
			} else if (e.getInventory().getType() == InventoryType.HOPPER) {
				pearl.setHolder(e.getInventory().getLocation().getBlock());
			}
		}
	}

	// prevent hoppers moving pearls
	@EventHandler
	public void onInventoryMoveItemEvent(InventoryMoveItemEvent e) {
		ExilePearl pearl = pearlApi.getPearlFromItemStack(e.getItem());
		if(pearl != null){
			e.setCancelled(true);
		}
	}

	/**
	 * Drops a pearl when the player leaves the game
	 * @param event The event args
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerQuit(PlayerQuitEvent event) {
		Player imprisoner = event.getPlayer();

		// Don't drop if the player is tagged
		if (pearlApi.isPlayerTagged(imprisoner.getUniqueId())) {
			return;
		}

		Location loc = imprisoner.getLocation();
		World world = imprisoner.getWorld();
		Inventory inv = imprisoner.getInventory();
		for (Entry<Integer, ? extends ItemStack> entry :
			inv.all(Material.ENDER_PEARL).entrySet()) {
			ItemStack item = entry.getValue();
			ExilePearl pearl = pearlApi.getPearlFromItemStack(item);
			if (pearl == null) {
				continue;
			}
			int slot = entry.getKey();
			inv.clear(slot);
			world.dropItemNaturally(loc, item);
		}
	}



	/**
	 * Prevents a pearl from despawning
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void onItemDespawn(ItemDespawnEvent e) {
		ExilePearl pearl = pearlApi.getPearlFromItemStack(e.getEntity().getItemStack());
		if (pearl != null) {
			e.setCancelled(true);
			pearlApi.log("Prevented pearl from despawning at %s for player %s.", pearl.getLocation().toString(), pearl.getPlayerName());
		}
	}


	/**
	 * Prevent chunk that contain pearls from unloading
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.HIGH, ignoreCancelled = true)
	public void onChunkUnload(ChunkUnloadEvent e) {
		Chunk chunk = e.getChunk();
		for (Entity entity : chunk.getEntities()) {
			if (!(entity instanceof Item)) {
				continue;
			}

			Item item = (Item)entity;
			ExilePearl pearl = pearlApi.getPearlFromItemStack(item.getItemStack());

			if (pearl == null) {
				return;
			}
			Block block = item.getLocation().getBlock();
			if (block.getType() != Material.BARREL) {
				block.setType(Material.BARREL);
			}
			block.getDrops().clear();
			Barrel barrel = (Barrel) block.getState();
			Map <Integer, ItemStack> notAdded = barrel.getInventory().addItem(item.getItemStack());
			if (!notAdded.isEmpty()) {
				pearlApi.freePearl(pearl, PearlFreeReason.CHUNK_UNLOADED);
				continue;
			}
			updatePearlHolder(pearl, barrel, null);
			item.remove();
		}
	}


	/**
	 * Free the pearl if it burns up
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onEntityCombustEvent(EntityCombustEvent e) {
		if (!(e.getEntity() instanceof Item)) {
			return;
		}

		ExilePearl pearl = pearlApi.getPearlFromItemStack(((Item) e.getEntity()).getItemStack());
		if (pearl == null) {
			return;
		}

		pearlApi.log("%s (%s) is being freed. Reason: ExilePearl combusted(lava/fire).", pearl.getPlayerName(), pearl.getPlayerId());
		pearlApi.freePearl(pearl, PearlFreeReason.PEARL_DESTROYED);
	}


	/**
	 * Handle inventory dragging properly
	 * @param event
	 */
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void onInventoryDrag(InventoryDragEvent event) {

		Map<Integer, ItemStack> items = event.getNewItems();

		for(Integer slot : items.keySet()) {
			ItemStack item = items.get(slot);

			ExilePearl pearl = pearlApi.getPearlFromItemStack(item);
			if(pearl != null) {
				boolean clickedTop = event.getView().convertSlot(slot) == slot;

				InventoryHolder holder = clickedTop ? event.getView().getTopInventory().getHolder() : event.getView().getBottomInventory().getHolder();

				updatePearlHolder(pearl, holder, event);

				if(event.isCancelled()) {
					return;
				}
			}
		}
	}


	/**
	 * Updates the pearl holder
	 * @param pearl The pearl to update
	 * @param holder The pearl holder
	 * @param event The event
	 */
	private void updatePearlHolder(ExilePearl pearl, InventoryHolder holder, Cancellable event) {

		if (holder instanceof Chest) {
			updatePearl(pearl, (Chest)holder);
		} else if (holder instanceof DoubleChest) {
			updatePearl(pearl, (Chest) ((DoubleChest) holder).getLeftSide());
		} else if (holder instanceof Furnace) {
			updatePearl(pearl, (Furnace) holder);
		} else if (holder instanceof Dispenser) {
			updatePearl(pearl, (Dispenser) holder);
		} else if (holder instanceof Dropper) {
			updatePearl(pearl, (Dropper) holder);
		} else if (holder instanceof Hopper) {
			updatePearl(pearl, (Hopper) holder);
		} else if (holder instanceof BrewingStand) {
			updatePearl(pearl, (BrewingStand) holder);
		} else if (holder instanceof Barrel) {
			updatePearl(pearl, (Barrel) holder);
		} else if (holder instanceof Player) {
			updatePearl(pearl, (Player) holder);
		} else {
			event.setCancelled(true);
		}
	}


	private void updatePearlHolder(ExilePearl pearl, Entity entity, Cancellable event) {
		if (entity instanceof ItemFrame) {
			updatePearl(pearl, (ItemFrame) entity);
		} else {
			event.setCancelled(true);
		}
	}


	/**
	 * Updates the pearl status
	 * @param pearl The prison pearl
	 * @param block The block storing the pearl
	 */
	private <ItemBlock extends InventoryHolder & BlockState> void updatePearl(ExilePearl pearl, ItemBlock block) {
		pearl.setHolder(block.getBlock());
	}


	/**
	 * Updates the pearl status
	 * @param pearl The prison pearl
	 * @param e The entity holding the pearl
	 */
	private void updatePearl(ExilePearl pearl, Entity e) {
		pearl.setHolder(e);
	}


	/**
	 * Updates the pearl status
	 * @param pearl The prison pearl
	 * @param player The player holding the pearl
	 */
	private void updatePearl(ExilePearl pearl, Player player) {
		pearl.setHolder(player);
	}


	/**
	 * Prevent imprisoned players from placing ExilePearls in their inventory.
	 * @param e The event args
	 */
	@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
	public void onExilePearlClick(InventoryClickEvent e) {
		Player clicker = (Player) e.getWhoClicked();

		ExilePearl pearl = pearlApi.getPearlFromItemStack(e.getCurrentItem());
		if(pearl != null) {
			if (pearlApi.isPlayerExiled(clicker)) {
				msg(clicker, Lang.pearlCantHold);
				e.setCancelled(true);
			}
		}
	}


	/**
	 * Prevent imprisoned players from picking up Prisonpearls.
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onPlayerPickupPearl(EntityPickupItemEvent e) {
		Item item = e.getItem();
		if (e.getEntityType() != EntityType.PLAYER) {
			return;
		}
		ExilePearl pearl = pearlApi.getPearlFromItemStack(item.getItemStack());
		if (pearl == null) {
			return;
		}

		if (pearlApi.isPlayerExiled((Player)e.getEntity())) {
			e.setCancelled(true);
		}
	}

	/**
	 * Listen for pearl being moved to item frame
	 * @param event
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
		if (event.getRightClicked() instanceof ItemFrame) {
			ExilePearl pearl = pearlApi.getPearlFromItemStack(event.getPlayer().getInventory().getItemInMainHand());
			if (pearl != null) {
				pearl.setHolder(event.getRightClicked());
			}
		}
	}


	/**
	 * Track the location of a pearl
	 * Forbid pearls from being put in storage minecarts
	 * @param event
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onInventoryClick(InventoryClickEvent event) {

		// Announce an prison pearl if it is clicked
		ItemStack newitem = validatePearl(event.getCurrentItem());
		if (newitem != null) {
			event.setCurrentItem(newitem);
		}

		InventoryAction a = event.getAction();
		//pearlApi.log("Inv Action: " + a.toString());
		if(a == InventoryAction.COLLECT_TO_CURSOR
				|| a == InventoryAction.PICKUP_ALL 
				|| a == InventoryAction.PICKUP_HALF
				|| a == InventoryAction.PICKUP_ONE) {
			ExilePearl pearl = pearlApi.getPearlFromItemStack(event.getCurrentItem());

			if(pearl != null) {
				pearl.setHolder(((Player) event.getWhoClicked()));
			}
		}
		else if(event.getAction() == InventoryAction.PLACE_ALL
				|| event.getAction() == InventoryAction.PLACE_SOME
				|| event.getAction() == InventoryAction.PLACE_ONE) {	
			ExilePearl pearl = pearlApi.getPearlFromItemStack(event.getCursor());

			if(pearl != null) {
				boolean clickedTop = event.getRawSlot() < event.getView().getTopInventory().getSize();
				InventoryHolder holder = clickedTop ? event.getView().getTopInventory().getHolder() : event.getView().getBottomInventory().getHolder();
				if (holder != null) {
					updatePearlHolder(pearl, holder, event);
				}
			}
		}
		else if(event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
			ExilePearl pearl = pearlApi.getPearlFromItemStack(event.getCurrentItem());

			if(pearl != null) {
				boolean clickedTop = event.getRawSlot() < event.getView().getTopInventory().getSize();

				InventoryHolder holder = !clickedTop ? event.getView().getTopInventory().getHolder() : event.getView().getBottomInventory().getHolder();

				// ShiftClicking into a furnace will not move the pearl into the furnace so the pearlHolder should not be updated
				if (event.getClick().isShiftClick() && holder != null && holder.getInventory() instanceof FurnaceInventory) {
					event.setCancelled(true);
					return;
				}

				if(holder != null && holder.getInventory().firstEmpty() >= 0) {
					updatePearlHolder(pearl, holder, event);
				}
			}
		}
		else if(event.getAction() == InventoryAction.HOTBAR_SWAP) {
			PlayerInventory playerInventory = event.getWhoClicked().getInventory();
			ExilePearl pearl = pearlApi.getPearlFromItemStack(playerInventory.getItem(event.getHotbarButton()));

			if(pearl != null) {
				boolean clickedTop = event.getRawSlot() < event.getView().getTopInventory().getSize();

				InventoryHolder holder = clickedTop ? event.getView().getTopInventory().getHolder() : event.getView().getBottomInventory().getHolder();

				updatePearlHolder(pearl, holder, event);
			}

			if(event.isCancelled())
				return;

			pearl = pearlApi.getPearlFromItemStack(event.getCurrentItem());

			if(pearl != null) {
				updatePearl(pearl, (Player) event.getWhoClicked());
			}
		}
		else if (event.getAction() == InventoryAction.SWAP_WITH_CURSOR) {
			ExilePearl pearl = pearlApi.getPearlFromItemStack(event.getCursor());

			if(pearl != null) {
				boolean clickedTop = event.getRawSlot() < event.getView().getTopInventory().getSize();

				InventoryHolder holder = clickedTop ? event.getView().getTopInventory().getHolder() : event.getView().getBottomInventory().getHolder();

				updatePearlHolder(pearl, holder, event);
			}

			if(event.isCancelled())
				return;

			pearl = pearlApi.getPearlFromItemStack(event.getCurrentItem());

			if(pearl != null) {
				updatePearl(pearl, (Player) event.getWhoClicked());
			}
		}
		else if(event.getAction() == InventoryAction.DROP_ALL_CURSOR
				|| event.getAction() == InventoryAction.DROP_ALL_SLOT
				|| event.getAction() == InventoryAction.DROP_ONE_CURSOR
				|| event.getAction() == InventoryAction.DROP_ONE_SLOT) {
			// Handled by onItemSpawn
		}
		else if (a != InventoryAction.NOTHING) {
			if(pearlApi.getPearlFromItemStack(event.getCurrentItem()) != null || pearlApi.getPearlFromItemStack(event.getCursor()) != null) {
				((Player) event.getWhoClicked()).sendMessage(ChatColor.RED + "You can't do that with an exile pearl.");

				event.setCancelled(true);
			}
		}
	}


	/**
	 * Track the location of a pearl if a player picks it up
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerPickupItem(EntityPickupItemEvent e) {
		Item item = e.getItem();

		ExilePearl pearl = pearlApi.getPearlFromItemStack(item.getItemStack());
		if (pearl == null || e.getEntityType() != EntityType.PLAYER) {
			return;
		}
		updatePearl(pearl, (Player)e.getEntity());
	}


	/**
	 * Imprison people upon death
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.HIGHEST, ignoreCancelled = true)
	public void onEntityDeath(EntityDeathEvent e) {
		if (!(e.getEntity() instanceof Player)) {
			return;
		}

		final UUID playerId;
		Player killer = null;
		
		// If the player was an NPC, grab the ID from it
		NpcIdentity npcId = null;
		try {
			npcId = pearlApi.getPlayerAsTaggedNpc((Player)e.getEntity());
		} catch(Exception ex) { }
		if (npcId != null) {
			playerId = npcId.getId();
		} else {
			playerId = ((Player)e.getEntity()).getUniqueId();
		}
		ExilePearl pearl = pearlApi.getPearl(playerId);
		

		if(pearlApi.isPlayerExiled(playerId)
		   && pearlApi.getPearl(playerId).getPearlType() == PearlType.PRISON
		   && !pearlApi.getPearl(playerId).isSummoned()
		   && e.getEntity().getLocation().getWorld().equals(pearlApi.getPearlConfig().getPrisonWorld())){
			return;
		}

		// These will be priority sorted according to the configured algorithm
		List<Player> damagers = pearlApi.getDamageLogger().getSortedDamagers(playerId);
		
		// Check is player is already exiled
		if (pearlApi.isPlayerExiled(playerId)) {
			//Reset bed of exiled player if killer is not null
			if (!damagers.isEmpty() && pearlApi.getPearlConfig().canPerform(ExileRule.SPAWN_RESET) && pearlApi.getPearl(playerId).getPearlType() == PearlType.EXILE) {
				pearl.getPlayer().setBedSpawnLocation(null,true);
				for(Player damager : damagers) {
					msg(damager, Lang.pearlAlreadyPearled, pearlApi.getRealPlayerName(playerId));
				}
				return;
			}
		}

		for(Player damager : damagers) {
			int firstpearl = Integer.MAX_VALUE;
			for (Entry<Integer, ? extends ItemStack> entry : damager.getInventory().all(Material.ENDER_PEARL).entrySet()) {

				// Make sure we're holding a blank pearl
				if (pearlApi.getPearlFromItemStack(entry.getValue()) == null) {
					firstpearl = Math.min(entry.getKey(), firstpearl);
				}
			}

			if (firstpearl == Integer.MAX_VALUE) {
				continue;
			}

			// Check if pearl in the hotbar
			if (pearlApi.getPearlConfig().getMustPrisonPearlHotBar() && firstpearl > 8) {
				continue; 
			}

			pearl = pearlApi.exilePlayer(playerId, damager);
			if (pearl == null) {
				// Check if player is already exiled
				// exilePlayer already handles the case where this is a pearl steal
				// and will not return null if it is, so we don't check that here
				if (pearlApi.isPlayerExiled(playerId)) {
					for(Player dmgr : damagers) {
						if(pearlApi.getPearlConfig().getShouldAnnounceExileLocation()){
							msg(dmgr, Lang.pearlAlreadyPearledAtLocation, pearlApi.getRealPlayerName(playerId), pearlApi.getPearl(playerId).getLocationDescription());
						}else{
							msg(dmgr, Lang.pearlAlreadyPearled, pearlApi.getRealPlayerName(playerId));
						}
					}
				}
				return; // The pearling failed for some reason
			}
			killer = damager;
			break;
		}

		if (killer != null) {
			//Reset bed of exiled player if killer is not null
			if (pearlApi.getPearlConfig().canPerform(ExileRule.SPAWN_RESET)) {
				pearl.getPlayer().setBedSpawnLocation(null,true);
			}
			// Notify other damagers if they were not awarded the pearl
			for(Player damager : damagers) {
				if (damager != killer) {
					msg(damager, Lang.pearlYouDamagedNotAwarded, pearlApi.getRealPlayerName(playerId), pearlApi.getRealPlayerName(killer.getUniqueId()));
				}
			}

			// Pearl succeeded
			// set up the imprisoner's inventory
			Inventory inv = killer.getInventory();
			ItemStack stack = null;
			int stacknum = -1;

			// Scan for the smallest stack of normal ender pearls
			for (Entry<Integer, ? extends ItemStack> entry :
				inv.all(Material.ENDER_PEARL).entrySet()) {
				ItemStack newstack = entry.getValue();
				int newstacknum = entry.getKey();
				if (newstack.getEnchantmentLevel(Enchantment.DURABILITY) == 0) {
					if (stack != null) {
						// don't keep a stack bigger than the previous one
						if (newstack.getAmount() > stack.getAmount()) {
							continue;
						}
						// don't keep an identical sized stack in a higher slot
						if (newstack.getAmount() == stack.getAmount() &&
								newstacknum > stacknum) {
							continue;
						}
					}

					stack = newstack;
					stacknum = entry.getKey();
				}
			}

			int pearlnum;
			ItemStack dropStack = null;
			if (stacknum == -1) { // no pearl (admin command)
				// give him a new one at the first empty slot
				pearlnum = inv.firstEmpty();
			} else if (stack.getAmount() == 1) { // if he's just got one pearl
				pearlnum = stacknum; // put the prison pearl there
			} else {
				// otherwise, put the prison pearl in the first empty slot
				pearlnum = inv.firstEmpty();
				if (pearlnum >= 0) {
					// and reduce his stack of pearls by one
					stack.setAmount(stack.getAmount() - 1);
					inv.setItem(stacknum, stack);
				} else { // no empty slot?
					dropStack = new ItemStack(Material.ENDER_PEARL, stack.getAmount() - 1);
					pearlnum = stacknum; // then overwrite his stack of pearls
				}
			}

			// Drop pearls that otherwise would be deleted
			Location l = killer.getLocation();
			if (dropStack != null) {
				killer.getWorld().dropItem(l, dropStack);
				pearlApi.log(l + ", " + dropStack.getAmount());
			}

			inv.setItem(pearlnum, pearl.createItemStack());
		}
	}


	/**
	 * Handles logging in players
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerJoin(PlayerJoinEvent e) {
		UUID uid = e.getPlayer().getUniqueId();
		ExilePearl pearl = pearlApi.getPearl(uid);
		if (pearl != null && pearl.getFreedOffline()) {
			msg(pearl.getPlayer(), Lang.pearlYouWereFreed);
			pearlApi.freePearl(pearl,PearlFreeReason.FREED_OFFLINE);
			removeHelpItem(pearl.getPlayer());
		} else if (pearl != null) {
			pearl.setLastOnline(new Date());
			if(pearl.getPearlType() == PearlType.PRISON && !pearl.isSummoned() && 
					!e.getPlayer().getLocation().getWorld().equals(pearlApi.getPearlConfig().getPrisonWorld())) {
				SpawnUtil.spawnPlayer(pearl.getPlayer(), pearlApi.getPearlConfig().getPrisonWorld());
			}
		}
	}

	/**
	 * Frees pearls when right-clicked
	 * @param e The event argst
	 */
	@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
	public void onPlayerInteract(PlayerInteractEvent e) {
		ExilePearl pearl = pearlApi.getPearlFromItemStack(e.getItem());
		if (pearl == null) {
			return;
		}
		if (e.getAction() == Action.RIGHT_CLICK_BLOCK) {
			Material m = e.getClickedBlock().getType();
			if (e.getClickedBlock().getState() instanceof BlockInventoryHolder ||
				m == Material.CRAFTING_TABLE || m == Material.ANVIL || m == Material.ENCHANTING_TABLE || m == Material.BEACON) {
				return;
			}
		} else if (e.getAction() != Action.RIGHT_CLICK_AIR) {
			return;
		}

		Player player = e.getPlayer();

		if (!pearlApi.getPearlConfig().getFreeByThrowing()) {
			msg(player, Lang.pearlCantThrow);
			e.setCancelled(true);
			player.getInventory().setItemInMainHand(pearl.createItemStack());
			return;
		}
		e.setCancelled(true);
		if (pearlApi.freePearl(pearl, PearlFreeReason.PEARL_THROWN)) {
			player.getInventory().setItemInMainHand(null);
			msg(player, Lang.pearlYouFreed, pearl.getPlayerName());
		}
	}

	/**
	 * Prevent pearling with an exile pearl
	 * @param e The event
	 */
	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onPearlThrow(ProjectileLaunchEvent e) {
		if (!(e.getEntity() instanceof EnderPearl)) {
			return;
		}

		final Player p = (Player)e.getEntity().getShooter();
		if (p == null) {
			return;
		}

		ExilePearl pearl = pearlApi.getPearlFromItemStack(p.getInventory().getItemInMainHand());
		if (pearl == null) {
			return;
		}

		msg(p, Lang.pearlCantThrow);
		e.setCancelled(true);

		// Need to schedule this or else the re-created pearl doesn't show up
		Bukkit.getScheduler().scheduleSyncDelayedTask(pearlApi, new Runnable() {
			@Override
			public void run() {
				p.getInventory().setItemInMainHand(pearl.createItemStack());
			}
		});
	}


	/**
	 * Handles new prison pearl events
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerPearled(PlayerPearledEvent e) {

		Player imprisoned = e.getPearl().getPlayer();
		Player imprisoner = pearlApi.getPlayer(e.getPearl().getKillerId());

		// Log the capturing ExilePearl event.
		pearlApi.log(String.format("%s has bound %s to a ExilePearl", e.getPearl().getKillerName(), e.getPearl().getPlayerName()));

		msg(imprisoner, Lang.pearlYouBound, e.getPearl().getPlayerName());
		msg(imprisoned, Lang.pearlYouWereBound, e.getPearl().getKillerName());
	}


	/**
	 * Handled exiled players re-spawning
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerRespawn(PlayerRespawnEvent e) {
		Player player = e.getPlayer();
		if (pearlApi.isPlayerExiled(player)) {
			if(useHelpItem) giveHelpItem(player);
			ExilePearl pearl = pearlApi.getPearl(player.getUniqueId());
			if(pearl.getPearlType() == PearlType.PRISON) e.setRespawnLocation(SpawnUtil.chooseSpawn(pearlApi.getPearlConfig().getPrisonWorld()));
			if(pearl.isSummoned()) {
				pearl.setSummoned(false);
				pearl.setReturnLocation(null);
			}
		}
	}


	/**
	 * Removes the help item when dropped
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onHelpItemDrop(ItemSpawnEvent e) {
		if (isHelpItem(e.getEntity().getItemStack())) {
			e.setCancelled(true);
		}
	}


	/**
	 * Prevents placing the help item
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onHelpItemPlace(BlockPlaceEvent e) {
		if (isHelpItem(e.getPlayer().getInventory().getItemInMainHand())) {
			e.setCancelled(true);
		}
	}


	/**
	 * Handles player freed events
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPlayerFreed(PlayerFreedEvent e) {
		Player player = e.getPearl().getPlayer();
		if (player != null && player.isOnline()) {
			msg(player, Lang.pearlYouWereFreed);
			removeHelpItem(player);
		}
	}


	/**
	 * Handles a pearl move event
	 * @param e The event args
	 */
	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPearlMoved(PearlMovedEvent e) {
		e.getPearl().performBroadcast();
	}

	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onPrepareCraftPearl(PrepareItemCraftEvent e) {
		CraftingInventory inv = e.getInventory();
		if (inv == null) {
			return;
		}

		ItemStack result = inv.getResult();
		if (result == null) {
			return;
		}

		if (result.getType() != Material.STONE_BUTTON) {
			return;
		}

		if (result.getEnchantmentLevel(Enchantment.DURABILITY) != 1) {
			//inv.setResult(new ItemStack(Material.AIR)); // Is preventing normal crafting of stone buttons.
			// It would be better to trigger against something that does not get crafted, like spider eyes or something
			return;
		}

		// Get the pearl item being crafted
		ItemStack pearlItem = inv.getItem(inv.first(Material.ENDER_PEARL));

		if (pearlItem == null) {
			inv.setResult(new ItemStack(Material.AIR));
			return;
		}

		ExilePearl pearl = pearlApi.getPearlFromItemStack(pearlItem);
		if (pearl == null) {
			inv.setResult(new ItemStack(Material.AIR));
			return;
		}

		ItemMap invItems = new ItemMap(inv);

		if(pearl.getPearlType() == PearlType.EXILE) {
			RepairMaterial upgradeItem = null;
			for(RepairMaterial item : upgradeMaterials) {
				if(invItems.getAmount(item.getStack()) >= item.getRepairAmount()) {
					upgradeItem = item;
					break;
				}
			}
			if(upgradeItem != null) {
				ItemStack resultStack = pearl.createItemStack();
				ItemMeta im = resultStack.getItemMeta();
				im.setLore(pearlApi.getLoreProvider().generateLoreWithModifiedType(pearl, PearlType.PRISON));
				resultStack.setItemMeta(im);
				e.getInventory().setResult(resultStack);
				return;
			}
		}

		// Ignore pearls that are at full health
		if (pearl.getHealth() == pearlApi.getPearlConfig().getPearlHealthMaxValue()) {
			inv.setResult(new ItemStack(Material.AIR));
			return;
		}

		RepairMaterial repairItem = null;

		// Find the repair material that is being used for crafting
		for(RepairMaterial item : repairMaterials.get(pearl.getPearlType())) {
			if (invItems.getAmount(item.getStack()) > 0) {
				repairItem = item;
				break;
			}
		}

		// Quit if no repair item was found
		if (repairItem == null) {
			inv.setResult(new ItemStack(Material.AIR));
			return;
		}

		// Get the total possible repair amount. This doesn't need to be limited
		// because the lore generator will cap at 100%
		int repairAmount = invItems.getAmount(repairItem.getStack()) * repairItem.getRepairAmount();
		repairAmount = (int) Math.ceil(repairAmount / pearl.getLongTimeMultiplier());

		// Generate a new item with the updated health value as the crafting result
		ItemStack resultStack = pearl.createItemStack();
		ItemMeta im = resultStack.getItemMeta();
		im.setLore(pearlApi.getLoreProvider().generateLoreWithModifiedHealth(pearl, pearl.getHealth() + repairAmount));
		resultStack.setItemMeta(im);

		e.getInventory().setResult(resultStack);
	}

	@EventHandler(priority=EventPriority.MONITOR, ignoreCancelled = true)
	public void onCraftPearl(CraftItemEvent e) {
		CraftingInventory inv = e.getInventory();
		ItemStack result = inv.getResult();

		// Check if we are crafting an exile pearl
		ExilePearl pearl = pearlApi.getPearlFromItemStack(result);
		if (pearl == null) {
			return;
		}

		ItemMap invItems = new ItemMap(inv);
		RepairMaterial repairItem = null;

		// Find the repair material that is being used for crafting
		for(RepairMaterial item : repairMaterials.get(pearl.getPearlType())) {
			if (invItems.getAmount(item.getStack()) > 0) {
				repairItem = item;
				break;
			}
		}

		// Quit if no repair items were found in the crafting inventory
		if (repairItem != null) {
			int maxHealth = pearlApi.getPearlConfig().getPearlHealthMaxValue();
			int repairPerItem = repairItem.getRepairAmount();
			int repairMatsAvailable = invItems.getAmount(repairItem.getStack());
			int repairMatsToUse = Math.min((int)Math.ceil((maxHealth - pearl.getHealth()) / (double)repairPerItem), repairMatsAvailable);
			int repairAmount = repairMatsToUse * repairPerItem;
			repairAmount = (int) Math.ceil(repairAmount / pearl.getLongTimeMultiplier());

			// Changing the value of the crafting items results in a dupe glitch so any remaining
			// materials need to be placed back into the player's inventory.
			inv.remove(Material.ENDER_PEARL);
			if (repairMatsAvailable > repairMatsToUse) {
				for (int i = 0; i < inv.getContents().length; i++) {
					ItemStack is = inv.getItem(i);
					if (is != null) {
						int numLeft = repairMatsAvailable - repairMatsToUse;
						Player player = (Player)(e.getWhoClicked());
						int openSlot = player.getInventory().firstEmpty();
						ItemStack giveBack = repairItem.getStack().clone();
						giveBack.setAmount(numLeft);
						if (openSlot >= 0) {
							player.getInventory().setItem(openSlot, giveBack);
							msg(player, "<i>The remaining %d repair items were put back in your inventory.", numLeft);
						} else {
							player.getWorld().dropItem(player.getLocation().add(0, 0.5, 0), giveBack);
							msg(player, "<i>The remaining %d repair items were dropped on the ground.", numLeft);
						}
						break;
					}
				}
			}
			inv.clear();

			// Repair the pearl and update the item stack
			pearl.setHealth(pearl.getHealth() + repairAmount);
			inv.setResult(pearl.createItemStack());
			pearlApi.log("The pearl for player %s was repaired by %d points.", pearl.getPlayerName(), repairAmount);
			return;
		}
		if(pearl.getPearlType() == PearlType.PRISON) {
			//can't be upgraded
			return;
		}
		//try to find an upgrade recipe
		RepairMaterial upgradeItem = null;

		for(RepairMaterial item : upgradeMaterials) {
			if(invItems.getAmount(item.getStack()) >= item.getRepairAmount()) {
				upgradeItem = item;
				break;
			}
		}

		if(upgradeItem != null) {
			//just using the repair amount as a stack size because fuck it
			int upgradeMatsRequired = upgradeItem.getRepairAmount();
			int upgradeMatsAvailable = invItems.getAmount(upgradeItem.getStack());
			if(upgradeMatsAvailable < upgradeMatsRequired) return;
			inv.remove(Material.ENDER_PEARL);
			if(upgradeMatsAvailable > upgradeMatsRequired) {
				for(int i = 0; i < inv.getContents().length; i++) {
					ItemStack is = inv.getItem(i);
					if(is != null) {
						int numLeft = upgradeMatsAvailable - upgradeMatsRequired;
						Player player = (Player)e.getWhoClicked();
						int openSlot = player.getInventory().firstEmpty();
						ItemStack giveBack = upgradeItem.getStack().clone();
						giveBack.setAmount(numLeft);
						if(openSlot >= 0) {
							player.getInventory().setItem(openSlot, giveBack);
							msg(player, "<i>The remaining %d upgrade items were put back in your inventory.", numLeft);
						} else {
							player.getWorld().dropItemNaturally(player.getLocation().add(0, 0.5, 0), giveBack);
							msg(player, "<i>The remaining %d upgrade items were dropped on the ground.", numLeft);
						}
						break;
					}
				}
			}
			inv.clear();
			pearl.setPearlType(PearlType.PRISON);
			pearl.setHealth(pearlApi.getPearlConfig().getPearlHealthStartValue());
			inv.setResult(pearl.createItemStack());
			if(pearl.getPlayer() != null && pearl.getPlayer().isOnline()) {
				SpawnUtil.spawnPlayer(pearl.getPlayer(), pearlApi.getPearlConfig().getPrisonWorld());
				msg(pearl.getPlayer(), "<i>You've been imprisoned in the end by %s.", ((Player)e.getWhoClicked()).getDisplayName());
			}
			pearlApi.log("The pearl for player %s was upgraded to a Prison Pearl.", pearl.getPlayerName());
		}
	}

	@EventHandler
	public void onPlayerPortal(PlayerPortalEvent event) {
		ExilePearl pearl = pearlApi.getPearl(event.getPlayer().getUniqueId());
		if(pearl != null && pearl.getPearlType() == PearlType.PRISON && event.getCause() == TeleportCause.END_PORTAL) {
			event.setTo(SpawnUtil.chooseSpawn(pearlApi.getPearlConfig().getPrisonWorld()));
		}
	}

	@Override
	public void loadConfig(PearlConfig config) {
		repairMaterials.clear();
		upgradeMaterials.clear();
		try {
			// This item is basically used as a trigger to catch the recipe being created
			ItemStack resultItem = new ItemStack(Material.STONE_BUTTON, 1);
			ItemMeta im = resultItem.getItemMeta();
			im.addEnchant(Enchantment.DURABILITY, 1, true);
			im.addItemFlags(ItemFlag.HIDE_ENCHANTS);
			resultItem.setItemMeta(im);

			
			for(PearlType type : PearlType.values()) {
				repairMaterials.put(type, config.getRepairMaterials(type));
			}
			for(Set<RepairMaterial> set : repairMaterials.values()) {
				for(RepairMaterial mat : set) {
					ShapelessRecipe r1 = new ShapelessRecipe(new NamespacedKey(pearlApi, "repairPearl"), resultItem);
					r1.addIngredient(1, Material.ENDER_PEARL);
					r1.addIngredient(1, mat.getStack().getData());

					Bukkit.getServer().addRecipe(r1);
				}
			}

		} catch (Exception ex) {
			pearlApi.log(Level.SEVERE, "Failed to register the pearl repair recipes " + ex.toString());
		}

		try {
			ItemStack resultItem = new ItemStack(Material.STONE_BUTTON, 1);
			ItemMeta im = resultItem.getItemMeta();
			im.addEnchant(Enchantment.DURABILITY, 1, true);
			im.addItemFlags(ItemFlag.HIDE_ENCHANTS);
			resultItem.setItemMeta(im);

			upgradeMaterials.addAll(config.getUpgradeMaterials());

			for(RepairMaterial mat : upgradeMaterials) {
				ShapelessRecipe r1 = new ShapelessRecipe(new NamespacedKey(pearlApi, "upgradePearl"),resultItem);
				r1.addIngredient(1, Material.ENDER_PEARL);
				r1.addIngredient(1, mat.getStack().getData());

				Bukkit.getServer().addRecipe(r1);
			}
		} catch (Exception ex) {
			pearlApi.log(Level.SEVERE, "Failed to register pearl upgrade recipes." + ex.toString());
		}

		useHelpItem = config.getUseHelpItem();
		helpItemName = config.getHelpItemName();

		helpItemText.clear();
		for(String s : config.getHelpItemText()) {
			helpItemText.add(TextUtil.parse(s));
		}
	}

	/**
	 * Checks whether an item is the help item
	 * @param is The item stack to check
	 * @return true if the item is the help item
	 */
	private boolean isHelpItem(ItemStack is) {
		if (is.getType() != Material.STICK) {
			return false;
		}

		ItemMeta im = is.getItemMeta();
		if (im == null) {
			return false;
		}

		return im.getEnchantLevel(Enchantment.DURABILITY) == 2;
	}

	/**
	 * Removes the item item from a player's inventory
	 * @param player The player
	 */
	private void removeHelpItem(Player player) {
		// Remove the help item
		for (Entry<Integer, ? extends ItemStack> entry : player.getInventory().all(Material.STICK).entrySet()) {
			if (isHelpItem(entry.getValue())) {
				player.getInventory().setItem(entry.getKey(), new ItemStack(Material.AIR));
				break;
			}
		}
	}

	/**
	 * Gives the help item to a player
	 * @param player The player
	 */
	private void giveHelpItem(Player player) {
		if (player.isOnline()) {
			ItemStack helpItem = new ItemStack(Material.STICK, 1);
			ItemMeta im = helpItem.getItemMeta();
			im.setDisplayName(helpItemName);
			im.setLore(helpItemText);
			im.addEnchant(Enchantment.DURABILITY, 2, true);
			im.addItemFlags(ItemFlag.HIDE_ENCHANTS);
			helpItem.setItemMeta(im);
			player.getInventory().setItem(0, helpItem);
		}
	}
} public class RandomSpawnListener extends RuleListener {


	public RandomSpawnListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}


	/**
	 * Prevents exiled players from random-spawning within their pearl radius
	 * @param e The event args
	 */
/*	@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
	public void onRandomSpawn(NewPlayerSpawn e) {
		int radius = config.getRulePearlRadius();
		if (radius <= 0) {
			return;
		}

		ExilePearl pearl = pearlApi.getPearl(e.getPlayer().getUniqueId());
		if (pearl == null) {
			return;
		}

		Location pearlLocation = pearl.getLocation();
		Location playerLocation = e.getLocation();

		if (pearlLocation.getWorld() != playerLocation.getWorld()) {
			return;
		}

		double distance = Math.sqrt(Math.pow(pearlLocation.getX() - playerLocation.getX(), 2) + Math.pow(pearlLocation.getZ() - playerLocation.getZ(), 2));

		if (distance < radius) {
			e.setCancelled(true);
		}
	}*/
} public class WorldBorderListener extends RuleListener {

	public WorldBorderListener(ExilePearlApi pearlApi) {
		super(pearlApi);
	}

	/**
	 * Frees any pearls that are stored beyond world border
	 * 
	 * @param e The event args
	 */
	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
	public void onPearlDecay(PearlDecayEvent e) {
		boolean autoFree = pearlApi.getPearlConfig().getShouldAutoFreeWorldBorder();
		if (!autoFree) {
			return;
		}
		// Free any pearls outside world border
		Location loc = e.getPearl().getLocation();
		if (!pearlApi.isLocationInsideBorder(loc)) {
			//delayed sync free to avoid ConcurrentModification as this is happening during Pearl iteration
			Bukkit.getScheduler().scheduleSyncDelayedTask(ExilePearlPlugin.getApi(), () -> {
				pearlApi.freePearl(e.getPearl(), PearlFreeReason.OUTSIDE_WORLD_BORDER);
			});
		}
	}

	@EventHandler(priority = EventPriority.NORMAL)
	public void onInventoryClick(InventoryClickEvent e) {
		boolean autoFree = pearlApi.getPearlConfig().getShouldAutoFreeWorldBorder();
		if (!autoFree) {
			return;
		}
		if (pearlApi.getPearlFromItemStack(e.getCurrentItem()) != null
				|| pearlApi.getPearlFromItemStack(e.getCursor()) != null) {
			Location loc = e.getView().getTopInventory().getLocation();
			if (loc != null && !pearlApi.isLocationInsideBorder(loc)) {
				e.getWhoClicked().sendMessage(ChatColor.RED +
						"This container is outside the world border, pearls left in it will automatically be freed");
			}
		}
	}
} public class CoreStorageProvider implements StorageProvider {

	private final ExilePearlApi pearlApi;
	private final PearlFactory pearlFactory;

	private PluginStorage storage;

	public CoreStorageProvider(final ExilePearlApi pearlApi, final PearlFactory pearlFactory) {
		Guard.ArgumentNotNull(pearlApi, "pearlApi");
		Guard.ArgumentNotNull(pearlFactory, "pearlFactory");

		this.pearlApi = pearlApi;
		this.pearlFactory = pearlFactory;
	}

	public PluginStorage createStorage() {
		if (storage != null) {
			throw new RuntimeException("Can't re-create the storage instance.");
		}

		StorageType storageType = pearlApi.getPearlConfig().getStorageType();

		if (storageType == StorageType.RAM) {
			pearlApi.log(Level.WARNING, "Using RAM storage. Data will not be saved.");
			storage = new RamStorage();
		}
		else if (storageType == StorageType.MYSQL) {
			storage = new AsyncStorageWriter(new MySqlStorage(pearlFactory, pearlApi, pearlApi.getPearlConfig()), pearlApi);
			pearlApi.log(Level.INFO, "Using MySQL storage.");
		}
		else {
			File storageFile = new File("plugins/ExilePearl/pearls.yml");
			storage = new AsyncStorageWriter(new FileStorage(storageFile, pearlFactory, pearlApi), pearlApi);
			pearlApi.log(Level.INFO, "Using File storage.");
		}
		return storage; 
	}

	@Override
	public PluginStorage getStorage() {
		return storage;
	}
} public interface PluginStorage extends PearlStorage {

	/**
	 * Connects the storage
	 * @return true if connection succeeds
	 */
	public boolean connect();

	/**
	 * Disconnects the storage
	 */
	public void disconnect();

	/**
	 * Gets whether the storage is connected
	 * @return true if connected
	 */
	public boolean isConnected();
} public class BastionWrapper {
	private BastionBlock bastion;

	public BastionWrapper(BastionBlock bastion) {
		this.bastion = bastion;
	}

	public BastionBlock getBastion() {
		return bastion;
	}

	public Vector getPushout(Location player, int KNOCKBACK) {
		Location bastionLocation = bastion.getLocation();
		if (!player.getWorld().getUID().equals(bastionLocation.getWorld().getUID())) return null; // what?
		double radius = bastion.getType().getEffectRadius() + (double) KNOCKBACK;
		double xd = player.getX() - bastionLocation.getX();
		double zd = player.getZ() - bastionLocation.getZ();
		double dist = Math.sqrt(xd*xd + zd*zd);		
		Vector vector = null;

		if (bastion.getType().isSquare()) {
			// get linear distance
			if (xd == 0.0 & zd == 0.0) {
				// pick a direction and _shove_
				xd = radius;
			} else {
				// make a unit triangle of same shape by dividing by longest side. Then multiply by radius to "shoveout".
				double ax = Math.abs(xd);
				double az = Math.abs(zd);
				if (ax > az) {
					xd = (xd / ax);
					zd = (zd / ax);
				} else {
					xd = (xd / az);
					zd = (zd / az);
				}
				double push = (radius * Math.sqrt(xd*xd + zd*zd)) - dist;
				// adjust pushout based on "unit" triangle to accomodate corners.
				// it will always be some factor between 1 and sqrt(2).
				xd *= push;
				zd *= push;
			}
			vector = new Vector(xd, 0.0, zd);	
		} else {
			// ez. straightline vector from radial distance.
			Location nP = player.clone().subtract(bastionLocation);
			vector = new Vector(nP.getX(), 0.0, nP.getZ());
			vector.normalize(); // points from Bastion to player.

			vector.multiply((radius - dist)); // pushout!
			System.out.println(vector.toString());
		}
		return vector;
	}
} public interface Clock {

	/**
	 * Gets the current time;
	 * @return The current time;
	 */
    long getCurrentTime();
} public interface ExilePearlRunnable extends Runnable, Configurable {

	/**
	 * Start the pearl worker
	 */
	void start();

	/**
	 * Stop the pearl worker
	 */
	void stop();

	/**
	 * Restart the pearl worker
	 */
	void restart();

	/**
	 * Gets whether the task is running
	 * @return true if the task is running
	 */
	boolean isRunning();

	/**
	 * Gets the task name
	 * @return The task name
	 */
	String getTaskName();
} public class NameLayerPermissions {
	public final static String BASTION_ALLOW_EXILED = "BASTION_ALLOW_EXILED";
	public final static String ALLOW_EXILE_BROADCAST = "ALLOW_EXILE_BROADCAST";

	private PermissionTracker permTracker;

	public NameLayerPermissions(PermissionTracker permTracker) {
		this.permTracker = permTracker;
		setup();
	}

	private void setup() {
		GroupAPI.registerPermission(BASTION_ALLOW_EXILED, DefaultPermissionLevel.MEMBER, "Allows exiled players to enter bastions on this group.");
		GroupAPI.registerPermission(ALLOW_EXILE_BROADCAST, DefaultPermissionLevel.MEMBER, "Allows exiled players to broadcast their pearl location on this group.");
	}


	public PermissionType getBastionsAllowExiles() {
		return permTracker.getPermission(BASTION_ALLOW_EXILED);
	}

	public PermissionType getExilesCanBroadcast() {
		return permTracker.getPermission(ALLOW_EXILE_BROADCAST);
	}
}
import com.devotedmc.ExilePearl.BorderHandler;
import com.devotedmc.ExilePearl.BrewHandler;
import com.devotedmc.ExilePearl.DamageLogger;
import com.devotedmc.ExilePearl.ExilePearl;
import com.devotedmc.ExilePearl.ExilePearlApi;
import com.devotedmc.ExilePearl.LoreProvider;
import com.devotedmc.ExilePearl.PearlFreeReason;
import com.devotedmc.ExilePearl.PearlManager;
import com.devotedmc.ExilePearl.StorageProvider;
import com.devotedmc.ExilePearl.SuicideHandler;
import com.devotedmc.ExilePearl.command.BaseCommand;
import com.devotedmc.ExilePearl.command.CmdAutoHelp;
import com.devotedmc.ExilePearl.command.CmdExilePearl;
import com.devotedmc.ExilePearl.command.CmdLegacy;
import com.devotedmc.ExilePearl.command.CmdSuicide;
import com.devotedmc.ExilePearl.command.PearlCommand;
import com.devotedmc.ExilePearl.config.PearlConfig;
import com.devotedmc.ExilePearl.holder.PearlHolder;
import com.devotedmc.ExilePearl.listener.ArtemisListener;
import com.devotedmc.ExilePearl.listener.BanStickListener;
import com.devotedmc.ExilePearl.listener.BastionListener;
import com.devotedmc.ExilePearl.listener.ChatListener;
import com.devotedmc.ExilePearl.listener.CitadelListener;
import com.devotedmc.ExilePearl.listener.ExileListener;
import com.devotedmc.ExilePearl.listener.JukeAlertListener;
import com.devotedmc.ExilePearl.listener.PlayerListener;
import com.devotedmc.ExilePearl.listener.RandomSpawnListener;
import com.devotedmc.ExilePearl.listener.WorldBorderListener;
import com.devotedmc.ExilePearl.storage.CoreStorageProvider;
import com.devotedmc.ExilePearl.storage.PluginStorage;
import com.devotedmc.ExilePearl.util.BastionWrapper;
import com.devotedmc.ExilePearl.util.Clock;
import com.devotedmc.ExilePearl.util.ExilePearlRunnable;
import com.devotedmc.ExilePearl.util.NameLayerPermissions;
import com.github.maxopoly.artemis.NameAPI;
import com.wimbli.WorldBorder.BorderData;
import com.wimbli.WorldBorder.WorldBorder;
import isaac.bastion.Bastion;
import isaac.bastion.BastionBlock;
import isaac.bastion.manager.BastionBlockManager;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minelink.ctplus.CombatTagPlus;
import net.minelink.ctplus.compat.api.NpcIdentity;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoader;
import vg.civcraft.mc.civmodcore.util.Guard;
import vg.civcraft.mc.namelayer.core.PermissionType;
import vg.civcraft.mc.namelayer.mc.NameLayerPlugin;
	@Override
	public File getDataFolder() {
		return plugin.getDataFolder();
	}

	@Override
	public ChunkGenerator getDefaultWorldGenerator(String arg0, String arg1) {
		return plugin.getDefaultWorldGenerator(arg0, arg1);
	}

	@Override
	public PluginDescriptionFile getDescription() {
		return plugin.getDescription();
	}

	@Override
	public Logger getLogger() {
		return plugin.getLogger();
	}

	@Override
	public String getName() {
		return plugin.getName();
	}

	@Override
	public PluginLoader getPluginLoader() {
		return plugin.getPluginLoader();
	}

	@Override
	public InputStream getResource(String arg0) {
		return plugin.getResource(arg0);
	}

	@Override
	public Server getServer() {
		return plugin.getServer();
	}

	@Override
	public boolean isEnabled() {
		return plugin.isEnabled();
	}

	@Override
	public boolean isNaggable() {
		return plugin.isNaggable();
	}

	@Override
	public void reloadConfig() {
		plugin.reloadConfig();
	}

	@Override
	public void saveConfig() {
		plugin.saveConfig();
	}

	@Override
	public void saveDefaultConfig() {
		plugin.saveDefaultConfig();
	}

	@Override
	public void saveResource(String arg0, boolean arg1) {
		plugin.saveResource(arg0, arg1);
	}

	@Override
	public void setNaggable(boolean arg0) {
		plugin.setNaggable(arg0);
	}

	@Override
	public boolean isLocationInsideBorder(Location location) {
		if (isWorldBorderEnabled()) {
			WorldBorder wb = WorldBorder.plugin;
			BorderData bd = wb.getWorldBorder(location.getWorld().getName());
			return bd.insideBorder(location);
		}
		return true;
	}

	@Override
	public boolean isPlayerInUnpermittedBastion(Player player) {
		if (!isBastionEnabled()) {
			return false;
		}

		try {
			final BastionBlockManager manager = Bastion.getBastionManager();

			Set<BastionBlock> bastions = manager.getBlockingBastions(player.getLocation());
			PermissionType perm = nameLayerPermissionsManager.getBastionsAllowExiles();

			for (BastionBlock bastion : bastions) {
				if (!bastion.permAccess(player, perm)) {
					return true;
				}
			}
		} catch(Exception ex) {
			ex.printStackTrace();
		}

		return false;
	}

	@Override
	public Clock getClock() {
		return clock;
	}

	@Override
	public BrewHandler getBrewHandler() {
		return this.brewHandler;
	}

	@Override
	public List<BastionWrapper> getPlayerInUnpermittedBastion(Player player) {