public class Game extends PermanentAspect {
    private MysteryBox active;
    private MysteryBoxFakeBeaconTask beacons;
    private DataContainer data = Ablockalypse.getData();
    private int level = 0, mobcount, startpoints, spawnedInThisRound = 0;
    private Teleporter mainframe;
    private GameScoreboard scoreBoard;
    private String name;
    private NextLevelTask nlt;
    private CopyOnWriteArrayList<GameAspect> objects = new CopyOnWriteArrayList<GameAspect>();
    private HashMap<Integer, Integer> wolfLevels = new HashMap<Integer, Integer>();
    private boolean wolfRound, armorRound, paused, started;// TODO armorRound not used (should it be?)
    private Random rand = new Random();
    private UUID uuid = UUID.randomUUID();

    public Game(Map<String, Object> savings) {
        this((String) savings.get("game_name"));
        // SerialLocation.returnLocation((SerialLocation) savings.get("active_chest_location"));
        active = null;// automatically set by loading the available mystery chests
        level = (Integer) savings.get("level");
        mobcount = (Integer) savings.get("mob_count");
        startpoints = (Integer) savings.get("starting_points");
        @SuppressWarnings("unchecked") List<Map<String, Object>> savedVersions = (List<Map<String, Object>>) savings.get("game_objects");
        for (Map<String, Object> save : savedVersions) {
            if (save == null) {
                continue;
            }
            try {
                PermanentAspect.load(Class.forName((String) save.get("saved_class_type")), save);
            } catch (ClassNotFoundException ex) {
                Ablockalypse.getTracker().error("The class designated to a saved object could not be found!", 20, ex);
            }
        }
        if (savings.get("mainframe_location") != null) {
            mainframe = data.getTeleporter(SerialLocation.returnLocation((SerialLocation) savings.get("mainframe_location")));
            if (mainframe != null) {
                mainframe.refresh();
            }
        }
        @SuppressWarnings("unchecked") HashMap<Integer, Integer> hashMap = (HashMap<Integer, Integer>) savings.get("wolf_levels");
        wolfLevels = hashMap;
        wolfRound = (Boolean) savings.get("wolf_round");
        armorRound = (Boolean) savings.get("armor_round");
        paused = (Boolean) savings.get("paused");
        started = (Boolean) savings.get("started");
        spawnedInThisRound = (Integer) savings.get("mobs_spawned_this_round");
        uuid = savings.get("uuid") == null ? uuid : (UUID) savings.get("uuid");
    }

    /**
     * Creates a new instance of a game.
     * 
     * @param name The name of the ZAGame
     */
    public Game(String name) {
        this.name = name;
        paused = false;
        started = false;
        @SuppressWarnings("unchecked") ArrayList<String> wolfLevelsList = (ArrayList<String>) Setting.WOLF_LEVELS.getSetting();
        for (String line : wolfLevelsList.toArray(new String[wolfLevelsList.size()])) {
            for (Integer level : MathUtility.parseIntervalNotation(line)) {
                wolfLevels.put(level, MathUtility.parsePercentage(line));
            }
        }
        startpoints = (Integer) Setting.STARTING_POINTS.getSetting();
        beacons = new MysteryBoxFakeBeaconTask(this, 200, (Boolean) Setting.BEACONS.getSetting());
        scoreBoard = new GameScoreboard(this);
        data.objects.add(this);
    }

    /**
     * Attaches an area to this game.
     * 
     * @param ga The area to load into this game
     */
    public void addObject(GameAspect obj) {
        if (obj != null && !objects.contains(obj) && !overlapsAnotherObject(obj)) {
            objects.add(obj);
        }
    }

    /**
     * Adds a player to the game.
     * NOTE: This does not change a players' status at all, that must be done through the ZAPlayer instance.
     * 
     * @param player The player to be added to the game
     */
    public void addPlayer(Player player) {
        ZAPlayer zap = data.getZAPlayer(player, name, true);
        if (!player.isOnline() && !PlayerJoin.isQueued(zap)) {
            return;
        }
        addObject(zap);
        if (!data.isZAPlayer(player)) {
            zap.setPoints(startpoints);
        }
        if (!playerIsInGame(zap)) {
            broadcast(ChatColor.RED + player.getName() + ChatColor.GRAY + " has joined the game!", player);
        }
        if (paused) {
            pause(false);
        }
        if (!started) {
            start();
        }
    }

    /**
     * Sends a message to all players in the game.
     * 
     * @param message The message to send
     * @param exception A player to be excluded from the broadcast
     */
    public void broadcast(String message, Player... exceptions) {
        for (ZAPlayer zap : getPlayers()) {
            boolean contained = false;
            if (exceptions != null) {
                for (Player except : exceptions) {
                    if (zap.getPlayer() == except) {
                        contained = true;
                    }
                }
            }
            if (!contained) {
                zap.getPlayer().sendMessage(message);
            }
        }
    }

    /**
     * Sends all players in the game the points of all players.
     */
    public void broadcastPoints() {
        for (ZAPlayer zap : getPlayers()) {
            zap.showPoints();
        }
    }

    public void broadcastSound(ZASound sound, Player... exceptions) {
        for (ZAPlayer zap : getPlayers()) {
            boolean contained = false;
            if (exceptions != null) {
                for (Player except : exceptions) {
                    if (zap.getPlayer() == except) {
                        contained = true;
                    }
                }
            }
            if (!contained) {
                sound.play(zap.getPlayer().getLocation());
            }
        }
    }

    /**
     * Ends the game, repairs all barriers, closes all areas, and removes all players.
     * @param countQueue If the player queue for offline players (see PlayerJoin.java) should be
     * checked before ending the game. If the queue is not empty, the game will then not start.
     */
    public void end(boolean countQueue) {
        if (started) {
            if (countQueue && PlayerJoin.getQueues(this).size() != 0) {
                return;
            }
            int points = 0;
            for (ZAPlayer zap : getObjectsOfType(ZAPlayer.class)) {
                points += zap.getPoints();
            }
            GameEndEvent GEE = new GameEndEvent(this, points);
            Bukkit.getPluginManager().callEvent(GEE);
            if (!GEE.isCancelled()) {
                for (GameAspect obj : objects) {
                    obj.onGameEnd();
                }
                spawnedInThisRound = 0;
                paused = true;
                started = false;
                level = 1;
                if (nlt != null && nlt.isRunning()) {
                    nlt.cancel();
                }
                mobcount = 0;
            }
        }
    }

    /**
     * Gets the currently active chest for this game.
     * 
     * @return The currently active chest for this game
     */
    public MysteryBox getActiveMysteryChest() {
        return active;
    }

    public Player getClosestLivingPlayer(Location location) {
        if (getRemainingPlayers().size() >= 1) {
            Player closest = getRandomLivingPlayer();
            Double distanceSquared = Double.MAX_VALUE;
            for (ZAPlayer zap : getPlayers()) {
                double currentDSq = zap.getPlayer().getLocation().distanceSquared(location);
                if (!zap.getPlayer().isDead() && !zap.isInLastStand() && !zap.isInLimbo() && currentDSq < distanceSquared) {
                    closest = zap.getPlayer();
                    distanceSquared = currentDSq;
                }
            }
            return closest;
        }
        return null;
    }

    public MysteryBoxFakeBeaconTask getFakeBeaconThread() {
        return beacons;
    }

    public GameScoreboard getGameScoreboard() {
        return scoreBoard;
    }

    @Override public String getHeader() {
        return this.getClass().getSimpleName() + " <UUID: " + getUUID().toString() + ">";
    }

    /**
     * Gets the current level that the game is on.
     * 
     * @return The current level of the game
     */
    public int getLevel() {
        return level;
    }

    /**
     * Gets the spawn location for this game.
     * 
     * @return The location of the spawn
     */
    public Teleporter getMainframe() {
        return mainframe;
    }

    /**
     * Gets the remaining custom mobs in the game.
     * 
     * @return The amount of remaining mobs in this game
     */
    public int getMobCount() {
        return mobcount;
    }

    public int getMobCountSpawnedInThisRound() {
        return spawnedInThisRound;
    }

    /**
     * Gets all mobs spawned in this game.
     * 
     * @return All mobs currently alive in this game
     */
    public List<ZAMob> getMobs() {
        return getObjectsOfType(ZAMob.class);
    }

    /**
     * Gets the name of this game.
     * 
     * @return The name of the ZAGame
     */
    public String getName() {
        return name;
    }

    public CopyOnWriteArrayList<GameAspect> getObjects() {
        return objects;
    }

    @SuppressWarnings("unchecked") public <T extends Object> List<T> getObjectsOfType(Class<T> type) {
        ArrayList<T> list = new ArrayList<T>();
        for (Object obj : objects) {
            if (obj != null && type != null) {
                if (type.isAssignableFrom(obj.getClass())) {
                    list.add((T) obj);
                }
            }
        }
        return list;
    }

    /**
     * Returns a list of players currently in the game.
     * 
     * @return A list of player names that are involved in this game
     */
    public List<ZAPlayer> getPlayers() {
        return getObjectsOfType(ZAPlayer.class);
    }

    /**
     * Gets a random living player.
     * Living is considered as not in limbo, last stand, respawn thread, or death.
     * 
     * @return The random living player
     */
    public Player getRandomLivingPlayer() {
        if (getRemainingPlayers().size() >= 1) {
            ArrayList<ZAPlayer> zaps = new ArrayList<ZAPlayer>();
            for (ZAPlayer zap : getPlayers()) {
                if (zap.getPlayer() != null && !zap.getPlayer().isDead() && !zap.isInLastStand() && !zap.isInLimbo()) {
                    zaps.add(zap);
                }
            }
            return zaps.get(rand.nextInt(zaps.size())).getPlayer();
        }
        return null;
    }

    /**
     * Returns a random player from this game.
     * 
     * @return The random player from this game
     */
    public ZAPlayer getRandomPlayer() {
        return getPlayers().get(rand.nextInt(getPlayers().size()));
    }

    /**
     * Gets the players still in the game.
     * This only counts players that are living and not in last stand or limbo.
     * 
     * @return How many living players are in the game
     */
    public List<ZAPlayer> getRemainingPlayers() {
        List<ZAPlayer> remaining = new ArrayList<ZAPlayer>();
        for (ZAPlayer zap : getPlayers()) {
            if (zap.getPlayer() != null && !zap.getPlayer().isDead() && !zap.isInLimbo() && !zap.isInLastStand()) {
                remaining.add(zap);
            }
        }
        return remaining;
    }

    @Override public Map<String, Object> getSave() {
        Map<String, Object> savings = new HashMap<String, Object>();
        savings.put("uuid", getUUID());
        savings.put("active_chest_location", active == null ? null : new SerialLocation(active.getLocation()));
        savings.put("level", level);
        savings.put("mob_count", mobcount);
        savings.put("starting_points", startpoints);
        savings.put("mobs_spawned_this_round", spawnedInThisRound);
        savings.put("mainframe_location", mainframe == null ? null : new SerialLocation(mainframe.getLocation()));
        savings.put("game_name", name);
        List<Map<String, Object>> savedVersions = new ArrayList<Map<String, Object>>();
        for (PermanentAspect aspect : getObjectsOfType(PermanentAspect.class)) {
            Map<String, Object> save = aspect.getSave();
            save.put("saved_class_type", aspect.getClass().getName());
            savedVersions.add(save);
        }
        savings.put("game_objects", savedVersions);
        savings.put("wolf_levels", wolfLevels);
        savings.put("wolf_round", wolfRound);
        savings.put("armor_round", armorRound);
        savings.put("paused", paused);
        savings.put("started", started);
        savings.put("mobs_spawned_this_round", spawnedInThisRound);
        return savings;
    }

    @Override public UUID getUUID() {
        return uuid;
    }

    public int getWolfPercentage() {
        return getWolfPercentage(level);
    }

