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

    public static BaseCommand getBaseCommandInstance() {
        return commandInstance;
    }

    public static DataContainer getData() {
        return data;
    }

    public static External getExternal() {
        return external;
    }

    public static Ablockalypse getInstance() {
        return instance;
    }

    public static MainThread getMainThread() {
        return mainThread;
    }

    public static Tracker getTracker() {
        return tracker;
    }

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

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

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

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

    protected void register() {
        PluginManager pm = getServer().getPluginManager();
        /* EVENTS */
        for (Listener listener : new Listener[] {new EntityDamage(), new PlayerDeath(), new PlayerInteract(),
                new SignChange(), new EntityDeath(), new PlayerPickupItem(), new ProjectileHit(), new PlayerMove(),
                new EntityBreakDoor(), new PlayerTeleport(), new PlayerQuit(), new EntityTarget(), new PlayerRespawn(),
                new EntityExplode(), new EntityDamageByEntity(), new PlayerJoin(), new BlockBreak(),
                new PlayerDropItem(), new PlayerKick(), new BlockPlace(), new BlockRedstone(), new PlayerToggleSneak()}) {
            pm.registerEvents(listener, instance);
        }
        /* COMMANDS */
        instance.getCommand("za").setExecutor(commandInstance);
    }
} public interface MapDatable extends Cloneable {
    public Location getPointClosestToOrigin();

    public void paste(Location pointClosestToOrigin);

    public void remove();
}
 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 enum ZAEffect {
	EXTINGUISH(4, Effect.EXTINGUISH, 1), 
	FLAMES(2, Effect.MOBSPAWNER_FLAMES, 1), 
	IRON_BREAK(6, Effect.ZOMBIE_CHEW_IRON_DOOR, 1), 
	POTION_BREAK(3, Effect.POTION_BREAK, 2),
	SMOKE(1, Effect.SMOKE, 1), 
	TELEPORTATION(7, Effect.ENDER_SIGNAL, 1), 
	WOOD_BREAK(5, Effect.ZOMBIE_CHEW_WOODEN_DOOR, 1);
    private final static Map<Integer, ZAEffect> BY_ID = Maps.newHashMap();
    static {
        for (ZAEffect setting : values()) {
            BY_ID.put(setting.id, setting);
        }
    }

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

    private Effect effect;
    private int id, type;

    ZAEffect(int id, Effect effect, int type) {
        this.id = id;
        this.effect = effect;
        this.type = type;
    }

    public int getId() {
        return id;
    }

    public void play(Location loc) {
        if ((Boolean) Setting.EXTRA_EFFECTS.getSetting()) {
            loc.getWorld().playEffect(loc, effect, type);
        }
    }
} public enum ZASound {
    ACHIEVEMENT(1, new Sound[] {Sound.ENTITY_PLAYER_LEVELUP}, 1),
    AREA_BUY(11, new Sound[] {Sound.BLOCK_WOODEN_DOOR_OPEN}, 3),
    AREA_REPLACE(12, new Sound[] {Sound.BLOCK_WOODEN_DOOR_CLOSE}, 3),
    BARRIER_BREAK(9, new Sound[] {Sound.ENTITY_ZOMBIE_BREAK_WOODEN_DOOR}, 2),
    BARRIER_REPAIR(10, new Sound[] {Sound.BLOCK_PISTON_EXTEND, Sound.ENTITY_ITEM_BREAK}, 1),
    DEATH(2, new Sound[] {Sound.ENTITY_GHAST_DEATH}, 1),
    END(3, new Sound[] {Sound.AMBIENT_CAVE}, 7),
    EXPLOSION(13, new Sound[] {Sound.ENTITY_GENERIC_EXPLODE}, 5),
    LAST_STAND(4, new Sound[] {Sound.ENTITY_GHAST_SCREAM}, 3),
    NEXT_LEVEL(5, new Sound[] {Sound.ENTITY_LIGHTNING_BOLT_THUNDER}, 7),
    PREV_LEVEL(6, new Sound[] {Sound.WEATHER_RAIN}, 7),
    START(7, new Sound[] {Sound.AMBIENT_CAVE}, 7),
    TELEPORT(8, new Sound[] {Sound.BLOCK_PORTAL_TRAVEL}, 1);
    private final static Map<Integer, ZASound> BY_ID = Maps.newHashMap();
    static {
        for (ZASound setting : values()) {
            BY_ID.put(setting.id, setting);
        }
    }

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

    private int id, type;
    private Sound[] sounds;

    ZASound(int id, Sound[] sounds, int type) {
        this.id = id;
        this.sounds = sounds;
    }

    public int getId() {
        return id;
    }

    public void play(Location loc) {
        if ((Boolean) Setting.EXTRA_SOUNDS.getSetting()) {
            for (Sound sound : sounds) {
                loc.getWorld().playSound(loc, sound, type, 1);
            }
        }
    }
} public class DelayedTask extends Task {
    private int countUp, delay;

    public DelayedTask(int delay, boolean autorun) {
        super(autorun);
        this.delay = delay;
    }

    public int getCountup() {
        return countUp;
    }

    public int getDelay() {
        return delay;
    }

    public void setCountup(int countUp) {
        this.countUp = countUp;
    }
}
 public class RepeatingTask extends Task {
    private int interval, count;

    public RepeatingTask(int interval) {
        this(interval, false);
    }

    public RepeatingTask(int interval, boolean autorun) {
        super(autorun);
        this.interval = interval;
    }

    public int getCount() {
        return count;
    }

    public int getInterval() {
        return interval;
    }

    public void setCount(int count) {
        this.count = count;
    }
}
 public class AblockalypseUtility {
    private static DataContainer data = Ablockalypse.getData();
    private static HashMap<String, Long> messagesSent = new HashMap<String, Long>();

    public static EntityHitBox createHellhoundHitBox(Hellhound hound) {
        Entity entity = hound.getWolf();
        World world = entity.getWorld();
        EntityHitBox hitBox = new EntityHitBox(entity, hound.getLength(), hound.getWidth(), hound.getHeight());
        Location from = hitBox.getFrom().toLocation(world);
        Location to = hitBox.getTo().toLocation(world);
        double boxX = Math.min(from.getX(), to.getX());
        double boxY = Math.min(from.getY(), to.getY());
        double boxZ = Math.min(from.getZ(), to.getZ());
        hitBox.addZone(new DataZone(world, new Vector(boxX, boxY + .45, boxZ + .1), new Vector(boxX + .5, boxY + .969, boxZ + .4), 2, .75));// head
        hitBox.addZone(new DataZone(world, new Vector(boxX + .4, boxY, boxZ), new Vector(boxX + 1.594, boxY + .969, boxZ + .5), 1, .6));// body
        return hitBox;
    }

    public static EntityHitBox createZombieHitBox(Zombie zombie) {
        Entity entity = zombie.getBukkitZombie();
        World world = entity.getWorld();
        EntityHitBox hitBox = new EntityHitBox(entity, zombie.getLength(), zombie.getWidth(), zombie.getHeight());
        Location from = hitBox.getFrom().toLocation(world);
        Location to = hitBox.getTo().toLocation(world);
        double boxX = Math.min(from.getX(), to.getX());
        double boxY = Math.min(from.getY(), to.getY());
        double boxZ = Math.min(from.getZ(), to.getZ());
        hitBox.addZone(new DataZone(world, new Vector(boxX, boxY + 1.4, boxZ), new Vector(boxX + 1.031, boxY + 2, boxZ + .891), 2, 1.25));// head
        hitBox.addZone(new DataZone(world, new Vector(boxX, boxY, boxZ), new Vector(boxX + .891, boxY + 1.4, boxZ + 1.031), 1, 1));// body
        return hitBox;
    }

    public static void dropItemAtPlayer(final Location from, final ItemStack item, final Player player, final int dropDelay, final int removalDelay) {
        new DelayedTask(dropDelay, true) {
            @Override public void run() {
                Item i = from.getWorld().dropItem(from, item);
                i.setPickupDelay(Integer.MAX_VALUE);
                final ItemStack is = i.getItemStack();
                final Item finali = i;
                new DelayedTask(removalDelay, true) {
                    @Override public void run() {
                        finali.remove();
                        Ablockalypse.getExternal().getItemFileManager().giveItem(player, is);
                    }
                };
            }
        };
    }

    @SuppressWarnings("serial") public static List<PathfinderGoal> getGoals(final World world, final double length, final double width, final double height, final double rot) {
        return new ArrayList<PathfinderGoal>() {
            {
                add(new PathfinderGoal() {
                    @Override public PathNode transform(List<PathNode> navigated, PathNode finish, PathNode node) {
                        if (navigated.contains(node)) {
                            setFinish(navigated.get(navigated.size() - 1), finish);
                        }
                        if (!canFit(new Location(world, node.x, node.y + height - .5, node.z), length, width, height, rot)) {
                            node.H = Double.POSITIVE_INFINITY;// no travelling through blocks
                        }
                        if (world.getBlockAt(node.x, node.y - 2, node.z).isEmpty()) {
                            node.H += 3;// discourage falling, prevent flying
                        }
                        return node;
                    }

                    private boolean canFit(Location center, double length, double width, double height, double rot) {
                        double diagonal = Math.sqrt(Math.pow(length, 2) + Math.pow(width, 2));
                        double viewWidth = diagonal * Math.cos(Math.toRadians(rot));
                        double viewLength = diagonal * Math.sin(Math.toRadians(rot));
                        for (double addX = -viewWidth / 2; addX <= viewWidth / 2; addX++) {
                            for (double addY = -height / 2; addY <= height / 2; addY++) {
                                for (double addZ = -viewLength / 2; addZ <= viewLength / 2; addZ++) {
                                    if (center.clone().add(addX, addY, addZ).getBlock().getType().isSolid()) {
                                        return false;
                                    }
                                }
                            }
                        }
                        return true;
                    }

                    private void setFinish(PathNode node, PathNode finish) {
                        finish.x = node.x;
                        finish.y = node.y;
                        finish.z = node.z;
                    }
                });
            }
        };
    }

    public static RepeatingTask scheduleNearbyWarning(final Location loc, final String warning, final double xRad, final double yRad, final double zRad, final int intervalInMilliseconds) {
        return scheduleNearbyWarning(loc, warning, new ArrayList<UUID>(), xRad, yRad, zRad, intervalInMilliseconds);
    }

    public static RepeatingTask scheduleNearbyWarning(final Location loc, final String warning, final double rad, final int intervalInMilliseconds) {
        return scheduleNearbyWarning(loc, warning, rad, rad, rad, intervalInMilliseconds);
    }

    public static RepeatingTask scheduleNearbyWarning(final Location loc, final String warning, final List<UUID> avoid, final double xRad, final double yRad, final double zRad, final int intervalInMilliseconds) {
        return new RepeatingTask(intervalInMilliseconds / 2000, true) {
            @Override public void run() {
                for (Entity nearby : BukkitUtility.getNearbyEntities(loc, xRad, yRad, zRad)) {
                    if (nearby instanceof Player && data.isZAPlayer((Player) nearby)) {
                        Player player = (Player) nearby;
                        if (avoid.contains(player.getUniqueId())) {
                            continue;
                        }
                        ZAPlayer zap = data.getZAPlayer(player);
                        if (!zap.isTeleporting()) {
                            String name = player.getName();
                            if (messagesSent.containsKey(name)) {
                                long timePast = System.currentTimeMillis() - messagesSent.get(name);
                                if (timePast >= intervalInMilliseconds) {
                                    messagesSent.remove(name);
                                }
                            } else {
                                messagesSent.put(name, System.currentTimeMillis());
                                // if ((Boolean) Setting.ITEM_NAME_MESSAGES.getSetting()) {
                                // new PopupMessage(player, "Press SHIFT to teleport.", 5);
                                // } else {
                                player.sendMessage(warning);
                                // }
                            }
                        }
                    }
                }
            }
        };
    }
}
 public class BukkitUtility {
    public static List<Material> swords = new ArrayList<Material>() {{
        add(Material.WOODEN_SWORD);
        add(Material.STONE_SWORD);
        add(Material.IRON_SWORD);
        add(Material.GOLDEN_SWORD);
        add(Material.DIAMOND_SWORD);
    }};

    public static HashMap<DyeColor, Material> woolByColor;

    private static Random rand = new Random();
    private static String nms_version = "v1.5.2";
    static {
        String bukkitVersion = Bukkit.getVersion();
        String cleanedVersion = bukkitVersion.split(Pattern.quote("(MC:"))[1].split(Pattern.quote(")"))[0].trim();
        nms_version = "v" + cleanedVersion;

        woolByColor = new HashMap<DyeColor, Material>()
        woolByColor.put(DyeColor.BLACK, Material.BLACK_WOOL);
        woolByColor.put(DyeColor.BLUE, Material.BLUE_WOOL);
        woolByColor.put(DyeColor.BROWN, Material.BROWN_WOOL);
        woolByColor.put(DyeColor.CYAN, Material.CYAN_WOOL);
        woolByColor.put(DyeColor.GRAY, Material.GRAY_WOOL);
        woolByColor.put(DyeColor.GREEN, Material.GREEN_WOOL);
        woolByColor.put(DyeColor.LIGHT_BLUE, Material.LIGHT_BLUE_WOOL);
        woolByColor.put(DyeColor.LIGHT_GRAY, Material.LIGHT_GRAY_WOOL);
        woolByColor.put(DyeColor.LIME, Material.LIME_WOOL);
        woolByColor.put(DyeColor.MAGENTA, Material.MAGENTA_WOOL);
        woolByColor.put(DyeColor.ORANGE, Material.ORANGE_WOOL);
        woolByColor.put(DyeColor.PINK, Material.PINK_WOOL);
        woolByColor.put(DyeColor.PURPLE, Material.PURPLE_WOOL);
        woolByColor.put(DyeColor.RED, Material.RED_WOOL);
        woolByColor.put(DyeColor.WHITE, Material.WHITE_WOOL);
        woolByColor.put(DyeColor.YELLOW, Material.YELLOW_WOOL);
    }

    public static Material getWoolByColor(DyeColor color) {
        return woolByColor.get(color);
    }

    public static Location floorLivingEntity(LivingEntity entity) {
        Location eyeLoc = entity.getEyeLocation().clone();
        double eyeHeight = entity.getEyeHeight();
        Location floor = eyeLoc.clone().subtract(0, Math.floor(eyeHeight) + .5, 0);
        for (int y = eyeLoc.getBlockY(); y > 0; y--) {
            Location loc = new Location(floor.getWorld(), floor.getX(), y, floor.getZ(), floor.getYaw(), floor.getPitch());
            if (!loc.getBlock().isEmpty()) {
                floor = loc;
                break;
            }
        }
        return eyeLoc.clone().subtract(0, eyeLoc.getY() - floor.getY() - 2 * eyeHeight, 0);
    }

    public static OfflinePlayer forceObtainPlayer(String name) {
        OfflinePlayer player = Bukkit.getPlayer(name);
        if (player == null) {
            return Bukkit.getOfflinePlayer(name);
        }
        if (player == null || !player.hasPlayedBefore()) {
            // npes will be thrown... player doesnt exist and never did (why was it saved?)
            return null;
        }
        return player;
    }

    public static Location fromString(String loc) {
        loc = loc.substring(loc.indexOf("{") + 1);
        loc = loc.substring(loc.indexOf("{") + 1);
        String worldName = loc.substring(loc.indexOf("=") + 1, loc.indexOf("}"));
        loc = loc.substring(loc.indexOf(",") + 1);
        String xCoord = loc.substring(loc.indexOf("=") + 1, loc.indexOf(","));
        loc = loc.substring(loc.indexOf(",") + 1);
        String yCoord = loc.substring(loc.indexOf("=") + 1, loc.indexOf(","));
        loc = loc.substring(loc.indexOf(",") + 1);
        String zCoord = loc.substring(loc.indexOf("=") + 1, loc.indexOf(","));
        loc = loc.substring(loc.indexOf(",") + 1);
        String pitch = loc.substring(loc.indexOf("=") + 1, loc.indexOf(","));
        loc = loc.substring(loc.indexOf(",") + 1);
        String yaw = loc.substring(loc.indexOf("=") + 1, loc.indexOf("}"));
        return new Location(Bukkit.getWorld(worldName), Double.parseDouble(xCoord), Double.parseDouble(yCoord), Double.parseDouble(zCoord), Float.parseFloat(yaw), Float.parseFloat(pitch));
    }

    public static Block getHighestEmptyBlockUnder(Location loc) {
        for (int y = loc.getBlockY(); y > 0; y--) {
            Location floor = new Location(loc.getWorld(), loc.getX(), y, loc.getZ(), loc.getYaw(), loc.getPitch());
            Block block = floor.getBlock();
            if (!block.isEmpty()) {
                return block;
            }
        }
        return loc.getBlock();
    }

    public static List<Entity> getNearbyEntities(Location loc, double x, double y, double z) {
        List<Entity> entities = new ArrayList<Entity>();
        for (Chunk chunk : getRelativeChunks(loc.getChunk())) {
            for (Entity entity : chunk.getEntities()) {
                Location entLoc = entity.getLocation();
                if (Math.abs(entLoc.getX() - loc.getX()) <= x && Math.abs(entLoc.getY() - loc.getY()) <= y && Math.abs(entLoc.getZ() - loc.getZ()) <= z && !entities.contains(entity)) {
                    entities.add(entity);
                }
            }
        }
        return entities;
    }

    public static Location getNearbyLocation(Location loc, double minXdif, double maxXdif, double minYdif, double maxYdif, double minZdif, double maxZdif) {
        double modX = difInRandDirection(maxXdif, minXdif);
        double modY = difInRandDirection(maxXdif, minXdif);
        double modZ = difInRandDirection(maxXdif, minXdif);
        return loc.clone().add(modX, modY, modZ);
    }

    public static String getNMSVersionSlug() {
        return nms_version;
    }

    /**
     * Gets all nearby chunks
     * @param chunk The chunk to find relatives
     * @return All nearby chunks, including the one passed as a parameter
     */
    public static Chunk[] getRelativeChunks(Chunk chunk) {
        World world = chunk.getWorld();
        return new Chunk[] {
            chunk, //TODO check current chunk?
            world.getChunkAt(chunk.getX() - 16, chunk.getZ() - 16),
            world.getChunkAt(chunk.getX() - 16, chunk.getZ()),
            world.getChunkAt(chunk.getX(), chunk.getZ() - 16),
            world.getChunkAt(chunk.getX(), chunk.getZ()), 
            world.getChunkAt(chunk.getX() + 16, chunk.getZ() - 16),
            world.getChunkAt(chunk.getX() - 16, chunk.getZ() + 16),
            world.getChunkAt(chunk.getX() + 16, chunk.getZ()), 
            world.getChunkAt(chunk.getX(), chunk.getZ() + 16)
        };
    }

    public static Block getSecondChest(Block b) {
        BlockFace[] faces = new BlockFace[] {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST};
        for (BlockFace face : faces) {
            Block bl = b.getRelative(face);
            if (bl.getState() instanceof Chest || bl.getState() instanceof DoubleChest) {
                return bl;
            }
        }
        return null;
    }

    public static boolean isDoubleChest(Block block) {
        if (block == null || !(block.getState() instanceof Chest)) {
            return false;
        }
        Chest chest = (Chest) block.getState();
        return chest.getInventory().getContents().length == 54;
    }

    public static boolean isEnchantableLikeSwords(ItemStack item) {
        return swords.Contains(item.getType());
    }

    public static boolean locationMatch(Location loc1, Location loc2) {
        boolean nearX = Math.floor(loc1.getBlockX()) == Math.floor(loc2.getBlockX());
        boolean nearY = Math.floor(loc1.getBlockY()) == Math.floor(loc2.getBlockY());
        boolean nearZ = Math.floor(loc1.getBlockZ()) == Math.floor(loc2.getBlockZ());
        return nearX && nearY && nearZ;
    }

    public static boolean locationMatch(Location loc1, Location loc2, int distance) {// TODO this method is exact, fix
        return Math.abs(loc1.getX() - loc2.getX()) <= distance && Math.abs(loc1.getY() - loc2.getY()) <= distance && Math.abs(loc1.getZ() - loc2.getZ()) <= distance;
    }

    public static boolean locationMatchExact(Location loc1, Location loc2) {
        return locationMatchExact(loc1, loc2, 0);
    }

    public static boolean locationMatchExact(Location loc1, Location loc2, double distance) {
        return loc1.distanceSquared(loc2) <= Math.pow(distance, 2);
    }

    public static void setChestOpened(List<Player> players, Block block, boolean opened) {
        if (block == null || !(block.getState() instanceof Chest)) {
            return;
        }
        byte open = opened ? (byte) 1 : (byte) 0;
        for (Player player : players) {
            player.playNote(block.getLocation(), (byte) 1, open);
            if (isDoubleChest(block)) {
                player.playNote(getSecondChest(block).getLocation(), (byte) 1, open);
            }
        }
    }

    public static void setChestOpened(Player player, Block block, boolean opened) {
        if (block == null || !(block.getState() instanceof Chest)) {
            return;
        }
        byte open = opened ? (byte) 1 : (byte) 0;
        player.playNote(block.getLocation(), (byte) 1, open);
        if (isDoubleChest(block)) {
            player.playNote(getSecondChest(block).getLocation(), (byte) 1, open);
        }
    }

    private static double difInRandDirection(double max, double min) {
        return (rand.nextBoolean() ? 1 : -1) * (rand.nextDouble() * Math.abs(max - min) + Math.abs(min));
    }
} public class BuyableItemData {
    private int id, data, cost, amount, level;
    private String name, upgName;

    public BuyableItemData(int id, int data, int amount) {
        this(id, data, new ItemStack(id, 1).getItemMeta().getDisplayName(), new ItemStack(id, 1).getItemMeta().getDisplayName(), 0, amount, 0);
    }

    public BuyableItemData(int id, int data, String name, String upgName, int cost, int amount, int level) {
        this.id = id;
        this.data = data;
        this.name = name;
        this.upgName = upgName;
        this.cost = cost;
        this.amount = amount;
        this.level = level;
    }

    public BuyableItemData(int id, String name, int cost) {
        this(id, name, cost, 1);
    }

    public BuyableItemData(int id, String name, int cost, int level) {
        this(id, name, name + " - UPG", cost, level);
    }

    public BuyableItemData(int id, String name, String upgName, int cost, int level) {
        this(id, 0, name, upgName, cost, 1, level);
    }

    public int getAmount() {
        return amount;
    }

    public int getCost() {
        return cost;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getRequiredLevel() {
        return level;
    }

    public String getUpgName() {
        return upgName;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public void setCost(int cost) {
        this.cost = cost;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setRequiredLevel(int level) {
        this.level = level;
    }

    public void setUpgName(String upgName) {
        this.upgName = upgName;
    }

    public ItemStack toItemStack() {
        return toItemStack(false);
    }

    public ItemStack toItemStack(boolean upgraded) {
        ItemStack stack = new ItemStack(id, amount);
        stack.setDurability((short) data);
        stack.getItemMeta().setDisplayName(upgraded ? upgName : name);
        return stack;
    }
}
 public class Region {
    private Location loc1, loc2, loc3, loc4, loc5, loc6, loc7, loc8, center;
    private double lowX, highX, lowZ, highZ, lowY, highY, midX, midY, midZ;
    private World world;

    /**
     * Creates a new 3D region from 2 corner points.
     * @param start The first corner selected
     * @param end The second corner selected
     */
    public Region(Location start, Location end) {
        world = start.getWorld();
        double startX = start.getX();
        double startY = start.getY();
        double startZ = start.getZ();
        double endX = end.getX();
        double endY = end.getY();
        double endZ = end.getZ();
        highX = startX > endX ? startX : endX;
        lowX = startX <= endX ? startX : endX;
        highY = startY > endY ? startY : endY;
        lowY = startY <= endY ? startY : endY;
        highZ = startZ > endZ ? startZ : endZ;
        lowZ = startZ <= endZ ? startZ : endZ;
        midX = lowX + (highX - lowX) / 2;
        midY = lowY + (highY - lowY) / 2;
        midZ = lowZ + (highZ - lowZ) / 2;
        center = new Location(world, midX, midY, midZ);
        loc1 = new Location(world, lowX, highY, highZ);
        loc2 = new Location(world, highX, highY, highZ);
        loc3 = new Location(world, highX, highY, lowZ);
        loc4 = new Location(world, lowX, highY, lowZ);
        loc5 = new Location(world, lowX, lowY, highZ);
        loc6 = new Location(world, highX, lowY, highZ);
        loc7 = new Location(world, highX, lowY, lowZ);
        loc8 = new Location(world, lowX, lowY, lowZ);
    }

    /**
     * Checks if the location is contained inside the region.
     * @param loc The location to check for
     * @return Whether or not the location is contained in the region
     */
    public boolean contains(Location loc) {
        boolean Xs = overlap_1D(lowX, highX, loc.getBlockX(), loc.getBlockX());
        boolean Ys = overlap_1D(lowY, highY, loc.getBlockY(), loc.getBlockY());
        boolean Zs = overlap_1D(lowZ, highZ, loc.getBlockZ(), loc.getBlockZ());
        return Xs && Ys && Zs;
    }

    /**
     * Checks if the player is contained inside the region.
     * @param p The player to check for
     * @return Whether or not the player is contained in the region
     */
    public boolean contains(Player p) {
        return contains(p.getLocation());
    }

    /**
     * Gets the location at the very center of this region.
     * This may be slightly inaccurate as an estimate, since the location
     * is found using doubles, and block locations can only be integers.
     * Therefore, the getBlock() method for this center location may get
     * a block next to the middle, if the middle is between two or more blocks.
     * @return The location at (or close) to the center of the region
     */
    public Location getCenter() {
        return center;
    }

    /**
     * Gets the corner that matches the given number.
     * Corners are matched up to numbers in this order: <br>
     * 1. Low X, High Y, High Z <br>
     * 2. High X, High Y, High Z <br>
     * 3. High X, High Y, Low Z <br>
     * 4. Low X, High Y, Low Z <br>
     * 5. Low X, Low Y, High Z <br>
     * 6. High X, Low Y, High Z <br>
     * 7. High X, Low Y, Low Z <br>
     * 8. Low X, Low Y, Low Z
     * @param corner The corner number from 1-8
     * @return The corner corresponding to the number
     */
    public Location getCorner(int corner) {
        Location[] locs = new Location[] {loc1, loc2, loc3, loc4, loc5, loc6, loc7, loc8};
        return locs[corner];
    }

    /**
     * Gets the highest X of all 8 locations in this region.
     * @return The highest X value for this region
     */
    public double getHighestX() {
        return highX;
    }

    /**
     * Gets the highest Y of all 8 locations in this region.
     * @return The highest Y value for this region
     */
    public double getHighestY() {
        return highY;
    }

    /**
     * Gets the highest Z of all 8 locations in this region.
     * @return The highest Z value for this region
     */
    public double getHighestZ() {
        return highZ;
    }

    /**
     * Gets the lowest X of all 8 locations in this region.
     * @return The lowest X value for this region
     */
    public double getLowestX() {
        return lowX;
    }

    /**
     * Gets the lowest Y of all 8 locations in this region.
     * @return The lowest Y value for this region
     */
    public double getLowestY() {
        return lowY;
    }

    /**
     * Gets the lowest Z of all 8 locations in this region.
     * @return The lowest Z value for this region
     */
    public double getLowestZ() {
        return lowZ;
    }

    /**
     * Checks if the given region touches or overlaps this region.
     * @param other The region to check for
     * @return Whether or not they touch or overlap
     */
    public boolean overlaps(Region other) {
        boolean Xs = overlap_1D(lowX, highX, other.getLowestX(), other.getHighestX());
        boolean Ys = overlap_1D(lowY, highY, other.getLowestY(), other.getHighestY());
        boolean Zs = overlap_1D(lowZ, highZ, other.getLowestZ(), other.getHighestZ());
        return Xs && Ys && Zs;
    }

    /* Checks a 1D rectangle for overlap. */
    private boolean overlap_1D(double low1, double high1, double low2, double high2) {
        return low1 <= low2 ? low2 <= high1 : low1 <= high2;
    }
}
 public final class SerialLocation implements Serializable {
    private static final long serialVersionUID = 2084461835352080670L;

    /**
     * Gets the org.bukkit.Location back from any SerializableLocation.
     * 
     * @param l The SerializableLocation to be returned as a Location
     * @return An instance of Location
     */
    public static Location returnLocation(SerialLocation l) {
        float pitch = l.pitch;
        float yaw = l.yaw;
        double x = l.x;
        double y = l.y;
        double z = l.z;
        World world = Bukkit.getWorld(l.world);
        Location location = new Location(world, x, y, z, yaw, pitch);
        return location;
    }

    private transient Location loc;
    private final String uuid;
    private final String world;
    private final double x, y, z;
    private final float yaw, pitch;

    /**
     * Creates a new SerializableLocation instance of any org.bukkit.Location.
     * 
     * @param l The location to be serialized
     */
    public SerialLocation(Location l) {
        world = l.getWorld().getName();
        uuid = l.getWorld().getUID().toString();
        x = l.getX();
        y = l.getY();
        z = l.getZ();
        yaw = l.getYaw();
        pitch = l.getPitch();
    }

    /**
     * Creates a new SerializableLocation instance of any org.bukkit.Location as a map.
     * 
     * @param map The map to be made into a SerializableLocation
     */
    public SerialLocation(Map<String, Object> map) {
        world = (String) map.get("world");
        uuid = (String) map.get("uuid");
        x = (Double) map.get("x");
        y = (Double) map.get("y");
        z = (Double) map.get("z");
        yaw = ((Float) map.get("yaw")).floatValue();
        pitch = ((Float) map.get("pitch")).floatValue();
    }

    public int getBlockX() {
        return (int) x;
    }

    public int getBlockY() {
        return (int) y;
    }

    public int getBlockZ() {
        return (int) z;
    }

    /**
     * Gets the SerializableLocation as an org.bukkit.Location for the defined server.
     * 
     * @param server The server to get the location from
     * @return A Location from this SerializableLocation, that is located on the server
     */
    public final Location getLocation(Server server) {
        if (loc == null) {
            World world = server.getWorld(uuid);
            if (world == null) {
                world = server.getWorld(this.world);
            }
            loc = new Location(world, x, y, z, yaw, pitch);
        }
        return loc;
    }

    public World getWorld() {
        return Bukkit.getWorld(world);
    }

    /**
     * Returns a map of the SerializableLocation.
     * 
     * @return A map where the key is the type of argument, and the value is the argument
     */
    public final Map<String, Object> serialize() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("world", world);
        map.put("uuid", uuid);
        map.put("x", x);
        map.put("y", y);
        map.put("z", z);
        map.put("yaw", yaw);
        map.put("pitch", pitch);
        return map;
    }

    @Override public String toString() {
        return "SerialLocation:(world: " + world + ", " + x + ", " + y + ", " + z + ", pitch: " + pitch + ", yaw: " + yaw + ")";
    }
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import com.github.jamesnorris.ablockalypse.Ablockalypse;
import com.github.jamesnorris.ablockalypse.behavior.MapDatable;
import com.github.jamesnorris.ablockalypse.enumerated.Setting;
import com.github.jamesnorris.ablockalypse.enumerated.ZAEffect;
import com.github.jamesnorris.ablockalypse.enumerated.ZASound;
import com.github.jamesnorris.ablockalypse.threading.DelayedTask;
import com.github.jamesnorris.ablockalypse.threading.RepeatingTask;
import com.github.jamesnorris.ablockalypse.utility.AblockalypseUtility;
import com.github.jamesnorris.ablockalypse.utility.BukkitUtility;
import com.github.jamesnorris.ablockalypse.utility.BuyableItemData;
import com.github.jamesnorris.ablockalypse.utility.Region;
import com.github.jamesnorris.ablockalypse.utility.SerialLocation;
package com.github.jamesnorris.ablockalypse.aspect;




public class MysteryBox extends SpecificGameAspect implements MapDatable {
    private boolean active = true, showing;
    private Object chest;
    private Game game;
    private Location loc;
    private Location[] locs;
    private Random rand = new Random();
    private int uses, cost = (Integer) Setting.MYSTERY_BOX_COST.getSetting();
    private UUID uuid = UUID.randomUUID();
    private ItemStack lastShownItem;
    private ZAPlayer lastShownZAPlayer;

    /**
     * Creates a new instance of the MysteryBox.
     * 
     * @param game The game to involve this mystery chest in
     * @param loc A location on the chest
     */
    public MysteryBox(Game game, Location loc) {
        this(game, loc, game.getActiveMysteryChest() == null);
    }

    /**
     * Creates a new instance of the MysteryBox.
     * 
     * @param game The game to involve this mystery chest in
     * @param loc A location on the chest
     * @param active Whether or not this chest should be active
     */
    public MysteryBox(Game game, Location loc, boolean active) {
        super(game, loc, !game.hasStarted());
        this.loc = loc;
        Block b2 = BukkitUtility.getSecondChest(loc.getBlock());
        locs = b2 != null ? new Location[] {loc, b2.getLocation()} : new Location[] {loc};
        chest = loc.getBlock().getState();
        this.game = game;
        this.active = active;
        uses = rand.nextInt(8) + 2;
        load();
    }

    public MysteryBox(Map<String, Object> savings) {
        this(Ablockalypse.getData().getGame((String) savings.get("game_name"), true), SerialLocation.returnLocation((SerialLocation) savings.get("location")), (Boolean) savings.get("is_active"));
        uses = (Integer) savings.get("uses_left");
        cost = (Integer) savings.get("cost");
        uuid = savings.get("uuid") == null ? uuid : (UUID) savings.get("uuid");
    }

    /**
     * Gets the uses that this chest has before the mystery chest moves to another location.
     * 
     * @return The uses before movement
     */
    public int getActiveUses() {
        return uses;
    }

    /**
     * Gets the chest associated with this instance.
     * 
     * @return The chest associated with this instance
     */
    public Chest getChest() {
        if (chest instanceof Chest) {
            return (Chest) chest;
        }
        return null;
    }

    @Override public List<Block> getDefiningBlocks() {
        List<Block> blocks = new ArrayList<Block>();
        for (Location location : locs) {
            blocks.add(location.getBlock());
        }
        return blocks;
    }

    public ItemStack getLastShownItem() {
        return lastShownItem;
    }

    public ZAPlayer getLastShownZAPlayer() {
        return lastShownZAPlayer;
    }

    @Override public int getLoadPriority() {
        return 2;
    }

    /**
     * Gets the location that the chest is located at.
     * 
     * @return The location of the chest
     */
    @Override public Location getLocation() {
        return locs[0];
    }

    @Override public Location getPointClosestToOrigin() {
        return loc;
    }

    @Override public Map<String, Object> getSave() {
        Map<String, Object> savings = new HashMap<String, Object>();
        savings.put("uuid", getUUID());
        savings.put("is_active", active);
        savings.put("game_name", game.getName());
        savings.put("location", loc == null ? null : new SerialLocation(loc));
        savings.put("uses_left", uses);
        savings.put("cost", cost);
        return savings;
    }

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

    /**
     * Checks if the chest is active or not.
     * 
     * @return Whether or not the chest is active and can be used
     */
    public boolean isActive() {
        return active;
    }

    public boolean isShowingItem() {
        return showing;
    }

    @Override public void onGameEnd() {
        setBlinking(true);
    }

    @Override public void onGameStart() {
        setBlinking(false);
    }

    /**
     * Randomizes the contents of the MysteryChest.
     * 
     * @param zap The zaplayer to give the random item to
     */
    @SuppressWarnings("deprecation") public void openToRandomItem(ZAPlayer zap) {
        final Player player = zap.getPlayer();
        if (!active) {
            player.sendMessage(ChatColor.RED + "This box is currently inactive!");
            return;
        }
        Map<Integer, BuyableItemData> maps = Ablockalypse.getExternal().getItemFileManager().getSignItemMap();
        ArrayList<Integer> idList = new ArrayList<Integer>();
        for (int testId : maps.keySet()) {
            idList.add(testId);
        }
        int id = idList.get(rand.nextInt(idList.size()));// Solution for the lack of integer positions in item maps
        BuyableItemData map = maps.get(id);
        if (zap.getPoints() < cost) {
            zap.getPlayer().sendMessage(ChatColor.RED + "You have " + zap.getPoints() + " / " + cost + " points to buy this.");
            return;
        }
        final Location topView = locs[1] != null ? new Region(locs[0], locs[1]).getCenter().clone().add(0, 1, 0) : locs[0].clone().add(0, 1, 0);
        final Item dropped = showItem(new ItemStack(Material.SKELETON_SKULL, 1, (short) 4), 140);
        lastShownItem = map.toItemStack();
        lastShownZAPlayer = zap;
        if ((Boolean) Setting.EXTRA_EFFECTS.getSetting()) {
            ZASound.ACHIEVEMENT.play(topView);
        }
        AblockalypseUtility.scheduleNearbyWarning(topView, ChatColor.GRAY + "Press " + ChatColor.AQUA + "SHIFT" + ChatColor.GRAY + " to take your item.", 2, 3.5, 2, 7000);
        new RepeatingTask(20, (Boolean) Setting.EXTRA_EFFECTS.getSetting()) {
            @Override public void run() {
                ZAEffect.FLAMES.play(topView);
            }
        };
        new DelayedTask(140, true) {