net.dmulloy2.ultimatearena.arenas.Arena.java Source code

Java tutorial

Introduction

Here is the source code for net.dmulloy2.ultimatearena.arenas.Arena.java

Source

/**
 * UltimateArena - fully customizable PvP arenas
 * Copyright (C) 2012 - 2015 MineSworn
 * Copyright (C) 2013 - 2015 dmulloy2
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.dmulloy2.ultimatearena.arenas;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;

import lombok.Getter;
import lombok.Setter;
import net.dmulloy2.chat.BaseComponent;
import net.dmulloy2.chat.ChatUtil;
import net.dmulloy2.chat.ComponentSerializer;
import net.dmulloy2.types.CustomScoreboard;
import net.dmulloy2.types.Reloadable;
import net.dmulloy2.ultimatearena.Config;
import net.dmulloy2.ultimatearena.UltimateArena;
import net.dmulloy2.ultimatearena.api.ArenaType;
import net.dmulloy2.ultimatearena.api.event.ArenaConcludeEvent;
import net.dmulloy2.ultimatearena.api.event.ArenaJoinEvent;
import net.dmulloy2.ultimatearena.api.event.ArenaLeaveEvent;
import net.dmulloy2.ultimatearena.api.event.ArenaSpawnEvent;
import net.dmulloy2.ultimatearena.api.event.ArenaStartEvent;
import net.dmulloy2.ultimatearena.arenas.pvp.PvPArena;
import net.dmulloy2.ultimatearena.gui.ClassSelectionGUI;
import net.dmulloy2.ultimatearena.types.ArenaClass;
import net.dmulloy2.ultimatearena.types.ArenaConfig;
import net.dmulloy2.ultimatearena.types.ArenaFlag;
import net.dmulloy2.ultimatearena.types.ArenaLocation;
import net.dmulloy2.ultimatearena.types.ArenaPlayer;
import net.dmulloy2.ultimatearena.types.ArenaSpectator;
import net.dmulloy2.ultimatearena.types.ArenaZone;
import net.dmulloy2.ultimatearena.types.KillStreak;
import net.dmulloy2.ultimatearena.types.LeaveReason;
import net.dmulloy2.ultimatearena.types.Permission;
import net.dmulloy2.ultimatearena.types.Team;
import net.dmulloy2.ultimatearena.types.WinCondition;
import net.dmulloy2.util.FormatUtil;
import net.dmulloy2.util.Util;

import org.apache.commons.lang.WordUtils;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.scheduler.BukkitRunnable;

/**
 * Base arena class. Can be extended to create custom arena types.
 * <p>
 * This class must be extended to function, since it does not create a
 * functional arena on its own. It does, however, provide useful methods that
 * can be used to create a functional arena.
 * <p>
 * The simplest example of a functional arena is the {@link PvPArena}
 *
 * @author dmulloy2
 */

@Getter
@Setter
public abstract class Arena implements Reloadable {
    public static enum Mode {
        LOBBY, INGAME, STOPPING, IDLE, DISABLED;
    }

    protected List<ArenaPlayer> active;
    protected List<ArenaPlayer> inactive;
    protected List<ArenaPlayer> toReward;
    protected List<ArenaSpectator> spectators;
    protected String lastJoin;

    protected List<String> finalLeaderboard;
    protected int leaderboardIndex;

    protected List<ArenaFlag> flags;
    protected List<ArenaLocation> spawns;

    private String defaultClass;
    private List<String> blacklistedClasses;
    private List<String> whitelistedClasses;

    private Map<Team, List<String>> mandatedClasses;
    private Map<Integer, List<KillStreak>> killStreaks;

    protected WinCondition winCondition;
    protected Team winningTeam;

    protected int broadcastTimer = 45;
    protected int maxPlayers = 24;
    protected int minPlayers = 1;
    protected int maxDeaths = 1;

    protected int startingAmount;
    protected int maxGameTime;
    protected int startTimer;
    protected int gameTimer;
    protected int redTeamSize;
    protected int blueTeamSize;
    protected int announced;

    protected boolean allowTeamKilling;
    protected boolean rewardBasedOnXp;
    protected boolean pauseStartTimer;
    protected boolean joinInProgress;
    protected boolean countMobKills;
    protected boolean forceBalance;
    protected boolean giveRewards;
    protected boolean limitSpam;

    protected boolean disabled;
    protected boolean stopped;
    protected boolean started;
    protected boolean inLobby;
    protected boolean inGame;

    protected String name;

    protected Mode gameMode = Mode.IDLE;

    protected final World world;
    protected final ArenaZone az;
    protected final ArenaType type;
    protected final UltimateArena plugin;

