public class Arcadia extends JavaPlugin {

    private ArcadiaAPI api;
    public Configuration mainConfiguration;

    public void onEnable() {
        this.api = new ArcadiaAPI(this);
        new File(this.getDataFolder().getPath() + "/translations/").mkdirs();
        if(new File(this.getDataFolder().getPath() + "/translations/").listFiles().length == 0) {
            this.getAPI().getTranslationManager().saveDefaultLocale("en_us.properties");
        }
        this.getAPI().getTranslationManager().refreshCache();

        this.getServer().getPluginManager().registerEvents(new ConnectionListener(), this);
        this.getServer().getPluginManager().registerEvents(new DamageListener(), this);
        this.getServer().getPluginManager().registerEvents(new WorldListener(), this);
        this.getServer().getPluginManager().registerEvents(new SpectatorListener(), this);
        this.getServer().getPluginManager().registerEvents(new ChatListener(), this);
        this.getServer().getPluginManager().registerEvents(new ItemListener(), this);

        this.getCommand("spec").setExecutor(new SpectateCommand());
        this.getCommand("setgame").setExecutor(new SetGameCommand());

        this.mainConfiguration = new Configuration(this.getDataFolder(), "config.yml", this);
        this.mainConfiguration.copyDefaults();

        this.getAPI().getTranslationManager().setDefaultLocale(mainConfiguration.fetch().getString("language"));
        this.getAPI().getTranslationManager().autoDetectLanguage
            = this.mainConfiguration.fetch().getBoolean("auto-detect-language");

        this.getAPI().getGameRegistry().registerGame(BombardmentGame.class);
        this.getAPI().getGameRegistry().registerGame(ColorShuffleGame.class);
        this.getAPI().getGameRegistry().registerGame(DeadEndGame.class);
        this.getAPI().getGameRegistry().registerGame(ElectricFloorGame.class);
        this.getAPI().getGameRegistry().registerGame(HorseRaceGame.class);
        this.getAPI().getGameRegistry().registerGame(KingOfTheHillGame.class);
        this.getAPI().getGameRegistry().registerGame(MineFieldGame.class);
        this.getAPI().getGameRegistry().registerGame(MusicalMinecartsGame.class);
        this.getAPI().getGameRegistry().registerGame(PotionDropGame.class);
        this.getAPI().getGameRegistry().registerGame(RainbowJumpGame.class);
        this.getAPI().getGameRegistry().registerGame(RedLightGreenLightGame.class);
        this.getAPI().getGameRegistry().registerGame(SpleefGame.class);
        this.getAPI().getGameRegistry().registerGame(TrampolinioGame.class);
        this.getAPI().getGameRegistry().registerGame(WingRushGame.class);

        this.getAPI().getGameRegistry().setVotingData(BombardmentGame.class,
            new VotingData(new MaterialData(Material.COAL_BLOCK),
                getAPI().getTranslationManager().fetchTranslation("game.bombardment.name").build()));
        this.getAPI().getGameRegistry().setVotingData(ColorShuffleGame.class,
                new VotingData(new MaterialData(Material.WOOL),
                        getAPI().getTranslationManager().fetchTranslation("game.colorshuffle.name").build()));
        this.getAPI().getGameRegistry().setVotingData(DeadEndGame.class,
                new VotingData(new MaterialData(Material.GOLD_BLOCK),
                        getAPI().getTranslationManager().fetchTranslation("game.deadend.name").build()));
        this.getAPI().getGameRegistry().setVotingData(ElectricFloorGame.class,
                new VotingData(new MaterialData(Material.STAINED_GLASS, (byte) 14),
                        getAPI().getTranslationManager().fetchTranslation("game.electricfloor.name").build()));
        this.getAPI().getGameRegistry().setVotingData(HorseRaceGame.class,
                new VotingData(new MaterialData(Material.GOLD_BARDING),
                        getAPI().getTranslationManager().fetchTranslation("game.horserace.name").build()));
        this.getAPI().getGameRegistry().setVotingData(KingOfTheHillGame.class,
                new VotingData(new MaterialData(Material.RAW_FISH),
                        getAPI().getTranslationManager().fetchTranslation("game.koth.name").build()));
        this.getAPI().getGameRegistry().setVotingData(MineFieldGame.class,
                new VotingData(new MaterialData(Material.STONE_PLATE),
                        getAPI().getTranslationManager().fetchTranslation("game.minefield.name").build()));
        this.getAPI().getGameRegistry().setVotingData(MusicalMinecartsGame.class,
                new VotingData(new MaterialData(Material.MINECART),
                        getAPI().getTranslationManager().fetchTranslation("game.musicalminecarts.name").build()));
        this.getAPI().getGameRegistry().setVotingData(PotionDropGame.class,
                new VotingData(new MaterialData(Material.POTION, (byte) 8197),
                        getAPI().getTranslationManager().fetchTranslation("game.potiondrop.name").build()));
        this.getAPI().getGameRegistry().setVotingData(RainbowJumpGame.class,
                new VotingData(new MaterialData(Material.WOOL, (byte) 10),
                        getAPI().getTranslationManager().fetchTranslation("game.rainbowjump.name").build()));
        this.getAPI().getGameRegistry().setVotingData(RedLightGreenLightGame.class,
                new VotingData(new MaterialData(Material.WOOL, (byte) 14),
                        getAPI().getTranslationManager().fetchTranslation("game.redlightgreenlight.name").build()));
        this.getAPI().getGameRegistry().setVotingData(SpleefGame.class,
                new VotingData(new MaterialData(Material.SNOW_BLOCK),
                        getAPI().getTranslationManager().fetchTranslation("game.spleef.name").build()));
        this.getAPI().getGameRegistry().setVotingData(TrampolinioGame.class,
                new VotingData(new MaterialData(Material.WOOL, (byte) 15),
                        getAPI().getTranslationManager().fetchTranslation("game.trampolinio.name").build()));
        this.getAPI().getGameRegistry().setVotingData(WingRushGame.class,
                new VotingData(new MaterialData(Material.ELYTRA),
                        getAPI().getTranslationManager().fetchTranslation("game.wingrush.name").build()));

        this.mainConfiguration.fetch().getStringList("default-rotation").forEach(line -> {
            try {
                Class<? extends BaseGame> clazz = (Class<? extends BaseGame>) Class.forName(line);
                this.getAPI().getGameManager().getRotation().addGame(clazz);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        this.mainConfiguration.fetch().getStringList("map-directories").forEach(line -> {
            File mapFolder = new File(line.replace("%data_folder%", this.getDataFolder().getPath()));
            mapFolder.mkdirs();
            this.getAPI().getMapRegistry().loadMaps(mapFolder);
        });

        if(this.mainConfiguration.fetch().getBoolean("randomize")) {
            this.getAPI().getGameManager().getRotation().setRotationOrder(RotationOrder.RANDOM);
        }
        if(this.mainConfiguration.fetch().getBoolean("allow-game-voting")) {
            this.getAPI().getGameManager().getRotation().setRotationOrder(RotationOrder.VOTE);
        }
        if(this.mainConfiguration.fetch().getBoolean("randomize-first-rotation")) {
            this.getAPI().getGameManager().getRotation().shuffle();
        }
        this.nextGameInRotation(true);

        if(this.getServer().getPluginManager().isPluginEnabled("UltraCosmetics")) {
            this.getServer().getPluginManager().registerEvents(new UltraCosmeticsSupport(), this);
        }
    }

    public void onDisable() {
        this.getAPI().getGameManager().setGameState(GameState.FINISHED);
        for(Player player : Bukkit.getOnlinePlayers()) {
            if(player.getVehicle() != null) {
                Entity vehicle = player.getVehicle();
                vehicle.eject();
                vehicle.remove();
            }
            this.getAPI().getGameManager().setAlive(player, false);
        }
        this.getAPI().getTranslationManager().kickPlayers("common.server-restarting");
        removeCustomWorlds();
    }

    public void removeCustomWorlds() {
        if(this.getAPI().getMapRegistry().getCurrentWorld() != null) {
            this.getAPI().getMapRegistry().unloadWorld(this.getAPI().getMapRegistry().getCurrentWorld());
        }
        if(this.getAPI().getMapRegistry().oldWorld != null) {
            this.getAPI().getMapRegistry().unloadWorld(this.getAPI().getMapRegistry().oldWorld);
        }
    }

    /**
     * Switches to the next game in the rotation!
     */
    public void nextGameInRotation(boolean first) {
        if(!first) this.getAPI().getGameManager().getRotation().nextGame();
        List<GameMap> possibleMaps = this.getAPI().getGameRegistry().getMaps(this.getAPI().getGameManager().getRotation().getCurrentGame());
        this.getAPI().getGameManager().setCurrentGame(
                this.getAPI().getGameManager().getRotation().getCurrentGame(),
                possibleMaps.get(new Random().nextInt(possibleMaps.size())));
    }

    /**
     * Returns the fun API for this madness.
     * @return
     */
    public ArcadiaAPI getAPI() {
        return this.api;
    }
} public class Cuboid implements Iterable<Block>, Cloneable, ConfigurationSerializable {

    protected final String worldName;
	protected final int x1, y1, z1;
	protected final int x2, y2, z2;

	/**
	 * Construct a Cuboid given two Location objects which represent any two corners
	 * of the Cuboid.
	 *
	 * @param l1 one of the corners
	 * @param l2 the other corner
	 */
	public Cuboid(Location l1, Location l2) {
		if (! l1.getWorld().equals(l2.getWorld())) {
			throw new IllegalArgumentException("locations must be on the same world");
		}
		worldName = l1.getWorld().getName();
		x1 = Math.min(l1.getBlockX(), l2.getBlockX());
		y1 = Math.min(l1.getBlockY(), l2.getBlockY());
		z1 = Math.min(l1.getBlockZ(), l2.getBlockZ());
		x2 = Math.max(l1.getBlockX(), l2.getBlockX());
		y2 = Math.max(l1.getBlockY(), l2.getBlockY());
		z2 = Math.max(l1.getBlockZ(), l2.getBlockZ());
	}

	/**
	 * Construct a one-block Cuboid at the given Location of the Cuboid.
	 *
	 * @param l1 location of the Cuboid
	 */
	public Cuboid(Location l1) {
		this(l1, l1);
	}

	/**
	 * Copy constructor.
	 *
	 * @param other the Cuboid to copy
	 */
	public Cuboid(Cuboid other) {
		this(other.getWorld().getName(), other.x1, other.y1, other.z1, other.x2, other.y2, other.z2);
	}

	/**
	 * Construct a Cuboid in the given World and xyz co-ordinates
	 *
	 * @param world the Cuboid's world
	 * @param x1 X co-ordinate of corner 1
	 * @param y1 Y co-ordinate of corner 1
	 * @param z1 Z co-ordinate of corner 1
	 * @param x2 X co-ordinate of corner 2
	 * @param y2 Y co-ordinate of corner 2
	 * @param z2 Z co-ordinate of corner 2
	 */
	public Cuboid(World world, int x1, int y1, int z1, int x2, int y2, int z2) {
		this.worldName = world.getName();
		this.x1 = Math.min(x1, x2);
		this.x2 = Math.max(x1, x2);
		this.y1 = Math.min(y1, y2);
		this.y2 = Math.max(y1, y2);
		this.z1 = Math.min(z1, z2);
		this.z2 = Math.max(z1, z2);
	}

	/**
	 * Construct a Cuboid in the given world name and xyz co-ordinates.
	 *
	 * @param worldName the Cuboid's world name
	 * @param x1 X co-ordinate of corner 1
	 * @param y1 Y co-ordinate of corner 1
	 * @param z1 Z co-ordinate of corner 1
	 * @param x2 X co-ordinate of corner 2
	 * @param y2 Y co-ordinate of corner 2
	 * @param z2 Z co-ordinate of corner 2
	 */
	private Cuboid(String worldName, int x1, int y1, int z1, int x2, int y2, int z2) {
		this.worldName = worldName;
		this.x1 = Math.min(x1, x2);
		this.x2 = Math.max(x1, x2);
		this.y1 = Math.min(y1, y2);
		this.y2 = Math.max(y1, y2);
		this.z1 = Math.min(z1, z2);
		this.z2 = Math.max(z1, z2);
	}

	public Cuboid(Map<String,Object> map) {
		worldName = (String)map.get("worldName");
		x1 = (Integer) map.get("x1");
		x2 = (Integer) map.get("x2");
		y1 = (Integer) map.get("y1");
		y2 = (Integer) map.get("y2");
		z1 = (Integer) map.get("z1");
		z2 = (Integer) map.get("z2");
	}

	@Override
	public Map<String, Object> serialize() {
		Map<String,Object> map = new HashMap<String, Object>();
		map.put("worldName", worldName);
		map.put("x1", x1);
		map.put("y1", y1);
		map.put("z1", z1);
		map.put("x2", x2);
		map.put("y2", y2);
		map.put("z2", z2);
		return map;
	}

	/**
	 * Get the Location of the lower northeast corner of the Cuboid (minimum XYZ
	 * co-ordinates).
	 *
	 * @return Location of the lower northeast corner
	 */
	public Location getLowerNE() {
		return new Location(getWorld(), x1, y1, z1);
	}

	/**
	 * Get the Location of the upper southwest corner of the Cuboid (maximum XYZ
	 * co-ordinates).
	 *
	 * @return Location of the upper southwest corner
	 */
	public Location getUpperSW() {
		return new Location(getWorld(), x2, y2, z2);
	}

	/**
	 * Get the the centre of the Cuboid
	 *
	 * @return Location at the centre of the Cuboid
	 */
	public Location getCenter() {
		int x1 = getUpperX() + 1;
		int y1 = getUpperY() + 1;
		int z1 = getUpperZ() + 1;
		return new Location(getWorld(), getLowerX() + (x1 - getLowerX()) / 2.0,
		                    getLowerY() + (y1 - getLowerY()) / 2.0,
		                    getLowerZ() + (z1 - getLowerZ()) / 2.0);
	}

	/**
	 * Get the Cuboid's world.
	 *
	 * @return the World object representing this Cuboid's world
	 * @throws IllegalStateException if the world is not loaded
	 */
	public World getWorld() {
		World world = Bukkit.getWorld(worldName);
		if (world == null) {
			throw new IllegalStateException("world '" + worldName + "' is not loaded");
		}
		return world;
	}

	/**
	 * Get the size of this Cuboid along the X axis
	 *
	 * @return	Size of Cuboid along the X axis
	 */
	public int getSizeX() {
		return (x2 - x1) + 1;
	}

	/**
	 * Get the size of this Cuboid along the Y axis
	 *
	 * @return	Size of Cuboid along the Y axis
	 */
	public int getSizeY() {
		return (y2 - y1) + 1;
	}

	/**
	 * Get the size of this Cuboid along the Z axis
	 *
	 * @return	Size of Cuboid along the Z axis
	 */
	public int getSizeZ() {
		return (z2 - z1) + 1;
	}

	/**
	 * Get the minimum X co-ordinate of this Cuboid
	 *
	 * @return	the minimum X co-ordinate
	 */
	public int getLowerX() {
		return x1;
	}

	/**
	 * Get the minimum Y co-ordinate of this Cuboid
	 *
	 * @return	the minimum Y co-ordinate
	 */
	public int getLowerY() {
		return y1;
	}

	/**
	 * Get the minimum Z co-ordinate of this Cuboid
	 *
	 * @return	the minimum Z co-ordinate
	 */
	public int getLowerZ() {
		return z1;
	}

	/**
	 * Get the maximum X co-ordinate of this Cuboid
	 *
	 * @return	the maximum X co-ordinate
	 */
	public int getUpperX() {
		return x2;
	}

	/**
	 * Get the maximum Y co-ordinate of this Cuboid
	 *
	 * @return	the maximum Y co-ordinate
	 */
	public int getUpperY() {
		return y2;
	}

	/**
	 * Get the maximum Z co-ordinate of this Cuboid
	 *
	 * @return	the maximum Z co-ordinate
	 */
	public int getUpperZ() {
		return z2;
	}

	/**
	 * Get the Blocks at the eight corners of the Cuboid.
	 *
	 * @return array of Block objects representing the Cuboid corners
	 */
	public Block[] corners() {
		Block[] res = new Block[8];
		World w = getWorld();
		res[0] = w.getBlockAt(x1, y1, z1);
		res[1] = w.getBlockAt(x1, y1, z2);
		res[2] = w.getBlockAt(x1, y2, z1);
		res[3] = w.getBlockAt(x1, y2, z2);
		res[4] = w.getBlockAt(x2, y1, z1);
		res[5] = w.getBlockAt(x2, y1, z2);
		res[6] = w.getBlockAt(x2, y2, z1);
		res[7] = w.getBlockAt(x2, y2, z2);
		return res;
	}

	/**
	 * Expand the Cuboid in the given direction by the given amount.  Negative amounts will
	 * shrink the Cuboid in the given direction.  Shrinking a cuboid's face past the opposite face
	 * is not an error and will return a valid Cuboid.
	 *
	 * @param dir	the direction in which to expand
	 * @param amount	the number of blocks by which to expand
	 * @return	a new Cuboid expanded by the given direction and amount
	 */
	public Cuboid expand(CuboidDirection dir, int amount) {
		switch (dir) {
		case North:
			return new Cuboid(worldName, x1 - amount, y1, z1, x2, y2, z2);
		case South:
			return new Cuboid(worldName, x1, y1, z1, x2 + amount, y2, z2);
		case East:
			return new Cuboid(worldName, x1, y1, z1 - amount, x2, y2, z2);
		case West:
			return new Cuboid(worldName, x1, y1, z1, x2, y2, z2 + amount);
		case Down:
			return new Cuboid(worldName, x1, y1 - amount, z1, x2, y2, z2);
		case Up:
			return new Cuboid(worldName, x1, y1, z1, x2, y2 + amount, z2);
		default:
			throw new IllegalArgumentException("invalid direction " + dir);
		}
	}

	/**
	 * Shift the Cuboid in the given direction by the given amount.
	 *
	 * @param dir	the direction in which to shift
	 * @param amount	the number of blocks by which to shift
	 * @return	a new Cuboid shifted by the given direction and amount
	 */
	public Cuboid shift(CuboidDirection dir, int amount) {
		return expand(dir, amount).expand(dir.opposite(), -amount);
	}

	/**
	 * Outset (grow) the Cuboid in the given direction by the given amount.
	 *
	 * @param dir	the direction in which to outset (must be Horizontal, Vertical, or Both)
	 * @param amount	the number of blocks by which to outset
	 * @return	a new Cuboid outset by the given direction and amount
	 */
	public Cuboid outset(CuboidDirection dir, int amount) {
		Cuboid c;
		switch (dir) {
		case Horizontal:
			c = expand(CuboidDirection.North, amount).expand(CuboidDirection.South, amount).expand(CuboidDirection.East, amount).expand(CuboidDirection.West, amount);
			break;
		case Vertical:
			c = expand(CuboidDirection.Down, amount).expand(CuboidDirection.Up, amount);
			break;
		case Both:
			c = outset(CuboidDirection.Horizontal, amount).outset(CuboidDirection.Vertical, amount);
			break;
		default:
			throw new IllegalArgumentException("invalid direction " + dir);
		}
		return c;
	}

	/**
	 * Inset (shrink) the Cuboid in the given direction by the given amount.  Equivalent
	 * to calling outset() with a negative amount.
	 *
	 * @param dir the direction in which to inset (must be Horizontal, Vertical, or Both)
	 * @param amount	the number of blocks by which to inset
	 * @return	a new Cuboid inset by the given direction and amount
	 */
	public Cuboid inset(CuboidDirection dir, int amount) {
		return outset(dir, -amount);
	}

	/**
	 * Return true if the point at (x,y,z) is contained within this Cuboid.
	 *
	 * @param x	the X co-ordinate
	 * @param y	the Y co-ordinate
	 * @param z	the Z co-ordinate
	 * @return	true if the given point is within this Cuboid, false otherwise
	 */
	public boolean contains(int x, int y, int z) {
		return x >= x1 && x <= x2 && y >= y1 && y <= y2 && z >= z1 && z <= z2;
	}

	/**
	 * Check if the given Block is contained within this Cuboid.
	 *
	 * @param b	the Block to check for
	 * @return	true if the Block is within this Cuboid, false otherwise
	 */
	public boolean contains(Block b) {
		return contains(b.getLocation());
	}

	/**
	 * Check if the given Location is contained within this Cuboid.
	 *
	 * @param l	the Location to check for
	 * @return	true if the Location is within this Cuboid, false otherwise
	 */
	public boolean contains(Location l) {
		return worldName.equals(l.getWorld().getName()) && contains(l.getBlockX(), l.getBlockY(), l.getBlockZ());
	}

	/**
	 * Get the volume of this Cuboid.
	 *
	 * @return	the Cuboid volume, in blocks
	 */
	public int volume() {
		return getSizeX() * getSizeY() * getSizeZ();
	}

	/**
	 * Get the average light level of all empty (air) blocks in the Cuboid.  Returns 0
	 * if there are no empty blocks.
	 *
	 * @return	the average light level of this Cuboid
	 */
	public byte averageLightLevel() {
		long total = 0;
		int n = 0;
		for (Block b : this) {
			if (b.isEmpty()) {
				total += b.getLightLevel();
				++n;
			}
		}
		return n > 0 ? (byte) (total / n) : 0;
	}

	/**
	 * Contract the Cuboid, returning a Cuboid with any air around the edges removed, just
	 * large enough to include all non-air blocks.
	 *
	 * @return a new Cuboid with no external air blocks
	 */
	public Cuboid contract() {
		return this.
				contract(CuboidDirection.Down).
				contract(CuboidDirection.South).
				contract(CuboidDirection.East).
				contract(CuboidDirection.Up).
				contract(CuboidDirection.North).
				contract(CuboidDirection.West);
	}

	/**
	 * Contract the Cuboid in the given direction, returning a new Cuboid which has no exterior empty space.
	 * E.g. a direction of Down will push the top face downwards as much as possible.
	 *
	 * @param dir	the direction in which to contract
	 * @return	a new Cuboid contracted in the given direction
	 */
	public Cuboid contract(CuboidDirection dir) {
		Cuboid face = getFace(dir.opposite());
		switch (dir) {
		case Down:
			while (face.containsOnly(Material.AIR) && face.getLowerY() > this.getLowerY()) {
				face = face.shift(CuboidDirection.Down, 1);
			}
			return new Cuboid(worldName, x1, y1, z1, x2, face.getUpperY(), z2);
		case Up:
			while (face.containsOnly(Material.AIR) && face.getUpperY() < this.getUpperY()) {
				face = face.shift(CuboidDirection.Up, 1);
			}
			return new Cuboid(worldName, x1, face.getLowerY(), z1, x2, y2, z2);
		case North:
			while (face.containsOnly(Material.AIR) && face.getLowerX() > this.getLowerX()) {
				face = face.shift(CuboidDirection.North, 1);
			}
			return new Cuboid(worldName, x1, y1, z1, face.getUpperX(), y2, z2);
		case South:
			while (face.containsOnly(Material.AIR) && face.getUpperX() < this.getUpperX()) {
				face = face.shift(CuboidDirection.South, 1);
			}
			return new Cuboid(worldName, face.getLowerX(), y1, z1, x2, y2, z2);
		case East:
			while (face.containsOnly(Material.AIR) && face.getLowerZ() > this.getLowerZ()) {
				face = face.shift(CuboidDirection.East, 1);
			}
			return new Cuboid(worldName, x1, y1, z1, x2, y2, face.getUpperZ());
		case West:
			while (face.containsOnly(Material.AIR) && face.getUpperZ() < this.getUpperZ()) {
				face = face.shift(CuboidDirection.West, 1);
			}
			return new Cuboid(worldName, x1, y1, face.getLowerZ(), x2, y2, z2);
		default:
			throw new IllegalArgumentException("Invalid direction " + dir);
		}
	}

	/**
	 * Get the Cuboid representing the face of this Cuboid.  The resulting Cuboid will be
	 * one block thick in the axis perpendicular to the requested face.
	 *
	 * @param dir	which face of the Cuboid to get
	 * @return	the Cuboid representing this Cuboid's requested face
	 */
	public Cuboid getFace(CuboidDirection dir	) {
		switch (dir) {
		case Down:
			return new Cuboid(worldName, x1, y1, z1, x2, y1, z2);
		case Up:
			return new Cuboid(worldName, x1, y2, z1, x2, y2, z2);
		case North:
			return new Cuboid(worldName, x1, y1, z1, x1, y2, z2);
		case South:
			return new Cuboid(worldName, x2, y1, z1, x2, y2, z2);
		case East:
			return new Cuboid(worldName, x1, y1, z1, x2, y2, z1);
		case West:
			return new Cuboid(worldName, x1, y1, z2, x2, y2, z2);
		default:
			throw new IllegalArgumentException("Invalid direction " + dir);
		}
	}

	/**
	 * Check if the Cuboid contains only blocks of the given type
	 *
	 * @param material the material to check for
	 * @return true if this Cuboid contains only blocks of the given type
	 */
	public boolean containsOnly(Material material) {
		for (Block b : this) {
			if (b.getType() != material) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Get the Cuboid big enough to hold both this Cuboid and the given one.
	 *
	 * @param other the other Cuboid to include
	 * @return	a new Cuboid large enough to hold this Cuboid and the given Cuboid
	 */
	public Cuboid getBoundingCuboid(Cuboid other) {
		if (other == null) {
			return this;
		}

		int xMin = Math.min(getLowerX(), other.getLowerX());
		int yMin = Math.min(getLowerY(), other.getLowerY());
		int zMin = Math.min(getLowerZ(), other.getLowerZ());
		int xMax = Math.max(getUpperX(), other.getUpperX());
		int yMax = Math.max(getUpperY(), other.getUpperY());
		int zMax = Math.max(getUpperZ(), other.getUpperZ());

		return new Cuboid(worldName, xMin, yMin, zMin, xMax, yMax, zMax);
	}

	/**
	 * Get a block relative to the lower NE point of the Cuboid.
	 *
	 * @param x	the X co-ordinate
	 * @param y	the Y co-ordinate
	 * @param z	the Z co-ordinate
	 * @return	the block at the given position
	 */
	public Block getRelativeBlock(int x, int y, int z) {
		return getWorld().getBlockAt(x1 + x, y1 + y, z1 + z);
	}

	/**
	 * Get a block relative to the lower NE point of the Cuboid in the given World.  This
	 * version of getRelativeBlock() should be used if being called many times, to avoid
	 * excessive calls to getWorld().
	 *
	 * @param w	the World
	 * @param x	the X co-ordinate
	 * @param y	the Y co-ordinate
	 * @param z	the Z co-ordinate
	 * @return	the block at the given position
	 */
	public Block getRelativeBlock(World w, int x, int y, int z) {
		return w.getBlockAt(x1 + x, y1 + y, z1 + z);
	}

	/**
	 * Get a list of the chunks which are fully or partially contained in this cuboid.
	 *
	 * @return a list of Chunk objects
	 */
	public List<Chunk> getChunks() {
		List<Chunk> res = new ArrayList<Chunk>();

		World w = getWorld();
		int x1 = getLowerX() & ~0xf; int x2 = getUpperX() & ~0xf;
		int z1 = getLowerZ() & ~0xf; int z2 = getUpperZ() & ~0xf;
		for (int x = x1; x <= x2; x += 16) {
			for (int z = z1; z <= z2; z += 16) {
				res.add(w.getChunkAt(x >> 4, z >> 4));
			}
		}
		return res;
	}

	/* (non-Javadoc)
	 * @see java.lang.Iterable#iterator()
	 */
	public Iterator<Block> iterator() {
		return new CuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#clone()
	 */
    @Override
	public Cuboid clone() throws CloneNotSupportedException {
		return new Cuboid(this);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Cuboid: " + worldName + "," + x1 + "," + y1 + "," + z1 + "=>" + x2 + "," + y2 + "," + z2;
	}

	public class CuboidIterator implements Iterator<Block> {
		private World w;
		private int baseX, baseY, baseZ;
		private int x, y, z;
		private int sizeX, sizeY, sizeZ;

		public CuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) {
			this.w = w;
			baseX = x1;
			baseY = y1;
			baseZ = z1;
			sizeX = Math.abs(x2 - x1) + 1;
			sizeY = Math.abs(y2 - y1) + 1;
			sizeZ = Math.abs(z2 - z1) + 1;
			x = y = z = 0;
		}

		public boolean hasNext() {
			return x < sizeX && y < sizeY && z < sizeZ;
		}

		public Block next() {
			Block b = w.getBlockAt(baseX + x, baseY + y, baseZ + z);
			if (++x >= sizeX) {
				x = 0;
				if (++y >= sizeY) {
					y = 0;
					++z;
				}
			}
			return b;
		}

		public void remove() {
			// nop
		}
	}

	public enum CuboidDirection {

		North, East, South, West, Up, Down, Horizontal, Vertical, Both, Unknown;

		public CuboidDirection opposite() {
			switch(this) {
			case North:
				return South;
			case East:
				return West;
			case South:
				return North;
			case West:
				return East;
			case Horizontal:
				return Vertical;
			case Vertical:
				return Horizontal;
			case Up:
				return Down;
			case Down:
				return Up;
			case Both:
				return Both;
			default:
				return Unknown;
			}
		}
	}

} public class Utils {

    public static String formatTime(int minutes, int seconds) {
        String temp = "";
        if(minutes < 10) temp+="0";
        temp+=minutes;
        temp+=":";
        if(seconds < 10) temp+="0";
        temp+=seconds;
        return temp;
    }

    public static String formatTimeFancy(int minutes, int seconds) {
        String temp = "";
        if(minutes > 0) temp+=minutes+"m";
        if(seconds > 0) temp+=seconds+"s";
        return temp;
    }

    private static Map<DyeColor, ChatColor> dyeChatMap;
    static {
        dyeChatMap = Maps.newHashMap();
        dyeChatMap.put(DyeColor.BLACK, ChatColor.DARK_GRAY);
        dyeChatMap.put(DyeColor.BLUE, ChatColor.DARK_BLUE);
        dyeChatMap.put(DyeColor.BROWN, ChatColor.GOLD);
        dyeChatMap.put(DyeColor.CYAN, ChatColor.AQUA);
        dyeChatMap.put(DyeColor.GRAY, ChatColor.GRAY);
        dyeChatMap.put(DyeColor.GREEN, ChatColor.DARK_GREEN);
        dyeChatMap.put(DyeColor.LIGHT_BLUE, ChatColor.BLUE);
        dyeChatMap.put(DyeColor.LIME, ChatColor.GREEN);
        dyeChatMap.put(DyeColor.MAGENTA, ChatColor.LIGHT_PURPLE);
        dyeChatMap.put(DyeColor.ORANGE, ChatColor.GOLD);
        dyeChatMap.put(DyeColor.PINK, ChatColor.LIGHT_PURPLE);
        dyeChatMap.put(DyeColor.PURPLE, ChatColor.DARK_PURPLE);
        dyeChatMap.put(DyeColor.RED, ChatColor.DARK_RED);
        dyeChatMap.put(DyeColor.SILVER, ChatColor.GRAY);
        dyeChatMap.put(DyeColor.WHITE, ChatColor.WHITE);
        dyeChatMap.put(DyeColor.YELLOW, ChatColor.YELLOW);
    }

    public static ChatColor convertDyeColor(DyeColor dyeColor) {
        return dyeChatMap.get(dyeColor);
    }

    public static String parseWinner(Player winner) {
        if(winner == null) return Arcadia.getPlugin(Arcadia.class).getAPI().getTranslationManager()
            .fetchTranslation("ui.unknown-player").build();
        return winner.getName();
    }

    public static Location parseLocation(String location) {
        if(location.split(",").length > 3) {
            return new Location(Arcadia.getPlugin(Arcadia.class).getAPI().getMapRegistry().getCurrentWorld(),
                    Double.valueOf(location.split(",")[0]), Double.valueOf(location.split(",")[1]), Double.valueOf(location.split(",")[2]),
                    Float.valueOf(location.split(",")[3]), Float.valueOf(location.split(",")[4]));
        } else {
            return new Location(Arcadia.getPlugin(Arcadia.class).getAPI().getMapRegistry().getCurrentWorld(),
                    Double.valueOf(location.split(",")[0]), Double.valueOf(location.split(",")[1]), Double.valueOf(location.split(",")[2]));
        }
    }

    public static MaterialData parseMaterialData(String data) {
        if(data.split(":").length > 1) {
            return new MaterialData(Material.getMaterial(data.split(":")[0]),
                Integer.valueOf(data.split(":")[1]).byteValue());
        } else {
            return new MaterialData(Material.getMaterial(data.split(":")[0]));
        }
    }

    public static String getNMSVersion() {
        return Bukkit.getServer().getClass().getPackage().getName().replace(".",  ",").split(",")[3];
    }

    public static boolean fullyUnloadWorld(World world) {
        for(Chunk chunk : world.getLoadedChunks()) {
            chunk.unload(false);
        }
        if(Bukkit.unloadWorld(world, false)) {
            flushRegionFileCache();
            FileUtils.deleteDirectory(new File(Bukkit.getWorldContainer().getAbsolutePath(), world.getName()));
            return true;
        }
        return false;
    }

    public static void flushRegionFileCache() {
        try {
            Method method = Class.forName("net.minecraft.server." + getNMSVersion() + ".RegionFileCache").getMethod("a");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void resetPlayer(Player player) {
        player.setLevel(0);
        player.setExp(0);
        player.setFoodLevel(20);
        player.setHealth(20);
        player.setHealthScale(20);
        player.setExhaustion(0);
        player.getInventory().clear();
        ItemStack blankItem = new ItemStack(Material.STAINED_GLASS_PANE, 1, (byte) 15);
        ItemMeta blankMeta = blankItem.getItemMeta();
        blankMeta.setDisplayName("" + ChatColor.RED);
        blankItem.setItemMeta(blankMeta);
        for(int i=9; i<=35; i++) {
            player.getInventory().setItem(i, blankItem);
        }
        player.getInventory().setHelmet(new ItemStack(Material.AIR, 1));
        player.getInventory().setChestplate(new ItemStack(Material.AIR, 1));
        player.getInventory().setLeggings(new ItemStack(Material.AIR, 1));
        player.getInventory().setBoots(new ItemStack(Material.AIR, 1));
        for(PotionEffect potionEffect : player.getActivePotionEffects()) {
            player.removePotionEffect(potionEffect.getType());
        }
        player.setFireTicks(0);
    }

    public static void setSpacer(Player player, int slot) {
        ItemStack blankItem = new ItemStack(Material.STAINED_GLASS_PANE, 1, (byte) 15);
        ItemMeta blankMeta = blankItem.getItemMeta();
        blankMeta.setDisplayName("" + ChatColor.RED);
        blankItem.setItemMeta(blankMeta);
        player.getInventory().setItem(slot, blankItem);
    }

    public static List<Map.Entry<String, Integer>> entriesSortedByValues(Map<String,Integer> map) {
            List<Map.Entry<String,Integer>> sortedEntries = new ArrayList<>(map.entrySet());
            Collections.sort(sortedEntries,
                    new Comparator<Map.Entry<String, Integer>>() {
                        @Override
                        public int compare(Map.Entry<String, Integer> e1, Map.Entry<String, Integer> e2) {
                            return e2.getValue().compareTo(e1.getValue());
                        }
                    }
            );
        return sortedEntries;
    }

    public static org.bukkit.util.Vector getRandomCircleVector() {
        double rnd = new Random().nextDouble() * 2.0D * Math.PI;
        double x = Math.cos(rnd);
        double z = Math.sin(rnd);
        return new org.bukkit.util.Vector(x, 0.0D, z);
    }

    public static Location getLocationAroundCircle(Location center, double radius, double angleInRadian) {
        double x = center.getX() + radius * Math.cos(angleInRadian);
        double z = center.getZ() + radius * Math.sin(angleInRadian);
        double y = center.getY();

        Location loc = new Location(center.getWorld(), x, y, z);
        org.bukkit.util.Vector difference = center.toVector().clone().subtract(loc.toVector());
        loc.setDirection(difference);

        return loc;
    }

    public static void showNotification(String title, String icon,
            AdvancementAPI.FrameType frameType, Player... players) {
        AdvancementAPI.builder(new NamespacedKey("test", "custom/" + UUID.randomUUID().toString()))
                .title(title)
                .description("")
                .icon(icon)
                .hidden(false)
                .toast(true)
                .announce(false)
                .trigger(AdvancementAPI.Trigger.builder(AdvancementAPI.Trigger.TriggerType.IMPOSSIBLE, "default"))
                .background("minecraft:textures/gui/advancements/backgrounds/stone.png")
                .frame(frameType)
                .build().show(Arcadia.getPlugin(Arcadia.class), players);
    }
} public abstract class BaseGame implements Listener {

    private final ArcadiaAPI api;

    private final String name;
    private final SidebarSettings sidebarSettings;
    private Sidebar sidebar;
    private GameMap gameMap;
    private final String description;
    private String[] requiredSettings;
    private List<Player> deathOrder = new ArrayList<Player>();
    public List<Player> spectatorCache = new ArrayList<Player>();

    public boolean allowPVP = false;
    public boolean killOnMapExit = true;
    public List<MaterialData> breakableBlocks = new ArrayList<>();

    /**
     * The base game layout.
     * @param name
     * @param requiredSettings
     * @param description
     */
    public BaseGame(String name, String[] requiredSettings, SidebarSettings sidebarSettings, GameMap gameMap, String description) {
        Preconditions.checkNotNull(name, "Game name cannot be null");
        Preconditions.checkNotNull(name, "Description cannot be null");
        this.name = name;
        if(requiredSettings != null) {
            this.requiredSettings = requiredSettings;
        } else {
            this.requiredSettings = new String[]{};
        }
        this.description = description;
        List<String> defaultRequiredSettings = new ArrayList<String>();
        for(String setting : this.requiredSettings) defaultRequiredSettings.add(setting);
        defaultRequiredSettings.add("spectatorLocation");
        defaultRequiredSettings.add("mapBoundsA");
        defaultRequiredSettings.add("mapBoundsB");
        this.requiredSettings = defaultRequiredSettings
            .toArray(new String[defaultRequiredSettings.size()]);
        this.sidebarSettings = sidebarSettings;
        try {
            this.sidebar = sidebarSettings.getClazz().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.api = Arcadia.getPlugin(Arcadia.class).getAPI();
        this.gameMap = gameMap;
    }

    @EventHandler
    public void onPlayerDeath(PlayerAliveStatusEvent event) {
        if(!event.isAlive() && !deathOrder.contains(event.getPlayer())) {
            deathOrder.add(event.getPlayer());
        }
    }

    /**
     * Kills all the players, which ends the game.
     */
    public void endGame() {
        GameManager manager = this.getAPI().getGameManager();
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (manager.isAlive(player)) {
                manager.setAlive(player, false);
            }
        }
    }

    /**
     * Returns the list of players who died in order.
     * @return
     */
    public List<Player> getDeathOrder() {
        return this.deathOrder;
    }

    /**
     * Returns the fun API :D
     * @return
     */
    public ArcadiaAPI getAPI() {
        return this.api;
    }

    /**
     * Returns the game name.
     * @return
     */
    public String getName() {
        return this.name;
    }

    /**
     * Returns all the required settings for a map.
     * @return
     */
    public String[] getRequiredSettings() {
        return this.requiredSettings;
    }

    /**
     * Returns the game description (line-by-line).
     * @return
     */
    public String getDescription() {
        return this.description;
    }

    /**
     * Returns the current GameMap.
     * @return
     */
    public GameMap getGameMap() {
        return this.gameMap;
    }

    /**
     * Returns the Sidebar instance.
     * @return
     */
    public Sidebar getSidebar() {
        return this.sidebar;
    }

    /**
     * Returns the SidebarSettings instance.
     * @return
     */
    public SidebarSettings getSidebarSettings() {
        return this.sidebarSettings;
    }

    /**
     * This event is called when a map is loaded in.
     * (Right before the countdown begins)
     */
    public abstract void onPreStart();

    /**
     * This event is called after the game countdown
     * has ended.
     */
    public abstract void onGameStart();

    /**
     * This event is called once the game has ended.
     * (Due to a winner, or the maximum time reached)
     */
    public abstract void onGameEnd();
} public enum GameState {

    STARTING(0),
    INGAME(1),
    FINISHED(2);

    private final int id;

    private GameState(int id) {
        this.id = id;
    }

    public int getID() {
        return this.id;
    }
} public class GameMap {

    private final String name;
    private final File mapDirectory;
    private final Map<String, Object> settings = new HashMap<>();

    /**
     * Initializes a new GameMap instance used in the game system.
     * @param name
     * @param mapDirectory
     */
    public GameMap(String name, File mapDirectory) {
        Preconditions.checkNotNull(name, "Map name cannot be null");
        Preconditions.checkNotNull(mapDirectory, "Map directory cannot be null");
        this.name = name;
        this.mapDirectory = mapDirectory;
    }

    /**
     * Returns the map name.
     * @return
     */
    public String getName() {
        return this.name;
    }

    /**
     * Returns the map directory.
     * @return
     */
    public File getMapDirectory() {
        return this.mapDirectory;
    }

    /**
     * Returns the specified map setting if it exists.
     * @param key
     * @return
     */
    public Object fetchSetting(String key) {
        if(!settings.containsKey(key)) return null;
        return settings.get(key);
    }

    /**
     * Returns true if the specified map setting exists.
     * @param key
     * @return
     */
    public boolean doesSettingExist(String key) {
        return settings.containsKey(key);
    }

    /**
     * Modifies a setting as long as the Object is not null.
     * @param key
     * @param value
     */
    public void modifySetting(String key, Object value) {
        Preconditions.checkNotNull(value, "Setting cannot be null");
        settings.put(key, value);
    }
} public class SidebarSettings {

    private final Class<? extends Sidebar> clazz;
    private final WinMethod winMethod;
    private final int gameMinutes;
    private final int gameSeconds;

    public SidebarSettings(Class<? extends Sidebar> clazz,
        WinMethod winMethod, int gameMinutes, int gameSeconds) {
        this.clazz = clazz;
        this.winMethod = winMethod;
        this.gameMinutes = gameMinutes;
        this.gameSeconds = gameSeconds+1;
    }

    public Class<? extends Sidebar> getClazz() {
        return this.clazz;
    }

    public WinMethod getWinMethod() {
        return this.winMethod;
    }

    public int getGameMinutes() {
        return this.gameMinutes;
    }

    public int getGameSeconds() {
        return this.gameSeconds;
    }
} public enum WinMethod {

    LAST_PLAYER_STANDING(0),
    HIGHEST_SCORE(1);

    private final int id;

    private WinMethod(int id) {
        this.id = id;
    }

    public int getID() {
        return this.id;
    }

    public Player calculateWinner(int place) {
        ArcadiaAPI api = Arcadia.getPlugin(Arcadia.class).getAPI();
        if(this.id == 0) {
            List<Player> deathOrder = api.getGameManager().getCurrentGame().getDeathOrder();
            Iterator<Player> iterator = deathOrder.iterator();
            while(iterator.hasNext()) {
                Player next = iterator.next();
                if(api.getGameManager().getCurrentGame().spectatorCache.contains(next)) {
                    iterator.remove();
                }
            }
            if(deathOrder.size() > (deathOrder.size()-place) && (deathOrder.size()-place) > -1) {
                return deathOrder.get((deathOrder.size()-place));
            } else {
                return null;
            }
        }
        if(this.id == 1) {
            Objective sidebar = api.getGameManager().getCurrentGame().getSidebar().getSidebar();
            Map<String, Integer> scores = new HashMap<>();
            Bukkit.getOnlinePlayers().forEach(player -> {
                if(sidebar.getScore(player.getName()) != null) {
                    if(!api.getGameManager().getCurrentGame().spectatorCache.contains(player)) {
                        scores.put(player.getUniqueId().toString(), sidebar.getScore(player.getName()).getScore());
                    }
                }
            });
            List<Map.Entry<String, Integer>> sorted = Utils.entriesSortedByValues(scores);
            if(sorted.size() > (place-1) && (place-1) > -1) {
                return Bukkit.getPlayer(UUID.fromString(sorted.get((place-1)).getKey()));
            } else {
                return null;
            }
        }
        return null;
    }
} public class ScoreSidebar extends Sidebar {

    public boolean removeDeadPlayers = false;
    public boolean fixScoreboard = true;

    @Override
    public void onCreation() {
        ArcadiaAPI api = Arcadia.getPlugin(Arcadia.class).getAPI();
        for(Player player : Bukkit.getOnlinePlayers()) {
            if(!api.getGameManager().isAlive(player)) continue;
            this.setScore(player, 0);
        }
    }

    @EventHandler
    public void onPlayerStatus(PlayerAliveStatusEvent event) {
        if(!event.isAlive() && removeDeadPlayers) {
            this.getSidebar().getScoreboard().resetScores(event.getPlayer().getName());
        }
    }

    /**
     * Updates the specified Player's score.
     * @param player
     * @param score
     */
    public void setScore(Player player, int score) {
        this.getSidebar().getScore(player.getName()).setScore(score);
    }

    /**
     * Returns the specified Player's score.
     * @param player
     * @return
     */
    public int getScore(Player player) {
        if(this.getSidebar().getScore(player) != null) {
            return this.getSidebar().getScore(player).getScore();
        }
        return 0;
    }
} public enum PointType {

    TEN(new MaterialData(Material.WOOL, (byte) 14), 10, ChatColor.RED + "+10"),
    FIVE(new MaterialData(Material.WOOL, (byte) 4), 5, ChatColor.YELLOW + "+5"),
    ONE(new MaterialData(Material.WOOL, (byte) 5), 1, ChatColor.GREEN + "+1"),
    SUPER_BOOST(new MaterialData(Material.WEB), 0, ChatColor.AQUA + "Super Boost");

    private final MaterialData materialData;
    private final int points;
    private final String translation;

    private PointType(MaterialData materialData, int points, String translation) {
        this.materialData = materialData;
        this.points = points;
        this.translation = translation;
    }

    public MaterialData getMaterialData() {
        return this.materialData;
    }

    public int getPoints() {
        return this.points;
    }

    public String getTranslation() {
        return this.translation;
    }

    public static PointType fetch() {
        double random = Math.random();
        if(random < 0.5D) {
            return PointType.ONE;
        } else if(random < 0.7D) {
            return PointType.TEN;
        } else if(random < 0.93D) {
            return PointType.FIVE;
        } else {
            return PointType.SUPER_BOOST;
        }
    }
}
import me.redraskal.arcadia.Arcadia;
import me.redraskal.arcadia.Cuboid;
import me.redraskal.arcadia.Utils;
import me.redraskal.arcadia.api.game.BaseGame;
import me.redraskal.arcadia.api.game.GameState;
import me.redraskal.arcadia.api.map.GameMap;
import me.redraskal.arcadia.api.scoreboard.SidebarSettings;
import me.redraskal.arcadia.api.scoreboard.WinMethod;
import me.redraskal.arcadia.api.scoreboard.defaults.ScoreSidebar;
import me.redraskal.arcadia.game.trampolino.PointType;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.FallingBlock;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
package me.redraskal.arcadia.game;



public class TrampolinioGame extends BaseGame {

    private List<Location> spawnLocations = new ArrayList<>();
    private Map<Location, Object[]> currentLocations = new HashMap<>();

    public TrampolinioGame(GameMap gameMap) {