    public int getWolfPercentage(int level) {
        if (!wolfLevels.containsKey(level)) {
            return 0;
        }
        return wolfLevels.get(level);
    }

    public boolean hasMob(ZAMob mob) {
        return getMobs().contains(mob);
    }

    /**
     * Returns whether or not the game has started.
     * 
     * @return Whether or not the game has been started, and mobs are spawning
     */
    public boolean hasStarted() {
        return started;
    }

    public boolean isArmorRound() {
        return armorRound;
    }

    /**
     * Checks if the game is paused or not.
     * 
     * @return Whether or not the game is paused
     */
    public boolean isPaused() {
        return paused;
    }

    /**
     * Returns true if the current round is a wolf round.
     * 
     * @return Whether or not the current round is a wolf round
     */
    public boolean isWolfRound() {
        return wolfRound;
    }

    /**
     * Starts the next level for the game, and adds a level to all players in this game.
     * Then, spawns a wave of zombies, and starts the thread for the next level.
     */
    public void nextLevel() {
        ++level;
        int maxWave = (Integer) Setting.MAX_WAVE.getSetting();
        if (level >= maxWave && maxWave != -1) {
            end(false);
            return;
        }
        spawnedInThisRound = 0;
        mobcount = 0;
        if (!started) {
            level = 1;
            List<MysteryBox> chests = getObjectsOfType(MysteryBox.class);
            if (chests != null && chests.size() > 0) {
                MysteryBox mc = chests.get(rand.nextInt(chests.size()));
                setActiveMysteryChest(mc);
            }
            for (GameAspect obj : objects) {
                obj.onGameStart();
            }
            started = true;
        }
        if (wolfLevels != null && wolfLevels.containsKey(level)) {
            wolfRound = true;
        }
        paused = false;
        nlt = new NextLevelTask(this, true);
        spawnWave(SpawnUtility.getCurrentSpawnAmount(this) - spawnedInThisRound);
        for (GameAspect obj : objects) {
            obj.onNextLevel();
        }
    }

    public void organizeObjects() {
        CopyOnWriteArrayList<GameAspect> newObjects = new CopyOnWriteArrayList<GameAspect>();
        int[][] priorities = new int[objects.size()][2];
        for (int i = 0; i < priorities.length; i++) {
            GameAspect obj = objects.get(i);
            priorities[i][0] = i;
            priorities[i][1] = obj == null ? Integer.MAX_VALUE : obj.getLoadPriority();
        }
        for (int j = 1; j < priorities.length; j++) {
            int[] temp = priorities[j];
            int current = j - 1;
            while (current >= 0 && priorities[current][1] > temp[1]) {
                priorities[current + 1] = priorities[current];
                current--;
            }
            priorities[current + 1] = temp;
        }
        for (int k = 0; k < priorities.length; k++) {
            newObjects.add(objects.get(priorities[k][0]));
        }
        objects = newObjects;
    }

    /**
     * Sets the game to pause or not.
     * 
     * @param tf Whether or not to pause or un-pause the game
     */
    public void pause(boolean tf) {
        if (tf && !paused) {
            // TODO freeze mobs, cancel damage, etc
        } else if (!tf && paused) {
            --level;
            nextLevel();
        }
        paused = tf;
    }

    /**
     * Ends the game, removes all attached instances, and finalizes this instance.
     */
    public void remove(boolean permanently) {
        end(false);
        for (GameAspect object : getObjects()) {
            object.remove();
        }
        if (beacons != null) {
            beacons.cancel();
        }
        File savedData = Ablockalypse.getExternal().getSavedDataFile(getName(), false);
        if (permanently && savedData != null) {
            savedData.delete();
        }
        data.objects.remove(this);
    }

    /**
     * Removes an area from this game.
     * 
     * @param ga The area to be unloaded from this game
     */
    public void removeObject(GameAspect obj) {
        if (obj != null && objects.contains(obj)) {
            objects.remove(obj);
        }
    }

    /**
     * Removes a player from the game.
     * 
     * @param player The player to be removed from the game
     */
    public void removePlayer(Player player) {
        ZAPlayer zap = data.getZAPlayer(player);
        if (zap != null && objects.contains(zap)) {
            objects.remove(zap);
            zap.removeFromGame();// removes zap from data.objects
            if (getPlayers().isEmpty()) {
                pause(true);
                end(true);
            }
        }
    }

    /**
     * Sets the active chest that can be used during this game.
     * 
     * @param mc The chest to be made active
     */
    public void setActiveMysteryChest(MysteryBox mc) {
        if ((Boolean) Setting.MOVING_MYSTERY_BOXES.getSetting()) {
            active = mc;
            for (MysteryBox chest : getObjectsOfType(MysteryBox.class)) {
                chest.setActive(false);
            }
            if (!mc.isActive()) {
                mc.setActive(true);
                mc.setActiveUses(rand.nextInt(8) + 2);
            }
        }
    }

    public void setArmorRound(boolean tf) {
        armorRound = tf;
    }

    public void setFakeBeaconThread(MysteryBoxFakeBeaconTask thread) {
        beacons = thread;
    }

    public void setGameScoreboard(GameScoreboard scoreBoard) {
        this.scoreBoard = scoreBoard;
    }

    /**
     * Sets the game to the specified level.
     * 
     * @param i The level the game will be set to
     */
    public void setLevel(int i) {
        level = i;
    }

    /**
     * Sets the spawn location and central teleporter of the game.
     * 
     * @param mf The mainframe to be set for the game
     */
    public void setMainframe(Teleporter mf) {
        mainframe = mf;
        mf.refresh();
    }

    /**
     * Sets the mob count.
     * If raised higher than the count currently is, this will require more mobs spawned.
     * If lower than the count currently is, this will require more mob removals.
     * 
     * @param i The count to set the current amount of mobs to.
     */
    public void setMobCount(int i) {
        mobcount = i;
        if (mobcount < 0) {
            mobcount = 0;
        }
    }

    public void setMobCountSpawnedInThisRound(int amt) {
        spawnedInThisRound = amt;
    }

    /**
     * Makes the game a wolf round.
     * 
     * @param tf Whether or not the game should be a wolf round
     */
    public void setWolfRound(boolean tf) {
        wolfRound = tf;
    }

    /**
     * Spawns a wave of mobs around random living players in this game.
     * If barriers are present and acessible, spawns the mobs at the barriers.
     */
    public void spawnWave(int amount) {
        if (mainframe == null && getRandomLivingPlayer() != null) {
            Location playerLoc = getRandomLivingPlayer().getLocation();
            mainframe = new Teleporter(this, playerLoc);
        }
        SpawnUtility.spawnWave(this, amount);
        started = true;
    }

    public void start() {
        started = false;
        level = 0;
        nextLevel();
    }