    /**
     * Base {@link Arena} constructor. All constructors must reference this.
     *
     * @param az {@link ArenaZone} to base the {@link Arena} around.
     */
    public Arena(ArenaZone az) {
        this.az = az;
        this.plugin = az.getPlugin();
        this.name = az.getName();
        this.type = az.getType();
        this.world = az.getWorld();
        this.az.setTimesPlayed(az.getTimesPlayed() + 1);

        this.active = new ArrayList<>();
        this.inactive = new ArrayList<>();
        this.spectators = new ArrayList<>();

        this.flags = new ArrayList<>();
        this.spawns = new ArrayList<>();

        this.gameMode = Mode.LOBBY;

        this.startTimer = az.getConfig().getLobbyTime();
        this.inLobby = true;

        this.reload();
    }

    /**
     * Reloads the Arena's settings.
     */
    @Override
    public final void reload() {
        this.allowTeamKilling = az.getConfig().isAllowTeamKilling();
        this.countMobKills = az.getConfig().isCountMobKills();
        this.rewardBasedOnXp = az.getConfig().isRewardBasedOnXp();
        this.giveRewards = az.getConfig().isGiveRewards();
        this.forceBalance = az.getConfig().isForceBalance();
        this.joinInProgress = az.getConfig().isJoinInProgress();

        this.maxGameTime = az.getConfig().getGameTime();
        this.gameTimer = az.getConfig().getGameTime();
        this.maxDeaths = az.getConfig().getMaxDeaths();
        this.maxPlayers = az.getConfig().getMaxPlayers();
        this.minPlayers = az.getConfig().getMinPlayers();

        this.defaultClass = az.getConfig().getDefaultClass();

        this.killStreaks = az.getConfig().getKillStreaks();
        this.winCondition = az.getConfig().getWinCondition();
        this.blacklistedClasses = az.getConfig().getBlacklistedClasses();
        this.whitelistedClasses = az.getConfig().getWhitelistedClasses();
        this.mandatedClasses = az.getConfig().getMandatedClasses();

        onReload();
    }

    /**
     * Called when the Arena is reloaded.
     */
    public void onReload() {
    }

    /**
     * Adds a player to the arena.
     *
     * @param player {@link Player} to add to an arena
     * @param teamId Team name/id
     */
    public final void addPlayer(Player player, String teamId) {
        player.sendMessage(plugin.getPrefix() + FormatUtil.format(getMessage("joiningArena"), name));

        ArenaPlayer ap = new ArenaPlayer(player, this, plugin);

        // API - cancellable join event
        ArenaJoinEvent event = new ArenaJoinEvent(this, ap);
        plugin.getServer().getPluginManager().callEvent(event);
        if (event.isCancelled()) {
            ap.sendMessage(event.getCancelMessage() != null ? event.getCancelMessage() : "&3Unexpected exit!");
            ap.clear();
            return;
        }

        // API - onJoin
        onJoin(ap);

        // Determine their team
        Team team = teamId == null ? getTeam() : Team.get(teamId);
        ap.setTeam(team);

        // Teleport the player to the lobby spawn
        spawnLobby(ap);

        // Save vital data
        ap.savePlayerData();

        player.setMetadata("UA", plugin.getUAIdentifier());

        ap.clearInventory();

        player.setGameMode(GameMode.SURVIVAL);

        // Heal up the Player
        player.setFoodLevel(20);
        player.setFireTicks(0);
        player.setHealth(20);

        // Don't allow flight
        player.setAllowFlight(false);
        player.setFlySpeed(0.1F);
        player.setFlying(false);

        // Disable Essentials god mode
        if (plugin.isEssentialsEnabled())
            plugin.getEssentialsHandler().disableGodMode(player);

        ap.clearPotionEffects();

        decideHat(ap);

        active.add(ap);
        updateTeams();

        // Alert the other players
        tellPlayers(getMessage("joinedArena"), ap.getName(), active.size(), maxPlayers);
        this.lastJoin = ap.getName();

        // Open Class Selector
        if (Config.classSelectorAutomatic) {
            ClassSelectionGUI csGUI = new ClassSelectionGUI(plugin, player);
            plugin.getGuiHandler().open(player, csGUI);
        }

        if (started)
            spawn(ap);
    }

    /**
     * Decide a player's hat
     * @param ap Player to give hat to
     */
    public void decideHat(ArenaPlayer ap) {
        ap.decideHat(false);
    }

    /**
     * Gets a list of available classes for a given Team.
     * 
     * @param team Player team
     * @return Available classes
     */
    public List<ArenaClass> getAvailableClasses(Team team) {
        List<String> classNames = mandatedClasses.get(team);
        if (classNames != null && classNames.size() > 0) {
            List<ArenaClass> classes = new ArrayList<>();
            for (String name : classNames) {
                ArenaClass ac = plugin.getArenaClass(name);
                if (ac != null)
                    classes.add(ac);
            }

            return classes;
        }

        return plugin.getClasses();
    }

