public class RadixClient extends ApplicationAdapter {

    // TODO CLEANUP the render loop is VERY undocumented and things happen all over the place, clean that up

    public static final String GAME_TITLE = "VoxelTest";
    private static RadixClient theGame;

    private SettingsManager settingsManager;

    private boolean done; // Whether the game has finished and should shut down

    private boolean remote; // Whether the player is connected to a remote server
    private IWorld world;
    private Player player;
    private Vec3i selectedBlock; // selected block to break next
    private Vec3i selectedNextPlace; // selected block to place at next

    private MinecraftClientConnection mcClientConn;

    private final Queue<Runnable> glQueue = new ConcurrentLinkedDeque<>();

    private GuiScreen currentScreen;
    private TransitionAnimation transitionAnimation;
    private Renderer renderer;
    private GameRenderer gameRenderer; // renderer for when in game
    private MainMenu mainMenu;
    private IngameHUD hud;
    private ChatGUI chatGUI;

    private Texture blockTextureAtlas;
    private PerspectiveCamera camera;
    private OrthographicCamera hudCamera;
    private SpriteBatch guiBatch;

    private final MovementHandler movementHandler = new MovementHandler(this);
    private final RepeatedTask[] handlers = new RepeatedTask[]{new WorldLoader(this), movementHandler, new EntityUpdater(this)};

    // Android specific stuff
    private boolean android;
    private Stage androidStage;
    private Skin androidOverlaySkin;
    private TouchpadStyle touchpadStyle;
    private TextureAtlas touchControlsAtlas;
    private Touchpad moveTouchpad, rotateTouchpad;
    private ImageButton jumpButton, placeButton, breakButton;

    private boolean wireframe, debugText;

    private SceneTheme sceneTheme;

    public static RadixClient getInstance() {
        return theGame;
    }

    @Override
    public void create() {
        //GLProfiler.enable();

        EntityModel.init();
        theGame = this;
        settingsManager = new SettingsManager();

        this.android = Gdx.app.getType().equals(Application.ApplicationType.Android);

        try {
            RadixAPI.instance.getEventManager().register(this);
        } catch (InvalidListenerException e) {
            e.printStackTrace();
        }

        try {
            RadixAPI.instance.registerBuiltinBlockRenderers();
            RadixAPI.instance.getEventManager().push(new EventRegisterBlockRenderers());
        } catch (RadixAPI.DuplicateRendererException ex) {
            ex.printStackTrace();
        }

        try {
            RadixAPI.instance.registerBuiltinItems();
            if(!settingsManager.getVisualSettings().getFancyTrees().getValue()) {
                RadixAPI.instance.getBlock(BuiltInBlockIds.LEAVES_ID).setOccludeCovered(true);
                RadixAPI.instance.getBlock(BuiltInBlockIds.LEAVES_TWO_ID).setOccludeCovered(true);
            }

            RadixAPI.instance.getEventManager().push(new EventRegisterItems());
        } catch (RadixAPI.BlockRegistrationException e) {
            e.printStackTrace();
        }

        RadixAPI.instance.registerMinecraftBiomes();
        RadixAPI.instance.getEventManager().push(new EventRegisterBiomes());

        RadixAPI.instance.getEventManager().push(new EventEarlyInit());

        this.setupOGL();

        gameRenderer = new GameRenderer(this);
        hud = new IngameHUD();
        chatGUI = new ChatGUI();
        chatGUI.init();
        mainMenu = new MainMenu();
        setCurrentScreen(mainMenu);

        this.startHandlers();
    }