    private boolean overlapsAnotherObject(GameAspect obj) {
        if (obj.getDefiningBlocks() == null) {
            return false;
        }
        for (GameAspect object : getObjects()) {
            if (object == null || object.getDefiningBlocks() == null) {
                continue;
            }
            for (Block block : object.getDefiningBlocks()) {
                if (block == null) {
                    continue;
                }
                Location bLoc = block.getLocation();
                for (Block otherBlock : obj.getDefiningBlocks()) {
                    if (BukkitUtility.locationMatch(otherBlock.getLocation(), bLoc)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean playerIsInGame(ZAPlayer zap) {
        for (ZAPlayer zap2 : getObjectsOfType(ZAPlayer.class)) {
            if (zap.getPlayer().getName().equalsIgnoreCase(zap2.getPlayer().getName())) {
                return true;
            }
        }
        return false;
    }
} public class PermanentAspect {
    public static final String[][] replacement = new String[][] { {"\\", ""}, {"/", ""}, {":", "="}, {"*", ""},
            {"?", ""}, {"\"", ""}, {"<", "("}, {">", ")"}, {"|", ""}};

    public static Object load(Class<?> cast, Map<String, Object> version) {
        try {
            Constructor<?> constr = cast.getConstructor(Map.class);
            if (constr == null) {
                Ablockalypse.getTracker().error("The PermanentAspect load constructor (" + cast.getSimpleName() + "(Map<String, Object> save)) was not found.", 10);
                return null;
            }
            if (!constr.isAccessible()) {
                constr.setAccessible(true);
            }
            return constr.newInstance(version);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String modifyForCompliance(String header) {
        String modHeader = header;
        for (int i = 0; i < replacement.length; i++) {
            modHeader = modHeader.replace(replacement[i][0], replacement[i][1]);
        }
        return modHeader;
    }

    public static File printData(PermanentAspect aspect) throws IOException {
        String header = modifyForCompliance(aspect.getHeader());
        File file = new File(Ablockalypse.getExternal().getPrintedSettingsFolder(), header + ".yml");
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        BufferedWriter writer = new BufferedWriter(new FileWriter(file));
        try {
            Map<String, Object> save = aspect.getSave();
            for (String key : save.keySet()) {
                Object value = save.get(key);
                writer.write(key + ": " + value + "\n");
            }
        } catch (IOException e) {
            try {
                writer.write(e.getMessage());
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        writer.close();
        return file;
    }

    public static Map<String, Object> save(PermanentAspect aspect, File file) {
        try {
            Map<String, Object> save = aspect.getSave();
            save.put("saved_class_type", aspect.getClass().getName());
            External.save(save, file);
            return save;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private final UUID uuid = UUID.randomUUID();

    public String getHeader() {
        return this.getClass().getSimpleName() + " <UUID: " + getUUID().toString() + ">";
    }

    public Map<String, Object> getSave() {
        return null;
    }

    public UUID getUUID() {
        return uuid;
    }
}
 public class PlayerState extends PermanentAspect {
    private int level, foodLevel, fireTicks, maxAir, maxNoDamageTicks, noDamageTicks, remainingAir, heldSlot;
    private double health, lastDamage, maxHealth;
    private ItemStack[] inventoryContents, armorContents;
    private float exp, saturation, fallDistance, exhaustion, flySpeed, walkSpeed;
    private Location location, bedSpawn, compassTarget;
    private boolean sleepingIgnored, allowFlight, canPickupItems, flying, banned, op, sneaking, sprinting, whitelisted;
    private Collection<PotionEffect> potionEffects;
    private String name, customName, displayName, listName;
    private ItemStack itemInHand;
    private EntityDamageEvent lastDamageCause;
    private long time;
    private WeatherType weather;
    private Scoreboard scoreboard;
    private Vector velocity;
    private GameMode gameMode;

    // TODO just serialize player and load?
    @SuppressWarnings("unchecked") public PlayerState(Map<String, Object> save) {
        level = (Integer) save.get("level");
        foodLevel = (Integer) save.get("food_level");
        fireTicks = (Integer) save.get("fire_ticks");
        maxAir = (Integer) save.get("max_air");
        maxNoDamageTicks = (Integer) save.get("max_no_damage_ticks");
        noDamageTicks = (Integer) save.get("no_damage_ticks");
        remainingAir = (Integer) save.get("remaining_air");
        heldSlot = (Integer) save.get("held_slot");
        health = (Double) save.get("health");
        lastDamage = (Double) save.get("last_damage");
        maxHealth = (Double) save.get("max_health");
        List<Map<String, Object>> serialInventoryContents = (List<Map<String, Object>>) save.get("inventory_contents");
        ItemStack[] contents = new ItemStack[serialInventoryContents.size()];
        for (int i = 0; i < serialInventoryContents.size(); i++) {
            contents[i] = ItemStack.deserialize(serialInventoryContents.get(i));
        }
        inventoryContents = contents;
        List<Map<String, Object>> serialArmorContents = (List<Map<String, Object>>) save.get("armor_contents");
        ItemStack[] contents2 = new ItemStack[serialArmorContents.size()];
        for (int i = 0; i < serialArmorContents.size(); i++) {
            contents2[i] = ItemStack.deserialize(serialArmorContents.get(i));
        }
        armorContents = contents2;
        exp = (Float) save.get("exp");
        saturation = (Float) save.get("saturation");
        fallDistance = (Float) save.get("fall_distance");
        exhaustion = (Float) save.get("exhaustion");
        flySpeed = (Float) save.get("fly_speed");
        walkSpeed = (Float) save.get("walk_speed");
        location = SerialLocation.returnLocation((SerialLocation) save.get("location"));
        if (save.get("bed_spawn") != null) {
            bedSpawn = SerialLocation.returnLocation((SerialLocation) save.get("bed_spawn"));
        }
        if (save.get("compass_target") != null) {
            compassTarget = SerialLocation.returnLocation((SerialLocation) save.get("compass_target"));
        }
        sleepingIgnored = (Boolean) save.get("sleeping_ignored");
        allowFlight = (Boolean) save.get("allow_flight");
        canPickupItems = (Boolean) save.get("can_pickup_items");
        flying = (Boolean) save.get("flying");
        banned = (Boolean) save.get("banned");
        op = (Boolean) save.get("op");
        sneaking = (Boolean) save.get("sneaking");
        sprinting = (Boolean) save.get("sprinting");
        whitelisted = (Boolean) save.get("whitelisted");
        potionEffects = new ArrayList<PotionEffect>();
        List<Map<String, Object>> serialPotionEffects = (List<Map<String, Object>>) save.get("potion_effects");
        for (Map<String, Object> serialEffect : serialPotionEffects) {
            potionEffects.add(new PotionEffect(serialEffect));
        }
        name = (String) save.get("name");
        customName = (String) save.get("custom_name");
        displayName = (String) save.get("display_name");
        listName = (String) save.get("list_name");
        itemInHand = ItemStack.deserialize((Map<String, Object>) save.get("item_in_hand"));
        time = (Long) save.get("time");
        weather = save.get("weather_name") == null ? WeatherType.CLEAR : WeatherType.valueOf((String) save.get("weather_name"));// common null pointer
        gameMode = GameMode.getByValue((Integer) save.get("game_mode"));
    }

    public PlayerState(Player player) {
        level = player.getLevel();
        health = player.getHealth();
        foodLevel = player.getFoodLevel();
        fireTicks = player.getFireTicks();
        lastDamage = player.getLastDamage();
        maxHealth = player.getMaxHealth();
        maxAir = player.getMaximumAir();
        maxNoDamageTicks = player.getMaximumNoDamageTicks();
        noDamageTicks = player.getNoDamageTicks();
        remainingAir = player.getRemainingAir();
        heldSlot = player.getInventory().getHeldItemSlot();
        inventoryContents = player.getInventory().getContents();
        armorContents = player.getInventory().getArmorContents();
        exp = player.getExp();
        saturation = player.getSaturation();
        fallDistance = player.getFallDistance();
        exhaustion = player.getExhaustion();
        flySpeed = player.getFlySpeed();
        walkSpeed = player.getWalkSpeed();
        location = player.getLocation();
        bedSpawn = player.getBedSpawnLocation();
        compassTarget = player.getCompassTarget();
        sleepingIgnored = player.isSleepingIgnored();
        allowFlight = player.getAllowFlight();
        canPickupItems = player.getCanPickupItems();
        flying = player.isFlying();
        banned = player.isBanned();
        op = player.isOp();
        sneaking = player.isSneaking();
        sprinting = player.isSprinting();
        whitelisted = player.isWhitelisted();
        potionEffects = player.getActivePotionEffects();
        name = player.getName();
        customName = player.getCustomName();
        displayName = player.getDisplayName();
        listName = player.getPlayerListName();
        itemInHand = player.getItemInHand();
        lastDamageCause = player.getLastDamageCause();
        time = player.getPlayerTime();
        weather = player.getPlayerWeather();
        scoreboard = player.getScoreboard();
        velocity = player.getVelocity();
        gameMode = player.getGameMode();
    }

    @Override public String getHeader() {
        return this.getClass().getSimpleName() + " <PLAYER: " + name + ">";
    }

    public OfflinePlayer getPlayer() {
        return BukkitUtility.forceObtainPlayer(name);
    }

    @Override public Map<String, Object> getSave() {
        Map<String, Object> save = new HashMap<String, Object>();
        save.put("level", level);
        save.put("food_level", foodLevel);
        save.put("fire_ticks", fireTicks);
        save.put("max_air", maxAir);
        save.put("max_no_damage_ticks", maxNoDamageTicks);
        save.put("no_damage_ticks", noDamageTicks);
        save.put("remaining_air", remainingAir);
        save.put("held_slot", heldSlot);
        save.put("health", health);
        save.put("last_damage", lastDamage);
        save.put("max_health", maxHealth);
        List<Map<String, Object>> serialInventory = new ArrayList<Map<String, Object>>();
        for (ItemStack stack : inventoryContents) {
            if (stack == null) {
                continue;
            }
            serialInventory.add(stack.serialize());
        }
        save.put("inventory_contents", serialInventory);
        List<Map<String, Object>> serialArmor = new ArrayList<Map<String, Object>>();
        for (ItemStack stack : armorContents) {
            if (stack == null) {
                continue;
            }
            serialArmor.add(stack.serialize());
        }
        save.put("armor_contents", serialArmor);
        save.put("exp", exp);
        save.put("saturation", saturation);
        save.put("fall_distance", fallDistance);
        save.put("exhaustion", exhaustion);
        save.put("fly_speed", flySpeed);
        save.put("walk_speed", walkSpeed);
        save.put("location", new SerialLocation(location));
        if (bedSpawn != null) {
            save.put("bed_spawn", new SerialLocation(bedSpawn));
        }
        if (compassTarget != null) {
            save.put("compass_target", new SerialLocation(compassTarget));
        }
        save.put("sleeping_ignored", sleepingIgnored);
        save.put("allow_flight", allowFlight);
        save.put("can_pickup_items", canPickupItems);
        save.put("flying", flying);
        save.put("banned", banned);
        save.put("op", op);
        save.put("sneaking", sneaking);
        save.put("sprinting", sprinting);
        save.put("whitelisted", whitelisted);
        List<Map<String, Object>> serialPotionEffects = new ArrayList<Map<String, Object>>();
        for (PotionEffect effect : potionEffects) {
            serialPotionEffects.add(effect.serialize());
        }
        save.put("potion_effects", serialPotionEffects);
        save.put("name", name);
        save.put("custom_name", customName);
        save.put("display_name", displayName);
        save.put("list_name", listName);
        save.put("item_in_hand", itemInHand.serialize());
        save.put("time", time);
        save.put("weather_name", weather != null ? weather.name() : WeatherType.CLEAR.name());
        save.put("game_mode", gameMode.getValue());
        return save;
    }

    public void update() {
        Player player = Bukkit.getPlayer(name);
        update(player);
    }

    @SuppressWarnings("deprecation") public void update(Player player) {
        player.setLevel(level);
        player.setHealth(health);
        player.setFoodLevel(foodLevel);
        player.setFireTicks(fireTicks);
        player.setLastDamage(lastDamage);
        player.setMaxHealth(maxHealth);
        player.setMaximumAir(maxAir);
        player.setMaximumNoDamageTicks(maxNoDamageTicks);
        player.setNoDamageTicks(noDamageTicks);
        player.setRemainingAir(remainingAir);
        player.getInventory().clear();
        player.getInventory().setContents(inventoryContents);
        player.getInventory().setArmorContents(armorContents);
        player.setExp(exp);
        player.setSaturation(saturation);
        player.setFallDistance(fallDistance);
        player.setExhaustion(exhaustion);
        player.setFlySpeed(flySpeed);
        player.setWalkSpeed(walkSpeed);
        player.teleport(location);
        player.setBedSpawnLocation(bedSpawn);
        player.setCompassTarget(compassTarget);
        player.setSleepingIgnored(sleepingIgnored);
        player.setAllowFlight(allowFlight);
        player.setCanPickupItems(canPickupItems);
        player.setFlying(flying);
        player.setBanned(banned);
        player.setOp(op);
        player.setSneaking(sneaking);
        player.setSprinting(sprinting);
        player.setWhitelisted(whitelisted);
        player.addPotionEffects(potionEffects);
        player.setCustomName(customName);
        player.setDisplayName(displayName);
        player.setPlayerListName(listName);
        player.getInventory().setHeldItemSlot(heldSlot);
        player.setItemInHand(itemInHand);// returned to normal by setting the inventory contents, this only messes it up
        player.setLastDamageCause(lastDamageCause);
        player.setPlayerTime(time, true);
        player.setPlayerWeather(weather);
        if (scoreboard != null) {
            player.setScoreboard(scoreboard);
        }
        if (velocity != null) {
            player.setVelocity(velocity);
        }
        player.setGameMode(gameMode);
        player.updateInventory();
    }
}
 public enum Setting {
    ATOM_BOMB_POINTS("pointsGivenOnAtomBomb", Integer.class),
    BARRIER_FULL_FIX_PAY("barrierCompleteFixIncrease", Integer.class),
    BARRIER_PART_FIX_PAY("barrierPerFixIncrease", Integer.class),
    BEACONS("chestBeacons", Boolean.class),
    BLINKERS("blinkers", Boolean.class),
    MYSTERY_BOX_COST("mysteryChestCost", Integer.class),
    CLEAR_MOBS("clearNearbyMobs", Boolean.class),
    CLEAR_MOBS_RADIUS("nearbyMobClearingRadius", Integer.class),
    DEADSHOT_DAIQUIRI_COST("deadshotDaiquiriPoints", Integer.class),
    DEADSHOT_DAIQUIRI_LEVEL("deadshotDaiquiriLevel", Integer.class),
    DEBUG("DEBUG", Boolean.class),
    DEFAULT_FRIENDLY_FIRE_MODE("defaultFriendlyFireMode", Boolean.class),
    DISPLAY_PLAYER_HEALTH("displayPlayerHealth", Boolean.class),
    END_ON_LAST_PLAYER_LAST_STAND("endOnLastPlayerLastStand", Boolean.class),
    EXPLOSIVE_KILL_PAY("explosivePointIncrease", Integer.class),
    EXTRA_EFFECTS("addedEffects", Boolean.class),
    EXTRA_SOUNDS("addedSounds", Boolean.class),
    JUGGERNOG_COST("juggernogPoints", Integer.class),
    JUGGERNOG_LEVEL("juggernogLevel", Integer.class),
    KILL_PAY("pointIncrease", Integer.class),
    LAST_STAND_HEALTH_THRESHOLD("lastStandThreshold", Double.class),
    LAST_STAND_HELPER_PAY("pointsGivenOnHelp", Integer.class),
    LEVEL_TRANSITION_TIME("levelTransitionTime", Integer.class),
    LOSE_PERKS_ON_LAST_STAND("losePerksOnLastStand", Boolean.class),
    MAX_FATALITY("MAX_FATALITY", Integer.class),
    MAX_PATHFINDER_NODES("maxPathfinderNodes", Integer.class),
    MAX_PLAYERS("maxPlayers", Integer.class),
    MAX_WAVE("maxWave", Integer.class),
    MOVING_MYSTERY_BOXES("movingChests", Boolean.class),
    PACK_A_PUNCH_COST("packapunchCost", Integer.class),
    PACK_A_PUNCH_LEVEL("packapunchLevel", Integer.class),
    PERK_DURATION("perkDuration", Integer.class),
    PHD_FLOPPER_COST("phdFlopperPoints", Integer.class),
    PHD_FLOPPER_LEVEL("phdFlopperLevel", Integer.class),
    POWERUP_CHANCE("powerupChance", Double.class),
    PREVENT_NEARBY_SPAWNING("preventNearbySpawning", Boolean.class),
    PREVENT_NEARBY_SPAWNING_RADIUS("preventNearbySpawningRadius", Integer.class),
    PRINT_STORAGE_DATA_TO_FILE("PRINT_STORAGE_DATA_TO_FILE", Boolean.class),
    STAMINUP_COST("staminupPoints", Integer.class),
    STAMINUP_LEVEL("staminupLevel", Integer.class),
    STARTING_POINTS("startPoints", Integer.class),
    TELEPORT_TIME("teleportTime", Integer.class),
    TELEPORTERS_REQUIRE_POWER("teleportersRequirePower", Boolean.class),
    WOLF_LEVELS("wolfLevels", String[].class);
    private final static Map<Integer, Setting> BY_ID = Maps.newHashMap();
    static {
        int id = 0;
        for (Setting setting : values()) {
            BY_ID.put(++id, setting);
        }
    }

    public static Setting getById(final int id) {
        return BY_ID.get(id);
    }

    public static int getHighestId() {
        return BY_ID.size();
    }

    private Object object;
    private String setting;
    private Class<?> type;

    Setting(String name, Class<?> type) {
        setting = name;
        this.type = type;
        object = Ablockalypse.getInstance().getConfig().get(name);
        if (object == null) {
            Ablockalypse.getTracker().error("The value for <" + toString() + ">, which uses the value of <" + name + "> in the configuration is null and/or missing!", 100 / values().length);
        }
    }

    public String getName() {
        return setting;
    }

    public Object getSetting() {
        return object;
    }

    public Class<?> getType() {
        return type;
    }

    public void set(Object object) {
        this.object = object;
    }
} public class BlockBreak implements Listener {
    private static DataContainer data = Ablockalypse.getData();

    public static boolean shouldBeBroken(Material type) {
        for (Material m : data.modifiableMaterials) {
            if (m == type) {
                return true;
            }
        }
        return false;
    }

    /* Called when a player breaks a block.
     * Mainly used for preventing ZA Players from breaking blocks while in-game. */
    @EventHandler(priority = EventPriority.HIGHEST) public void BBE(BlockBreakEvent event) {
        Player p = event.getPlayer();
        Block b = event.getBlock();
        Location loc = b.getLocation();
        if (data.isZAPlayer(p) && !BlockBreak.shouldBeBroken(b.getType())) {
            event.setCancelled(true);
        } else if ((b.getType() == Material.FLOWER_POT || b.getType() == Material.FLOWER_POT_ITEM) && data.isClaymore(loc)) {
            Claymore more = data.getClaymore(loc);
            if (more.getPlacer().getPlayer().getName().equalsIgnoreCase(p.getName())) {
                Ablockalypse.getExternal().getItemFileManager().giveItem(p, new ItemStack(Material.FLOWER_POT_ITEM, 1));
                p.sendMessage(ChatColor.GRAY + "You have picked up your claymore.");
            } else {
                p.sendMessage(ChatColor.RED + "That is not your claymore!");
                event.setCancelled(true);
            }
        }
    }
} public class BlockPlace implements Listener {
    private static DataContainer data = Ablockalypse.getData();

    public static boolean shouldBePlaced(Material type) {
        for (Material m : data.modifiableMaterials) {
            if (m == type) {
                return true;
            }
        }
        return false;
    }

    /* Called when a player places a block.
     * Mainly used to create claymores. */
    @EventHandler(priority = EventPriority.HIGHEST) public void BPE(BlockPlaceEvent event) {
        Player p = event.getPlayer();
        Block b = event.getBlockPlaced();
        if (data.isZAPlayer(p) && (b.getType() == Material.FLOWER_POT || b.getType() == Material.FLOWER_POT_ITEM)) {// See PlayerInteract.java to allow placement
            ZAPlayer zap = data.getZAPlayer(p);
            Location loc = b.getLocation();
            new Claymore(loc, zap.getGame(), zap);
            p.sendMessage(ChatColor.GRAY + "You have placed a claymore.");
        }
    }
}
 public class BlockRedstone implements Listener {
    private DataContainer data = Ablockalypse.getData();

    @EventHandler(priority = EventPriority.HIGHEST) public void BRE(BlockRedstoneEvent event) {
        Block redstoneBlock = event.getBlock();
        Cube cube = new Cube(redstoneBlock.getLocation(), 1);
        for (Location loc : cube.getLocations()) {
            Block block = loc.getBlock();
            if (data.isGameObject(block.getLocation())) {
                GameAspect obj = data.getGameObjectByLocation(block.getLocation());
                if (obj instanceof NonspecificGameAspect) {
                    ((NonspecificGameAspect) obj).checkForPower();
                }
            }
        }
    }
}
 public class EntityBreakDoor implements Listener {
    private DataContainer data = Ablockalypse.getData();

    @EventHandler(priority = EventPriority.HIGHEST) public void EBDE(EntityBreakDoorEvent event) {
        LivingEntity e = event.getEntity();
        if (data.isZAMob(e)) {
            event.setCancelled(true);
        }
    }
} public class EntityDamage implements Listener {
    private DataContainer data = Ablockalypse.getData();
    protected DamageCause[] cancelCauses = new DamageCause[] {DamageCause.SUFFOCATION, DamageCause.LIGHTNING};

    /* Called when an entity is damaged.
     * Used mostly for cancelling fire damage to ZA mobs. */
    @EventHandler(priority = EventPriority.HIGHEST) public void EDE(EntityDamageEvent event) {
        Entity entity = event.getEntity();
        if (!(entity instanceof LivingEntity)) {
            return;
        }
        LivingEntity e = (LivingEntity) entity;
        if (e != null && data.isZAMob(e)) {
            if ((event.getCause() == DamageCause.FIRE || event.getCause() == DamageCause.FIRE_TICK) && e instanceof org.bukkit.entity.Zombie) {
                Zombie gu = data.getZombie(e);
                if (gu.isFireproof()) {
                    e.setFireTicks(0);
                    event.setCancelled(true);
                }
            } else if (event.getCause() == DamageCause.FALL && event.getDamage() <= 2) {
                event.setCancelled(true);
            } else if (shouldBeCancelled(event.getCause())) {
                event.setCancelled(true);
            }
        } else if (e != null && e instanceof Player) {
            Player player = (Player) e;
            if (data.isZAPlayer(player)) {
                ZAPlayer zap = data.getZAPlayer(player);
                if (zap.isInLastStand()) {
                    event.setCancelled(true);
                }
            }
        }
    }

    protected boolean shouldBeCancelled(DamageCause cause) {
        for (DamageCause dmg : cancelCauses) {
            if (dmg == cause) {
                return true;
            }
        }
        return false;
    }
} public class EntityDamageByEntity implements Listener {
    public static ArrayList<UUID> instakillids = new ArrayList<UUID>();
    private DataContainer data = Ablockalypse.getData();
    public static final double MIN_FIREBALL_DMG = 5, MIN_ARROW_DMG = 8, MIN_HIT_DMG = 1.5, MIN_FRIENDLY_FIRE_DMG = 1;
    private static final Random RAND = new Random();

    /* Called when an entity damaged another entity.
     * Used mostly for picking someone out of last stand, changing damage, and cancelling damage. */
    @EventHandler(priority = EventPriority.HIGHEST) public void EDBEE(EntityDamageByEntityEvent event) {
        Entity d = event.getDamager();
        Entity entity = event.getEntity();
        double evtdmg = event.getDamage();
        if (!(entity instanceof LivingEntity) || !(d instanceof LivingEntity)) {
            return;
        }
        LivingEntity e = (LivingEntity) entity;
        LivingEntity damager = (LivingEntity) d;
        if (data.isZAMob(e)) {
            mobDamage(event, damager, e, evtdmg);
        } else if (e instanceof Player) {
            playerDamage(event, damager, e, evtdmg);
        }
    }

    /* Used to separate mob damage from player damage.
     * This is the mob version. */
    public void mobDamage(EntityDamageByEntityEvent event, LivingEntity damager, LivingEntity e, double evtdmg) {
        ZAMob zam = data.getZAMob(e);
        if (damager instanceof Fireball) {
            Fireball f = (Fireball) damager;
            if (instakillids.contains(f.getUniqueId())) {
                event.setDamage(zam.getEntity().getHealth() * 10);
            } else {
                double dmg = 40 - zam.getHitAbsorption();// fireball damage
                if (dmg <= MIN_FIREBALL_DMG) {
                    dmg = MIN_FIREBALL_DMG;
                }
                event.setDamage(dmg);
            }
        } else if (damager instanceof Arrow) {
            Arrow a = (Arrow) damager;
            if (instakillids.contains(a.getUniqueId())) {
                event.setDamage(zam.getEntity().getHealth() * 10);
            } else {
                double dmg = 50 - zam.getHitAbsorption();// arrow damage
                if (dmg <= MIN_ARROW_DMG) {
                    dmg = MIN_ARROW_DMG;
                }
                event.setDamage(dmg);
            }
        } else if (damager instanceof Player) {
            Player p = (Player) damager;
            if (data.isZAPlayer(p)) {
                ZAPlayer zap = data.getZAPlayer(p);
                List<PathNode> nodes = new Pathfinder(e.getLocation(), p.getLocation(), AblockalypseUtility.getGoals(p.getWorld(), .891D, 1.031D, 2D, 0)).calculate((Integer) Setting.MAX_PATHFINDER_NODES.getSetting()).getNodes();
                if (nodes.get(nodes.size() - 1).H > 10) {
                    if (RAND.nextInt(99) + 1 < 5) {
                        zam.getTargetter().panic(120);
                    } else if (!zap.isTargettedBy(zam)) {
                        zam.retarget();
                    }
                }
                if (zap.hasInstaKill()) {
                    event.setDamage(zam.getEntity().getHealth() * 5);
                } else {
                    double dmg = evtdmg - zam.getHitAbsorption();// regular hit damage
                    if (dmg <= MIN_HIT_DMG) {
                        dmg = MIN_HIT_DMG;
                    }
                    event.setDamage(dmg);
                }
            }
        } else if (data.isZAMob(damager) && damager instanceof Wolf) {
            event.setDamage((evtdmg - zam.getHitAbsorption()) / 2);
        }
    }

    /* Used to separate mob damage from player damage.
     * This is the player version. */
    public void playerDamage(EntityDamageByEntityEvent event, Entity damager, Entity e, double evtdmg) {
        Player p = (Player) e;
        if (data.isZAPlayer(p)) {
            ZAPlayer zap = data.getZAPlayer(p);
            if (damager instanceof Player) {
                Player p2 = (Player) damager;
                if (data.isZAPlayer(p2)) {
                    ZAPlayer hitter = data.getZAPlayer(p2);
                    if (zap.isInLastStand()) {
                        new LastStandPickupTask(hitter, zap, 20, 5, true);
                        event.setCancelled(true);
                    }
                    if (!(Boolean) Setting.DEFAULT_FRIENDLY_FIRE_MODE.getSetting()) {
                        event.setCancelled(true);
                    } else {
                        double dmg = evtdmg - zap.getHitAbsorption();
                        if (dmg < MIN_FRIENDLY_FIRE_DMG) {
                            dmg = MIN_FRIENDLY_FIRE_DMG;
                        }
                        event.setDamage(dmg);
                    }
                } else {
                    event.setCancelled(true);
                }
            } else if (p.getHealth() <= (Double) Setting.LAST_STAND_HEALTH_THRESHOLD.getSetting() && !zap.isInLastStand() && !zap.isInLimbo()) {
                p.setHealth((Double) Setting.LAST_STAND_HEALTH_THRESHOLD.getSetting());
                zap.toggleLastStand();
            } else if (zap.isInLastStand()) {
                event.setCancelled(true);
            }
            if (damager instanceof Fireball) {
                if (!(Boolean) Setting.DEFAULT_FRIENDLY_FIRE_MODE.getSetting()) {
                    event.setCancelled(true);
                } else {
                    double dmg = evtdmg - zap.getHitAbsorption();
                    if (dmg < MIN_FIREBALL_DMG) {
                        dmg = MIN_FIREBALL_DMG;
                    }
                    event.setDamage(dmg);
                }
            }
        }
    }
} public class EntityDeath implements Listener {
    private static DataContainer data = Ablockalypse.getData();
    private static Random rand = new Random();

    public static void behaveLikeKill(Player killer, Entity entityKilled) {
        ZAPlayer zap = data.getZAPlayer(killer);
        EntityDamageEvent damage = entityKilled.getLastDamageCause();
        boolean exploded = damage != null && damage.getCause() != null && damage.getCause() == DamageCause.ENTITY_EXPLOSION;
        int pay = !exploded ? (Integer) Setting.KILL_PAY.getSetting() : (Integer) Setting.EXPLOSIVE_KILL_PAY.getSetting();
        zap.addPoints(pay);
        zap.setKills(zap.getKills() + 1);
        int food = killer.getFoodLevel();
        if (food < 20) {
            killer.setFoodLevel(20);
        }
        int chance = rand.nextInt(100) + 1;
        if (chance <= (Double) Setting.POWERUP_CHANCE.getSetting()) {
            zap.givePowerup(PowerupType.getById(rand.nextInt(5) + 1), killer);
        }
    }

    /* Called when an Entity is killed.
     * Used for adding points when a player kills an entity, while they are in-game. */
    @EventHandler(priority = EventPriority.HIGHEST) public void EDE(EntityDeathEvent event) {
        Entity entity = event.getEntity();
        Player p = event.getEntity().getKiller();
        if (!(entity instanceof LivingEntity)) {
            return;
        }
        LivingEntity e = (LivingEntity) entity;
        if (data.isZAMob(e)) {
            ZAMob mob = data.getZAMob(e);
            event.getDrops().clear();
            event.setDroppedExp(0);
            mob.kill();
            if (p != null && data.isZAPlayer(p)) {
                behaveLikeKill(p, e);
            }
        }
    }
} public class EntityExplode implements Listener {
    private static HashMap<UUID, Boolean> uuids = new HashMap<UUID, Boolean>();// UUID, whether or not to cancel the event entirely

    public static void createNonBlockDesructionExplosion(Location loc, float yield) {
        loc.getWorld().createExplosion(loc.getX(), loc.getY(), loc.getZ(), yield, false, false);
    }

    public static void createNonBlockDestructionExplosionWithPoints(ZAPlayer zap, Location loc, float yield) {
        createNonBlockDesructionExplosion(loc, yield);
        for (Entity ent : BukkitUtility.getNearbyEntities(loc, yield, yield, yield)) {
            EntityDamageEvent lastDamage = ent.getLastDamageCause();
            if (ent instanceof LivingEntity && !(ent instanceof Player) && lastDamage != null && lastDamage.getCause() == DamageCause.ENTITY_EXPLOSION) {
                EntityDeath.behaveLikeKill(zap.getPlayer(), ent);
            }
        }
    }

    /**
     * Prevents an explosion from happening or from damaging blocks.
     * 
     * @param uuid The UUID of the entity exploding
     * @param complete True if the event should be completely cancelled, otherwise the blocklist is cleared
     */
    public static void preventExplosion(UUID uuid, boolean complete) {
        uuids.put(uuid, complete);
    }

    /* Called when an entity explodes.
     * Used mainly to prevent grenades from doing damage to land. */
    @EventHandler(priority = EventPriority.HIGHEST) public void EEE(EntityExplodeEvent event) {
        Entity e = event.getEntity();
        if (uuids.containsKey(e.getUniqueId()) && uuids.get(e.getUniqueId())) {
            event.setCancelled(true);
            uuids.remove(e.getUniqueId());
        } else if (uuids.containsKey(e.getUniqueId())) {
            event.blockList().clear();
            uuids.remove(e.getUniqueId());
        }
    }
} public class EntityTarget implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when an entity targets another entity.
     * Mostly used for making sure non-supported entites do not attack ZA players. */
    @EventHandler(priority = EventPriority.HIGHEST) public void ETE(EntityTargetEvent event) {
        Entity target = event.getTarget();
        Entity entity = event.getEntity();
        if (entity instanceof LivingEntity && target instanceof Player) {
            Player p = (Player) target;
            if (data.isZAPlayer(p)) {
                ZAPlayer zap = data.getZAPlayer(p);
                if (zap.isInLastStand() || !data.isZAMob((LivingEntity) entity)) {
                    event.setCancelled(true);
                }
            }
        }
    }
} public class PlayerDeath implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player is killed.
     * 
     * Used for respawning the player after the current level. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PDE(PlayerDeathEvent event) {// RespawnThread is activated by PlayerRespawn.java
        Player p = event.getEntity();
        if (data.isZAPlayer(p)) {
            event.getDrops().clear();
            event.setKeepLevel(true);
            ZAPlayer zap = data.getZAPlayer(p);
            zap.setStatus(PlayerStatus.LIMBO);
            Game zag = zap.getGame();
            zap.clearPerks();// remove perks
            if (zag.getRemainingPlayers().size() > 0) {
                if (zap.isInLastStand()) {
                    zap.toggleLastStand();
                }
            } else {
                zag.end(false);
            }
        }
    }
} public class PlayerDropItem implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when an item is dropped by a player.
     * Used mainly for disabling drops for players in games. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PDIE(PlayerDropItemEvent event) {
        if (data.isZAPlayer(event.getPlayer())) {
            event.setCancelled(true);
        }
    }
} public class PlayerInteract implements Listener {
    public static ArrayList<Game> fireSale = new ArrayList<Game>();// TODO queue all of these remaining lists/maps
    public static HashMap<ZAPlayer, Teleporter> mainframeLinkers = new HashMap<ZAPlayer, Teleporter>();
    public static HashMap<ZAPlayer, TeleporterLinkageTimerTask> mainframeLinkers_Timers = new HashMap<ZAPlayer, TeleporterLinkageTimerTask>();
    public static HashMap<String, Game> passagePlayers = new HashMap<String, Game>();
    public static HashMap<String, String> mapDataSavePlayers = new HashMap<String, String>();
    private static HashMap<String, Location> mapDataPoint1SaveClickers = new HashMap<String, Location>();
    public static HashMap<String, String> mapDataLoadPlayers = new HashMap<String, String>();
    private DataContainer data = Ablockalypse.getData();
    public static BlockingQueue<QueuedPlayerInteractData> queue = new LinkedBlockingQueue<QueuedPlayerInteractData>();

    public boolean isPassage(Block block) {
        for (Passage passage : data.getObjectsOfType(Passage.class)) {
            if (passage.getDefiningBlocks().contains(block)) {
                return true;
            }
        }
        return false;
    }

    /* The event called when a player clicks a block.
     * 
     * USED:
     * *When a ZAPlayer clicks a sign, to check the lines for strings that trigger a response. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PIE(PlayerInteractEvent event) {
        for (QueuedPlayerInteractData data : queue) {
            if ((data.getKey().equals(event.getPlayer().getName()) || data.getKey().equals(QueuedPlayerInteractData.ANY_PLAYER)) && data.isCompatible(event)) {
                data.importPIE(event);
                data.run();
                if (data.removeAfterRun()) {
                    queue.remove(data);
                }
                return;
            }
        }
        Block block = event.getClickedBlock();
        Player player = event.getPlayer();
        Action action = event.getAction();
        if (block != null) {
            Location loc = block.getLocation();
            if (!data.isZAPlayer(player) && mapDataSavePlayers.containsKey(player.getName()) && action == Action.RIGHT_CLICK_BLOCK) {
                if (!mapDataPoint1SaveClickers.containsKey(player.getName())) {
                    mapDataPoint1SaveClickers.put(player.getName(), block.getLocation());
                    player.sendMessage(ChatColor.GRAY + "Please click the other corner of the map.");
                    return;
                } else {
                    boolean saved = new MapData(mapDataSavePlayers.get(player.getName())).save(new Cuboid(mapDataPoint1SaveClickers.get(player.getName()), block.getLocation()));
                    mapDataPoint1SaveClickers.remove(player.getName());
                    mapDataSavePlayers.remove(player.getName());
                    String successful = saved ? ChatColor.GREEN + "successfully" + ChatColor.RESET : ChatColor.RED + "unsuccessfully" + ChatColor.RESET;
                    player.sendMessage(ChatColor.GRAY + "Mapdata saved " + successful + ChatColor.GRAY + ".");
                }
            } else if (!data.isZAPlayer(player) && mapDataLoadPlayers.containsKey(player.getName()) && action == Action.RIGHT_CLICK_BLOCK) {
                boolean loaded = MapData.getFromGame(mapDataLoadPlayers.get(player.getName())).load(block.getLocation());
                mapDataLoadPlayers.remove(player.getName());
                String successful = loaded ? ChatColor.GREEN + "successfully" + ChatColor.RESET : ChatColor.RED + "unsuccessfully" + ChatColor.RESET;
                player.sendMessage(ChatColor.GRAY + "Mapdata loaded " + successful + ".");
            } else if (!data.isZAPlayer(player) && data.isGameObject(loc)) {
                event.setCancelled(true);
            } else if ((block.getType() == Material.SIGN || block.getType() == Material.WALL_SIGN) && action == Action.RIGHT_CLICK_BLOCK) {
                Sign s = (Sign) block.getState();
                if (s.getLine(0).equalsIgnoreCase(Local.BASE_STRING.getSetting())) {
                    event.setUseInteractedBlock(Result.DENY);
                    runLines(s, player);
                    return;
                }
                return;
            } else if (data.isZAPlayer(player)) {
                if (!BlockPlace.shouldBePlaced(player.getItemInHand().getType())) {
                    event.setUseItemInHand(Result.DENY);
                }
                ZAPlayer zap = data.getZAPlayer(player);
                Game game = zap.getGame();
                if (block.getType() == Material.CHEST && action == Action.RIGHT_CLICK_BLOCK) {
                    event.setUseInteractedBlock(Result.DENY);
                    Location l = block.getLocation();
                    if (data.isMysteryChest(l)) {
                        MysteryBox mc = data.getMysteryChest(l);
                        if (mc != null && mc.isActive()) {
                            mc.openToRandomItem(zap);
                        } else {
                            player.sendMessage(ChatColor.RED + "That chest is not active!");
                            event.setCancelled(true);
                            return;
                        }
                    }
                } else if (block.getType() == Material.WOOD_DOOR || block.getType() == Material.IRON_DOOR) {
                    event.setCancelled(true);
                } else if (block.getType() == Material.LEVER && action == Action.RIGHT_CLICK_BLOCK) {
                    Lever lever = (Lever) block.getState().getData();
                    if (lever.isPowered()) {
                        player.sendMessage(lever.isPowered() ? ChatColor.GRAY + "The switch is on." : ChatColor.RED + "That switch is in use by another game!");
                        event.setUseInteractedBlock(Result.DENY);
                        event.setUseItemInHand(Result.DENY);
                        event.setCancelled(true);
                        return;
                    }
                    new PowerSwitch(game, loc, lever);
                } else if (!block.getType().isOccluding() && (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK)) {
                    // through-fence damage
                    ItemStack handItem = player.getItemInHand();
                    short damage = handItem == null ? 1/* hand damage */
                    : handItem.getDurability();
                    HitThroughWallShot shotData = new HitThroughWallShot(damage);
                    Shot shot = new Shot(player.getEyeLocation(), shotData);
                    List<Hit> results = shot.shoot(shot.arrangeClosest(data.getObjectsOfType(HitBox.class)));
                    if (results.isEmpty()) {
                        return;
                    }
                    Hit closestHit = results.get(0);
                    if (closestHit == null || !(closestHit.getBoxHit() instanceof EntityHitBox)) {
                        return;
                    }
                    Entity hitBoxEntity = ((EntityHitBox) closestHit.getBoxHit()).getEntity();
                    if (!(hitBoxEntity instanceof LivingEntity)) {
                        return;
                    }
                    ZAMob zam = data.getZAMob((LivingEntity) hitBoxEntity);
                    if (!zam.getGame().getUUID().equals(zap.getGame().getUUID())) {
                        return;
                    }
                    zam.getEntity().damage(shotData.getDamage(0), player);
                    return;
                } else if (action == Action.RIGHT_CLICK_BLOCK) {
                    if (block.getType() == Material.CHEST) {
                        event.setUseInteractedBlock(Result.DENY);
                    }
                    if (BukkitUtility.locationMatch(block.getLocation(), game.getMainframe().getLocation(), 2)) {
                        // mainframe
                        if (PlayerInteract.mainframeLinkers.containsKey(zap)) {
                            TeleporterLinkageTimerTask tltt = PlayerInteract.mainframeLinkers_Timers.get(zap);
                            if (tltt.canBeLinked()) {
                                player.sendMessage(ChatColor.GREEN + "Teleporter linked!");
                                tltt.setLinked(true);
                                PlayerInteract.mainframeLinkers.get(zap).setLinked(true);
                            }
                            PlayerInteract.mainframeLinkers.remove(zap);
                            PlayerInteract.mainframeLinkers_Timers.remove(zap);
                        }
                    } else if (data.isTeleporter(block.getLocation())) {
                        Teleporter tele = data.getTeleporter(block.getLocation());
                        if (!tele.isPowered() && (Boolean) Setting.TELEPORTERS_REQUIRE_POWER.getSetting()) {
                            return;
                        }
                        // not mainframe
                        double distanceSquared = Double.MAX_VALUE;
                        double time = -1;
                        boolean requiresLinkage = true;
                        Cube cube = new Cube(block.getLocation(), 2);
                        for (Location sqLoc : cube.getLocations()) {
                            if (sqLoc.getBlock().getState() instanceof Sign) {
                                Sign sign = (Sign) sqLoc.getBlock().getState();
                                String[] lines = sign.getLines();
                                if (lines[0].equalsIgnoreCase(Local.BASE_STRING.getSetting()) && lines[1].equalsIgnoreCase(Local.BASE_TELEPORTER_SETTINGS_STRING.getSetting())) {
                                    try {
                                        if (sqLoc.distanceSquared(block.getLocation()) < distanceSquared) {
                                            requiresLinkage = Boolean.parseBoolean(lines[2]);
                                            time = Double.parseDouble(lines[3]);
                                            distanceSquared = sqLoc.distanceSquared(block.getLocation());
                                        }
                                    } catch (Exception e) {
                                        // nothing
                                    }
                                }
                            }
                        }
                        tele.refresh();
                        if (data.getTeleporter(block.getLocation()).isLinked() || !requiresLinkage) {
                            // this teleporter is linked to the mainframe...
                            if (!zap.isTeleporting()) {
                                player.sendMessage(ChatColor.GRAY + "Teleportation sequence started...");
                                new TeleportTask(zap, (Integer) Setting.TELEPORT_TIME.getSetting(), true);
                                return;
                            } else {
                                player.sendMessage(ChatColor.GRAY + "You are already teleporting!");
                                return;
                            }
                        } else {
                            // this teleporter is not linked to the mainframe...
                            time = time == -1 ? (int) (loc.distanceSquared(game.getMainframe().getLocation()) * .1) : time;// 1 second per block difference (4.5 approx sqrt 20)
                            tele.setLinkTime(time);
                            PlayerInteract.mainframeLinkers.put(zap, tele);
                            PlayerInteract.mainframeLinkers_Timers.put(zap, new TeleporterLinkageTimerTask(game.getMainframe(), zap, (int) time * 20, true)); // difference
                            player.sendMessage(ChatColor.GRAY + "You now have " + time + " seconds to link the teleporter to the mainframe!");
                        }
                    }
                }
            }
        }
    }

    /**
     * Checks the lines of the sign for strings in the config, that enable changes to be made to the player.
     * 
     * @param sign The sign to run lines from
     * @param player The player to affect if the lines are run through
     */
    @SuppressWarnings("deprecation") public void runLines(Sign sign, Player player) {
        String l1 = sign.getLine(0);
        String l2 = sign.getLine(1);
        String l3 = sign.getLine(2);
        // String l4 = sign.getLine(3);//UNUSED
        if (l1.equalsIgnoreCase(Local.BASE_STRING.getSetting())) {
            GameSignClickEvent gsce = new GameSignClickEvent(sign, player);
            Bukkit.getPluginManager().callEvent(gsce);
            if (!gsce.isCancelled()) {
                if (!data.isZAPlayer(player)) {// JOIN
                    if (l2.equalsIgnoreCase(Local.BASE_JOIN_STRING.getSetting())) {
                        joinGame(sign, player, l3);
                    } else if (l2.equalsIgnoreCase(Local.BASE_PERK_STRING.getSetting())) {
                        player.sendMessage(ChatColor.GRAY + "This sign can be used in-game to purchase the " + l3 + " perk.");
                    } else if (l2.equalsIgnoreCase(Local.BASE_ENCHANTMENT_STRING.getSetting())) {
                        player.sendMessage(ChatColor.GRAY + "This sign can be used in-game to purchase the " + l3 + " enchantment for any weapon that behaves like a sword.");
                    } else if (l2.equalsIgnoreCase(Local.BASE_WEAPON_STRING.getSetting())) {
                        player.sendMessage(ChatColor.GRAY + "This sign can be used in-game to purchase the " + l3 + " weapon.");
                    } else if (l2.equalsIgnoreCase(Local.BASE_PASSAGE_STRING.getSetting())) {
                        player.sendMessage(ChatColor.GRAY + "This sign can be used in-game to open the closest passage for " + l3 + " points.");
                    }
                } else if (data.isZAPlayer(player)) {
                    ZAPlayer zap = data.getZAPlayer(player);
                    int points = zap.getPoints();
                    if (l2.equalsIgnoreCase(Local.BASE_PERK_STRING.getSetting())) {
                        givePerk(sign, player, zap, l3, points);
                    } else if (l2.equalsIgnoreCase(Local.BASE_ENCHANTMENT_STRING.getSetting()) && BukkitUtility.isEnchantableLikeSwords(player.getItemInHand())) {
                        giveEnchantment(sign, player, zap, l3, points);
                    } else if (l2.equalsIgnoreCase(Local.BASE_WEAPON_STRING.getSetting())) {
                        giveItem(sign, player, zap, l3, points);
                    } else if (l2.equalsIgnoreCase(Local.BASE_PASSAGE_STRING.getSetting())) {
                        buyPassage(sign, player, zap, l3, points);
                    }
                    player.updateInventory();
                }
            }
        }
    }

    private void buyPassage(Sign sign, Player player, ZAPlayer zap, String l3, int points) {
        int cost = 1500;
        try {
            cost = Integer.parseInt(l3);
        } catch (Exception e) {
            Ablockalypse.getTracker().error("The sign at " + sign.getLocation().toString() + " does not have a cost value on line 4.", 0);
            player.sendMessage(ChatColor.RED + "That sign is incorrectly formatted.\nThe server has already been alerted.");
            return;
        }
        sign.setLine(3, " " + cost + " ");
        if (zap.getPoints() >= cost) {
            Passage a = getClosestPassage(sign.getBlock(), zap.getGame());
            if (a != null) {
                if (!a.isOpened()) {
                    a.open();
                    ZAEffect.POTION_BREAK.play(sign.getLocation());
                    zap.subtractPoints(cost);
                    player.sendMessage(ChatColor.BOLD + "You have bought a passage for " + cost + " points.");
                    return;
                } else {
                    player.sendMessage(ChatColor.RED + "This passage has already been purchased!");
                }
            } else {
                player.sendMessage(ChatColor.RED + "There is no passage close to this sign!");
            }
            return;
        } else {
            player.sendMessage(ChatColor.RED + "You have " + zap.getPoints() + " / " + cost + " points to buy this.");
            return;
        }
    }

    /* Gets the closest game area to the given block. */
    private Passage getClosestPassage(Block b, Game zag) {
        double distanceSquared = Double.MAX_VALUE;
        Location loc = b.getLocation();
        Passage lp = null;
        for (Passage a : data.getObjectsOfType(Passage.class)) {
            if (a.getGame() == zag) {
                Location l = a.getPoint(1);
                double current = l.distanceSquared(loc);
                if (current < distanceSquared) {
                    distanceSquared = current;
                    lp = a;
                }
            }
        }
        if (lp != null) {
            return lp;
        }
        return null;
    }

    private void giveEnchantment(Sign sign, Player player, ZAPlayer zap, String l3, int points) {
        for (ZAEnchantment ench : ZAEnchantment.values()) {
            if (l3.equalsIgnoreCase(ench.getLabel())) {
                if (zap.getPoints() < ench.getCost()) {
                    player.sendMessage(ChatColor.RED + "You have " + points + " / " + ench.getCost() + " points to buy this.");
                    return;
                }
                ItemStack hand = player.getItemInHand();
                player.getInventory().remove(hand);
                hand.addEnchantment(ench.getEnchantment(), 3);
                zap.subtractPoints(ench.getCost());
                player.sendMessage(ChatColor.BOLD + "You have bought " + l3 + " for " + ench.getCost() + " points!");
                ZAEffect.POTION_BREAK.play(sign.getLocation());
                AblockalypseUtility.dropItemAtPlayer(sign.getLocation(), hand, player, 1, 1);
                return;
            }
        }
    }

    private void giveItem(Sign sign, Player player, ZAPlayer zap, String l3, int points) {
        Map<Integer, BuyableItemData> items = Ablockalypse.getExternal().getItemFileManager().getSignItemMap();
        for (int id : items.keySet()) {
            BuyableItemData map = items.get(id);
            int cost = sign.getLine(3).isEmpty() ? map.getCost() : Integer.parseInt(sign.getLine(3));
            if (l3.equalsIgnoreCase(map.getName()) && zap.getGame().getLevel() >= map.getRequiredLevel()) {
                if (points < cost) {
                    player.sendMessage(ChatColor.RED + "You have " + points + " / " + cost + " points to buy this.");
                    return;
                }
                AblockalypseUtility.dropItemAtPlayer(sign.getLocation(), map.toItemStack(), player, 1, 1);
                if (fireSale.contains(zap.getGame())) {
                    cost = 10;
                }
                zap.subtractPoints(cost);
                player.sendMessage(ChatColor.BOLD + "You have bought " + l3 + " for " + cost + " points!");
                ZAEffect.POTION_BREAK.play(sign.getLocation());
                return;
            }
        }
    }

    private void givePerk(Sign sign, Player player, ZAPlayer zap, String l3, int points) {
        for (ZAPerk perk : ZAPerk.values()) {
            if (l3.equalsIgnoreCase(perk.getLabel()) && zap.getGame().getLevel() >= perk.getLevel()) {
                if (zap.getPoints() < perk.getCost()) {
                    player.sendMessage(ChatColor.RED + "You have " + points + " / " + perk.getCost() + " points to buy this.");
                    return;
                }
                perk.givePerk(zap);
                zap.subtractPoints(perk.getCost());
                player.sendMessage(ChatColor.BOLD + "You have bought " + l3 + " for " + perk.getCost() + " points!");
                ZAEffect.POTION_BREAK.play(sign.getLocation());
                return;
            }
        }
    }

    private void joinGame(Sign sign, Player player, String l3) {
        if (player.hasPermission("za.create") && !data.isGame(l3)) {
            setupPlayerWithGame(l3, player);
            player.sendMessage(ChatColor.RED + "This game does not have any barriers. Ignoring...");
            return;
        } else if (data.isGame(l3)) {
            setupPlayerWithGame(l3, player);
            ZAEffect.POTION_BREAK.play(sign.getLocation());
            return;
        } else {
            player.sendMessage(ChatColor.RED + "That game does not exist!");
            return;
        }
    }

    /* Checks for the game and player to create a new game instance and player instance. */
    private void setupPlayerWithGame(String name, Player player) {
        boolean exists = data.gameExists(name);
        Game zag = data.getGame(name, true);
        ZAPlayer zap = data.getZAPlayer(player, name, true);
        if (zag.getMainframe() == null) {
            Location pLoc = player.getLocation();
            zag.setMainframe(new Teleporter(zag, pLoc));
        }
        if (!exists) {
            GameCreateEvent gce = new GameCreateEvent(zag, null, player);
            Bukkit.getServer().getPluginManager().callEvent(gce);
            if (!gce.isCancelled()) {
                zap.loadPlayerToGame(name, true);
            } else {
                zag.remove(true);
            }
        } else {
            zap.loadPlayerToGame(name, true);
        }
    }
} public class PlayerJoin implements Listener {
    public static HashMap<String, Integer> gameLevels = new HashMap<String, Integer>();
    private static HashMap<String, ZAPlayer> offlinePlayers = new HashMap<String, ZAPlayer>();
    private static HashMap<String, Location> spawnLocations = new HashMap<String, Location>();
    private static HashMap<String, Map<String, Object>> playerSavings = new HashMap<String, Map<String, Object>>();
    public static List<PlayerState> toLoad = new ArrayList<PlayerState>();

    public static List<ZAPlayer> getQueues(Game game) {
        List<ZAPlayer> queues = new ArrayList<ZAPlayer>();
        for (String name : offlinePlayers.keySet()) {
            ZAPlayer zap = offlinePlayers.get(name);
            if (zap.getGame().getName().equals(game.getName())) {
                queues.add(zap);
            }
        }
        return queues;
    }

    public static boolean isQueued(ZAPlayer offlineZAPlayer) {
        return offlinePlayers.containsKey(offlineZAPlayer.getState().getSave().get("name"));
    }

    public static void queuePlayer(ZAPlayer offlineZAPlayer, Location spawn, Map<String, Object> savings) {
        if (offlineZAPlayer.hasBeenSentIntoGame()) {
            return;
        }
        Map<String, Object> save = offlineZAPlayer.getState().getSave();
        offlinePlayers.put((String) save.get("name"), offlineZAPlayer);
        spawnLocations.put((String) save.get("name"), spawn);
        playerSavings.put((String) save.get("name"), savings);
    }

    /* Called when a player joins the server.
     * Used mainly for loading game data if it has not already been loaded. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PJE(PlayerJoinEvent event) {
        Player p = event.getPlayer();
        for (PlayerState state : toLoad) {
            if (state.getPlayer().getPlayer().getUniqueId().equals(p.getUniqueId())) {
                state.update();
                toLoad.remove(state);
                break;
            }
        }
        if (offlinePlayers.containsKey(p.getName()) && spawnLocations.containsKey(p.getName())) {
            ZAPlayer zap = offlinePlayers.get(p.getName());
            zap.loadSavedVersion(playerSavings.get(p.getName()));
            zap.setStatus(PlayerStatus.LIMBO);
            zap.getState().update();
            RespawnTask rt = new RespawnTask(zap.getPlayer(), 5, true, false);
            rt.setSpawnLocation(spawnLocations.get(p.getName()));
            offlinePlayers.remove(p.getName());
            spawnLocations.remove(p.getName());
            playerSavings.remove(p.getName());
        }
    }
} public class PlayerKick implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player is kicked from the game.
     * Used mostly to prevent multiple level gains after a player is kicked. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PKE(PlayerKickEvent event) {
        Player player = event.getPlayer();
        if (data.isZAPlayer(player)) {
            ZAPlayer zap = data.getZAPlayer(player);
            Game zag = zap.getGame();
            PlayerLeaveGameEvent GPLE = new PlayerLeaveGameEvent(zap, zag);
            Bukkit.getPluginManager().callEvent(GPLE);
            if (!GPLE.isCancelled()) {
                zag.removePlayer(player);
                zap.remove();
            }
        }
    }
} public class PlayerMove implements Listener {
    private DataContainer data = Ablockalypse.getData();
    private HashMap<String, Double> PHDPlayers = new HashMap<String, Double>();

    public boolean isMoving(PlayerMoveEvent event) {
        return isMoving(event, 0);
    }

    public boolean isMoving(PlayerMoveEvent event, double threshold) {
        Location from = event.getFrom();
        Location to = event.getTo();
        boolean anyX = Math.abs(to.getX() - from.getX()) < threshold;
        boolean upY = from.getY() - to.getY() < threshold;
        boolean anyZ = Math.abs(to.getZ() - from.getZ()) < threshold;
        return !anyX || !upY || !anyZ;
    }

    /* Called whenever a player moves.
     * Mostly used for preventing players from going through barriers. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PME(PlayerMoveEvent event) {
        Player p = event.getPlayer();
        Location from = event.getFrom();
        Location to = event.getTo();
        if (data.isZAPlayer(p)) {
            String name = p.getName();
            ZAPlayer zap = data.getZAPlayer(p);
            if (zap.getPerks().contains(ZAPerk.PHD_FLOPPER)) {
                if (from.getY() - to.getY() < 0 && PHDPlayers.containsKey(name)) {
                    PHDPlayers.put(name, from.getY());
                } else if (PHDPlayers.containsKey(name) && PHDPlayers.get(name) - to.getY() >= 2) {
                    EntityExplode.createNonBlockDestructionExplosionWithPoints(zap, to, 1F);
                    PHDPlayers.remove(name);
                }
            }
            for (Barrier gb : zap.getGame().getObjectsOfType(Barrier.class)) {
                for (Block b : gb.getBlocks()) {
                    Location l = b.getLocation();
                    if (BukkitUtility.locationMatch(l, to)) {
                        // p.teleport(from);
                        event.setCancelled(true);
                    }
                }
            }
        }
    }
} public class PlayerPickupItem implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when an item is picked up by a player.
     * Used mainly for disabling pickups for players in games. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PPIE(PlayerPickupItemEvent event) {
        if (data.isZAPlayer(event.getPlayer())) {
            event.setCancelled(true);
        }
    }
} public class PlayerQuit implements Listener {
    private DataContainer data = Ablockalypse.getData();
    public static Map<Player, PlayerState> playerSaves = new HashMap<Player, PlayerState>();

    /* Called when a player leaves the server.
     * Used for removing a player from the ZAGame when they leave. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PQE(PlayerQuitEvent event) {
        Player p = event.getPlayer();
        if (data.isZAPlayer(p)) {
            ZAPlayer zap = data.getZAPlayer(p);
            Game zag = zap.getGame();
            PlayerLeaveGameEvent GPLE = new PlayerLeaveGameEvent(zap, zag);
            Bukkit.getPluginManager().callEvent(GPLE);
            if (!GPLE.isCancelled()) {
                playerSaves.put(p, zap.getState());
                zag.removePlayer(p);
                zap.remove();
            }
        }
    }
} public class PlayerRespawn implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player respawns.
     * Mainly used for sending the player back to the mainframe. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PRE(PlayerRespawnEvent event) {
        Player p = event.getPlayer();
        if (data.isZAPlayer(p)) {
            new RespawnTask(p, 5, true, true);
        }
    }
} public class PlayerTeleport implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player teleports from one location to the other.
     * Used mainly for preventing teleportation for ZAPlayers when an ender pearl is thrown. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PTE(PlayerTeleportEvent event) {
        Player player = event.getPlayer();
        if (data.isZAPlayer(player)) {
            ZAPlayer zap = data.getZAPlayer(player);
            if (event.getCause() == TeleportCause.ENDER_PEARL) {
                event.setCancelled(true);
            } else if (zap.isInLastStand()) {
                zap.getSeat().sit(player);
                event.setCancelled(true);
            }
        }
    }
} public class PlayerToggleSneak implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player changes from walking to sneaking.
     * Used mostly for triggering shift interact objects. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PTSE(PlayerToggleSneakEvent event) {
        Player player = event.getPlayer();
        Location loc = player.getLocation();
        if (data.isZAPlayer(player)) {
            ZAPlayer zap = data.getZAPlayer(player);
            {
                Grenade closestGrenade = data.getClosest(Grenade.class, loc, 2, 3.5, 2);
                if (closestGrenade != null && closestGrenade.getGrenadeEntity() != null) {
                    zap.giveItem(new ItemStack(Material.ENDER_PEARL, 1));
                    closestGrenade.setLive(false);
                    closestGrenade.remove();
                    return;
                }
                ZAPlayer closestPlayer = data.getClosest(ZAPlayer.class, loc, 2, 3.5, 2);
                if (closestPlayer != null && closestPlayer.isInLastStand()) {
                    new LastStandPickupTask(zap, closestPlayer, 20, 5, true);// can only be done once for each downed player
                    return;
                }
                Teleporter closestTeleporter = data.getClosest(Teleporter.class, loc, 1, 2.5, 1);
                if (closestTeleporter != null && closestTeleporter.isPowered() && closestTeleporter.isLinked() && !zap.isTeleporting()) {
                    closestTeleporter.playEffects(Teleporter.TELEPORT_EFFECTS);
                    new TeleportTask(zap, 5, true);// only alllows one teleport thread per player
                    return;
                }
                Claymore closestClaymore = data.getClosest(Claymore.class, loc, 2, 2, 2);
                if (closestClaymore != null) {
                    Ablockalypse.getExternal().getItemFileManager().giveItem(player, new ItemStack(Material.FLOWER_POT_ITEM, 1));
                    closestClaymore.remove();
                    return;
                }
                Barrier closestBarrier = data.getClosest(Barrier.class, loc, 2, 3, 2);
                if (closestBarrier != null && closestBarrier.getHP() < 5) {
                    closestBarrier.fixBarrier(zap);
                    return;
                }
                MysteryBox closestBox = data.getClosest(MysteryBox.class, loc, 2, 3.5, 2);
                if (closestBox != null && closestBox.isShowingItem()) {
                    closestBox.getLastShownZAPlayer().giveItem(closestBox.getLastShownItem());
                    closestBox.stopShowing();
                    return;
                }
            }
        }
    }
} public class ProjectileHit implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player throws an object.
     * Used for changing ender pearls to grenades for ZAPlayers. */
    @EventHandler(priority = EventPriority.HIGHEST) public void PHE(ProjectileHitEvent event) {
        Entity e = event.getEntity();
        if (e instanceof EnderPearl) {
            EnderPearl ep = (EnderPearl) e;
            Player p = (Player) ep.getShooter();
            if (data.isZAPlayer(p) && !data.isGrenade(e)) {
                Location loc = ep.getLocation();
                Location pLoc = p.getLocation();
                ZAPlayer zap = data.getZAPlayer(p);
                double xDiffForce = (loc.getX() - pLoc.getX()) / 20;
                double zDiffForce = (loc.getZ() - pLoc.getZ()) / 20;
                new Grenade(e, zap, 60, true, false, new Vector(xDiffForce, .1, zDiffForce));
            }
        } else if (e instanceof Arrow && ((Arrow) e).getShooter() instanceof Player) {
            Arrow a = (Arrow) e;
            Player p = (Player) a.getShooter();
            if (data.isZAPlayer(p)) {
                ZAPlayer zap = data.getZAPlayer(p);
                UUID uuid = a.getUniqueId();
                if (zap.hasInstaKill()) {
                    EntityDamageByEntity.instakillids.add(uuid);
                }
                Location loc2 = e.getLocation();
                /* through-barrier damage */
                for (ZAMob zam : zap.getGame().getMobs()) {
                    LivingEntity c = zam.getEntity();
                    Location loc3 = c.getLocation();
                    if (loc3.distanceSquared(loc2) <= 3) {// within 1.5 blocks (2.25 approx 1.5 squared)
                        double dmg = 40;
                        EntityDamageByEntityEvent EDBE = new EntityDamageByEntityEvent(p, c, DamageCause.CUSTOM, dmg);
                        Bukkit.getPluginManager().callEvent(EDBE);
                        if (!EDBE.isCancelled()) {
                            ZAEffect.IRON_BREAK.play(loc3);
                            c.damage(EDBE.getDamage());
                        }
                        break;
                    }
                }
            }
        }
    }
} public class SignChange implements Listener {
    private DataContainer data = Ablockalypse.getData();

    /* Called when a player places a block.
     * Used mainly for avoiding unwanted players from placing ZASigns.
     * Also used to add map data storage signs to the arraylist in GlobalData to be loaded on disable. */
    @EventHandler(priority = EventPriority.HIGHEST) public void SCE(SignChangeEvent event) {
        Player player = event.getPlayer();
        // Sign sign = (Sign) event.getBlock().getState();
        String[] lines = event.getLines();
        if (lines[0].equalsIgnoreCase(Local.BASE_STRING.getSetting())) {
            if (data.isZAPlayer(player) || !player.hasPermission(PermissionManager.PLACE_SIGNS)) {
                event.setCancelled(true);
                ZAEffect.FLAMES.play(player.getLocation());
                player.sendMessage(ChatColor.RED + "You do not have permissions to place ZA signs!");
                return;
            } else if (player.hasPermission(PermissionManager.PLACE_SIGNS)) {
                player.sendMessage(ChatColor.GRAY + "You have created a ZA sign.");
                return;
            }
        }
    }
} public class MainThread {
    private int id = -1, interval = 1;

    public MainThread(boolean autorun) {
        if (autorun) {
            run();
        }
    }

    public synchronized void cancel() {
        Bukkit.getScheduler().cancelTask(id);
        id = -1;
    }

    public void run() {
        if (id != -1) {
            Ablockalypse.getTracker().error("The main thread has been run several times over. Ablockalypse will now stop to prevent serious issues.", Integer.MAX_VALUE);
            return;
        }
        id = Bukkit.getScheduler().scheduleSyncRepeatingTask(Ablockalypse.getInstance(), new Runnable() {
            @Override public void run() {
                tick();
            }
        }, interval, interval);
    }

    public synchronized void tick() {
        for (Task task : Ablockalypse.getData().getObjectsOfType(Task.class)) {
            if (task.isRunning()) {
                if (task instanceof RepeatingTask) {
                    RepeatingTask rthread = (RepeatingTask) task;
                    rthread.setCount(rthread.getCount() + 1);
                    if (rthread.getCount() >= rthread.getInterval()) {
                        rthread.run();
                        rthread.setCount(0);
                    }
                } else if (task instanceof DelayedTask) {
                    DelayedTask dthread = (DelayedTask) task;
                    dthread.setCountup(dthread.getCountup() + 1);
                    if (dthread.getCountup() >= dthread.getDelay()) {
                        dthread.run();
                        dthread.cancel();
                    }
                }
            }
        }
    }
} public class ServerBarrierActionTask extends RepeatingTask {
    private static final int INTERVAL = 20;
    private DataContainer data = Ablockalypse.getData();

    public ServerBarrierActionTask(boolean autorun) {
        super(INTERVAL, autorun);
    }

    @Override public void run() {
        for (Barrier bg : data.getObjectsOfType(Barrier.class)) {
            if (bg.getCenter() == null) {
                continue;
            }
            for (Entity nearby : BukkitUtility.getNearbyEntities(bg.getCenter(), 2, 3, 2)) {
                if (!(nearby instanceof LivingEntity)) {
                    continue;
                }
                if (data.isZAMob((LivingEntity) nearby)) {
                    if (bg.getCenter().distanceSquared(nearby.getLocation()) < 4 && !bg.isBroken()) {
                        bg.breakBarrier((LivingEntity) nearby);// will not let the mob fix if it is already
                    }
                }
            }
        }
    }
} public class ServerHellhoundActionTask extends RepeatingTask {
    private static final int RE_TELEPORT_WAIT = 400;// ticks
    private int countup_to_teleport = 0;
    private DataContainer data = Ablockalypse.getData();

    public ServerHellhoundActionTask(int interval, boolean autorun) {
        super(interval, autorun);
    }

    @Override public void run() {
        for (Hellhound f : data.getObjectsOfType(Hellhound.class)) {
            Wolf wolf = f.getWolf();
            if (wolf.isDead()) {
                continue;
            }
            ZAEffect.FLAMES.play(wolf.getLocation());
            f.setAggressive(true);
            if (++countup_to_teleport >= RE_TELEPORT_WAIT / getInterval()) {
                countup_to_teleport = 0;
                Location target = f.getGame().getRandomLivingPlayer().getLocation();
                Location strike = BukkitUtility.getNearbyLocation(target, 2, 4, 0, 0, 2, 4);
                wolf.teleport(strike);
                strike.getWorld().strikeLightning(strike);
            }
        }
    }
} public class ServerMobSpawnPreventionTask extends RepeatingTask {
    private DataContainer data = Ablockalypse.getData();

    public ServerMobSpawnPreventionTask(int interval, boolean autorun) {
        super(interval, autorun);
    }

    @Override public void run() {
        for (ZAPlayer zap : data.getObjectsOfType(ZAPlayer.class)) {
            Player p = zap.getPlayer();
            int radius = (Integer) Setting.CLEAR_MOBS_RADIUS.getSetting();
            for (Entity e : p.getNearbyEntities(radius, radius, radius)) {
                if (!(e instanceof LivingEntity)) {
                    continue;
                }
                if (e != null && !data.isZAMob((LivingEntity) e) && !(e instanceof Player)) {
                    if (e instanceof LivingEntity) {
                        ((LivingEntity) e).setHealth(0);
                    } else {
                        e.remove();
                    }
                }
            }
        }
    }
} public class ServerTeleporterActionTask extends RepeatingTask {
    private DataContainer data = Ablockalypse.getData();

    public ServerTeleporterActionTask(int interval, boolean autorun) {
        super(interval, autorun);
    }

    @Override public void run() {
        for (Teleporter tele : data.getObjectsOfType(Teleporter.class)) {
            if (tele.isLinked()) {
                tele.playEffects(Teleporter.LINKED_EFFECTS);
            }
        }
    }
}

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import com.github.jamesnorris.ablockalypse.aspect.Game;
import com.github.jamesnorris.ablockalypse.aspect.PermanentAspect;
import com.github.jamesnorris.ablockalypse.aspect.PlayerState;
import com.github.jamesnorris.ablockalypse.enumerated.Setting;
import com.github.jamesnorris.ablockalypse.event.bukkit.BlockBreak;
import com.github.jamesnorris.ablockalypse.event.bukkit.BlockPlace;
import com.github.jamesnorris.ablockalypse.event.bukkit.BlockRedstone;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityBreakDoor;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityDamage;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityDamageByEntity;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityDeath;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityExplode;
import com.github.jamesnorris.ablockalypse.event.bukkit.EntityTarget;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerDeath;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerDropItem;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerInteract;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerJoin;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerKick;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerMove;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerPickupItem;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerQuit;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerRespawn;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerTeleport;
import com.github.jamesnorris.ablockalypse.event.bukkit.PlayerToggleSneak;
import com.github.jamesnorris.ablockalypse.event.bukkit.ProjectileHit;
import com.github.jamesnorris.ablockalypse.event.bukkit.SignChange;
import com.github.jamesnorris.ablockalypse.threading.MainThread;
import com.github.jamesnorris.ablockalypse.threading.inherent.ServerBarrierActionTask;
import com.github.jamesnorris.ablockalypse.threading.inherent.ServerHellhoundActionTask;
import com.github.jamesnorris.ablockalypse.threading.inherent.ServerMobSpawnPreventionTask;
import com.github.jamesnorris.ablockalypse.threading.inherent.ServerTeleporterActionTask;
package com.github.jamesnorris.ablockalypse;




public class Ablockalypse extends JavaPlugin {
    private static Ablockalypse instance;
    private static DataContainer data = new DataContainer();
    private static MainThread mainThread;
    private static External external;
    private static BaseCommand commandInstance;
    private static Tracker tracker;

    public static BaseCommand getBaseCommandInstance() {
        return commandInstance;
    }

    public static DataContainer getData() {
        return data;
    }

    public static External getExternal() {
        return external;
    }

    public static Ablockalypse getInstance() {
        return instance;
    }

    public static MainThread getMainThread() {
        return mainThread;
    }

    public static Tracker getTracker() {
        return tracker;
    }

    // Kills the plugin.
    protected static void kill() {
        Ablockalypse.instance.setEnabled(false);
    }

    @Override public void onDisable() {
        for (Game game : data.getObjectsOfType(Game.class)) {
            game.organizeObjects();
            PermanentAspect.save(game, external.getSavedDataFile(game.getName(), true));
            if ((Boolean) Setting.PRINT_STORAGE_DATA_TO_FILE.getSetting()) {
                try {
                    PermanentAspect.printData(game);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            game.remove(false);
        }
        for (PlayerState state : PlayerJoin.toLoad) {
            PlayerQuit.playerSaves.put(state.getPlayer().getPlayer(), state);
        }
        for (Player player : PlayerQuit.playerSaves.keySet()) {
            PlayerState state = PlayerQuit.playerSaves.get(player);
            PermanentAspect.save(state, external.getSavedPlayerDataFile(player, true));
        }
        mainThread.cancel();
    }

    @SuppressWarnings("unchecked") @Override public void onEnable() {
        Ablockalypse.instance = this;
        external = new External(this);
        tracker = new Tracker("Ablockalypse", getDescription().getVersion(), "https://github.com/JamesNorris/Ablockalypse/issues", (Integer) Setting.MAX_FATALITY.getSetting());
        commandInstance = new BaseCommand();
        register();
        initServerThreads(false);
        try {
            for (File file : external.getSavedDataFolder().listFiles()) {
                if (file != null && !file.isDirectory()) {
                    Map<String, Object> save = (Map<String, Object>) External.load(file);
                    PermanentAspect.load(Game.class, save);
                }
            }
            for (File file : external.getSavedPlayerDataFolder().listFiles()) {
                if (file != null && !file.isDirectory()) {
                    Map<String, Object> save = (Map<String, Object>) External.load(file);
                    PlayerJoin.toLoad.add((PlayerState) PermanentAspect.load(PlayerState.class, save));
                    file.delete();
                }
            }
        } catch (EOFException e) {
            // nothing, the file is empty and no data was saved
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        mainThread.run();// starts the main thread, which was told not to autorun earlier. Done to prevent NPEs with loading saves and running threads.
    }

    protected void initServerThreads(boolean startMain) {
        mainThread = new MainThread(startMain);
        new ServerBarrierActionTask(true);
        new ServerMobSpawnPreventionTask(360, (Boolean) Setting.CLEAR_MOBS.getSetting());
        new ServerHellhoundActionTask(20, true);
        new ServerTeleporterActionTask(20, true);
    }

    protected void register() {
        PluginManager pm = getServer().getPluginManager();
        /* EVENTS */
        for (Listener listener : new Listener[] {new EntityDamage(), new PlayerDeath(), new PlayerInteract(),
                new SignChange(), new EntityDeath(), new PlayerPickupItem(), new ProjectileHit(), new PlayerMove(),
                new EntityBreakDoor(), new PlayerTeleport(), new PlayerQuit(), new EntityTarget(), new PlayerRespawn(),
                new EntityExplode(), new EntityDamageByEntity(), new PlayerJoin(), new BlockBreak(),
                new PlayerDropItem(), new PlayerKick(), new BlockPlace(), new BlockRedstone(), new PlayerToggleSneak()}) {