    /**
     * Called when an {@link ArenaPlayer} joins the arena.
     *
     * @param ap {@link ArenaPlayer} who joined
     */
    public void onJoin(ArenaPlayer ap) {
    }

    /**
     * Gets which team a new player should be on.
     * <p>
     * Can be overriden in certain cases.
     *
     * @return The team, defaults to RED.
     */
    public Team getTeam() {
        return Team.RED;
    }

    /**
     * Announces the arena's existance and reminds players to join.
     */
    public final void announce() {
        if (!Config.globalMessages)
            return;

        // Allow players to click and insert into chat
        String clickToJoin = getMessage("clickToJoin");
        BaseComponent[] components = clickToJoin.isEmpty() ? null
                : ComponentSerializer.parse(FormatUtil.format(clickToJoin.replace("%s", name)));

        for (Player player : Util.getOnlinePlayers()) {
            if (!plugin.isInArena(player)) {
                if (plugin.getPermissionHandler().hasPermission(player, Permission.JOIN)) {
                    String message = "";

                    if (announced == 0)
                        message = FormatUtil.format(getMessage("arenaCreated"), type.getStylizedName());
                    else
                        message = FormatUtil.format(getMessage("hurryAndJoin"), type.getStylizedName());

                    if (!message.isEmpty())
                        player.sendMessage(plugin.getPrefix() + message);

                    if (components != null)
                        ChatUtil.sendMessage(player, components);
                }
            }
        }

        announced++;
    }

    /**
     * Gets the team with the least amount of players on it.
     *
     * @return The team with the least amount of players
     */
    public final Team getBalancedTeam() {
        return redTeamSize > blueTeamSize ? Team.BLUE : Team.RED;
    }

    /**
     * A simple team check.
     *
     * @return True if there are people on both teams, false if not
     */
    public boolean simpleTeamCheck() {
        return (redTeamSize == 0 || blueTeamSize == 0) ? startingAmount < 1 : true;
    }

    /**
     * Gets a {@link Player}'s {@link ArenaPlayer} instance.
     * <p>
     * Every player who has joined this arena will have an ArenaPlayer instance.
     * It is important to note, however, that players who are out will still
     * have arena player instances until the arena concludes. Use
     * {@code player.isOut()} to make sure they're still in the arena.
     *
     * @param player Player
     * @param checkInactive Whether or not to check the inactive list as well
     * @return The player's ArenaPlayer instance, or null if not found
     */
    public final ArenaPlayer getArenaPlayer(Player player, boolean checkInactive) {
        for (ArenaPlayer ap : getActivePlayers()) {
            if (player.getUniqueId().equals(ap.getUniqueId()))
                return ap;
        }

        if (checkInactive) {
            for (ArenaPlayer ap : getInactivePlayers()) {
                if (player.getUniqueId().equals(ap.getUniqueId()))
                    return ap;
            }
        }

        return null;
    }

    /**
     * Alias for {@link #getArenaPlayer(Player, boolean)}.
     * <p>
     * Has the same effect as <code>getArenaPlayer(player, false)</code>
     *
     * @param player Player
     */
    public final ArenaPlayer getArenaPlayer(Player player) {
        return getArenaPlayer(player, false);
    }

    /**
     * Spawns all players in the arena.
     */
    public final void spawnAll() {
        plugin.debug("Spawning players for arena {0}", name);

        for (ArenaPlayer ap : active) {
            spawn(ap);
        }
    }

    /**
     * Gets the spawn for an {@link ArenaPlayer}.
     * <p>
     * Can be overriden under certain circumstances
     *
     * @param ap {@link ArenaPlayer} instance
     * @return Their spawn
     */
    public Location getSpawn(ArenaPlayer ap) {
        ArenaLocation loc = null;
        if (isInLobby()) {
            loc = az.getLobbyREDspawn();
            if (ap.getTeam() == Team.BLUE)
                loc = az.getLobbyBLUspawn();
        } else {
            loc = az.getTeam1spawn();
            if (ap.getTeam() == Team.BLUE)
                loc = az.getTeam2spawn();
        }

        return loc.getLocation();
    }

    /**
     * Spawns a player in the {@link Arena}.
     *
     * @param ap Player to spawn
     * @param alreadySpawned Whether or not they've already spawned
     */
    public final void spawn(ArenaPlayer ap, boolean alreadySpawned) {
        plugin.debug("Attempting to spawn player {0}", ap.getName());

        if (!stopped) {
            if (ap.getDeaths() < maxDeaths) {
                Location loc = getSpawn(ap);
                if (loc != null) {
                    plugin.debug("Spawning player {0}", ap.getName());

                    ArenaSpawnEvent event = new ArenaSpawnEvent(this, ap, loc);
                    loc = event.getLocation();

                    ap.teleport(loc);
                    ap.spawn();

                    if (!alreadySpawned) {
                        onSpawn(ap);
                    }
                } else {
                    // We couldn't find a spawnpoint for some reason :(
                    ap.sendMessage(getMessage("nullSpawnpoint"));
                    ap.leaveArena(LeaveReason.ERROR);
                }
            } else {
                // If they've reached the death cap, remove them
                ap.leaveArena(LeaveReason.DEATHS);
            }
        }
    }