    private void setupTouchControls() {
        // Create touch control element atlas
        touchControlsAtlas = new TextureAtlas(Gdx.files.internal("textures/gui/touch/touch.atlas"));
        // Create skin
        androidOverlaySkin = new Skin();
        for(TextureAtlas.AtlasRegion r : touchControlsAtlas.getRegions()) {
            androidOverlaySkin.add(r.name, r, TextureRegion.class);
        }

        // Create touchpad skin/style
        touchpadStyle = new TouchpadStyle();
        touchpadStyle.background = androidOverlaySkin.getDrawable("touchpadBackground");
        touchpadStyle.knob = androidOverlaySkin.getDrawable("touchpadKnob");

        moveTouchpad = new Touchpad(10, touchpadStyle);
        moveTouchpad.setBounds(15, 15, 200, 200);

        rotateTouchpad = new Touchpad(10, touchpadStyle);
        rotateTouchpad.setBounds(1280 - 200 - 15, 15, 200, 200);

        jumpButton = new ImageButton(androidOverlaySkin.getDrawable("jumpButtonIcon"));
        jumpButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                movementHandler.jump();
            }
        });
        placeButton = new ImageButton(androidOverlaySkin.getDrawable("placeButtonIcon"));
        placeButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                placeBlock();
            }
        });
        breakButton = new ImageButton(androidOverlaySkin.getDrawable("breakButtonIcon"));
        breakButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                breakBlock();
            }
        });
        ImageButton[] buttons = new ImageButton[]{
                jumpButton, placeButton, breakButton
        };
        int totalButtonWidth = 0;
        for(ImageButton button : buttons) {
            totalButtonWidth += button.getImage().getPrefWidth();
        }
        int btnX = 1280/2-totalButtonWidth/2;
        for(ImageButton button : buttons) {
            button.setBounds(btnX, 0, button.getImage().getPrefWidth(), button.getImage().getPrefHeight());
            btnX += button.getImage().getPrefWidth();
        }

        // Setup android stage
        androidStage = new Stage(new FitViewport(1280, 720));
        androidStage.addActor(moveTouchpad);
        androidStage.addActor(rotateTouchpad);
        for(ImageButton button : buttons) {
            androidStage.addActor(button);
        }
    }

    private void startHandlers() {
        for (RepeatedTask r : handlers) {
            new Thread(r, r.getIdentifier()).start();
        }
    }

    private void setupOGL() {
        camera = new PerspectiveCamera(90, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        camera.position.set(10f, 150f, 10f);
        camera.lookAt(0, 0, 0);
        camera.near = 0.1f;
        camera.far = 450f;
        camera.update();
        hudCamera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());

        guiBatch = new SpriteBatch();
        guiBatch.setProjectionMatrix(hudCamera.combined);

        if(android) {
            setupTouchControls();
        }

        Gdx.input.setInputProcessor(new RadixInputHandler(this));

        if(settingsManager.getVisualSettings().getNonContinuous().getValue()) {
            Gdx.graphics.setContinuousRendering(false);
        }

        sceneTheme = new SceneTheme();
        sceneTheme.init();
    }

    @Override
    public void dispose() {
        super.dispose();
        done = true;
        if (isRemote()) {
            if(mcClientConn != null)
                mcClientConn.getClient().getSession().disconnect("Closing game");
        }

        if(android) {
            androidStage.dispose();
            androidOverlaySkin.dispose();
            touchControlsAtlas.dispose();
        }
    }

    @Override
    public void render() {
        try {
            if (done)
                Gdx.app.exit();

            prepareNewFrame();

            runQueuedOGL();

            if (renderer != null) {
                renderer.render();
            }

            guiBatch.begin();
            if (renderer != null) {
                renderer.draw2d(guiBatch);
            }

            if (currentScreen != null) {
                currentScreen.render(world != null, guiBatch);
            }

            if(transitionAnimation != null) {
                transitionAnimation.render(guiBatch);
                if(transitionAnimation.isFinished()) {
                    transitionAnimation.finish();
                    transitionAnimation = null;
                }
            }

            guiBatch.end();

            if(world != null && android) {
                updatePositionRotationAndroid();
                androidStage.act();
                androidStage.draw();
            }

            if(settingsManager.getVisualSettings().getFinishEachFrame().getValue())
                Gdx.gl.glFinish();
        } catch (Exception e) {
            done = true;
            e.printStackTrace();
            Gdx.input.setCursorCatched(false);
            Gdx.app.exit();
        }

    }

    private void runQueuedOGL() {
        Runnable currentRunnable;
        while ((currentRunnable = glQueue.poll()) != null) {
            currentRunnable.run();
        }

    }

    private void prepareNewFrame() {
        int clear = GL_DEPTH_BUFFER_BIT;
        if(world == null || getCurrentScreen() != hud || (transitionAnimation != null && !transitionAnimation.isFinished())) {
            /*
            Clear color buffer too when not in a world
            When in a world, since there is a skybox, clearing the color buffer would be pointless.
             */
            clear |= GL_COLOR_BUFFER_BIT;
            Gdx.gl.glClearColor(0.7f, 0.8f, 1f, 1f);
        }
        Gdx.gl.glClear(clear);
    }

    public void updateSelectedBlock() {
        PlotCell3f plotter = new PlotCell3f(0, 0, 0, 1, 1, 1);
        float x = player.getPosition().getX();
        float y = player.getPosition().getY() + player.getEyeHeight();
        float z = player.getPosition().getZ();
        float pitch = player.getRotation().getPitch();
        float yaw = player.getRotation().getYaw();
        float reach = player.getReach();

        float deltaX = (float) (Math.cos(Math.toRadians(pitch)) * Math.sin(Math.toRadians(yaw)));
        float deltaY = (float) (Math.sin(Math.toRadians(pitch)));
        float deltaZ = (float) (-Math.cos(Math.toRadians(pitch)) * Math.cos(Math.toRadians(yaw)));

        final Vec3i oldPos = selectedBlock;

        plotter.plot(new Vector3(x, y, z), new Vector3(deltaX, deltaY, deltaZ), MathUtils.ceil(reach * reach));
        Vec3i last = null;
        while (plotter.next()) {
            Vec3i v = plotter.get();
            Vec3i bp = new Vec3i(v.x, v.y, v.z);
            IChunk theChunk = world.getChunk(bp);
            if (theChunk != null) {
                if(bp.y >= world.getHeight())
                    continue;
                try {
                    Block b = theChunk.getBlock(bp.x & (world.getChunkSize() - 1), bp.y, bp.z & (world.getChunkSize() - 1));
                    if (b != null && b.isSelectable()) {
                        selectedBlock = bp;
                        if (last != null) {
                            Block lastBlock = theChunk.getBlock(last.x & (world.getChunkSize() - 1),
                                    last.y, last.z & (world.getChunkSize() - 1));
                            if (lastBlock == null || !lastBlock.isSelectable()) {
                                selectedNextPlace = last;
                            }
                        }

                        plotter.end();
                        if(!selectedBlock.equals(oldPos)) {
                            player.resetBlockBreak();
                        }
                        return;
                    }
                } catch(CoordinatesOutOfBoundsException ex) {
                    ex.printStackTrace();
                    plotter.end();
                    player.resetBlockBreak();
                    return;
                }

                last = bp;
            }

        }

        selectedNextPlace = null;
        selectedBlock = null;
        if(player != null) {
            player.resetBlockBreak();
        }
    }

    public void addToGLQueue(Runnable runnable) {
        glQueue.add(runnable);
    }

    public IWorld getWorld() {
        return world;
    }

    public Player getPlayer() {
        return player;
    }

    public boolean isDone() {
        return done;
    }

    public SettingsManager getSettingsManager() {
        return settingsManager;
    }

    public Vec3i getSelectedBlock() {
        return selectedBlock;
    }

    public Vec3i getNextPlacePos() {
        return selectedNextPlace;
    }

    public void startShutdown() {
        done = true;
    }

    private void setRenderer(Renderer renderer) {
        if (renderer == null) {
            if (this.renderer != null) {
                this.renderer.cleanup();
                this.renderer = null;
            }
        } else if (this.renderer == null) {
            this.renderer = renderer;
            this.renderer.init();
        } else {
            this.renderer.cleanup();
            this.renderer = renderer;
            this.renderer.init();
        }
    }

    public boolean isRemote() {
        return remote;
    }

    public MinecraftClientConnection getMinecraftConn() {
        return mcClientConn;
    }

    public void handleCriticalException(Exception ex) {
        ex.printStackTrace();
        this.done = true;
        Gdx.input.setCursorCatched(false);
        if(!android) {
            JOptionPane.showMessageDialog(null, GAME_TITLE + " crashed. " + String.valueOf(ex), GAME_TITLE + " crashed", JOptionPane.ERROR_MESSAGE);
        }
    }

    public GuiScreen getCurrentScreen() {
        return currentScreen;
    }

    public void setCurrentScreen(GuiScreen screen) {
        if (currentScreen == null) {
            currentScreen = screen;
            screen.init();
        } else if (!currentScreen.equals(screen)) {
            currentScreen.finish();
            currentScreen = screen;
            screen.init();
        }

        if (screen.equals(hud) && !android) {
            Gdx.input.setCursorCatched(true);
        } else {
            Gdx.input.setCursorCatched(false);
        }
    }

    public void enterRemoteWorld(final String hostname, final short port) {
        selectedBlock = null;
        selectedNextPlace = null;

        enterWorld(new World(true, false), true);
        (mcClientConn = new MinecraftClientConnection(RadixClient.this, hostname, port)).start();
        chatGUI.setup(mcClientConn);
    }

    public void enterLocalWorld(IWorld world) {
        enterWorld(world, false);
    }

    public void exitWorld() {
        addToGLQueue(() -> {
            setCurrentScreen(mainMenu);
            setRenderer(null);
            getWorld().cleanup();
            if (isRemote()) {
                mcClientConn.getClient().getSession().disconnect("Exiting world");
            }

            world = null;
            remote = false;
            player = null;
        });

        if(android) {
            Gdx.input.setInputProcessor(new RadixInputHandler(this));
        }
    }

    private void enterWorld(final IWorld world, final boolean remote) {
        this.world = world;
        this.remote = remote;
        player = new Player(new EntityPosition(0, 256, 0), new EntityRotation(0, 0));
        addToGLQueue(() -> {
            setRenderer(getGameRenderer());
            getPlayer().init();
//            world.addEntity(getPlayer());
            transitionAnimation = new SlideUpAnimation(getCurrentScreen(), 1000);
            transitionAnimation.init();
            setCurrentScreen(getHud());

            if (!remote) {
                world.loadChunks(new EntityPosition(0, 0, 0), getSettingsManager().getVisualSettings().getViewDistance());
            }

            RadixAPI.instance.getEventManager().push(new EventWorldStart());
            // Delays are somewhere in this function. Above here.
        });

        if(android) {
            Gdx.input.setInputProcessor(androidStage);
        }
    }

    public Texture getBlockTextureAtlas() throws NotInitializedException {
        if (blockTextureAtlas == null) throw new NotInitializedException();
        return blockTextureAtlas;
    }

    @EventListener(Priority.FIRST)
    public void createBlockTextureMap(EventEarlyInit event) {
        // Create a texture atlas for all of the blocks
        addToGLQueue(() -> {
            Pixmap bi = new Pixmap(2048, 2048, Pixmap.Format.RGBA8888);
            bi.setColor(1, 1, 1, 1);
            final int BLOCK_TEX_SIZE = 32;
            int textureIndex = 0;
            for (Block b : RadixAPI.instance.getBlocks()) {
                if (b == null)
                    continue;
                b.setTextureIndex(textureIndex);
                for (String texLoc : b.getTextureLocations()) {
                    int x = textureIndex * BLOCK_TEX_SIZE % bi.getWidth();
                    int y = BLOCK_TEX_SIZE * ((textureIndex * BLOCK_TEX_SIZE) / bi.getWidth());
                    Pixmap tex = new Pixmap(Gdx.files.internal(texLoc));
                    bi.drawPixmap(tex, x, y);
                    tex.dispose();
                    textureIndex++;
                }
            }

            blockTextureAtlas = new Texture(bi);
            bi.dispose();
        });
    }

    @Override
    public void resize(int width, int height) {
        Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());

        hudCamera.setToOrtho(false, width, height);
        hudCamera.update();
        guiBatch.setProjectionMatrix(hudCamera.combined);

        camera.viewportWidth = width;
        camera.viewportHeight = height;
        camera.update();

        if(android)
            androidStage.getViewport().update(width, height, true);
    }

    public void beginBreak() {
        if (this.getSelectedBlock() != null
                && this.currentScreen == this.hud) {
            IChunk chunk = world.getChunk(selectedBlock);
            if(chunk != null) {
                try {
                    Block block = chunk.getBlock(selectedBlock.x & (world.getChunkSize() - 1), selectedBlock.y,
                            selectedBlock.z & (world.getChunkSize() - 1));
                    if (block != null && block.isSelectable()) {
                        if (this.isRemote()) {
                            mcClientConn.getClient().getSession().send(new ClientPlayerSwingArmPacket(Hand.MAIN_HAND));
                            mcClientConn.getClient().getSession().send(
                                    new ClientPlayerActionPacket(PlayerAction.START_DIGGING,
                                            new Position(selectedBlock.x, selectedBlock.y, selectedBlock.z), BlockFace.UP));
                        }
                    }
                } catch (CoordinatesOutOfBoundsException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public void cancelBreak() {
        if (this.getSelectedBlock() != null
                && this.currentScreen == this.hud) {
            IChunk chunk = world.getChunk(selectedBlock);
            if(chunk != null) {
                try {
                    Block block = chunk.getBlock(selectedBlock.x & (world.getChunkSize() - 1), selectedBlock.y,
                            selectedBlock.z & (world.getChunkSize() - 1));
                    if (block != null && block.isSelectable()) {
                        if (this.isRemote()) {
                            mcClientConn.getClient().getSession().send(
                                    new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING,
                                            new Position(selectedBlock.x, selectedBlock.y, selectedBlock.z), BlockFace.UP));
                        }
                    }
                    player.resetBlockBreak();
                } catch (CoordinatesOutOfBoundsException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public void breakBlock() {
        if (this.getSelectedBlock() != null
                && this.currentScreen == this.hud) {
            IChunk chunk = world.getChunk(selectedBlock);
            if(chunk != null) {
                try {
                    Block block = chunk.getBlock(selectedBlock.x & (world.getChunkSize() - 1), selectedBlock.y,
                            selectedBlock.z & (world.getChunkSize() - 1));
                    if (block != null && block.isSelectable()) {
                        if (this.isRemote()) {
                            mcClientConn.getClient().getSession().send(new ClientPlayerSwingArmPacket(Hand.MAIN_HAND));
                            mcClientConn.getClient().getSession().send(
                                    new ClientPlayerActionPacket(PlayerAction.FINISH_DIGGING,
                                            new Position(selectedBlock.x, selectedBlock.y, selectedBlock.z), BlockFace.UP));
                        } else {
                            this.getWorld().removeBlock(this.getSelectedBlock().x,
                                    this.getSelectedBlock().y, this.getSelectedBlock().z);
                        }
                        updateSelectedBlock();
                    }
                } catch (CoordinatesOutOfBoundsException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public void placeBlock() {
        if (this.getNextPlacePos() != null && this.currentScreen == this.hud) {
            if (this.isRemote() && this.mcClientConn != null) {
                this.mcClientConn.getClient().getSession().send(new ClientPlayerPlaceBlockPacket(
                        new Position(this.getNextPlacePos().x, this.getNextPlacePos().y, this.getNextPlacePos().z),
                        BlockFace.UP /* TODO MCPROTO send correct face */, Hand.MAIN_HAND, 0, 0, 0));
            } else {
                this.getWorld().addBlock(this.getPlayer().getItemInHand(), this.getNextPlacePos().x,
                        this.getNextPlacePos().y, this.getNextPlacePos().z);
            }
            updateSelectedBlock();
        }
    }

    private void updatePositionRotationAndroid() {
        if(!MathUtils.isZero(rotateTouchpad.getKnobPercentX()) || !MathUtils.isZero(rotateTouchpad.getKnobPercentY())) {
            float rotMult = 200 * Gdx.graphics.getDeltaTime();

            player.getRotation().offset(rotateTouchpad.getKnobPercentY() * rotMult, rotateTouchpad.getKnobPercentX() * rotMult);
            updateSelectedBlock();
            gameRenderer.calculateFrustum();
        }

        if(!MathUtils.isZero(moveTouchpad.getKnobPercentX()) || !MathUtils.isZero(moveTouchpad.getKnobPercentY())) {
            float posMult = 4.5f * Gdx.graphics.getDeltaTime();

            float deltaX = 0;
            float deltaZ = 0;
            float yawSine = MathUtils.sinDeg(player.getRotation().getYaw());
            float yawCosine = MathUtils.cosDeg(player.getRotation().getYaw());
            deltaX += yawSine * moveTouchpad.getKnobPercentY() * posMult;
            deltaZ += -yawCosine * moveTouchpad.getKnobPercentY() * posMult;
            deltaX += yawCosine * moveTouchpad.getKnobPercentX() * posMult;
            deltaZ += yawSine * moveTouchpad.getKnobPercentX() * posMult;

            if(!movementHandler.checkDeltaCollision(player, deltaX, 0, deltaZ)
                    && !movementHandler.checkDeltaCollision(player, deltaX, 0, deltaZ)) {
                player.getPosition().offset(deltaX, 0, deltaZ);
                gameRenderer.calculateFrustum();
            }
        }
    }

    public boolean onAndroid() { return android; }

    public PerspectiveCamera getCamera() { return camera; }

    public OrthographicCamera getHudCamera() { return hudCamera; }

    public MovementHandler getMovementHandler() { return movementHandler; }

    public IngameHUD getHud() { return this.hud; }

    public ChatGUI getChatGUI() { return this.chatGUI; }

    public GameRenderer getGameRenderer() { return this.gameRenderer; }

    public boolean isWireframe() { return wireframe; }
    public void setWireframe(boolean wireframe) { this.wireframe = wireframe; }

    // Debug text that shows in the ingame HUD
    public boolean debugInfoEnabled() { return debugText; }
    public void setDebugInfo(boolean debugText) { this.debugText = debugText; }

    public MainMenu getMainMenu() {
        return mainMenu;
    }

    public SceneTheme getSceneTheme() { return sceneTheme; }

} public class RadixAPI {

    public static final RadixAPI instance = new RadixAPI();
    private final EventManager eventManager;
    private final Block[] registeredBlocks = new Block[4096];
    private final Item[] registeredItems = new Item[4096];
    private final Map<String, BlockRenderer> registeredRenderers = new HashMap<>();
    private final Biome[] registeredBiomeArray = new Biome[256];
    private int highestID = 0;

    private RadixAPI() {
        this.eventManager = new IridiumEventManager();
    }

    /**
     * Gets the event manager that is used to fire and listen to API events
     */
    public EventManager getEventManager() {
        return this.eventManager;
    }

    /**
     * @return Whether the current game is a client
     */
    public boolean isClient() {
        return RadixClient.getInstance() != null;
    }

    /**
     * Registers the built in items and blocks
     *
     * If you're a mod, please don't call this
     *
     * Called before the item registration event is fired.
     */
    public void registerBuiltinItems() throws BlockRegistrationException {
        try {
            registerItems(
                    new Tool(270, "Wooden Pickaxe", ToolMaterial.WOOD, ToolType.PICKAXE),
                    new Tool(274, "Stone Pickaxe", ToolMaterial.STONE, ToolType.PICKAXE),
                    new Tool(257, "Iron Pickaxe", ToolMaterial.IRON, ToolType.PICKAXE)
            );

            String builtinBlockJson = Gdx.files.internal("defaultRegistry/blocks.json").readString();
            try {
                JsonBlock[] blocks = new Gson().fromJson(builtinBlockJson, JsonBlock[].class);
                for(JsonBlock block : blocks) {
                    registerBlock(block.createBlock());
                }
            } catch (BlockBuilder.CustomClassException e) {
                System.err.println(e.getMessage());
                e.printStackTrace();
            }
        } catch (BlockBuilder.MissingElementException ex) {
            throw new BlockRegistrationException(ex.getMessage());
        }
    }

    /**
     * Register builtin block renderers.
     *
     * If you're a mod, please don't call this.
     *
     * Called before the BlockRenderer registration event is fired.
     *
     * @throws DuplicateRendererException Usually thrown if this is called twice or someone tried to register a block renderer that's already built in.
     */
    public void registerBuiltinBlockRenderers() throws DuplicateRendererException {
        registerBlockRenderers(
                new NormalBlockRenderer(),
                new ColoredFoliageRenderer(),
                new FlatFoliageRenderer(),
                new GrassRenderer(),
                new TallGrassRenderer(),
                new MetadataHeightRenderer(7), // used for snow
                new MetadataHeightRenderer(15, true), // used for liquids
                new FenceRenderer()

        );
    }

    public void registerMinecraftBiomes() {
        registerBiomes(
                new Gson().fromJson(Gdx.files.internal("defaultRegistry/biomes.json").readString(), Biome[].class)
        );
    }

    public void registerBiomes(Biome... biomes) {
        for(Biome b : biomes) {
            registerBiome(b);
        }
    }

    public void registerBiome(Biome biome) {
        registeredBiomeArray[biome.getID()] = biome;
    }

    public void registerBlockRenderers(BlockRenderer ... renderers) throws DuplicateRendererException {
        for(BlockRenderer br : renderers) {
            registerBlockRenderer(br);
        }
    }

    public void registerBlockRenderer(BlockRenderer renderer) throws DuplicateRendererException {
        String id = renderer.getUniqueID();
        if(registeredRenderers.containsKey(id)) {
            throw new DuplicateRendererException(id);
        }
        this.registeredRenderers.put(renderer.getUniqueID(), renderer);
    }

    /**
     * Gets the biome with the specific ID
     *
     * If using Minecraft IDs, try the id-128 as variations are usually just parent+128
     */
    public Biome getBiomeByID(int id) {
        if(id >= registeredBiomeArray.length || id < 0)
            return null;
        return registeredBiomeArray[id];
    }

    private void registerItems(Item... items) throws BlockRegistrationException {
        for (Item i : items) {
            registerItem(i);
        }
    }

    private void registerItem(Item i) throws BlockRegistrationException {
        if (i.getID() == -1) {
            i.setID(highestID);
            System.err.printf("Item ID not defined for %s. Using auto generated ID. IF YOU\'RE THE MOD DEVELOPER, FIX THIS!!!\n", i.toString());
        }

        if(registeredItems[i.getID()] != null) {
            throw new BlockRegistrationException("ID already in use by " + registeredItems[i.getID()].getHumanName());
        }

        this.registeredItems[i.getID()] = i;
        highestID = Math.max(highestID, i.getID());
    }

    private void registerBlocks(Block... blocks) throws BlockRegistrationException {
        for (Block b : blocks) {
            registerBlock(b);
        }
    }

    private void registerBlock(Block b) throws BlockRegistrationException {
        if (b.getID() == -1) {
            b.setID(highestID);
            System.err.println("Block ID not defined for " + String.valueOf(b) + ". Using auto generated ID. IF YOU\'RE THE MOD DEVELOPER, FIX THIS!!!");
        }

        if(registeredBlocks[b.getID()] != null) {
            throw new BlockRegistrationException("ID already in use by " + registeredBlocks[b.getID()]);
        }

        this.registeredBlocks[b.getID()] = b;
        highestID = Math.max(highestID, b.getID());

        registerItem(b);
    }

    public Block[] getBlocks() {
        return registeredBlocks;
    }

    /**
     * Get block by its ID.
     * Should only be used with static IDs for builtin blocks since mods will often (and should) have configurable IDs.
     *
     * @param id ID that the block has in the registry.
     * @return null (should be treated as air) if id is 0 or below, is greater than the maximum allowed id, or is not registered.
     */
    public Block getBlock(int id) {
        if(id <= 0)
            return null;
        if(id >= registeredBlocks.length)
            return null;

        return registeredBlocks[id];
    }

    /**
     * Get item by its ID.
     * Should only be used with static IDs for builtin items since mods will often (and should) have configurable IDs.
     *
     * @param id ID that the item has in the registry.
     * @return null if id is 0 or below, is greater than the maximum allowed id, or is not registered.
     */
    public Item getItem(int id) {
        if(id <= 0)
            return null;
        if(id >= registeredItems.length)
            return null;

        return registeredItems[id];
    }

    /**
     * Get the BlockRenderer registered with the specified unique ID
     *
     * @param uid Unique ID of the renderer to get. Returned by BlockRenderer.getUniqueID().
     * @throws sx.lambda.voxel.api.RadixAPI.NoSuchRendererException No renderer existed with the specified UID.
     */
    public BlockRenderer getBlockRenderer(String uid) throws NoSuchRendererException {
        BlockRenderer renderer = registeredRenderers.get(uid);
        if(renderer != null) {
            return renderer;
        } else {
            throw new NoSuchRendererException(uid);
        }
    }

    public class BlockRegistrationException extends Exception {
        public BlockRegistrationException(String reason) {
            super(reason);
        }
    }

    public class NoSuchRendererException extends BlockRegistrationException {
        public NoSuchRendererException(String renderer) {
            super(String.format(
                    "Renderer \"%s\" does not exist. " +
                            "Make sure you registered it before the block load phase. Also check for typos.",
                    renderer
            ));
        }

        public NoSuchRendererException(String blockName, String renderer) {
            super(String.format(
                    "Renderer \"%s\" does not exist for block \"%s\". " +
                            "Make sure you registered it before the block load phase. Also check for typos.",
                    renderer, blockName
            ));
        }
    }

    public class DuplicateRendererException extends BlockRegistrationException {
        public DuplicateRendererException(String renderer) {
            super(String.format(
                    "Renderer with id \"%s\" already exists and is cannot be redifined. " +
                            "Make sure you are using a unique ID and are not registering twice.",
                    renderer
            ));
        }
    }

} public class EventChunkRender implements Event {

    private final IChunk chunk;

    public EventChunkRender(IChunk chunk) {
        this.chunk = chunk;
    }

    /**
     * @return The chunk that was rendered
     */
    public IChunk getChunk() {
        return this.chunk;
    }

} public class Block extends Item {

    private final transient BlockRenderer renderer;
    private boolean translucent, solid, lightPassthrough, selectable, occludeCovered, decreaseLight, greedyMerge;
    private final String[] textureLocations;
    private int textureIndex;
    private int lightValue;
    private final float hardness;
    private final ToolMaterial requiredMaterial;
    private final ToolType requiredType;

    /**
     * DO NOT USE THIS CONSTRUCTOR, USE BLOCKBUILDER INSTEAD
     */
    Block(int id, String humanName, BlockRenderer renderer, String textureLocations[],
          boolean translucent, boolean solid, boolean lightPassthrough, boolean selectable, boolean occludeCovered,
          boolean decreaseLight, boolean greedyMerge,
          int lightValue, float hardness, ToolMaterial requiredMaterial, ToolType requiredType) {
        super(id, humanName);
        this.renderer = renderer;
        this.translucent = translucent;
        this.solid = solid;
        this.textureLocations = textureLocations;
        this.lightPassthrough = lightPassthrough;
        this.selectable = selectable;
        this.occludeCovered = occludeCovered;
        this.decreaseLight = decreaseLight;
        this.lightValue = lightValue;
        this.greedyMerge = greedyMerge;
        this.hardness = hardness;
        this.requiredMaterial = requiredMaterial;
        this.requiredType = requiredType;
    }

    public BlockRenderer getRenderer() {
        return this.renderer;
    }

    public boolean isTranslucent() {
        return this.translucent;
    }

    public boolean isSolid() {
        return this.solid;
    }

    public boolean doesLightPassThrough() {
        return this.lightPassthrough;
    }

    public String getTextureLocation() {
        return textureLocations[0];
    }

    public String[] getTextureLocations() {
        return textureLocations;
    }

    /**
     * Set the beginning index in the texture map for the texture set for the block
     *
     * THIS IS USED INTERNALLY BY THE TEXTURING SYSTEM
     * YOU PROBABLY DO NOT WANT TO CALL THIS
     *
     * @param index Number of the first texture of the block in the texture map
     */
    public void setTextureIndex(int index) {
        this.textureIndex = index;
    }

    /**
     * @return The index at which you can find the first texture of the block in the texture map
     */
    public int getTextureIndex() {
        return this.textureIndex;
    }

    /**
     * @return True if the block is selectable to be broken or placed on
     */
    public boolean isSelectable() {
        return this.selectable;
    }

    /**
     * @return True if covered sides of the block should be occluded when rendering
     */
    public boolean occludeCovered() {
        return occludeCovered;
    }

    /**
     * @return True if, when going down to another non-air block, light is decreased
     */
    public boolean decreasesLight() {
        return this.decreaseLight;
    }

    /**
     * Get the light emission value of the block
     * @return Integer between 0 and 15 representing the light emission value of the block
     */
    public int getLightValue() {
        return this.lightValue;
    }

    public boolean shouldGreedyMerge() {
        return this.greedyMerge;
    }

    /**
     * Set whether you can select the block in the world
     */
    public void setSelectable(boolean selectable) {
        this.selectable = selectable;
    }

    /**
     * Set whether light should decrease when passing through this block to other blocks
     */
    public void setDecreaseLight(boolean decrease) {
        this.decreaseLight = decrease;
    }

    /**
     * Set whether light should always ignore this block
     */
    public void setLightPassthrough(boolean passthrough) {
        this.lightPassthrough = passthrough;
    }

    /**
     * Set whether the block can be walked through
     */
    public void setSolid(boolean solid) {
        this.solid = solid;
    }

    /**
     * Set whether to consider the block translucent and to render it after opaque blocks
     */
    public void setTranslucent(boolean translucent) {
        this.translucent = translucent;
    }

    /**
     * Set whether to occlude neighbors that the block is covering
     */
    public void setOccludeCovered(boolean occludeCovered) {
        this.occludeCovered = occludeCovered;
    }

    /**
     * Get the bounding box of an instance of the block at the specified coordinates
     *
     * @param c Chunk the block is in
     * @param x Chunk-relative x position of the block. 0->(chunkWidth-1) inclusive.
     * @param y Chunk-relative y position of the block. 0->(chunkHeight-1) inclusive.
     * @param z Chunk-relative z position of the block. 0->(chunkWidth-1) inclusive.
     */
    public BoundingBox calculateBoundingBox(IChunk c, int x, int y, int z) {
        Vector3 corner1 = new Vector3(c.getStartPosition().x+x, c.getStartPosition().y+y, c.getStartPosition().z+z);
        Vector3 corner2 = corner1.cpy().add(1, 1, 1);
        return new BoundingBox(corner1, corner2);
    }

    public float getHardness() {
        return this.hardness;
    }

    public ToolMaterial getRequiredToolMaterial() {
        return this.requiredMaterial;
    }

    public ToolType getRequiredToolType() {
        return this.requiredType;
    }

    /**
     * Get the time it takes to break the block with the given tool
     * @return Length, in milliseconds, to break the block
     */
    public int getBreakTimeMS(Tool tool) {
        if(hardness > 100 || hardness < 0) {
            return Integer.MAX_VALUE;
        }

        ToolType tType = ToolType.THESE_HANDS;
        ToolMaterial tMaterial = ToolMaterial.THESE_HANDS;
        if(tool != null) {
            tType = tool.getType();
            tMaterial = tool.getMaterial();
        }

        float timeSeconds = hardness * 1.5f;
        if((tType != requiredType && requiredType != ToolType.THESE_HANDS && requiredMaterial != ToolMaterial.THESE_HANDS)
                || (tMaterial.materialStrength < requiredMaterial.materialStrength)) {
            timeSeconds *= 3.33;
        } else if(tMaterial.materialStrength >= requiredMaterial.materialStrength) {
            timeSeconds /= requiredMaterial.speedMult;
        }

        return (int)(timeSeconds * 1000f);
    }

} public class NormalBlockRenderer implements BlockRenderer {

    private static final float TEXTURE_PERCENTAGE = 32f / 2048f;
    private static final int BLOCKS_PER_WIDTH = 2048 / 32;

    private static Texture blockMap;

    private static boolean initialized;

    private VertexInfo c00 = new VertexInfo();
    private VertexInfo c01 = new VertexInfo();
    private VertexInfo c10 = new VertexInfo();
    private VertexInfo c11 = new VertexInfo();

    public NormalBlockRenderer() {}

    @Override
    public void render2d(SpriteBatch batcher, int atlasIndex, float x, float y, float width) {
        if (!initialized) {
            initialize();
        }
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        float u2 = u + TEXTURE_PERCENTAGE - .001f;
        float v2 = v + TEXTURE_PERCENTAGE - .001f;
        batcher.draw(getBlockMap(), x, y, width, width, u, v, u2, v2);
    }

    @Override
    public void renderNorth(int atlasIndex, float x1, float y1, float x2, float y2, float z, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // POSITIVE Z
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x1, y1, z).setNor(0, 0, 1);
        c01.setPos(x1, y2, z).setNor(0, 0, 1);
        c10.setPos(x2, y1, z).setNor(0, 0, 1);
        c11.setPos(x2, y2, z).setNor(0, 0, 1);

        Color c = getColor((int) x1, (int) y1, (int) z - 1);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c00, c10, c11, c01);
    }

    @Override
    public void renderSouth(int atlasIndex, float x1, float y1, float x2, float y2, float z, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // NEGATIVE Z
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x1, y1, z).setNor(0, 0, -1);
        c01.setPos(x1, y2, z).setNor(0, 0, -1);
        c10.setPos(x2, y1, z).setNor(0, 0, -1);
        c11.setPos(x2, y2, z).setNor(0, 0, -1);

        Color c = getColor((int) x1, (int) y1, (int) z);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c01, c11, c10, c00);
    }

    @Override
    public void renderWest(int atlasIndex, float z1, float y1, float z2, float y2, float x, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // NEGATIVE X
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x, y1, z1).setNor(-1, 0, 0);
        c01.setPos(x, y1, z2).setNor(-1, 0, 0);
        c10.setPos(x, y2, z1).setNor(-1, 0, 0);
        c11.setPos(x, y2, z2).setNor(-1, 0, 0);

        Color c = getColor((int) x, (int) y1, (int) z1);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c01, c11, c10, c00);
    }

    @Override
    public void renderEast(int atlasIndex, float z1, float y1, float z2, float y2, float x, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // POSITIVE X
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x, y1, z1).setNor(1, 0, 0);
        c01.setPos(x, y1, z2).setNor(1, 0, 0);
        c10.setPos(x, y2, z1).setNor(1, 0, 0);
        c11.setPos(x, y2, z2).setNor(1, 0, 0);

        Color c = getColor((int) x - 1, (int) y1, (int) z1);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c00, c10, c11, c01);
    }

    @Override
    public void renderTop(int atlasIndex, float x1, float z1, float x2, float z2, float y, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // POSITIVE Y
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x1, y, z1).setNor(0, 1, 0);
        c01.setPos(x1, y, z2).setNor(0, 1, 0);
        c10.setPos(x2, y, z1).setNor(0, 1, 0);
        c11.setPos(x2, y, z2).setNor(0, 1, 0);

        Color c = getColor((int) x1, (int) y - 1, (int) z1);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c01, c11, c10, c00);
    }

    @Override
    public void renderBottom(int atlasIndex, float x1, float z1, float x2, float z2, float y, float lightLevel, PerCornerLightData pcld, MeshBuilder builder) {
        // NEGATIVE Y
        float u = getU(atlasIndex);
        float v = getV(atlasIndex);
        builder.setUVRange(u, v, u, v);

        c00.setPos(x1, y, z1).setNor(0, -1, 0);
        c01.setPos(x1, y, z2).setNor(0, -1, 0);
        c10.setPos(x2, y, z1).setNor(0, -1, 0);
        c11.setPos(x2, y, z2).setNor(0, -1, 0);

        Color c = getColor((int) x1, (int) y, (int) z1);
        if(pcld == null) {
            builder.setColor(c.r*lightLevel, c.g*lightLevel, c.b*lightLevel, c.a);
        } else {
            c00.setCol(c.r*pcld.l00, c.g*pcld.l00, c.b*pcld.l00, c.a);
            c01.setCol(c.r*pcld.l01, c.g*pcld.l01, c.b*pcld.l01, c.a);
            c10.setCol(c.r*pcld.l10, c.g*pcld.l10, c.b*pcld.l10, c.a);
            c11.setCol(c.r*pcld.l11, c.g*pcld.l11, c.b*pcld.l11, c.a);
        }

        builder.rect(c00, c10, c11, c01);
    }

    protected Color getColor(int x, int y, int z) {
        return Color.WHITE;
    }

    private static void initialize() {
        try {
            blockMap = RadixClient.getInstance().getBlockTextureAtlas();
        } catch (NotInitializedException e) {
            System.err.println("Error getting block texture atlas!");
            e.printStackTrace();
        }
        initialized = true;
    }

    public static Texture getBlockMap() {
        if (blockMap == null) {
            initialize();
        }
        return blockMap;
    }

    public static float getU(int atlasIndex) {
        return ((atlasIndex % BLOCKS_PER_WIDTH) * TEXTURE_PERCENTAGE);
    }

    public static float getV(int atlasIndex) {
        return ((atlasIndex / BLOCKS_PER_WIDTH) * TEXTURE_PERCENTAGE);
    }

    @Override
    public String getUniqueID() {
        return "Builtin.NORMAL";
    }

} public enum Side {TOP, BOTTOM, WEST, EAST, NORTH, SOUTH} public class GreedyMesher implements Mesher {

    private final IChunk chunk;
    private final boolean perCornerLight;

    /**
     * @param chunk Chunk to mesh
     * @param perCornerLight Whether to average light on a per-corner basis
     */
    public GreedyMesher(IChunk chunk, boolean perCornerLight) {
        this.chunk = chunk;
        this.perCornerLight = perCornerLight;
    }

    @Override
    public Mesh meshVoxels(MeshBuilder builder, UseCondition condition) {
        List<Face> faces = getFaces(condition);

        return meshFaces(faces, builder);
    }

    public List<Face> getFaces(UseCondition condition, OccludeCondition ocCond, MergeCondition shouldMerge) {
        List<Face> faces = new ArrayList<>();

        PerCornerLightData bright;
        if(perCornerLight) {
            bright = new PerCornerLightData();
            bright.l00 = 1;
            bright.l01 = 1;
            bright.l10 = 1;
            bright.l11 = 1;
        }

        int width = chunk.getWorld().getChunkSize();
        int depth = width;
        int height = chunk.getHighestPoint()+1;
        // Top, bottom
        for (int y = 0; y <= chunk.getHighestPoint(); y++) {
            boolean[][] topMask = new boolean[width][depth];
            PerCornerLightData[][] topPcld = null;
            if(perCornerLight) {
                topPcld = new PerCornerLightData[width][depth];
            }
            boolean[][] btmMask = new boolean[width][depth];
            PerCornerLightData[][] btmPcld = null;
            if(perCornerLight) {
                btmPcld = new PerCornerLightData[width][depth];
            }
            for (int z = 0; z < depth; z++) {
                for (int x = 0; x < width; x++) {
                    try {
                        Block curBlock = chunk.getBlock(x, y, z);
                        if (curBlock == null || !condition.shouldUse(curBlock))
                            continue;

                        if (y < height - 1) {
                            if (!ocCond.shouldOcclude(curBlock, chunk.getBlock(x, y + 1, z))) {
                                topMask[x][z] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.TOP, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.TOP, x, y, z + 1);
                                    pcld.l10 = calcPerCornerLight(Side.TOP, x + 1, y, z);
                                    pcld.l11 = calcPerCornerLight(Side.TOP, x + 1, y, z + 1);
                                    topPcld[x][z] = pcld;
                                }
                            }
                        }
                        if (y > 0) {
                            if (!ocCond.shouldOcclude(curBlock, chunk.getBlock(x, y - 1, z))) {
                                btmMask[x][z] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.BOTTOM, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.BOTTOM, x, y, z + 1);
                                    pcld.l10 = calcPerCornerLight(Side.BOTTOM, x + 1, y, z);
                                    pcld.l11 = calcPerCornerLight(Side.BOTTOM, x + 1, y, z + 1);
                                    btmPcld[x][z] = pcld;
                                }
                            }
                        }
                    } catch (CoordinatesOutOfBoundsException ex) {
                        ex.printStackTrace();
                    }
                }
            }
            greedy(faces, Side.TOP, shouldMerge, topMask, topPcld, y, chunk.getStartPosition().x, chunk.getStartPosition().z, chunk.getStartPosition().y);
            greedy(faces, Side.BOTTOM, shouldMerge, btmMask, btmPcld, y, chunk.getStartPosition().x, chunk.getStartPosition().z, chunk.getStartPosition().y);
        }

        // East, west
        for (int x = 0; x < width; x++) {
            boolean[][] westMask = new boolean[depth][chunk.getHighestPoint()+1];
            PerCornerLightData[][] westPcld = null;
            if(perCornerLight) {
                westPcld = new PerCornerLightData[depth][chunk.getHighestPoint()+1];
            }
            boolean[][] eastMask = new boolean[depth][chunk.getHighestPoint()+1];
            PerCornerLightData[][] eastPcld = null;
            if(perCornerLight) {
                eastPcld = new PerCornerLightData[depth][chunk.getHighestPoint()+1];
            }
            for (int y = 0; y <= chunk.getHighestPoint(); y++) {
                for (int z = 0; z < depth; z++) {
                    try {
                        Block curBlock = chunk.getBlock(x, y, z);
                        if (curBlock == null || !condition.shouldUse(curBlock))
                            continue;

                        int westNeighborX = x - 1;
                        IChunk westNeighborChunk = chunk;
                        if(westNeighborX < 0) {
                            westNeighborChunk = chunk.getWorld().getChunk(chunk.getStartPosition().x + westNeighborX, chunk.getStartPosition().z + z);
                            westNeighborX += chunk.getWorld().getChunkSize();
                        }
                        if (westNeighborChunk != null) {
                            Block westNeighborBlk = westNeighborChunk.getBlock(westNeighborX, y, z);
                            if (!ocCond.shouldOcclude(curBlock, westNeighborBlk)) {
                                westMask[z][y] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.WEST, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.WEST, x, y, z + 1);
                                    pcld.l10 = calcPerCornerLight(Side.WEST, x, y + 1, z);
                                    pcld.l11 = calcPerCornerLight(Side.WEST, x, y + 1, z + 1);
                                    westPcld[z][y] = pcld;
                                }
                            }
                        }

                        int eastNeighborX = x + 1;
                        IChunk eastNeighborChunk = chunk;
                        if(eastNeighborX >= chunk.getWorld().getChunkSize()) {
                            eastNeighborChunk = chunk.getWorld().getChunk(chunk.getStartPosition().x + eastNeighborX, chunk.getStartPosition().z + z);
                            eastNeighborX -= chunk.getWorld().getChunkSize();
                        }
                        if (eastNeighborChunk != null) {
                            Block eastNeighborBlk = eastNeighborChunk.getBlock(eastNeighborX, y, z);
                            if (!ocCond.shouldOcclude(curBlock, eastNeighborBlk)) {
                                eastMask[z][y] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.EAST, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.EAST, x, y, z + 1);
                                    pcld.l10 = calcPerCornerLight(Side.EAST, x, y + 1, z);
                                    pcld.l11 = calcPerCornerLight(Side.EAST, x, y + 1, z + 1);
                                    eastPcld[z][y] = pcld;
                                }
                            }
                        }
                    } catch (CoordinatesOutOfBoundsException ex) {
                        ex.printStackTrace();
                    }
                }
            }

            greedy(faces, Side.EAST, shouldMerge, eastMask, eastPcld, x, chunk.getStartPosition().z, chunk.getStartPosition().y, chunk.getStartPosition().x);
            greedy(faces, Side.WEST, shouldMerge, westMask, westPcld, x, chunk.getStartPosition().z, chunk.getStartPosition().y, chunk.getStartPosition().x);
        }

        // North, south
        for (int z = 0; z < depth; z++) {
            boolean[][] northMask = new boolean[width][chunk.getHighestPoint()+1];
            PerCornerLightData[][] northPcld = null;
            if(perCornerLight) {
                northPcld = new PerCornerLightData[width][chunk.getHighestPoint()+1];
            }
            boolean[][] southMask = new boolean[width][chunk.getHighestPoint()+1];
            PerCornerLightData[][] southPcld = null;
            if(perCornerLight) {
                southPcld = new PerCornerLightData[width][chunk.getHighestPoint()+1];
            }
            for (int y = 0; y <= chunk.getHighestPoint(); y++) {
                for (int x = 0; x < width; x++) {
                    try {
                        Block curBlock = chunk.getBlock(x, y, z);
                        if (curBlock == null || !condition.shouldUse(curBlock))
                            continue;

                        int northNeighborZ = z + 1;
                        int southNeighborZ = z - 1;
                        IChunk northNeighborChunk = chunk;
                        IChunk southNeighborChunk = chunk;
                        if(northNeighborZ >= chunk.getWorld().getChunkSize()) {
                            northNeighborChunk = chunk.getWorld().getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + northNeighborZ);
                            northNeighborZ -= chunk.getWorld().getChunkSize();
                        } else if(southNeighborZ < 0) {
                            southNeighborChunk = chunk.getWorld().getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + southNeighborZ);
                            southNeighborZ += chunk.getWorld().getChunkSize();
                        }

                        if (northNeighborChunk != null) {
                            Block northNeighborBlock = northNeighborChunk.getBlock(x, y, northNeighborZ);
                            if (!ocCond.shouldOcclude(curBlock, northNeighborBlock)) {
                                northMask[x][y] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.NORTH, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.NORTH, x, y + 1, z);
                                    pcld.l10 = calcPerCornerLight(Side.NORTH, x + 1, y, z);
                                    pcld.l11 = calcPerCornerLight(Side.NORTH, x + 1, y + 1, z);
                                    northPcld[x][y] = pcld;
                                }
                            }
                        }

                        if (southNeighborChunk != null) {
                            Block southNeighborBlock = southNeighborChunk.getBlock(x, y, southNeighborZ);
                            if (!ocCond.shouldOcclude(curBlock, southNeighborBlock)) {
                                southMask[x][y] = true;

                                if (perCornerLight) {
                                    PerCornerLightData pcld = new PerCornerLightData();
                                    pcld.l00 = calcPerCornerLight(Side.SOUTH, x, y, z);
                                    pcld.l01 = calcPerCornerLight(Side.SOUTH, x, y + 1, z);
                                    pcld.l10 = calcPerCornerLight(Side.SOUTH, x + 1, y, z);
                                    pcld.l11 = calcPerCornerLight(Side.SOUTH, x + 1, y + 1, z);
                                    southPcld[x][y] = pcld;
                                }
                            }
                        }
                    } catch (CoordinatesOutOfBoundsException ex) {
                        ex.printStackTrace();
                    }
                }
            }

            greedy(faces, Side.NORTH, shouldMerge, northMask, northPcld, z, chunk.getStartPosition().x, chunk.getStartPosition().y, chunk.getStartPosition().z);
            greedy(faces, Side.SOUTH, shouldMerge, southMask, southPcld, z, chunk.getStartPosition().x, chunk.getStartPosition().y, chunk.getStartPosition().z);
        }

        return faces;
    }

    private float calcLightLevel(Side side, int x, int y, int z) throws CoordinatesOutOfBoundsException {
        switch(side) {
            case TOP:
                y += 1;
                break;
            case BOTTOM:
                y -= 1;
                break;
            case WEST:
                x -= 1;
                break;
            case EAST:
                x += 1;
                break;
            case NORTH:
                z += 1;
                break;
            case SOUTH:
                z -= 1;
                break;
        }

        IWorld world = chunk.getWorld();
        int chunkSize = world.getChunkSize();
        IChunk sChunk = chunk;
        if (z < 0) {
            sChunk = world.getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + z);
            z += chunkSize;
        } else if (z > chunkSize - 1) {
            sChunk = world.getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + z);
            z -= chunkSize;
        } else if (x < 0) {
            sChunk = world.getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + z);
            x += chunkSize;
        } else if (x > chunkSize - 1) {
            sChunk = world.getChunk(chunk.getStartPosition().x + x, chunk.getStartPosition().z + z);
            x -= chunkSize;
        }

        if(sChunk == null)
            return 1;

        return Math.min(1, sChunk.getBrightness(sChunk.getSunlight(x, y, z))
                + sChunk.getBrightness(sChunk.getBlocklight(x, y, z)));
    }

    public List<Face> getFaces(UseCondition condition) {
        return getFaces(condition,
                (curBlock, blockToSide) ->
                        !(blockToSide == null || (blockToSide.isTranslucent() && !curBlock.isTranslucent()))
                                && (curBlock.occludeCovered() && blockToSide.occludeCovered()),
                (id1, meta1, light1, pcld1, id2, meta2, light2, pcld2) -> {
                    Block block1 = RadixAPI.instance.getBlock(id1);
                    if (!block1.shouldGreedyMerge())
                        return false;
                    boolean sameBlock = id1 == id2 && meta1 == meta2;
                    boolean sameLight = light1 == light2;
                    boolean tooDarkToTell = light1 < 0.1f; // Too dark to tell they're not the same block
                    if(perCornerLight) {
                        sameLight = pcld1.equals(pcld2);
                    }
                    if (sameLight && !sameBlock && tooDarkToTell) {
                        Block block2 = RadixAPI.instance.getBlock(id2);
                        // Other block renderers may alter shape in an unpredictable way
                        if (block1.getRenderer().getClass() == NormalBlockRenderer.class
                                && block2.getRenderer().getClass() == NormalBlockRenderer.class
                                && !block1.isTranslucent() && !block2.isTranslucent())
                            sameBlock = true; // Consider them the same block
                    }
                    return sameBlock && sameLight;
                });
    }

    /**
     * @param outputList List to put faces in
     * @param side       Side being meshed
     * @param z          Depth on the plane
     */
    private void greedy(List<Face> outputList, Side side, MergeCondition mergeCond, boolean[][] mask, PerCornerLightData[][] pclds, int z, int offsetX, int offsetY, int offsetZ) {
        int width = mask.length;
        int height = mask[0].length;
        boolean[][] used = new boolean[mask.length][mask[0].length];

        try {
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    if (!mask[x][y])
                        continue;

                    // "real" values of x,y,z
                    int rx = realX(side, x, y, z);
                    int ry = realY(side, x, y, z);
                    int rz = realZ(side, x, y, z);

                    int blk = chunk.getBlockId(rx, ry, rz);
                    short meta = chunk.getMeta(rx, ry, rz);
                    if (blk == 0 || used[x][y])
                        continue;
                    used[x][y] = true;
                    float ll = 15;
                    PerCornerLightData pcld = null;
                    if (perCornerLight) {
                        pcld = pclds[x][y];
                    } else {
                        ll = calcLightLevel(side, rx, ry, rz);
                    }
                    int endX = x + 1;
                    int endY = y + 1;
                    while (true) {
                        int newX = endX;
                        boolean shouldPass = false;
                        if (newX < width) {
                            int newRX = realX(side, newX, y, z);
                            int newRY = realY(side, newX, y, z);
                            int newRZ = realZ(side, newX, y, z);
                            int newBlk = chunk.getBlockId(newRX, newRY, newRZ);
                            int newMeta = chunk.getMeta(newRX, newRY, newRZ);
                            float newll = 15;
                            PerCornerLightData newPcld = null;
                            if (perCornerLight) {
                                newPcld = pclds[newX][y];
                            } else {
                                newll = calcLightLevel(side, newRX, newRY, newRZ);
                            }
                            shouldPass = !used[newX][y] && newBlk != 0 && mergeCond.shouldMerge(blk, meta, ll, pcld, newBlk, newMeta, newll, newPcld);
                        }
                        // expand right if the same block
                        if (shouldPass) {
                            endX++;
                            used[newX][y] = true;
                        } else { // done on initial pass right. Start passing up.
                            while (true) {
                                if (endY == height) break;
                                boolean allPassed = true;
                                // sweep right
                                for (int lx = x; lx < endX; lx++) {
                                    // "real" coordinates for the length block
                                    int lRX = realX(side, lx, endY, z);
                                    int lRY = realY(side, lx, endY, z);
                                    int lRZ = realZ(side, lx, endY, z);

                                    int lblk = chunk.getBlockId(lRX, lRY, lRZ);
                                    if (lblk == 0) {
                                        allPassed = false;
                                        break;
                                    }
                                    int lmeta = chunk.getMeta(lRX, lRY, lRZ);
                                    float llight = 15;
                                    PerCornerLightData lPcld = null;
                                    if (perCornerLight) {
                                        lPcld = pclds[lx][endY];
                                    } else {
                                        llight = calcLightLevel(side, lRX, lRY, lRZ);
                                    }

                                    if (used[lx][endY] || !mergeCond.shouldMerge(blk, meta, ll, pcld, lblk, lmeta, llight, lPcld)) {
                                        allPassed = false;
                                        break;
                                    }
                                }
                                if (allPassed) {
                                    for (int lx = x; lx < endX; lx++) {
                                        used[lx][endY] = true;
                                    }
                                    endY++;
                                } else {
                                    break;
                                }
                            }
                            break;
                        }
                    }
                    outputList.add(new Face(side, blk, ll, pcld, x + offsetX, y + offsetY, endX + offsetX, endY + offsetY, z + offsetZ));
                }
            }
        } catch (CoordinatesOutOfBoundsException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Mesh meshFaces(List<Face> faces, MeshBuilder builder) {
        builder.begin(VertexAttributes.Usage.Position | VertexAttributes.Usage.TextureCoordinates | VertexAttributes.Usage.ColorPacked | VertexAttributes.Usage.Normal, GL20.GL_TRIANGLES);

        builder.ensureVertices(faces.size() * 4);
        for (Face f : faces) {
            f.render(builder);
        }
        return builder.end();
    }

    /**
     * Averages light values at a corner.
     *
     * @param side Side of the face being calculated
     * @param cx Chunk-relative X coordinate for the corner. NOT PRE-OFFSET FOR THE FACE!
     * @param y Chunk-relative Y coordinate for the corner. NOT PRE-OFFSET FOR THE FACE!
     * @param cz Chunk-relative Z coordinate for the corner. NOT PRE-OFFSET FOR THE FACE!
     */
    private float calcPerCornerLight(Side side, int cx, int y, int cz) {
        // coordinate offsets for getting the blocks to average
        int posX = 0, negX = 0,
                posY = 0, negY = 0,
                posZ = 0, negZ = 0;
        switch(side) {
            case TOP:
                // Use the light values from the blocks above the face
                negY = posY = 1;
                // Get blocks around the point
                negZ = negX = -1;
                break;
            case BOTTOM:
                // Use the light values from the blocks below the face
                negY = posY = -1;
                // Get blocks around the point
                negZ = negX = -1;
                break;
            case WEST:
                // Use the light values from the blocks to the west of the face
                negX = posX = -1;
                // Get blocks around the point
                negY = negZ = -1;
                break;
            case EAST:
                // Use the light values from the blocks to the east of the face
                negX = posX = 1;
                // Get blocks around the point
                negY = negZ = -1;
                break;
            case NORTH:
                // Use the light values from the blocks to the north of the face
                negZ = posZ = 1;
                // Get blocks around the point
                negY = negX = -1;
                break;
            case SOUTH:
                // Use the light values from the blocks to the south of the face
                negZ = posZ = -1;
                // Get blocks around the point
                negY = negX = -1;
                break;
        }
        // sx,sy,sz are the x, y, and z positions of the side block
        int count = 0;
        float lightSum = 0;
        for(int sy = y + negY; sy <= y + posY; sy++) {
            if(sy < 0 || sy >= chunk.getWorld().getHeight())
                continue;
            for(int sz = cz + negZ; sz <= cz + posZ; sz++) {
                for(int sx = cx + negX; sx <= cx + posX; sx++) {
                    IChunk sChunk = chunk;
                    boolean getChunk = false; // whether the block is not in the current chunk and a new chunk should be found
                    int getChunkX = chunk.getStartPosition().x + sx;
                    int getChunkZ = chunk.getStartPosition().z + sz;
                    int fixedSz = sz;
                    int fixedSx = sx;
                    if(sz < 0) {
                        fixedSz = chunk.getWorld().getChunkSize() + sz;
                        getChunk = true;
                    } else if(sz >= chunk.getWorld().getChunkSize()) {
                        fixedSz = sz - chunk.getWorld().getChunkSize();
                        getChunk = true;
                    }
                    if(sx < 0) {
                        fixedSx = chunk.getWorld().getChunkSize() + sx;
                        getChunk = true;
                    } else if(sx >= chunk.getWorld().getChunkSize()) {
                        fixedSx = sx - chunk.getWorld().getChunkSize();
                        getChunk = true;
                    }
                    if(getChunk) {
                        sChunk = chunk.getWorld().getChunk(getChunkX, getChunkZ);
                    }
                    if (sChunk == null)
                        continue;

                    try {
                        // Convert to chunk-relative coords
                        lightSum += sChunk.getLightLevel(fixedSx, sy, fixedSz);
                        count++;
                    } catch (CoordinatesOutOfBoundsException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
        return lightSum / count;
    }

    public static class Face {
        private final Side side;
        private final int x1, y1, x2, y2, z;
        private final Block block;
        private final float lightLevel;
        private final PerCornerLightData pcld;

        /**
         * @param pcld Per corner light data. Pass null if per corner lighting is disabled.
         */
        public Face(Side side, int block, float lightLevel, PerCornerLightData pcld, int startX, int startY, int endX, int endY, int z) {
            this.block = RadixAPI.instance.getBlock(block);
            this.lightLevel = lightLevel;
            this.x1 = startX;
            this.y1 = startY;
            this.x2 = endX;
            this.y2 = endY;
            this.z = z;
            this.side = side;
            this.pcld = pcld;
        }

        public void render(MeshBuilder builder) {
            BlockRenderer renderer = block.getRenderer();

            switch (side) {
                case TOP:
                    renderer.renderTop(block.getTextureIndex(), x1, y1, x2, y2, z + 1, lightLevel, pcld, builder);
                    break;
                case BOTTOM:
                    renderer.renderBottom(block.getTextureIndex(), x1, y1, x2, y2, z, lightLevel, pcld, builder);
                    break;
                case NORTH:
                    renderer.renderNorth(block.getTextureIndex(), x1, y1, x2, y2, z + 1, lightLevel, pcld, builder);
                    break;
                case SOUTH:
                    renderer.renderSouth(block.getTextureIndex(), x1, y1, x2, y2, z, lightLevel, pcld, builder);
                    break;
                case EAST:
                    renderer.renderEast(block.getTextureIndex(), x1, y1, x2, y2, z + 1, lightLevel, pcld, builder);
                    break;
                case WEST:
                    renderer.renderWest(block.getTextureIndex(), x1, y1, x2, y2, z, lightLevel, pcld, builder);
                    break;

            }
        }
    }

    // Find "real" x based on relative position in the greedy method
    private int realX(Side side, int x, int y, int z) {
        switch(side) {
            case TOP:
            case BOTTOM:
            case NORTH:
            case SOUTH:
                return x;
            case EAST:
            case WEST:
                return z;
        }
        throw new RuntimeException("WTF SIDE???");
    }

    // Find "real" y based on relative position in the greedy method
    private int realY(Side side, int x, int y, int z) {
        switch(side) {
            case EAST:
            case WEST:
            case NORTH:
            case SOUTH:
                return y;
            case TOP:
            case BOTTOM:
                return z;
        }
        throw new RuntimeException("WTF SIDE???");
    }

    // Find "real" z based on relative position in the greedy method
    private int realZ(Side side, int x, int y, int z) {
        switch(side) {
            case TOP:
            case BOTTOM:
                return y;
            case WEST:
            case EAST:
                return x;
            case NORTH:
            case SOUTH:
                return z;
        }
        throw new RuntimeException("WTF SIDE???");
    }

    public interface OccludeCondition {
        /**
         * @param curBlock Current block being checked
         * @param blockToSide Block the the side of the current block
         * @return True if the side of the curBlock should be occluded
         */
        boolean shouldOcclude(Block curBlock, Block blockToSide);
    }

    public interface MergeCondition {
        boolean shouldMerge(int id1, int meta1, float light1, PerCornerLightData pcld1, int id2, int meta2, float light2, PerCornerLightData pcld2);
    }

} public class Vec3i implements Serializable {
    public int x, y, z;

    /**
     * Constructs a new Vec3i with this value: (0, 0, 0)
     */
    public Vec3i() {
        this(0, 0, 0);
    }

    public Vec3i(int x, int y, int z) {
        super();
        set(x, y, z);
    }

    /**
     * Constructs a new Vec3i and copies the values of the passed vector.
     * @param v the vector to be copied
     */
    public Vec3i(Vec3i v) {
        this(v.x, v.y, v.z);
    }

    public void set(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    /**
     * Subtracts this vector with the passed vector.
     *
     * @param v
     *            the vector to subtract from this
     * @return {@code this}
     */
    public Vec3i sub(Vec3i v) {
        set(x - v.x, y - v.y, z - v.z);
        return this;
    }

    /**
     * Adds the passed vector to this vector
     *
     * @param v
     *            the vector to add
     * @return {@code this}
     */
    public Vec3i add(Vec3i v) {
        set(x + v.x, y + v.y, z + v.z);
        return this;
    }


    /**
     * Performs a scalar product on this vector
     * @param f Scale factor
     * @return {@code this}
     */
    public Vec3i scale(float f) {
        set(MathUtils.floor(x * f), MathUtils.floor(y * f), MathUtils.floor(z * f));
        return this;
    }

    /**
     * Uses cache.
     *
     * @return the squared length of this vector
     */
    public float lengthSquared() {
        return x * x + y * y + z * z;
    }

    /**
     * Uses cache.
     *
     * @return the length of this vector
     */
    public float length() {
        return (float) Math.sqrt(lengthSquared());
    }

    /**
     * This vector will be the result of the cross product, performed on the two
     * vectors passed. Returns {@code this} vector.
     *
     * @param a Vector 1
     * @param b Vector 2
     * @return {@code this}
     */
    public Vec3i cross(Vec3i a, Vec3i b) {
        set(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
        return this;
    }

    /**
     * Performs a dot product on the two specified vectors.
     * @param a Vector 1
     * @param b Vector 2
     * @return the result of the dot product.
     */
    public static int dot(Vec3i a, Vec3i b) {
        return a.x * b.x + a.y * b.y + a.z * b.z;
    }

    public void set(Vec3i vec) {
        set(vec.x, vec.y, vec.z);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        result = prime * result + z;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Vec3i other = (Vec3i) obj;
        if (x != other.x)
            return false;
        if (y != other.y)
            return false;

        return z == other.z;
    }

    public Vec3i translate(int x, int y, int z) {
        return new Vec3i(this.x + x, this.y + y, this.z + z);
    }


    @Override
    public String toString() {
        return "Vec3i [x=" + x + ", y=" + y + ", z=" + z + "]";
    }

    public boolean equals(int x, int y, int z) {
        return this.x == x && this.y == y && this.z == z;
    }
} public interface IWorld {

    /**
     * Gets the chunk size used in the world.
     *
     * The number returned should be a 2^x number.
     *
     * @return 2^x number representing chunk size
     */
    int getChunkSize();

    int getHeight();

    IChunk getChunk(Vec3i position);
    IChunk getChunk(int x, int z);

    void render();

    void loadChunks(EntityPosition playerPosition, int viewDistance);

    int getSeaLevel();

    /**
     * @return Gravity of the world in m/(s^2)
     */
    float getGravity();

    /**
     * @param velocity Velocity to modify, in m/s
     * @param ms       Time elapsed in MS since last gravity application
     * @return Velocity affected by gravity
     */
    float applyGravity(float velocity, long ms);

    void removeBlock(int x, int y, int z);

    void addBlock(int block, int x, int y, int z);

    IChunk[] getChunksInRange(EntityPosition pos, int viewDistance);

    void addChunk(IChunk chunk);

    ConcurrentHashMap<Integer, Entity> getLoadedEntities();

    void addEntity(int entityId, Entity e);

    void rerenderChunk(IChunk c);

    int getChunkPosition(float value);

    ChunkGenerator getChunkGen();

    /**
     * Add a block to a list of blocks to process sunlight for
     * The block at the position passed should be translucent or null and have a sunlight level greater than 0
     */
    void addToSunlightQueue(int x, int y, int z);

    /**
     * Add a block to the list of blocks to process block light for
     * The block at the position should not be null and should have a blocklight level greater than 0.
     */
    void addToBlocklightQueue(int x, int y, int z);

    /**
     * Add a position to the sunlight removal queue
     */
    void addToSunlightRemovalQueue(int x, int y, int z);

    void processLightQueue();

    void cleanup();

    void rerenderChunks();

    void rmChunk(IChunk chunk);
    /**
     * Add to the queue of chunk meshing.
     * This queue may be processed on another thread, so shouldn't do any GL stuff.
     *
     * This method is for internal use by chunks!
     *
     * @param updateFaces Method that generates the mesh
     */
    void addToMeshQueue(Runnable updateFaces);

    /**
     * Add to the queue of chunks to upload.
     * This queue is used to distribute chunk uploading across frames if the option is enabled.
     * This queue is processed on the main thread, so gl can be used (and the mesh queue should be used for non-gl).
     *
     * This method is for internal use by chunks!
     *
     * @param upload Method that uploads the chunk
     */
    void addToChunkUploadQueue(Runnable upload);

} public class Biome {
    // Biome color math from https://github.com/erich666/Mineways/blob/master/Win/biomes.cpp

    private static final int[][] grassColors =
    {
        { 191, 183,  85 },	// lower left, temperature starts at 1.0 on left
        { 128, 180, 151 },	// lower right
        {  71, 205,  51 }	// upper left
    };

    private static final int[][] foliageCorners =
    {
        { 174, 164,  42 },	// lower left, temperature starts at 1.0 on left
        {  96, 161, 123 },	// lower right
        {  26, 191,  0 }	// upper left
    };

    private final String humanName;
    private final float temperature, rainfall;
    private final int id;

    public Biome(int id, String humanName, float temperature, float rainfall) {
        this.humanName = humanName;
        this.temperature = temperature;
        this.rainfall = rainfall;
        this.id = id;
    }

    public float getTemperature() {
        return this.temperature;
    }

    public float getRainfall() {
        return this.rainfall;
    }

    public int getID() {
        return id;
    }

    public int[] getGrassColor(int elevation) {
        return getColor(elevation, grassColors);
    }

    public int[] getFoliageColor(int elevation) {
        return getColor(elevation, foliageCorners);
    }

    private static final float getColTmp[] = new float[3];
    private int[] getColor(int elevation, int[][] corners) {
        float adjTemp = MathUtils.clamp(temperature - elevation*0.00166667f, 0, 1);
        float adjRainfall = MathUtils.clamp(rainfall, 0, 1) * adjTemp;

        getColTmp[0] = adjTemp - adjRainfall;
        getColTmp[1] = 1 - temperature;
        getColTmp[2] = rainfall;

        float red = 0, green = 0, blue = 0;
        for(int i = 0; i < 3; i++) {
            red += getColTmp[i] * corners[i][0/*red*/];
            green += getColTmp[i] * corners[i][1/*green*/];
            blue += getColTmp[i] * corners[i][2/*blue*/];
        }

        return new int[]{
                (int)MathUtils.clamp(red, 0, 255),
                (int)MathUtils.clamp(green, 0, 255),
                (int)MathUtils.clamp(blue, 0, 255)
        };
    }

} class CoordinatesOutOfBoundsException extends Exception {}
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.g3d.*;
import com.badlogic.gdx.graphics.g3d.attributes.BlendingAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.FloatAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.graphics.g3d.model.NodePart;
import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.math.MathUtils;
import sx.lambda.voxel.RadixClient;
import sx.lambda.voxel.api.RadixAPI;
import sx.lambda.voxel.api.events.render.EventChunkRender;
import sx.lambda.voxel.block.Block;
import sx.lambda.voxel.block.NormalBlockRenderer;
import sx.lambda.voxel.block.Side;
import sx.lambda.voxel.client.render.meshing.GreedyMesher;
import sx.lambda.voxel.util.Vec3i;
import sx.lambda.voxel.world.IWorld;
import sx.lambda.voxel.world.biome.Biome;
import sx.lambda.voxel.world.chunk.BlockStorage.CoordinatesOutOfBoundsException;
import java.util.List;
package sx.lambda.voxel.world.chunk;



public class Chunk implements IChunk {

    private static final int MAX_LIGHT_LEVEL = 15;

    private final transient GreedyMesher mesher;
    private final int size;
    private final int height;
    /**
     * Block storage, in pieces 16 high
     *
     * Done this way so that piece full of air don't take up more memory than they have to
     */
    private final FlatBlockStorage[] blockStorage;
    /**
     * Map of light levels (ints 0-15) to brightness multipliers
     */
    private final float[] lightLevelMap = new float[MAX_LIGHT_LEVEL+1];
    private final transient IWorld parentWorld;
    private final Biome biome;
    private transient MeshBuilder meshBuilder;
    private transient ModelBuilder modelBuilder;
    private transient Model opaqueModel, translucentModel;
    private transient ModelInstance opaqueModelInstance, translucentModelInstance;
    private final Vec3i startPosition;
    private int highestPoint;
    private transient boolean sunlightChanging;
    private transient boolean sunlightChanged;
    private boolean setup;
    private boolean cleanedUp;
    private boolean lighted;

    private List<GreedyMesher.Face> translucentFaces;
    private List<GreedyMesher.Face> opaqueFaces;
    private boolean meshing, meshed, meshWhenDone;

    public Chunk(IWorld world, Vec3i startPosition, Biome biome, boolean local) {
        this.parentWorld = world;
        this.startPosition = startPosition;
        this.biome = biome;
        this.size = world.getChunkSize();
        this.height = world.getHeight();

        this.blockStorage = new FlatBlockStorage[MathUtils.ceilPositive((float)this.height/16)];

        for (int i = 0; i <= MAX_LIGHT_LEVEL; i++) {
            int reduction = MAX_LIGHT_LEVEL - i;
            lightLevelMap[i] = (float) Math.pow(0.8, reduction);
        }

        if (RadixClient.getInstance() != null) {// We're a client
            mesher = new GreedyMesher(this, RadixClient.getInstance().getSettingsManager().getVisualSettings().getPerCornerLight().getValue());
        } else {
            mesher = null;
        }

        if(local)
            highestPoint = world.getChunkGen().generate(startPosition, this);
    }

    @Override
    public void rerender() {
        if (cleanedUp)
            return;

        boolean neighborSunlightChanging = false;
        for(int x = startPosition.x - 16; x <= startPosition.x + 16; x += 16) {
            for(int z = startPosition.z - 16; z <= startPosition.z + 16; z += 16) {
                IChunk c = getWorld().getChunk(x, z);
                if(c != null && c.waitingOnLightFinish()) {
                    neighborSunlightChanging = true;
                }
            }
        }
        if(neighborSunlightChanging)
            return;

        if (!setup) {
            meshBuilder = new MeshBuilder();
            modelBuilder = new ModelBuilder();

            setup = true;
        }

        sunlightChanged = false;

        if(meshing) {
            meshWhenDone = true;
        } else {
            meshing = true;
            parentWorld.addToMeshQueue(this::updateFaces);
        }
    }

    @Override
    public void render(ModelBatch batch) {
        if (cleanedUp) return;

        if(!meshing && meshed) {
            getWorld().addToChunkUploadQueue(this::updateModelInstances);
            meshed = false;
        }

        if (sunlightChanged && !sunlightChanging || (!meshing && !meshed && meshWhenDone)) {
            meshWhenDone = false;
            rerender();
        }

        if(opaqueModelInstance != null) {
            batch.render(opaqueModelInstance);
        }
    }

    @Override
    public void renderTranslucent(ModelBatch batch) {
        if(translucentModelInstance != null) {
            batch.render(translucentModelInstance);
        }
    }

    @Override
    public void eachBlock(EachBlockCallee callee) {
        for (int y = 0; y < height; y++) {
            for (int z = 0; z < size; z++) {
                BlockStorage storage = blockStorage[y / 16];
                for (int x = 0; x < size; x++) {
                    try {
                        Block blk = storage.getBlock(x, y & 0xF, z);
                        callee.call(blk, x, y, z);
                    } catch (CoordinatesOutOfBoundsException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void addNeighborsToLightQueues(int x, int y, int z) {// X Y and Z are relative coords, not world coords
        assert x >= 0 && x < size && z >= 0 && z < size && y >= 0 && y < height;

        int cx = x;
        int cz = z;
        x += startPosition.x;
        z += startPosition.z;

        Side[] sides = Side.values();