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;