    /**
     * Alias for {@link #spawn(ArenaPlayer, boolean)}.
     * <p>
     * Has the same effect of <code>spawn(player, false)</code>
     *
     * @param ap Player to spawn
     */
    public final void spawn(ArenaPlayer ap) {
        spawn(ap, false);
    }

    /**
     * Called when a player is spawned.
     *
     * @param ap {@link ArenaPlayer} who was spawned
     */
    public void onSpawn(ArenaPlayer ap) {
    }

    /**
     * Spawns an {@link ArenaPlayer} into the lobby.
     *
     * @param ap {@link ArenaPlayer} to spawn
     */
    public final void spawnLobby(ArenaPlayer ap) {
        Location loc = getSpawn(ap);
        if (loc != null) {
            plugin.debug("Spawning player {0} in the lobby", ap.getName());

            ap.teleport(loc);
        } else {
            ap.sendMessage(getMessage("nullSpawnpoint"));
            ap.leaveArena(LeaveReason.ERROR);
        }
    }

    /**
     * Called when a player dies.
     *
     * @param pl {@link ArenaPlayer} who died
     */
    public void onPlayerDeath(ArenaPlayer pl) {
    }

    /**
     * Default rewarding system. May be overriden in some cases.
     *
     * @param ap {@link ArenaPlayer} to reward
     */
    public void reward(ArenaPlayer ap) {
        if (ap != null && az.getConfig().isGiveRewards())
            az.giveRewards(ap);
    }

    /**
     * Rewards an entire team.
     *
     * @param team Team to reward
     */
    public final void rewardTeam(Team team) {
        if (az.getConfig().isGiveRewards()) {
            for (ArenaPlayer ap : toReward) {
                if (ap.isCanReward()) {
                    if (ap.getTeam() == team || team == null)
                        reward(ap);
                }
            }
        }

        toReward.clear();
    }

    /**
     * Sets the winning team.
     *
     * @param team Winning team
     */
    public final void setWinningTeam(Team team) {
        this.toReward = new ArrayList<>();

        for (ArenaPlayer ap : active) {
            ap.setCanReward(false);
            if (ap.getTeam() == team || team == null) {
                ap.setCanReward(true);
                toReward.add(ap);
            }
        }

        this.winningTeam = team;
    }

    /**
     * Stops the arena if empty.
     *
     * @return Whether or not the arena is empty
     */
    public final boolean checkEmpty() {
        boolean ret = isEmpty();
        if (ret)
            stop();

        return ret;
    }

    /**
     * Checks if the arena is empty.
     *
     * @return Whether or not the arena is empty
     */
    public final boolean isEmpty() {
        return isInGame() && active.size() <= 1;
    }

    /**
     * Tells all active players in the arena a message.
     *
     * @param string Base message
     * @param objects Objects to format in
     */
    public final void tellPlayers(String string, Object... objects) {
        if (string.isEmpty())
            return;

        for (ArenaPlayer ap : active) {
            ap.sendMessage(string, objects);
        }

        for (ArenaSpectator as : spectators) {
            as.sendMessage(string, objects);
        }
    }

    /**
     * Tells all players a message.
     * <p>
     * Includes inactive players
     *
     * @param string Base message
     * @param objects Objects to format in
     */
    public final void tellAllPlayers(String string, Object... objects) {
        if (string.isEmpty())
            return;

        tellPlayers(string, objects);

        for (ArenaPlayer ap : inactive) {
            if (ap != null && ap.isOnline()) {
                ap.sendMessage(string, objects);
            }
        }
    }

    /**
     * Kills all players within a certain radius of a {@link Location}
     *
     * @param loc Center {@link Location}
     * @param rad Radius to kill within
     */
    public final void killAllNear(Location loc, double rad) {
        plugin.debug("Killing all players near {0} in a radius of {1}", Util.locationToString(loc), rad);

        for (ArenaPlayer ap : active) {
            if (ap.getPlayer().getHealth() > 0.0D && ap.getPlayer().getLocation().distance(loc) < rad)
                ap.getPlayer().setHealth(0.0D);
        }
    }

    /**
     * Returns a random spawn for an {@link ArenaPlayer}.
     *
     * @param ap {@link ArenaPlayer} to get spawn for
     */
    public Location getRandomSpawn(ArenaPlayer ap) {
        plugin.debug("Getting a random spawn for {0}", ap.getName());

        if (!spawns.isEmpty()) {
            return spawns.get(Util.random(spawns.size())).getLocation();
        }

        return null;
    }

    /**
     * Handles an {@link ArenaPlayer}'s kill streak (if applicable)
     *
     * @param ap {@link ArenaPlayer} to handle kill streak for
     */
    public final void handleKillStreak(ArenaPlayer ap) {
        if (killStreaks.isEmpty())
            return;

        if (killStreaks.containsKey(ap.getKillStreak())) {
            List<KillStreak> streaks = killStreaks.get(ap.getKillStreak());
            for (KillStreak streak : streaks) {
                if (streak != null)
                    streak.perform(ap);
            }
        }
    }

    /**
     * Disables this arena.
     */
    public final void disable() {
        tellPlayers(getMessage("hasBeenDisabled"));

        this.gameTimer = -1;

        stop();

        this.disabled = true;
        this.gameMode = Mode.DISABLED;

        updateSigns();
        onDisable();
    }

    /**
     * Called when the arena is disabled.
     */
    public void onDisable() {
    }

    /**
     * Ends the arena.
     */
    public final void stop() {
        if (stopped)
            return; // No need to stop multiple times

        plugin.log("Stopping arena {0}!", name);

        this.inGame = false;
        this.stopped = true;

        this.gameMode = Mode.STOPPING;

        clearSigns();
        onStop();

        announceWinner();

        for (ArenaPlayer ap : getActivePlayers()) {
            endPlayer(ap, LeaveReason.END_GAME);
        }

        for (ArenaSpectator as : spectators) {
            as.endPlayer();
        }

        clearEntities();
        clearMaterials();

        conclude(120L);
    }

    /**
     * Called when the arena is stopped.
     */
    public void onStop() {
    }

    private final void conclude(long delay) {
        if (delay <= 0 || plugin.isStopping()) {
            conclude();
            return;
        }

        new BukkitRunnable() {
            @Override
            public void run() {
                conclude();
            }
        }.runTaskLater(plugin, 120L);
    }

    private final void conclude() {
        // API - conclude event
        ArenaConcludeEvent event = new ArenaConcludeEvent(this);
        plugin.getServer().getPluginManager().callEvent(event);

        if (!disabled)
            this.gameMode = Mode.IDLE;

        plugin.removeActiveArena(this);
        plugin.broadcast(getMessage("concluded"), WordUtils.capitalize(name));

        plugin.getSignHandler().onArenaCompletion(this);
        plugin.getSignHandler().updateSigns(az);
    }

    /**
     * Ends an {@link ArenaPlayer}.
     *
     * @param ap Player to end
     * @param reason Reason they're being ended
     */
    public final void endPlayer(ArenaPlayer ap, LeaveReason reason) {
        plugin.debug("Ending player {0}, reason: {1}", ap.getName(), reason);

        // Dispose of the scoreboard
        ap.getBoard().dispose();

        // API - leave event
        ArenaLeaveEvent event = new ArenaLeaveEvent(this, ap);
        plugin.getServer().getPluginManager().callEvent(event);

        // Reset them
        ap.reset();
        ap.teleport(ap.getSpawnBack());

        ap.setOut(true);

        // Remove from lists
        active.remove(ap);
        if (reason != LeaveReason.QUIT)
            inactive.add(ap);
        updateTeams();

        // Add them to the final leaderboard if applicable
        if (finalLeaderboard != null) {
            try {
                finalLeaderboard.set(leaderboardIndex, ap.getName());
                leaderboardIndex--;
            } catch (IndexOutOfBoundsException ex) {
            }
        }

        // Let everyone know why
        switch (reason) {
        case COMMAND:
        case QUIT:
            tellPlayers(getMessage("leaveBroadcast"), ap.getName());
            break;
        case DEATHS:
            tellPlayers(getMessage("elimination"), ap.getName());
            break;
        case ERROR:
            tellPlayers(getMessage("errorBroadcast"), ap.getName());
            break;
        case KICK:
            tellPlayers(getMessage("kickBroadcast"), ap.getName());
            break;
        case END_GAME:
        case POWER:
            break;
        }

        if (reason != LeaveReason.END_GAME && active.size() > 1)
            tellPlayers(getMessage("playersRemaining"), active.size());

        // API - onPlayerEnd
        onPlayerEnd(ap);
    }

    /**
     * Called after an {@link ArenaPlayer} is ended.
     * 
     * @param ap Ended ArenaPlayer
     */
    public void onPlayerEnd(ArenaPlayer ap) {
    }

    /**
     * Basic timer checker.
     * <p>
     * Can not be overriden.
     */
    public final void checkTimers() {
        if (stopped)
            return;

        if (!pauseStartTimer) {
            startTimer--;
            broadcastTimer--;
        }

        if (startTimer <= 0) {
            start();
            gameTimer--;
        } else {
            if (broadcastTimer < 0) {
                broadcastTimer = 45;
                announce();
            }
        }

        // End the game
        if (gameTimer <= 0) {
            tellPlayers(getMessage("outOfTime"));

            onPreOutOfTime();
            stop();
            onOutOfTime();
        }
    }

    /**
     * Called right before the arena runs out of time.
     */
    public void onPreOutOfTime() {
    }

    /**
     * Called after the arena runs out of time.
     */
    public void onOutOfTime() {
    }

    /**
     * Starts the arena.
     * <p>
     * Can not be overriden.
     */
    public final void start() {
        if (!started) {
            // Make sure there are enough players
            if (active.size() < minPlayers) {
                tellPlayers(getMessage("notEnoughPeople"), minPlayers);
                stop();
                return;
            }

            // API - start event
            ArenaStartEvent event = new ArenaStartEvent(this);
            plugin.getServer().getPluginManager().callEvent(event);

            // API - onStart
            onStart();

            // Balance teams if applicable
            if (forceBalance && lastJoin != null) {
                if (redTeamSize != blueTeamSize) {
                    Player extra = Util.matchPlayer(lastJoin);
                    if (extra != null) {
                        ArenaPlayer ap = getArenaPlayer(extra);
                        if (ap != null) {
                            ap.leaveArena(LeaveReason.KICK);
                            ap.sendMessage(getMessage("fairnessKick"));
                        }
                    }
                }
            }

            plugin.log("Starting arena {0} with {1} players", name, active.size());

            this.started = true;
            this.inGame = true;
            this.inLobby = false;

            this.gameMode = Mode.INGAME;

            this.startingAmount = active.size();

            // Setup the leaderboard for leaderboard signs
            this.finalLeaderboard = new ArrayList<>(startingAmount);
            Collections.fill(finalLeaderboard, "");

            this.leaderboardIndex = startingAmount - 1;

            this.gameTimer = maxGameTime;
            this.startTimer = -1;

            spawnAll();
        }
    }

    /**
     * Called when the arena starts.
     */
    public void onStart() {
    }

    /**
     * Arena Updater.
     */
    public final void update() {
        if (stopped) // Don't tick if stopped
            return;

        checkTimers();
        check();

        for (ArenaPlayer ap : getActivePlayers()) {
            // Make sure they're still online
            if (!ap.isOnline()) {
                ap.leaveArena(LeaveReason.QUIT);
                continue;
            }

            // Kick them if they've reached the death limit and they're alive
            if (ap.getDeaths() >= maxDeaths && ap.getPlayer().getHealth() > 0.0D) {
                ap.leaveArena(LeaveReason.DEATHS);
                continue;
            }

            // Lobby hats
            if (isInLobby())
                decideHat(ap);

            // Class stuff
            ArenaClass ac = ap.getArenaClass();
            if (ac != null) {
                if (ac.getName().equalsIgnoreCase("healer")) {
                    ap.putData("healTimer", ap.getDataInt("healTimer") - 1);
                    if (ap.getDataInt("healTimer") <= 0) {
                        if (ap.getPlayer().getHealth() > 0 && ap.getPlayer().getHealth() + 1 <= 20) {
                            ap.getPlayer().setHealth(ap.getPlayer().getHealth() + 1);
                            ap.getData().put("healTimer", 2);
                        }
                    }
                }

                // Potion effects
                if (!ap.isChangeClassOnRespawn() && ac.isHasPotionEffects()) {
                    if (ac.getPotionEffects().size() > 0) {
                        for (PotionEffect effect : ac.getPotionEffects()) {
                            if (!ap.getPlayer().hasPotionEffect(effect.getType()))
                                ap.getPlayer().addPotionEffect(effect);
                        }
                    }
                }
            }

            // Timer Stuff
            if (!pauseStartTimer) {
                if (startTimer == 120 || startTimer == 60 || startTimer == 45 || startTimer == 30
                        || startTimer == 15) {
                    ap.sendMessage(getMessage("secondsUntilStart"), startTimer);
                }
                if (startTimer > 0 && startTimer < 11) {
                    ap.sendMessage(getMessage("secondsUntilStart"), startTimer);
                }
            }

            if (gameTimer > 0 && gameTimer < 21) {
                ap.sendMessage(getMessage("secondsUntilEnd"), gameTimer);
            }
            if (gameTimer == 60 && maxGameTime > 60) {
                ap.sendMessage(getMessage("minutesUntilEnd"), gameTimer / 60);
            }
            if (gameTimer == maxGameTime / 2) {
                ap.sendMessage(getMessage("secondsUntilEnd"), maxGameTime / 2);
            }

            // XP Bar
            decideXPBar(ap);

            // Scoreboard
            ap.updateScoreboard();
        }

        // Stop if its empty
        if (active.size() == 0)
            stop();

        // Update signs
        updateSigns();
    }

    /**
     * Called when the arena is updated. Generally every second or so.
     */
    public void check() {
    }

    /**
     * Called when a player in this Arena moves.
     * @param ap Moving player
     */
    public void onMove(ArenaPlayer ap) {
    }

    /**
     * Decides the timer xp bar for an {@link ArenaPlayer}.
     *
     * @param ap {@link ArenaPlayer} to decide xp bar for
     */
    public final void decideXPBar(ArenaPlayer ap) {
        if (Config.timerXPBar) {
            if (isInGame()) {
                ap.getPlayer().setLevel(gameTimer);
            }

            if (isInLobby()) {
                ap.getPlayer().setLevel(startTimer);
            }
        }
    }

    /**
     * Forces the start of the arena.
     *
     * @param player {@link Player} forcing the start of the arena
     */
    public final void forceStart(Player player) {
        if (isInGame()) {
            player.sendMessage(plugin.getPrefix() + FormatUtil.format(getMessage("arenaInProgress")));
            return;
        }

        plugin.log("Forcefully starting arena {0}", name);

        start();

        gameTimer--;

        player.sendMessage(plugin.getPrefix() + FormatUtil.format(getMessage("forceStart"), name));
    }

    private static final List<EntityType> persistentEntities = Arrays.asList(EntityType.PLAYER, EntityType.PAINTING,
            EntityType.ITEM_FRAME, EntityType.VILLAGER);

    private final void clearEntities() {
        try {
            int count = 0;
            for (Entity entity : world.getEntities()) {
                if (entity != null && entity.isValid()) {
                    if (!persistentEntities.contains(entity.getType())) {
                        if (isInside(entity.getLocation())) {
                            if (entity instanceof LivingEntity)
                                ((LivingEntity) entity).setHealth(0.0D);

                            entity.remove();
                            count++;
                        }
                    }
                }
            }

            plugin.debug("Removed {0} entities from {1}", count, name);
        } catch (Throwable ex) {
            plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "clearing entities in " + this));
        }
    }

    private final void clearMaterials() {
        List<Material> clear = getConfig().getClearMaterials();
        if (!clear.isEmpty())
            az.getArena().removeMaterials(clear);
    }

    /**
     * Gets a list of active players, sorted by KDR.
     *
     * @return The list
     */
    public List<ArenaPlayer> getLeaderboard() {
        Map<ArenaPlayer, Double> kdrMap = new HashMap<>();
        for (ArenaPlayer ap : getActivePlayers()) {
            kdrMap.put(ap, ap.getKDR());
        }

        List<Entry<ArenaPlayer, Double>> sortedEntries = new ArrayList<>(kdrMap.entrySet());
        Collections.sort(sortedEntries, new Comparator<Entry<ArenaPlayer, Double>>() {
            @Override
            public int compare(Entry<ArenaPlayer, Double> entry1, Entry<ArenaPlayer, Double> entry2) {
                return -entry1.getValue().compareTo(entry2.getValue());
            }
        });

        List<ArenaPlayer> leaderboard = new ArrayList<>();
        for (Entry<ArenaPlayer, Double> entry : sortedEntries) {
            leaderboard.add(entry.getKey());
        }

        return leaderboard;
    }

    /**
     * Gets a customized in-game leaderboard for a given player
     *
     * @param player Player to get the leaderboard for
     * @return Leaderboard
     * @see #getLeaderboard()
     */
    public List<String> getLeaderboard(Player player) {
        List<String> leaderboard = new ArrayList<>();

        int pos = 1;
        for (ArenaPlayer ap : getLeaderboard()) {
            if (ap != null) {
                leaderboard.add(FormatUtil.format(getMessage("leaderboard"), pos, decideColor(ap),
                        ap.getName().equals(player.getName()) ? "&l" : "", ap.getName(), ap.getKills(),
                        ap.getDeaths(), ap.getKDR()));
                pos++;
            }
        }

        return leaderboard;
    }

    /**
     * Decides a player's team color.
     *
     * @param ap Player to decide team color for
     */
    protected String decideColor(ArenaPlayer ap) {
        return ap.getTeam().getColor().toString();
    }

    /**
     * Updates the signs for the arena.
     */
    protected final void updateSigns() {
        plugin.getSignHandler().updateSigns(az);
    }

    /**
     * Clears the signs for the arena.
     */
    protected final void clearSigns() {
        plugin.getSignHandler().clearSigns(az);
    }

    /**
     * Announces the winner of the arena.
     */
    protected void announceWinner() {
        if (winningTeam == Team.BLUE) {
            tellAllPlayers(getMessage("blueWon"));
        } else if (winningTeam == Team.RED) {
            tellAllPlayers(getMessage("redWon"));
        } else if (winningTeam == null) {
            tellAllPlayers(getMessage("tie"));
        }
    }

    /**
     * Whether or not a class can be used in this arena. This method compares
     * the class against whitelisted and blacklisted classes.
     *
     * @param ac Class to check
     * @return True if the class can be used in this arena, false if not.
     */
    public final boolean isValidClass(ArenaClass ac) {
        if (!whitelistedClasses.isEmpty()) {
            return whitelistedClasses.contains(ac.getName());
        }

        if (!blacklistedClasses.isEmpty()) {
            return !blacklistedClasses.contains(ac.getName());
        }

        return true;
    }

    /**
     * Workaround for concurrency issues.
     *
     * @return An array of active players
     */
    public final ArenaPlayer[] getActivePlayers() {
        return active.toArray(new ArenaPlayer[active.size()]);
    }

    /**
     * Workaround for concurrency issues.
     * 
     * @return An array of inactive players
     */
    public final ArenaPlayer[] getInactivePlayers() {
        return inactive.toArray(new ArenaPlayer[inactive.size()]);
    }

    /**
     * Updates the size of the red and blue teams.
     * @see #getRedTeamSize()
     * @see #getBlueTeamSize()
     */
    protected final void updateTeams() {
        this.redTeamSize = 0;
        this.blueTeamSize = 0;

        for (ArenaPlayer ap : getActivePlayers()) {
            if (ap.getTeam() == Team.RED)
                redTeamSize++;
            else if (ap.getTeam() == Team.BLUE)
                blueTeamSize++;
        }
    }

    /**
     * Checks whether or not a {@link Location} is inside this arena.
     *
     * @param location Location to check
     * @return True if inside, false if not
     */
    public final boolean isInside(Location location) {
        return az.isInside(location);
    }

    /**
     * Gets this Arena's configuration.
     *
     * @return This arena's configuration
     */
    public ArenaConfig getConfig() {
        return az.getConfig();
    }

    /**
     * Gets extra info for /ua info
     *
     * @return Extra info, or null if none
     */
    public List<String> getExtraInfo() {
        return null;
    }

    public final ArenaFlag[] getFlags() {
        return flags.toArray(new ArenaFlag[flags.size()]);
    }

    // ---- Public API

    /**
     * Gets the number of Players currently in this Arena.
     * 
     * @return Number of Players.
     */
    public final int getPlayerCount() {
        return active.size();
    }

    /**
     * Gets the maximum number of Players permitted by this Arena.
     * 
     * @return Max number of Players.
     */
    public final int getMaxPlayers() {
        return maxPlayers;
    }

    /**
     * Gets this Arena's {@link ArenaType}.
     * 
     * @return This Arena's type.
     */
    public final ArenaType getType() {
        return type;
    }

    /**
     * Gets this Arena's type name. Equivalent to
     * <code>getType().getStylizedName()</code>
     * 
     * @return This Arena's type name.
     */
    public final String getTypeName() {
        return type.getStylizedName();
    }

    /**
     * Whether or not this arena is active.
     *
     * @return True if active, false if not
     */
    public final boolean isActive() {
        return !stopped;
    }

    /**
     * Gets this Arena's status. Timer is included if this Arena is ingame or in
     * the lobby.
     * 
     * @return This Arena's status.
     */
    public final String getStatus() {
        switch (gameMode) {
        case DISABLED:
            return ChatColor.RED + "Disabled";
        case IDLE:
            return ChatColor.YELLOW + "Idle";
        case INGAME:
            return ChatColor.GREEN + "In Game - " + gameTimer;
        case LOBBY:
            return ChatColor.GREEN + "Lobby - " + startTimer;
        case STOPPING:
            return ChatColor.YELLOW + "Stopping";
        default:
            // This really shouldn't happen, but just in case
            return ChatColor.YELLOW + FormatUtil.getFriendlyName(gameMode);
        }
    }

    /**
     * Adds scoreboard entries specific to this arena.
     * @param board Scoreboard to add entries to
     * @param player Player to add entries for
     */
    public void addScoreboardEntries(CustomScoreboard board, ArenaPlayer player) {
    }

    public final String getMessage(String key) {
        return plugin.getMessage(key);
    }

    // ---- Generic Methods

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Arena) {
            Arena that = (Arena) obj;
            return this.name.equals(that.name);
        }

        return false;
    }

    @Override
    public int hashCode() {
        int hash = 35;
        hash *= 1 + name.hashCode();
        return hash;
    }

    @Override
    public String toString() {
        return name;
    }
}