public final class BuiltInBlockIds {

        public static final int
                STONE_ID = 1,
                DIRT_ID  = 3,
                GRASS_ID = 2,
                COBBLESTONE_ID = 4,
                BEDROCK_ID = 7,
                GRAVEL_ID = 13,
                IRON_ORE_ID = 15,
                COAL_ORE_ID = 16,
                REDSTONE_ORE_ID = 73,
                GOLD_ORE_ID = 14,
                DIAMOND_ORE_ID = 56,
                LAPIS_ORE_ID = 21,
                EMERALD_ORE_ID = 129,
                OBSIDIAN_ID = 49,
                STONE_MONSTER_EGG_ID = 97,
                SNOW_ID = 78,
                SAND_ID = 12,
                WATER_ID = 8,
                WATER_FLOW_ID = 9,
                LAVA_FLOW_ID = 10,
                LAVA_STILL_ID = 11,
                PLANKS_ID = 5,
                LOG_ID = 17,
                LOG_TWO_ID = 162,
                LEAVES_TWO_ID = 161,
                LEAVES_ID = 18,
                STONE_BRICK_ID = 98,
                TALL_GRASS_ID = 31,
                FLOWER_ID = 175,
                FLOWER_TWO_ID = 37,
                FLOWER_THREE_ID = 38,
                BROWN_MUSHROOM_BLOCK_ID = 99,
                RED_MUSHROOM_BLOCK_ID = 100,
                CLAY_ID = 82,
                SUGAR_CANE_ID = 83,
                MOSS_STONE_ID = 48,
                BROWN_MUSHROOM_ID = 39,
                RED_MUSHROOM_ID = 40,
                FENCE_ID = 85,
                UNKNOWN_ID = 1023;

} 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 EventEarlyInit implements Event {
} public class EventWorldStart implements Event {
} public class EventRegisterBiomes implements Event {
} public class EventRegisterBlockRenderers implements Event {
} public class EventRegisterItems implements Event {
} 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 interface GuiScreen {
    /**
     * Initializes in an opengl context
     */
    void init();

    /**
     * Draws the screen
     *
     * @param inGame Whether the screen is being drawn inside a world
     */
    void render(boolean inGame, SpriteBatch guiBatch);

    /**
     * Called after a screen is no longer active.
     */
    void finish();

    void onMouseClick(int button, boolean up);
    void mouseMoved(int x, int y);

    void keyTyped(char c);
} public class SceneTheme {

    private BitmapFont labelFont;

    private Texture buttonBgTex;
    private Texture buttonBgPressTex;
    private Texture buttonBgDisabledTex;

    private ImageTextButtonStyle buttonStyle;

    public void init() {
        this.labelFont = new BitmapFont();

        this.buttonBgTex = new Texture(Gdx.files.internal("textures/gui/guiButtonBackground.png"));
        this.buttonBgPressTex = new Texture(Gdx.files.internal("textures/gui/guiButtonBackground-pressed.png"));
        this.buttonBgDisabledTex = new Texture(Gdx.files.internal("textures/gui/guiButtonBackground-disabled.png"));

        SpriteDrawable buttonBgSprite = new SpriteDrawable(new Sprite(buttonBgTex));
        SpriteDrawable buttonBgPressSprite = new SpriteDrawable(new Sprite(buttonBgPressTex));
        SpriteDrawable buttonBgDisabledSprite =new SpriteDrawable(new Sprite(buttonBgDisabledTex));
        this.buttonStyle = new ImageTextButtonStyle(buttonBgSprite, buttonBgPressSprite, buttonBgDisabledSprite, labelFont);
    }

    public void dispose() {
        this.labelFont.dispose();

        this.buttonBgTex.dispose();
        this.buttonBgPressTex.dispose();
        this.buttonBgDisabledTex.dispose();
    }

    public ImageTextButtonStyle getButtonStyle() {
        return this.buttonStyle;
    }

} public class ChatGUI implements GuiScreen, ChatHandler.ChatMessageListener {

    private final List<String> chatMessages = new ArrayList<>();

    private BitmapFont font;
    private BitmapFontCache fontCache;
    private Texture chatBackground;

    private MinecraftClientConnection connection;

    private GlyphLayout chatAreaLayout;

    private String currentMessage = "";

    public ChatGUI() {
        font = new BitmapFont();
        fontCache = font.newFontCache();
        chatBackground = new Texture(Gdx.files.internal("textures/gui/chatbackground.png"));
    }

    @Override
    public void init() {
    }

    @Override
    public void render(boolean inGame, SpriteBatch guiBatch) {
        if(chatAreaLayout != null) {
            guiBatch.draw(chatBackground, fontCache.getX() - 4, fontCache.getY() + 4, chatAreaLayout.width + 8, -(chatAreaLayout.height));
            renderHud(guiBatch);
        }
        font.draw(guiBatch, "> " + currentMessage + (System.currentTimeMillis() % 1000 >= 500 ? "_" : ""), 4, 4 + font.getLineHeight());
    }

    @Override
    public void finish() {
    }

    @Override
    public void onMouseClick(int button, boolean up) {

    }

    @Override
    public void mouseMoved(int x, int y) {}

    @Override
    public void keyTyped(char c) {
        if(c == 8 && currentMessage.length() > 0) { // backspace
            currentMessage = currentMessage.substring(0, currentMessage.length()-1);
            return;
        }
        if(c == 13 && currentMessage.length() > 0) { // enter
            RadixClient.getInstance().getMinecraftConn().getClient().getSession().send(new ClientChatPacket(currentMessage));
            currentMessage = "";
            RadixClient.getInstance().setCurrentScreen(RadixClient.getInstance().getHud());
            return;
        }
        if(currentMessage.length() < 100 && c >= 32 && c <= 126) { // Ascii printable range
            currentMessage += c;
        }
    }

    /**
     * Setup the chat GUI with a new Minecraft server connection
     */
    public void setup(MinecraftClientConnection conn) {
        if(this.connection != null)
            this.connection.getChatHandler().removeMessageListener(this);
        this.connection = conn;
        chatMessages.clear();
        conn.getChatHandler().addMessageListener(this);
    }

    public void renderHud(SpriteBatch batch) {
        // TODO only render most recent x messages
        if(chatMessages.size() > 0) {
            fontCache.draw(batch);
        }
    }

    @Override
    public void onChatMessage(Message message) {
        if (message instanceof TranslationMessage) {
            TranslationMessage tmsg = (TranslationMessage)message;
            try {
                chatMessages.add(String.format("<%s> %s", tmsg.getTranslationParams()[0], tmsg.getTranslationParams()[1]));
            } catch (ArrayIndexOutOfBoundsException e) {
                e.printStackTrace();
                chatMessages.add(message.getFullText());
            }
        } else {
            chatMessages.add(message.getFullText());
        }

        StringBuilder messages = new StringBuilder();
        for(String s : chatMessages) {
            messages.append(s).append("\n");
        }
        fontCache.clear();
        chatAreaLayout = fontCache.addText(messages, 0, 0, 512, Align.left, true);
        fontCache.setPosition(4, chatAreaLayout.height + 50);
    }

} public class IngameHUD implements GuiScreen {

    private final float TEXTURE_PERCENT = 0.05f;
    private Texture icons;

    public IngameHUD() {
        super();
    }

    @Override
    public void render(boolean inGame, SpriteBatch guiBatch) {
        int blockInHead = RadixClient.getInstance().getPlayer().getBlockInHead(RadixClient.getInstance().getWorld());
        switch (blockInHead) {
            case BuiltInBlockIds.WATER_FLOW_ID:
            case BuiltInBlockIds.WATER_ID:
                guiBatch.setColor(1, 1, 1, 0.6f);
                Block bl = RadixAPI.instance.getBlock(blockInHead);
                bl.getRenderer().render2d(guiBatch, bl.getTextureIndex(), 0, 0, Math.max(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()));
                guiBatch.setColor(1, 1, 1, 1);
                break;
        }

        Block handBlock = RadixAPI.instance.getBlock(RadixClient.getInstance().getPlayer().getItemInHand());
        handBlock.getRenderer().render2d(guiBatch, handBlock.getTextureIndex(), 0, 0, 50);

        // Draw crosshair (after guiBatcher render because it needs to render with its own blending mode
        float centerX = Gdx.graphics.getWidth() / 2f;
        float centerY = Gdx.graphics.getHeight() / 2f;
        int crosshairSize = 32;
        float halfCrosshairSize = crosshairSize / 2f;
        this.drawTexture(guiBatch, 0, MathUtils.round(centerX - halfCrosshairSize), MathUtils.round(centerY - halfCrosshairSize), crosshairSize, crosshairSize);

        RadixClient.getInstance().getChatGUI().renderHud(guiBatch);
    }

    @Override
    public void finish() {
        icons.dispose();
    }

    @Override
    public void onMouseClick(int clickType, boolean up) {
    }

    @Override
    public void mouseMoved(int x, int y) {
    }

    @Override
    public void keyTyped(char c) {
    }

    @Override
    public void init() {
        this.icons = new Texture(Gdx.files.internal("textures/gui/icons.png"));
    }

    private void drawTexture(SpriteBatch batch, int number, int x, int y, int width, int height) {
        float u = ((number % 9) * TEXTURE_PERCENT);
        float v = ((number / 9) * TEXTURE_PERCENT);
        float u2 = u + TEXTURE_PERCENT - 0.001f;
        float v2 = v + TEXTURE_PERCENT - 0.001f;

        batch.draw(icons, x, y, width, height, u, v, u2, v2);
    }

    public Texture getIcons() {
        return icons;
    }

} public class MainMenu implements GuiScreen {

    private static final int TARGET_BUTTON_SIZE = 150;
    private static final int BUTTON_SPACING = 4;

    private final MainMenuButton[] buttons;
    private boolean initialized;
    private SpriteBatch batch;

    private FrameBuffer mmPrerenderFbo;
    private Texture mmPrerender;

    private BitmapFont buttonFont;

    private Texture mmButtonTexture;

    private OrthographicCamera camera;

    public MainMenu() {
        buttons = new MainMenuButton[] {
                new MainMenuButton("Multiplayer (Local)", this::enterMPLocal, TARGET_BUTTON_SIZE),
                new MainMenuButton("Multiplayer (Select)", () -> RadixClient.getInstance().setCurrentScreen(new ServerConnectGUI()), TARGET_BUTTON_SIZE),
                new MainMenuButton("Settings", () -> Gdx.app.debug("", "Settings pressed!"), TARGET_BUTTON_SIZE),
                new MainMenuButton("Quit Game", () -> RadixClient.getInstance().startShutdown(), TARGET_BUTTON_SIZE),
        };
    }

    private void enterMPLocal() {
        Gdx.app.log("", "Entering local mp");
        RadixClient.getInstance().enterRemoteWorld("127.0.0.1", (short) 25565);
    }

    private void resize() {
        camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        camera.update();

        if (mmPrerenderFbo != null)
            mmPrerenderFbo.dispose();
        mmPrerenderFbo = new FrameBuffer(Pixmap.Format.RGB888, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
        mmPrerender = mmPrerenderFbo.getColorBufferTexture();

        int dWidth = Gdx.graphics.getWidth();
        int buttonsPerRow = dWidth / (TARGET_BUTTON_SIZE + BUTTON_SPACING);

        int currentButtonNum = 0;
        for (MainMenuButton button : buttons) {
            int btnX = BUTTON_SPACING + (currentButtonNum % buttonsPerRow) * (TARGET_BUTTON_SIZE + BUTTON_SPACING);
            int btnY = Gdx.graphics.getHeight() - (BUTTON_SPACING + ((int) (currentButtonNum / buttonsPerRow) + 1) * (TARGET_BUTTON_SIZE + BUTTON_SPACING));
            button.setPosition(btnX, btnY);
            currentButtonNum++;
        }
    }

    public void init() {
        if (!initialized) {
            camera = new OrthographicCamera();
            batch = new SpriteBatch();
            batch.setTransformMatrix(RadixClient.getInstance().getHudCamera().combined);
            batch.setTransformMatrix(camera.combined);
            mmButtonTexture = new Texture(Gdx.files.internal("textures/gui/mmBtn.png"));

            buttonFont = new BitmapFont();

            resize();
            rerender();

            initialized = true;
        }
    }

    protected void rerender() {
        mmPrerenderFbo.begin();
        batch.begin();
        Gdx.gl.glClearColor(0, 0, 0, 1);
        for (MainMenuButton button : buttons) {
            button.drawLabel();
            button.render();
        }
        batch.end();
        mmPrerenderFbo.end();
    }

    @Override
    public void render(boolean ingame, SpriteBatch guiBatch) {
        guiBatch.draw(mmPrerender, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
    }

    @Override
    public void onMouseClick(int clickType, boolean up) {
        int mouseX = Gdx.input.getX();
        int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();

        if (clickType == 0) {
            for (MainMenuButton b : buttons) {
                if (b.bounds.contains(mouseX, mouseY)) {
                    b.click();
                }
            }
        }
    }

    @Override
    public void mouseMoved(int x, int y) {}

    @Override
    public void keyTyped(char c) {

    }


    @Override
    public void finish() {
        if (mmPrerenderFbo != null)
            mmPrerenderFbo.dispose();
        if (batch != null)
            batch.dispose();

        initialized = false;
    }

    class MainMenuButton {
        private final String title;
        private final Runnable onClick;
        private final Rectangle bounds;

        private GlyphLayout labelLayout;

        MainMenuButton(String title, Runnable onClick, int size) {
            this.title = title;
            this.onClick = onClick;
            this.bounds = new Rectangle(0, 0, size, size);
        }

        Rectangle getBounds() {
            return this.bounds;
        }

        void click() {
            onClick.run();
        }

        void setSize(int size) {
            this.bounds.setSize(size, size);
            if (buttonFont != null) {
                rerenderLabel();
            }
        }

        void setPosition(int x, int y) {
            this.bounds.setPosition(x, y);
            if (buttonFont != null) {
                rerenderLabel();
            }
        }

        private void render() {
            batch.draw(mmButtonTexture, bounds.x, bounds.y, bounds.width, bounds.height);
        }

        void drawLabel() {
            if (buttonFont != null) {
                if (labelLayout == null) {
                    labelLayout = buttonFont.draw(batch, title, -1000, -1000);
                }

                int textStartX = (int)(bounds.x + (bounds.width / 2.0f - labelLayout.width / 2.0f));
                int textStartY = (int)(bounds.y + (bounds.height / 2.0f + labelLayout.height / 2.0f));
                buttonFont.draw(batch, title, textStartX, textStartY);
            }
        }

        void rerenderLabel() {
            labelLayout = null; // force redraw next frame
        }
    }
} public class SlideUpAnimation extends TimedTransitionAnimation {

    private final GuiScreen currentScreen;

    private FrameBuffer fbo;
    private Texture fboTex;

    private OrthographicCamera cam;

    /**
     * @param currentScreen The currently shown screen
     * @param length        Time in milliseconds to run the animation for
     */
    public SlideUpAnimation(GuiScreen currentScreen, int length) {
        super(length);
        this.currentScreen = currentScreen;
    }

    @Override
    public void init() {
        super.init();
        fbo = new FrameBuffer(Pixmap.Format.RGBA4444, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
        fboTex = fbo.getColorBufferTexture();
        Gdx.gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        Gdx.gl.glEnable(GL_BLEND);
        SpriteBatch batch = new SpriteBatch();
        batch.setProjectionMatrix(RadixClient.getInstance().getHudCamera().combined);
        fbo.bind();
        batch.begin();
        currentScreen.render(false, batch);
        batch.end();
        FrameBuffer.unbind();
    }

    @Override
    public void render(SpriteBatch guiBatch) {
        float percentageDone = (float) getTimeSinceStart() / (float) getLength();

        guiBatch.draw(fboTex, 0, Gdx.graphics.getHeight() + Gdx.graphics.getHeight()*percentageDone, Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
    }

    @Override
    public void finish() {
        fbo.dispose();
        fboTex.dispose();
    }

} public interface TransitionAnimation {

    /**
     * Initialize the animation in an OpenGL context
     */
    void init();

    /**
     * Render the animation on the screen
     */
    void render(SpriteBatch guiBatch);

    /**
     * Clean up transition animation resources
     */
    void finish();

    /**
     * Check if the animation is finished
     *
     * @return Whether the animation has finished
     */
    boolean isFinished();

} public class EntityModel {
    private static Map<MobType, ModelInstance> modelInstances = new HashMap<>();
    private static Map<MobType, Model> models = new HashMap<>();
    private static Map<MobType, Texture> textures = new HashMap<>();

    private static ModelInstance playerModelInstance;
    private static Model playerModel;
    private static Texture playerTexture;
    private static ObjLoader loader = new ObjLoader();
    // TODO: make it configurable
    private static final String ENTITY_PATH = "textures/world/entity";

    public static void init() {
        playerModelInstance = getInstance(ENTITY_PATH + "/human/human.obj", ENTITY_PATH + "/human/human.png");
        modelInstances.put(MobType.CREEPER, getInstance(ENTITY_PATH + "/creeper/creeper.obj", ENTITY_PATH + "/creeper/creeper.png"));
        modelInstances.put(MobType.ENDERMAN, getInstance(ENTITY_PATH + "/enderman/enderman.obj", ENTITY_PATH + "/enderman/enderman.png"));
        modelInstances.put(MobType.SKELETON, getInstance(ENTITY_PATH + "/skeleton/skeleton.obj", ENTITY_PATH + "/skeleton/skeleton.png"));
        modelInstances.put(MobType.ZOMBIE, getInstance(ENTITY_PATH + "/zombie/zombie.obj", ENTITY_PATH + "/zombie/zombie.png"));
        modelInstances.put(MobType.ZOMBIE_PIGMAN, getInstance(ENTITY_PATH + "/pigzombie/pigzombie.obj", ENTITY_PATH + "/pigzombie/pigzombie.png"));

        playerModel = getModel(ENTITY_PATH + "/human/human.obj");
        models.put(MobType.CREEPER, getModel(ENTITY_PATH + "/creeper/creeper.obj"));
        models.put(MobType.ENDERMAN, getModel(ENTITY_PATH + "/enderman/enderman.obj"));
        models.put(MobType.SKELETON, getModel(ENTITY_PATH + "/skeleton/skeleton.obj"));
        models.put(MobType.ZOMBIE, getModel(ENTITY_PATH + "/zombie/zombie.obj"));
        models.put(MobType.ZOMBIE_PIGMAN, getModel(ENTITY_PATH + "/pigzombie/pigzombie.obj"));

        playerTexture = getTexture(ENTITY_PATH + "/human/human.png");
        textures.put(MobType.CREEPER, getTexture(ENTITY_PATH + "/creeper/creeper.png"));
        textures.put(MobType.ENDERMAN, getTexture(ENTITY_PATH + "/enderman/enderman.png"));
        textures.put(MobType.SKELETON, getTexture(ENTITY_PATH + "/skeleton/skeleton.png"));
        textures.put(MobType.ZOMBIE, getTexture(ENTITY_PATH + "/zombie/zombie.png"));
        textures.put(MobType.ZOMBIE_PIGMAN, getTexture(ENTITY_PATH + "/pigzombie/pigzombie.png"));
    }

    public static ModelInstance getInstance(MobType type) {
        return modelInstances.get(type);
    }

    private static ModelInstance getInstance(String objPath, String texturePath) {
        Model model = loader.loadModel(Gdx.files.internal(objPath), new ObjLoader.ObjLoaderParameters(true));
        ModelInstance instance = new ModelInstance(model);
        Texture texture = new Texture(Gdx.files.internal(texturePath));
        instance.materials.get(0).set(TextureAttribute.createDiffuse(texture));
        return instance;
    }

    public static Model getModel(MobType type) {
        return models.get(type);
    }

    private static Model getModel(String objPath) {
        return loader.loadModel(Gdx.files.internal(objPath), new ObjLoader.ObjLoaderParameters(true));
    }

    public static Texture getTexture(MobType type) {
        return textures.get(type);
    }

    private static Texture getTexture(String texturePath) {
        return new Texture(Gdx.files.internal(texturePath));
    }

    // player
    public static ModelInstance getPlayerInstance() {
        return playerModelInstance;
    }

    public static Model getPlayerModel() {
        return playerModel;
    }

    public static Texture getPlayerTexture() {
        return playerTexture;
    }
} public class EntityPosition extends Vector3 implements Cloneable {

    public EntityPosition(float x, float y, float z) {
        super(x, y, z);
    }

    public float planeDistance(float xp, float zp) {
        return (float)Math.sqrt(Math.pow(xp - getX(), 2) + Math.pow(zp - getZ(), 2));
    }

    public float planeDistance(Vector3 pos2) {
        return (float)Math.sqrt(Math.pow(pos2.x - getX(), 2) + Math.pow(pos2.z - getZ(), 2));
    }

    public float getX() {
        return this.x;
    }

    public float getY() {
        return this.y;
    }

    public float getZ() {
        return this.z;
    }

    public void offset(float x, float y, float z) {
        super.add(x, y, z);
    }

} public class EntityRotation implements Serializable {
    private float yaw;
    private float pitch;

    public EntityRotation(float yaw, float pitch) {
        this.yaw = yaw;
        this.pitch = pitch;
    }

    public EntityRotation() {
        this.yaw = 0;
        this.pitch = 0;
    }

    public float getYaw() {
        return this.yaw;
    }

    public float getPitch() {
        return this.pitch;
    }

    public void offset(float pitch, float yaw) {
        synchronized (this) {
            this.pitch = MathUtils.clamp((this.pitch + pitch) % 360, -90, 90);
            this.yaw = (this.yaw + yaw) % 360;
        }

    }

    public void setRot(float pitch, float yaw) {
        this.pitch = pitch;
        this.yaw = yaw;
    }
} public class Player extends LivingEntity implements Serializable {

    private static final float WIDTH = 0.6f;
    private static final float HEIGHT = 1.8f;
    private static final float REACH = 4;
    private static Model playerModel;
    private transient boolean moved = false;
    private int itemInHand = BuiltInBlockIds.STONE_ID;
    private GameMode gameMode = GameMode.CREATIVE;
    private float breakPercent;
    private boolean wasBreaking;

    public Player() {
        this(new EntityPosition(0, 0, 0), new EntityRotation());
    }

    public Player(EntityPosition pos, EntityRotation rot) {
        super(EntityModel.getPlayerModel(), EntityModel.getPlayerTexture(), pos, rot, WIDTH, HEIGHT);
    }

    public float getEyeHeight() {
        return HEIGHT * 0.75f;
    }

    public float getReach() {
        return REACH;
    }

    public int getItemInHand() {
        return itemInHand;
    }

    public void setItemInHand(int block) {
        itemInHand = block;
    }

    @Override
    public void onUpdate() {
        super.onUpdate();
        if (moved) {
            if (RadixClient.getInstance().getMinecraftConn() != null) {
                ClientPlayerPositionRotationPacket positionRotationPacket =
                        new ClientPlayerPositionRotationPacket(this.isOnGround(),
                        this.getPosition().getX(), this.getPosition().getY(), this.getPosition().getZ(),
                        (float) 180 - this.getRotation().getYaw(), -this.getRotation().getPitch());
                RadixClient.getInstance().getMinecraftConn().getClient().getSession().send(positionRotationPacket);
            }
            moved = false;
        }

        Vec3i selBlkPos = RadixClient.getInstance().getSelectedBlock();
        try {
            if(selBlkPos != null) {
                int sbcx = selBlkPos.x & (RadixClient.getInstance().getWorld().getChunkSize() - 1);
                int sbcz = selBlkPos.z & (RadixClient.getInstance().getWorld().getChunkSize() - 1);
                Block selectedBlock = RadixClient.getInstance().getWorld().getChunk(selBlkPos.x, selBlkPos.z)
                        .getBlock(sbcx, selBlkPos.y, sbcz);
                if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) { // left mouse button down
                    if (selectedBlock != null && selectedBlock.isSelectable()) {
                        if (breakPercent <= 0) {
                            RadixClient.getInstance().beginBreak();
                        }
                        Item curItem = RadixAPI.instance.getItem(itemInHand);
                        if (selectedBlock.getHardness() <= 0 || gameMode.equals(GameMode.CREATIVE)) {
                            breakPercent = 1;
                        } else {
                            float incr;
                            if (curItem instanceof Tool) {
                                incr = 50f / selectedBlock.getBreakTimeMS((Tool) curItem);
                            } else {
                                incr = 50f / selectedBlock.getBreakTimeMS(null);
                            }
                            breakPercent = MathUtils.clamp(breakPercent + incr, 0, 1);
                            wasBreaking = true;
                        }
                        if (breakPercent >= 1) {
                            RadixClient.getInstance().breakBlock();
                            resetBlockBreak();
                            wasBreaking = false;
                        }
                    }
                } else {
                    if(wasBreaking) {
                        wasBreaking = false;
                        RadixClient.getInstance().cancelBreak();
                    }
                }
            }
        } catch (BlockStorage.CoordinatesOutOfBoundsException e) {
            e.printStackTrace();
        }
    }

    public void setMoved(boolean moved) {
        this.moved = moved;
    }

    public boolean hasMoved() {
        return this.moved;
    }

    @Override
    public Model getDefaultModel() {
        return playerModel;
    }

    public int getBlockInHead(IWorld world) {
        int x = MathUtils.floor(getPosition().getX());
        int z = MathUtils.floor(getPosition().getZ());
        int y = MathUtils.floor(getPosition().getY() + HEIGHT);
        IChunk chunk = world.getChunk(x, z);
        if (chunk != null) {
            if (y >= world.getHeight()) return 0;
            try {
                return chunk.getBlockId(x & (world.getChunkSize() - 1), y, z & (world.getChunkSize() - 1));
            } catch (BlockStorage.CoordinatesOutOfBoundsException e) {
                e.printStackTrace();
                return 0;
            }
        } else {
            return 0;
        }
    }

    public void setGameMode(GameMode gameMode) {
        this.gameMode = gameMode;
    }

    public GameMode getGameMode() {
        return this.gameMode;
    }

    /**
     * Get the percentage of the current block break
     */
    public float getBreakPercent() {
        return this.breakPercent;
    }

    /**
     * Reset the block break percent
     */
    public void resetBlockBreak() {
        this.breakPercent = 0;
    }

} public class MinecraftClientConnection {

    private final RadixClient game;
    private final Client client;
    private final ChatHandler chatHandler;

    private final Map<Class<? extends Packet>, PacketHandler> handlerMap = new HashMap<>();

    public MinecraftClientConnection(final RadixClient game, String hostname, short port) {
        this.game = game;
        MinecraftProtocol protocol = new MinecraftProtocol("radix-dev");
        this.client = new Client(hostname, port, protocol, new TcpSessionFactory());
        this.chatHandler = new ChatHandler(game);

        handlerMap.put(ServerChunkDataPacket.class, new ChunkDataHandler(game));
        handlerMap.put(ServerPlayerPositionRotationPacket.class, new PlayerPositionHandler(game));
        handlerMap.put(ServerSpawnPlayerPacket.class, new PlayerSpawnHandler(game));
        handlerMap.put(ServerBlockChangePacket.class, new BlockChangeHandler(game));
        handlerMap.put(ServerMultiBlockChangePacket.class, new MultiBlockChangeHandler(game));
        handlerMap.put(ServerJoinGamePacket.class, new JoinGameHandler(game));
        handlerMap.put(ServerEntityMovementPacket.class, new EntityHandler(game));
        handlerMap.put(ServerEntityPositionPacket.class, new EntityHandler(game));
        handlerMap.put(ServerChatPacket.class, chatHandler);
        handlerMap.put(LoginSetCompressionPacket.class, new SetCompressionHandler());
        handlerMap.put(LoginSuccessPacket.class, new LoginSuccessHandler());
        handlerMap.put(ServerPluginMessagePacket.class, new PluginMessageHandler());
        handlerMap.put(ServerDifficultyPacket.class, new ServerDifficultyHandler());
        handlerMap.put(ServerPlayerAbilitiesPacket.class, new PlayerAbilitiesHandler());
        handlerMap.put(ServerSpawnPositionPacket.class, new SpawnPositionHandler(game));
        handlerMap.put(ServerUpdateTimePacket.class, new UpdateTimeHandler());
        handlerMap.put(ServerPlayerListEntryPacket.class, new PlayerListEntryHandler());
        handlerMap.put(ServerPlayerHealthPacket.class, new UpdateHealthHandler());
        handlerMap.put(ServerSpawnMobPacket.class, new SpawnMobHandler(game));
        handlerMap.put(ServerEntityHeadLookPacket.class, new EntityHandler(game));
        handlerMap.put(ServerEntityMetadataPacket.class, new EntityHandler(game));
        client.getSession().addListener(new SessionAdapter() {
            @Override
            public void packetReceived(PacketReceivedEvent event) {
                if (event.getPacket() == null) return;
                try {
                    PacketHandler handler = handlerMap.get(event.getPacket().getClass());
                    if (handler != null) {
                        handler.handle(event.getPacket());
                        return;
                    }
                    if (event.getPacket() instanceof ServerChatPacket) {
                        Message message = event.<ServerChatPacket>getPacket().getMessage();
                        if (message instanceof TranslationMessage) {
                            Gdx.app.debug("", "Received Translation Components: "
                                    + Arrays.toString(((TranslationMessage) message).getTranslationParams()));
                        } else {
                            Gdx.app.debug("", "Received Message: " + message.getFullText());
                        }
                    } else {
                        // Gdx.app.debug("", "Received Unknown Components: " + event.getPacket());
                    }
                } catch(Exception e) {
                    e.printStackTrace();
                }

            }

            @Override
            public void disconnected(DisconnectedEvent event) {
                RadixClient.getInstance().addToGLQueue(() -> {
                            RadixClient.getInstance().setCurrentScreen(RadixClient.getInstance().getMainMenu());
                            Gdx.app.log("", "Disconnected: " + Message.fromString(event.getReason()).getFullText());
                        }
                );
            }
        });
    }

    public void start() {
        client.getSession().connect();
    }

    public Client getClient() { return client; }

    public ChatHandler getChatHandler() {
        return this.chatHandler;
    }

} public class NotInitializedException extends Exception {
} public interface Renderer {

    void render();
    void init();
    void draw2d(SpriteBatch guiBatch);
    void cleanup();

} public class GameRenderer implements Renderer {

    private final RadixClient game;
    private BitmapFont debugTextRenderer;
    private boolean initted = false;
    private boolean calcFrustum;
    private long lastDynamicTextRerenderMS = 0;
    private Frustum frustum;
    private BitmapFontCache fpsRender,
            glInfoRender,
            positionRender,
            headingRender,
            chunkposRender,
            awrtRender,
            lightlevelRender,
            activeThreadsRender,
            selectedBlockRender,
            glDebugRender;

    private Texture[] blockBreakStages = new Texture[10];
    private Model blockBreakModel;
    private ModelInstance blockBreakModelInstance;
    private ModelBatch blockOverlayBatch;
    private ModelBatch entityBatch;

    private int blockBreakStage = 0;

    // GL debug counters
    private int lastDC, lastGLC, lastVTCS, lastTB, lastSS;
    private int curDC, curGLC, curVTCS, curTB, curSS;

    public GameRenderer(RadixClient game) {
        this.game = game;
    }

    @Override
    public void render() {
        if (!initted)
            init();

        prepareWorldRender();
        game.getWorld().render();
        RadixAPI.instance.getEventManager().push(new EventPostWorldRender());
        drawBlockSelection();
        renderEntities();

        curDC = GLProfiler.drawCalls - lastDC;
        curGLC = GLProfiler.calls - lastGLC;
        curVTCS = (int) GLProfiler.vertexCount.total - lastVTCS;
        curTB = GLProfiler.textureBindings - lastTB;
        curSS = GLProfiler.shaderSwitches - lastSS;

        lastDC = GLProfiler.drawCalls;
        lastGLC = GLProfiler.calls;
        lastVTCS = (int) GLProfiler.vertexCount.total;
        lastTB = GLProfiler.textureBindings;
        lastSS = GLProfiler.shaderSwitches;
    }

    @Override
    public void draw2d(SpriteBatch batch) {
        if (!initted)
            init();

        if (System.currentTimeMillis() - lastDynamicTextRerenderMS >= 250) { // Rerender the dynamic texts every quarter second
            createDynamicRenderers();
            lastDynamicTextRerenderMS = System.currentTimeMillis();
        }

        if (game.debugInfoEnabled()) {
            glInfoRender.draw(batch);
            fpsRender.draw(batch);
            positionRender.draw(batch);
            headingRender.draw(batch);
            chunkposRender.draw(batch);
            awrtRender.draw(batch);
            lightlevelRender.draw(batch);
            activeThreadsRender.draw(batch);
            selectedBlockRender.draw(batch);
            glDebugRender.draw(batch);
        }
    }

    @Override
    public void cleanup() {
        debugTextRenderer.dispose();

        for (Texture t : blockBreakStages) {
            t.dispose();
        }

        if (blockBreakModel != null) {
            blockBreakModel.dispose();
        }

        initted = false;
    }

    @Override
    public void init() {
        initted = true;

        frustum = game.getCamera().frustum;

        debugTextRenderer = new BitmapFont();
        fpsRender = debugTextRenderer.newFontCache();
        awrtRender = debugTextRenderer.newFontCache();
        glInfoRender = debugTextRenderer.newFontCache();
        positionRender = debugTextRenderer.newFontCache();
        headingRender = debugTextRenderer.newFontCache();
        chunkposRender = debugTextRenderer.newFontCache();
        lightlevelRender = debugTextRenderer.newFontCache();
        activeThreadsRender = debugTextRenderer.newFontCache();
        selectedBlockRender = debugTextRenderer.newFontCache();
        glDebugRender = debugTextRenderer.newFontCache();

        blockOverlayBatch = new ModelBatch(Gdx.files.internal("shaders/gdx/world.vert.glsl"),
                Gdx.files.internal("shaders/gdx/world.frag.glsl"));
        entityBatch = new ModelBatch(Gdx.files.internal("shaders/gdx/world.vert.glsl"),
                Gdx.files.internal("shaders/gdx/world.frag.glsl"));
        String destroyStageFmt = "textures/block/overlay/destroy_stage_%d.png";
        for (int stage = 0; stage < 10; stage++) {
            blockBreakStages[stage] = new Texture(Gdx.files.internal(String.format(destroyStageFmt, stage)));
        }
    }

    private void prepareWorldRender() {
        if (shouldCalcFrustum()) {
            game.getCamera().position.set(game.getPlayer().getPosition().getX(),
                    game.getPlayer().getPosition().getY() + game.getPlayer().getEyeHeight(),
                    game.getPlayer().getPosition().getZ());
            game.getCamera().up.set(0, 1, 0);
            game.getCamera().direction.set(0, 0, -1);
            game.getCamera().rotate(game.getPlayer().getRotation().getPitch(), 1, 0, 0);
            game.getCamera().rotate(-game.getPlayer().getRotation().getYaw(), 0, 1, 0);

            game.getCamera().update(true);
            calcFrustum = false;
        } else {
            game.getCamera().update();
        }
    }

    private void drawBlockSelection() {
        int curProgressInt = Math.round(RadixClient.getInstance().getPlayer().getBreakPercent() * 10) - 1;
        if ((blockBreakModel == null || blockBreakStage != curProgressInt) && curProgressInt >= 0) {
            if (blockBreakModel != null)
                blockBreakModel.dispose();

            blockBreakStage = curProgressInt;

            ModelBuilder builder = new ModelBuilder();
            blockBreakModel = builder.createBox(1f, 1f, 1f,
                    new Material(TextureAttribute.createDiffuse(blockBreakStages[blockBreakStage]),
                            new BlendingAttribute(),
                            FloatAttribute.createAlphaTest(0.25f)),
                    VertexAttributes.Usage.Position | VertexAttributes.Usage.TextureCoordinates);
            blockBreakModelInstance = new ModelInstance(blockBreakModel);
        }

        Vec3i curBlk = RadixClient.getInstance().getSelectedBlock();
        if (curBlk != null && curProgressInt >= 0) {
            Gdx.gl.glPolygonOffset(100000, 2000000);
            blockOverlayBatch.begin(RadixClient.getInstance().getCamera());
            blockBreakModelInstance.transform.translate(curBlk.x + 0.5f, curBlk.y + 0.5f, curBlk.z + 0.5f);
            blockOverlayBatch.render(blockBreakModelInstance);
            blockBreakModelInstance.transform.translate(-(curBlk.x + 0.5f), -(curBlk.y + 0.5f), -(curBlk.z + 0.5f));
            blockOverlayBatch.end();
            Gdx.gl.glPolygonOffset(100000, -2000000);
        }
    }

    private void renderEntities() {
//        entityBatch.begin(RadixClient.getInstance().getCamera());
//        game.getWorld().getLoadedEntities().forEach((entityId, entity) -> {
//            if (!entity.equals(game.getPlayer()) && entity.getModel() != null) {
//                BoundingBox ebb = new BoundingBox();
//                entity.getModel().calculateBoundingBox(ebb);
//                if(frustum.boundsInFrustum(entity.getPosition().getX(), entity.getPosition().getY(), entity.getPosition().getZ(),
//                        ebb.getWidth()/2.0f, ebb.getDepth()/2.0f, ebb.getHeight()/2.0f)) {
//                    entity.render(entityBatch);
//                    RadixAPI.instance.getEventManager().push(new EventEntityRender(entity));
//                }
//            }
//        });
//        entityBatch.end();
    }

    public void calculateFrustum() {
        this.calcFrustum = true;
    }

    public boolean shouldCalcFrustum() {
        return calcFrustum;
    }

    public Frustum getFrustum() {
        return frustum;
    }

    private void createDynamicRenderers() {
        float currentHeight = 2;

        String glInfoStr = String.format("%s (%s) [%s]",
                Gdx.gl.glGetString(GL_RENDERER), Gdx.gl.glGetString(GL_VERSION), Gdx.gl.glGetString(GL_VENDOR));
        GlyphLayout glGl = glInfoRender.setText(glInfoStr, 0, 0);
        glInfoRender.setPosition((float) Gdx.graphics.getWidth() - glGl.width, (Gdx.graphics.getHeight() - currentHeight));
        currentHeight += debugTextRenderer.getLineHeight();

        String fpsStr = "FPS: " + String.valueOf(Gdx.graphics.getFramesPerSecond());
        if (RadixClient.getInstance().getSettingsManager().getVisualSettings().getNonContinuous().getValue()) {
            fpsStr = fpsStr.concat(" (NON-CONTINUOUS! INACCURATE!)");
            fpsRender.setColor(Color.RED);
        }

        GlyphLayout fpsGl = fpsRender.setText(fpsStr, 0, 0);
        fpsRender.setPosition((float) Gdx.graphics.getWidth() - fpsGl.width, (Gdx.graphics.getHeight() - currentHeight));
        currentHeight += debugTextRenderer.getLineHeight();

        DecimalFormat posFormat = new DecimalFormat("#.00");
        String coordsStr = String.format("(x,y,z): %s,%s,%s",
                posFormat.format(game.getPlayer().getPosition().getX()),
                posFormat.format(game.getPlayer().getPosition().getY()),
                posFormat.format(game.getPlayer().getPosition().getZ()));
        GlyphLayout posGl = positionRender.setText(coordsStr, 0, 0);
        positionRender.setPosition((float) Gdx.graphics.getWidth() - posGl.width, (Gdx.graphics.getHeight() - currentHeight));
        currentHeight += debugTextRenderer.getLineHeight();

        String chunk = String.format("Chunk (x,z): %s,%s",
                game.getWorld().getChunkPosition(game.getPlayer().getPosition().getX()),
                game.getWorld().getChunkPosition(game.getPlayer().getPosition().getZ()));
        GlyphLayout chunkGl = chunkposRender.setText(chunk, 0, 0);
        chunkposRender.setPosition((float) Gdx.graphics.getWidth() - chunkGl.width,
                (Gdx.graphics.getHeight() - currentHeight));
        currentHeight += debugTextRenderer.getLineHeight();

        String headingStr = String.format("(yaw,pitch): %s,%s",
                posFormat.format(game.getPlayer().getRotation().getYaw()),
                posFormat.format(game.getPlayer().getRotation().getPitch()));
        GlyphLayout headingGl = headingRender.setText(headingStr, 0, 0);
        headingRender.setPosition((float) Gdx.graphics.getWidth() - headingGl.width,
                (Gdx.graphics.getHeight() - currentHeight));
        currentHeight += debugTextRenderer.getLineHeight();

        int playerX = MathUtils.floor(game.getPlayer().getPosition().getX());
        int playerY = MathUtils.floor(game.getPlayer().getPosition().getY());
        int playerZ = MathUtils.floor(game.getPlayer().getPosition().getZ());
        IChunk playerChunk = game.getWorld().getChunk(playerX, playerZ);
        try {
            if (playerChunk != null) {
                String llStr = String.format("Light Level @ Feet: %d",
                        playerChunk.getSunlight(playerX & (game.getWorld().getChunkSize() - 1),
                                playerY, playerZ & (game.getWorld().getChunkSize() - 1)));
                GlyphLayout llGl = lightlevelRender.setText(llStr, 0, 0);
                lightlevelRender.setPosition((float) Gdx.graphics.getWidth() - llGl.width,
                        (Gdx.graphics.getHeight() - currentHeight));
                currentHeight += debugTextRenderer.getLineHeight();
            }
        } catch (BlockStorage.CoordinatesOutOfBoundsException ex) {
            ex.printStackTrace();
        }

        String threadsStr = "Active threads: " + Thread.activeCount();
        GlyphLayout threadsGl = activeThreadsRender.setText(threadsStr, 0, 0);
        activeThreadsRender.setPosition((float) Gdx.graphics.getWidth() - threadsGl.width,
                (Gdx.graphics.getHeight() - currentHeight));

        // Current looked-at block info. Draws next to the crosshair
        String currentBlockStr = "";
        Vec3i cbLoc = game.getSelectedBlock();
        if (cbLoc != null) {
            try {
                Block cbBlk = game.getWorld().getChunk(cbLoc.x, cbLoc.z).getBlock(
                        cbLoc.x & (game.getWorld().getChunkSize() - 1),
                        cbLoc.y,
                        cbLoc.z & (game.getWorld().getChunkSize() - 1)
                );
                if (cbBlk != null) {
                    currentBlockStr = String.format(
                            "%s (%d)\n" + // Name (id)
                                    "%d%%", // Breaking percentage
                            cbBlk.getHumanName(), cbBlk.getID(),
                            100 - Math.round(game.getPlayer().getBreakPercent() * 100));
                }
            } catch (CoordinatesOutOfBoundsException e) { // Shouldn't happen
                e.printStackTrace();
            }
        }
        selectedBlockRender.setText(currentBlockStr, 0, 0);
        selectedBlockRender.setPosition(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2);

        glDebugRender.setText(String.format("DC: %d, GLC: %d, VTCS: %d, TB: %d, SS: %d",
                        curDC, curGLC, curVTCS, curTB, curSS),
                0, debugTextRenderer.getLineHeight() + 45);
    }

} public class SettingsManager {

    private final VisualSettings visualSettings;

    public SettingsManager() {
        visualSettings = new VisualSettings();
    }

    public VisualSettings getVisualSettings() {
        return visualSettings;
    }

} public class EntityUpdater implements RepeatedTask {

    private RadixClient game;

    public EntityUpdater(RadixClient game) {
        this.game = game;
    }

    @Override
    public String getIdentifier() {
        return "Entity Updater";
    }

    @Override
    public void run() {
        try {
            while (!game.isDone()) {
                if (game.getWorld() != null && game.getPlayer() != null) {
                    game.getWorld().getLoadedEntities().forEach(
                            (entityId, entity) -> entity.onUpdate());
                    RadixAPI.instance.getEventManager().push(new EventGameTick(game.getWorld()));
                    Thread.sleep(50);
                } else {
                    Thread.sleep(1000);
                }
            }
        } catch (Exception e) {
            game.handleCriticalException(e);
        }
    }

} public class MovementHandler implements RepeatedTask {

    // In meters per milliseconds
    private static final float WALK_SPEED = 0.0032f;

    public MovementHandler(RadixClient game) {
        this.game = game;
    }

    @Override
    public String getIdentifier() {
        return "Movement Handler";
    }

    @Override
    public void run() {
        try {
            long lastMoveCheckMS = System.currentTimeMillis();

            while (!game.isDone()) {
                if (game.getWorld() == null || game.getPlayer() == null) {
                    Thread.sleep(1000);
                    lastMoveCheckMS = System.currentTimeMillis();
                } else {
                    Player player = game.getPlayer();
                    IWorld world = game.getWorld();
                    long moveDiffMS = System.currentTimeMillis() - lastMoveCheckMS;
                    lastMoveCheckMS = System.currentTimeMillis();
                    final boolean threeDMove = false;
                    Vector3 lastPosition = player.getPosition().cpy();
                    if (game.getCurrentScreen().equals(game.getHud())) {
                        float deltaX = 0;
                        float deltaY = 0;
                        float deltaZ = 0;
                        if (Gdx.input.isKeyPressed(Input.Keys.W)) {// Forward TODO Config - Make keys configurable
                            float yaw = player.getRotation().getYaw();
                            float pitch = player.getRotation().getPitch();
                            if (threeDMove) {
                                deltaX += MathUtils.cosDeg(pitch) * MathUtils.sinDeg(yaw);
                                deltaY += MathUtils.sinDeg(pitch);
                                deltaZ += -MathUtils.cosDeg(pitch) * MathUtils.cosDeg(yaw);
                            } else {
                                deltaX += MathUtils.sinDeg(yaw);
                                deltaZ += -MathUtils.cosDeg(yaw);
                            }
                        }
                        if (Gdx.input.isKeyPressed(Input.Keys.S)) {
                            float yaw = player.getRotation().getYaw();
                            float pitch = player.getRotation().getPitch();
                            if (threeDMove) {
                                deltaX += -MathUtils.cosDeg(pitch) * MathUtils.sinDeg(yaw);
                                deltaY += -MathUtils.sinDeg(pitch);
                                deltaZ += MathUtils.cosDeg(pitch) * MathUtils.cosDeg(yaw);
                            } else {
                                deltaX += -MathUtils.sinDeg(yaw);
                                deltaZ += MathUtils.cosDeg(yaw);
                                deltaY += 0;
                            }
                        }
                        if (Gdx.input.isKeyPressed(Input.Keys.A)) {//Strafe left
                            float yaw = player.getRotation().getYaw();

                            deltaX += MathUtils.sinDeg(yaw - 90);
                            deltaZ += -MathUtils.cosDeg(yaw - 90);
                        }
                        if (Gdx.input.isKeyPressed(Input.Keys.D)) {//Strafe right
                            float yaw = player.getRotation().getYaw();

                            deltaX += MathUtils.sinDeg(yaw + 90);
                            deltaZ += -MathUtils.cosDeg(yaw + 90);
                        }

                        float movementMultiplier = moveDiffMS*WALK_SPEED;
                        Vector3 deltaVec = new Vector3(deltaX, deltaY, deltaZ);
                        deltaVec.nor();
                        deltaVec.set(deltaVec.x*movementMultiplier, deltaVec.y*movementMultiplier, deltaVec.z*movementMultiplier);

                        if (!checkDeltaCollision(player, 0, 0, deltaVec.z)) {
                            player.getPosition().offset(0, 0, deltaVec.z);
                        }
                        if (!checkDeltaCollision(player, deltaVec.x, 0, 0)) {
                            player.getPosition().offset(deltaVec.x, 0, 0);
                        }
                        if (!checkDeltaCollision(player, 0, deltaVec.y, 0)) {
                            player.getPosition().offset(0, deltaVec.y, 0);
                        }
                    }

                    int playerX = MathUtils.floor(player.getPosition().getX());
                    int playerZ = MathUtils.floor(player.getPosition().getZ());
                    IChunk playerChunk = world.getChunk(playerX, playerZ);
                    if (playerChunk != null) {
                        int cx = playerX & (game.getWorld().getChunkSize() - 1);
                        int cz = playerZ & (game.getWorld().getChunkSize() - 1);
                        int feetBlockY = MathUtils.floor(player.getPosition().getY() - 0.02f);
                        try {
                            Block blockAtPlayer = playerChunk.getBlock(cx, feetBlockY, cz);
                        if (blockAtPlayer != null && blockAtPlayer.isSolid() &&
                                blockAtPlayer.calculateBoundingBox(playerChunk, cx, feetBlockY, cz)
                                        .contains(player.getPosition().cpy().add(0, -0.02f, 0))) {
                                player.setOnGround(true);
                            } else {
                                player.setOnGround(false);
                            }
                        } catch (CoordinatesOutOfBoundsException ex) {
                            System.out.println(feetBlockY);
                            player.setOnGround(true);
                            ex.printStackTrace();
                        }
                    } else {
                        player.setOnGround(true);
                    }

                    int blockInFeet = player.getBlockInFeet(world);
                    boolean inWater = blockInFeet == BuiltInBlockIds.WATER_ID || blockInFeet == BuiltInBlockIds.WATER_FLOW_ID
                            || blockInFeet == BuiltInBlockIds.LAVA_STILL_ID || blockInFeet == BuiltInBlockIds.LAVA_FLOW_ID;
                    if (inWater) {
                        if (!Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
                            player.setYVelocity(-3f); // TODO use the same value as mc
                        } else {
                            player.setYVelocity(3f); // TODO use the same value as mc
                        }
                    } else {
                        player.setYVelocity(world.applyGravity(player.getYVelocity(), moveDiffMS));
                    }

                    player.updateMovement(this, moveDiffMS/1000f);

                    player.setMoved(player.getPosition().x != lastPosition.x || player.getPosition().y != lastPosition.y || player.getPosition().z != lastPosition.z);

                    if (player.hasMoved()) {
                        game.getGameRenderer().calculateFrustum();
                        Gdx.graphics.requestRendering();
                    }

                    Thread.sleep(5l);
                }
            }
        } catch (Exception e) {
            game.handleCriticalException(e);
        }
    }

    public boolean checkDeltaCollision(LivingEntity e, float deltaX, float deltaY, float deltaZ) {
        BoundingBox curBB = e.calculateBoundingBox();
        BoundingBox newBB = new BoundingBox(curBB.min.cpy().add(deltaX, deltaY, deltaZ), curBB.max.cpy().add(deltaX, deltaY, deltaZ));

        boolean collideSuccess = false;

        int x = MathUtils.floor(e.getPosition().x);
        int y = MathUtils.floor(e.getPosition().y);
        int z = MathUtils.floor(e.getPosition().z);

        IChunk chunk = game.getWorld().getChunk(x, z);
        if (chunk == null)
            return true;

        int cx = x & (game.getWorld().getChunkSize() - 1);
        int cz = z & (game.getWorld().getChunkSize() - 1);
        try {
            Block block = chunk.getBlock(cx, y, cz);

            for (Vector3 corner : getCorners(newBB)) {
                collideSuccess = collideSuccess || checkCollision(corner);
            }

            return collideSuccess ||
                    (block != null && block.isSolid()
                            && block.calculateBoundingBox(chunk, cx, y, cz).intersects(newBB));
        } catch(CoordinatesOutOfBoundsException ex) {
            ex.printStackTrace();
            return true;
        }
    }

    public boolean checkCollision(Vector3 pos) {
        if (pos.y >= RadixClient.getInstance().getWorld().getHeight() || pos.y < 0)
            return false;

        int x = MathUtils.floor(pos.x);
        int y = MathUtils.floor(pos.y);
        int z = MathUtils.floor(pos.z);

        IChunk chunk = game.getWorld().getChunk(x, z);
        if (chunk == null)
            return true;

        int cx = x & (game.getWorld().getChunkSize() - 1);
        int cz = z & (game.getWorld().getChunkSize() - 1);
        try {
            Block block = chunk.getBlock(cx, y, cz);

            return block != null && block.isSolid() && block.calculateBoundingBox(chunk, cx, y, cz).contains(pos);
        } catch (CoordinatesOutOfBoundsException ex) {
            ex.printStackTrace();
            return true;
        }
    }

    public void jump() {
        int blockInFeet = game.getPlayer().getBlockInFeet(game.getWorld());
        boolean inWater = blockInFeet == BuiltInBlockIds.WATER_ID || blockInFeet == BuiltInBlockIds.WATER_FLOW_ID
                || blockInFeet == BuiltInBlockIds.LAVA_STILL_ID || blockInFeet == BuiltInBlockIds.LAVA_FLOW_ID;
        if (game.getPlayer().isOnGround()) {
            if(!inWater)
                game.getPlayer().setYVelocity(8.5f);
            game.getPlayer().setOnGround(false);
        }
    }

    private Vector3[] getCorners(BoundingBox bb) {
        return new Vector3[]{bb.getCorner000(new Vector3()), bb.getCorner001(new Vector3()), bb.getCorner010(new Vector3()), bb.getCorner011(new Vector3()), bb.getCorner100(new Vector3()), bb.getCorner101(new Vector3()), bb.getCorner110(new Vector3()), bb.getCorner111(new Vector3())};
    }

    private final RadixClient game;
} public interface RepeatedTask extends Runnable {
    String getIdentifier();
} public class WorldLoader implements RepeatedTask {

    private final RadixClient game;
    private IChunk currentChunk;

    public WorldLoader(RadixClient game) {
        this.game = game;
    }

    @Override
    public void run() {
        try {
            while (!game.isDone()) {
                if (game.getPlayer() != null && game.getWorld() != null && !game.isRemote()) {
                    int viewDistance = game.getSettingsManager().getVisualSettings().getViewDistance();
                    EntityPosition playerPos = game.getPlayer().getPosition();
                    if (currentChunk == null) {
                        game.getWorld().loadChunks(playerPos, viewDistance);
                        currentChunk = game.getWorld().getChunk(MathUtils.floor(playerPos.getX()), MathUtils.floor(playerPos.getZ()));
                    } else {
                        if (Math.abs((int) currentChunk.getStartPosition().x - game.getPlayer().getPosition().getX()) > game.getWorld().getChunkSize() || Math.abs((int) currentChunk.getStartPosition().z - game.getPlayer().getPosition().getZ()) > game.getWorld().getChunkSize()) {
                            currentChunk = game.getWorld().getChunk(MathUtils.floor(playerPos.getX()), MathUtils.floor(playerPos.getZ()));
                            game.getWorld().loadChunks(playerPos, viewDistance);
                        }
                    }
                }

                Thread.sleep(1000);
            }

        } catch (Exception e) {
            RadixClient.getInstance().handleCriticalException(e);
        }
    }

    @Override
    public String getIdentifier() {
        return "World Loader";
    }

} public class PlotCell3f implements Plot<Vec3i> {

    private final Vector3 size = new Vector3();
    private final Vector3 off = new Vector3();
    private final Vector3 pos = new Vector3();
    private final Vector3 dir = new Vector3();

    private final Vec3i index = new Vec3i();

    private final Vector3 delta = new Vector3();
    private final Vec3i sign = new Vec3i();
    private final Vector3 max = new Vector3();

    private int limit;
    private int plotted;

    public PlotCell3f(float offx, float offy, float offz, float width, float height, float depth) {
        off.set(offx, offy, offz);
        size.set(width, height, depth);
    }

    public void plot(Vector3 position, Vector3 direction, int cells) {
        limit = cells;

        pos.set(position);

        // Normalize dir w/ direction
        float invLen = 1.0f / direction.len();
        dir.x = direction.x * invLen;
        dir.y = direction.y * invLen;
        dir.z = direction.z * invLen;

        delta.set(size);
        delta.set(delta.x / dir.x, delta.y / dir.y, delta.z / dir.z);

        sign.x = (dir.x > 0) ? 1 : (dir.x < 0 ? -1 : 0);
        sign.y = (dir.y > 0) ? 1 : (dir.y < 0 ? -1 : 0);
        sign.z = (dir.z > 0) ? 1 : (dir.z < 0 ? -1 : 0);

        reset();
    }

    @Override
    public boolean next() {
        if (plotted++ > 0) {
            float mx = sign.x * max.x;
            float my = sign.y * max.y;
            float mz = sign.z * max.z;

            if (mx < my && mx < mz) {
                max.x += delta.x;
                index.x += sign.x;
            } else if (mz < my && mz < mx) {
                max.z += delta.z;
                index.z += sign.z;
            } else {
                max.y += delta.y;
                index.y += sign.y;
            }
        }
        return (plotted <= limit);
    }

    @Override
    public void reset() {
        plotted = 0;

        index.x = MathUtils.floor((pos.x - off.x) / size.x);
        index.y = MathUtils.floor((pos.y - off.y) / size.y);
        index.z = MathUtils.floor((pos.z - off.z) / size.z);

        float ax = index.x * size.x + off.x;
        float ay = index.y * size.y + off.y;
        float az = index.z * size.z + off.z;

        max.x = (sign.x > 0) ? ax + size.x - pos.x : pos.x - ax;
        max.y = (sign.y > 0) ? ay + size.y - pos.y : pos.y - ay;
        max.z = (sign.z > 0) ? az + size.z - pos.z : pos.z - az;
        max.set(max.x / dir.x, max.y / dir.y, max.z / dir.z);
    }

    @Override
    public void end() {
        plotted = limit + 1;
    }

    @Override
    public Vec3i get() {
        return index;
    }

    public Vector3 actual() {
        return new Vector3(index.x * size.x + off.x,
                index.y * size.y + off.y,
                index.z * size.z + off.z);
    }

    public Vector3 size() {
        return size;
    }

    public void size(float w, float h, float d) {
        size.set(w, h, d);
    }

    public Vector3 offset() {
        return off;
    }

    public void offset(float x, float y, float z) {
        off.set(x, y, z);
    }

    public Vector3 position() {
        return pos;
    }

    public Vector3 direction() {
        return dir;
    }

    public Vec3i sign() {
        return sign;
    }

    public Vector3 delta() {
        return delta;
    }

    public Vector3 max() {
        return max;
    }

    public int limit() {
        return limit;
    }

    public int plotted() {
        return plotted;
    }


} 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 World implements IWorld {

    private static final int CHUNK_SIZE = 16; // SHOULD ALWAYS BE 2^x
    private static final int WORLD_HEIGHT = 256;
    private static final int SEA_LEVEL = 64;
    private static final float GRAVITY = 32f;
    private static final float TERMINAL_VELOCITY = 78.4f;

    /**
     * The amount of lighting workers to have per light type (sunlight and blocklight)
     */
    private static final int LIGHTING_WORKERS = 2;

    private final IntMap<IChunk> chunkMap = new IntMap<>();
    private final Queue<IChunk> chunksToRerender = new ConcurrentLinkedQueue<>();

    private final boolean remote, server;
    private final ChunkGenerator chunkGen;
    private final ConcurrentHashMap<Integer, Entity> loadedEntities = new ConcurrentHashMap<>();

    // Light-related stuff
    private final ExecutorService sunlightPoolExecutor = Executors.newFixedThreadPool(LIGHTING_WORKERS);
    private final ExecutorService blocklightPoolExecutor = Executors.newFixedThreadPool(LIGHTING_WORKERS);
    private final Queue<int[]> sunlightQueue = new LinkedBlockingQueue<>();
    private final Queue<int[]> sunlightRemovalQueue = new LinkedBlockingQueue<>();
    private final Queue<int[]> blocklightQueue = new LinkedBlockingQueue<>();

    // Mesh related stuff
    // Manages meshing chunks off of the main thread
    private final Queue<Runnable> chunkMeshQueue = new LinkedBlockingQueue<>();
    // Manages uploading chunks on the gl thread
    private final Queue<Runnable> chunkUploadQueue = new LinkedList<>();

    // Skybox stuff
    private ModelBatch modelBatch;
    private ModelBatch wiremeshBatch;
    private ModelInstance skybox;
    private Model skyboxModel;
    private Texture skyboxTexture;

    public World(boolean remote, boolean server) {
        this.remote = remote;
        this.server = server;
        if (!remote) {
            this.chunkGen = new SimplexChunkGenerator(this, 200, new Random().nextInt());
        } else {
            this.chunkGen = null;
        }

        for(int i = 0; i < LIGHTING_WORKERS; i++) {
            sunlightPoolExecutor.submit(new SunlightQueueWorker(this, sunlightQueue));
        }
        for(int i = 0; i < LIGHTING_WORKERS; i++) {
            blocklightPoolExecutor.submit(new BlocklightQueueWorker(this, blocklightQueue));
        }

        new MeshQueueWorker(chunkMeshQueue).start();
    }

    public int getChunkSize() {
        return CHUNK_SIZE;
    }

    public int getHeight() {
        return WORLD_HEIGHT;
    }

    public IChunk getChunk(Vec3i position) {
        return getChunk(position.x, position.z);
    }

    private int getChunkKey(int x, int z) {
        short nx = (short)Math.floorDiv(x, getChunkSize());
        short nz = (short)Math.floorDiv(z, getChunkSize());

        return (nx << 16) | (nz & 0xFFFF);
    }

    @Override
    public IChunk getChunk(int x, int z) {
        try {
            return chunkMap.get(getChunkKey(x, z));
        } catch(ArrayIndexOutOfBoundsException ex) {
            return null;
        }
    }

    private void removeChunkFromMap(Vec3i pos) {
        removeChunkFromMap(pos.x, pos.z);
    }

    private void removeChunkFromMap(int x, int z) {
        chunkMap.remove(getChunkKey(x, z));
    }

    @Override
    public void render() {
        assert !server;

        if (modelBatch == null) {
            modelBatch = new ModelBatch(Gdx.files.internal("shaders/gdx/world.vert.glsl"), Gdx.files.internal("shaders/gdx/world.frag.glsl"));
            wiremeshBatch = new ModelBatch(Gdx.files.internal("shaders/gdx/world.vert.glsl"), Gdx.files.internal("shaders/gdx/wiremesh.frag.glsl"));
        }
        if(skybox == null) {
            skybox = createSkybox();
        }

        processChunkUploadQueue();

        processLightQueue(); // If a chunk is doing its rerender, we want it to have the most recent lighting possible

        {
            Queue<IChunk> skippedChunks = new LinkedList<>(); // Chunks that didn't rerender and need to be added back to the queue after iteration
            IChunk c;
            while((c = chunksToRerender.poll()) != null) {
                if(RadixClient.getInstance().getPlayer().getPosition().planeDistance(c.getStartPosition().x, c.getStartPosition().z) <=
                        RadixClient.getInstance().getSettingsManager().getVisualSettings().getViewDistance() * CHUNK_SIZE) {
                    c.rerender();
                } else {
                    skippedChunks.add(c); // Add back onto the queue
                }
            }

            while((c = skippedChunks.poll()) != null) {
                chunksToRerender.add(c);
            }
        }

        float playerX = RadixClient.getInstance().getPlayer().getPosition().getX(),
                playerY = RadixClient.getInstance().getPlayer().getPosition().getY(),
                playerZ = RadixClient.getInstance().getPlayer().getPosition().getZ();
        boolean wireframe = RadixClient.getInstance().isWireframe();
        if(wireframe) {
            RadixClient.getInstance().setWireframe(false);
        }
        modelBatch.begin(RadixClient.getInstance().getCamera());
        skybox.transform.translate(playerX, playerY, playerZ);
        modelBatch.render(skybox);
        skybox.transform.translate(-playerX, -playerY, -playerZ);
        if(chunkMap != null) {
            List<IChunk> visibleChunks = new LinkedList<>();
            for (IChunk c : chunkMap.values()) {
                if(c == null)
                    continue;

                int x = c.getStartPosition().x;
                int z = c.getStartPosition().z;
                int halfWidth = getChunkSize() / 2;
                int midX = x + halfWidth;
                int midZ = z + halfWidth;
                int midY = c.getHighestPoint() / 2;
                boolean visible = RadixClient.getInstance().getGameRenderer().getFrustum().boundsInFrustum(midX, midY, midZ, halfWidth, midY, halfWidth);
                if (visible) {
                    visibleChunks.add(c);
                    c.render(modelBatch);
                }
            }

            for (IChunk c : visibleChunks) {
                c.renderTranslucent(modelBatch);
            }
            modelBatch.end();
            if(wireframe) {
                RadixClient.getInstance().setWireframe(true);
                Gdx.gl.glLineWidth(2);
                wiremeshBatch.begin(RadixClient.getInstance().getCamera());
                for (IChunk c : visibleChunks) {
                    c.render(wiremeshBatch);
                }
                wiremeshBatch.end();
            }
        } else {
            modelBatch.end();
        }
    }

    @Override
    public void loadChunks(EntityPosition playerPosition, int viewDistance) {
        if (!remote) { //don't gen chunks if we're not local
            this.getChunksInRange(playerPosition, viewDistance);
        }

    }

    @Override
    public int getSeaLevel() {
        return SEA_LEVEL;
    }

    @Override
    public int getChunkPosition(float value) {
        int subtraction = MathUtils.floor(value) & (CHUNK_SIZE-1);
        return MathUtils.floor(value - subtraction);
    }

    @Override
    public float getGravity() {
        return GRAVITY;
    }

    @Override
    public float applyGravity(float velocity, long ms) {
        return Math.max(-TERMINAL_VELOCITY, velocity - getGravity() * (ms / 1000f));
    }

    @Override
    public void removeBlock(int x, int y, int z) {
        final IChunk c = this.getChunk(x, z);
        if (c != null) {
            x &= getChunkSize()-1;
            y &= getChunkSize()-1;
            try {
                c.removeBlock(x, y, z);
                c.setMeta((short) 0, x, y, z);
            } catch (CoordinatesOutOfBoundsException ex) {
                ex.printStackTrace();
            }
        }
    }

    @Override
    public void addBlock(int block, int x, int y, int z) {
        final IChunk c = this.getChunk(x, z);
        if(c != null) {
            try {
                c.setBlock(block, x & (getChunkSize() - 1), y, z * (getChunkSize() - 1));
            } catch (CoordinatesOutOfBoundsException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public IChunk[] getChunksInRange(EntityPosition epos, int viewDistance) {
        List<IChunk> chunkList = new ArrayList<>();
        int playerChunkX = getChunkPosition(epos.getX());
        int playerChunkZ = getChunkPosition(epos.getZ());
        int range = viewDistance * CHUNK_SIZE;
        for (int x = playerChunkX - range; x < playerChunkX + range; x += CHUNK_SIZE) {
            for (int z = playerChunkZ - range; z < playerChunkZ + range; z += CHUNK_SIZE) {
                chunkList.add(loadChunk(x, z));
            }
        }
        return chunkList.toArray(new IChunk[chunkList.size()]);
    }

    @Override
    public void addChunk(final IChunk chunk) {
        IChunk c = getChunk(chunk.getStartPosition());
        if (c != null) {
            removeChunkFromMap(chunk.getStartPosition());
        }
        addChunk(chunk, chunk.getStartPosition().x, chunk.getStartPosition().z);

        // Rerender neighbors
        for(int x = chunk.getStartPosition().x - getChunkSize(); x <= chunk.getStartPosition().x + getChunkSize(); x += getChunkSize()) {
            for (int z = chunk.getStartPosition().z - getChunkSize(); z <= chunk.getStartPosition().z + getChunkSize(); z += getChunkSize()) {
                if(x == chunk.getStartPosition().x && z == chunk.getStartPosition().z)
                    continue;
                IChunk neighbor = getChunk(x, z);
                if(neighbor != null) {
                    rerenderChunk(neighbor);
                }
            }
        }
    }

    @Override
    public ConcurrentHashMap<Integer, Entity> getLoadedEntities() {
        return this.loadedEntities;
    }

    private void addChunk(IChunk chunk, int x, int z) {
        this.chunkMap.put(getChunkKey(x, z), chunk);
    }

    private IChunk loadChunk(int startX, int startZ) {
        IChunk foundChunk = getChunk(startX, startZ);
        if (foundChunk == null && !remote) {
            final IChunk c = new Chunk(this, new Vec3i(startX, 0, startZ), RadixAPI.instance.getBiomeByID(0), true);
            RadixAPI.instance.getEventManager().push(new EventFinishChunkGen(c));
            addChunk(c, startX, startZ);
            return c;
        } else {
            return foundChunk;
        }
    }

    public void addEntity(int entityId, Entity e) {
        loadedEntities.put(entityId, e);
    }


    @Override
    public void rerenderChunk(IChunk c) {
        if(c == null)
            return;
        chunksToRerender.add(c);
    }

    @Override
    public ChunkGenerator getChunkGen() {
        return this.chunkGen;
    }

    @Override
    public void addToSunlightQueue(int x, int y, int z) {
        sunlightQueue.add(new int[]{x, y, z});
        synchronized(sunlightQueue) {
            sunlightQueue.notify();
        }
    }

    @Override
    public void addToBlocklightQueue(int x, int y, int z) {
        blocklightQueue.add(new int[]{x, y, z});
        synchronized(blocklightQueue) {
            blocklightQueue.notify();
        }
    }

    @Override
    public void addToSunlightRemovalQueue(int x, int y, int z) {
        sunlightRemovalQueue.add(new int[]{x, y, z});
    }

    @Override
    public void processLightQueue() {
        boolean noLightUpdatesQueued = sunlightQueue.isEmpty() && sunlightRemovalQueue.isEmpty();

        for(IChunk c : chunkMap.values()) {
            // If the chunk is not lighted and it is in range, setup lighting then set as lighted
            if(c == null)
                continue;
            
            if(!c.hasInitialSun()
                    && RadixClient.getInstance().getPlayer().getPosition().planeDistance(
                        c.getStartPosition().x, c.getStartPosition().z)
                        <= RadixClient.getInstance().getSettingsManager().getVisualSettings().getViewDistance() * CHUNK_SIZE) {
                setupLighting(c);
                c.finishAddingSun();
            }

            if(noLightUpdatesQueued && c.waitingOnLightFinish()) {
                c.finishChangingSunlight();
            }
        }

        processLightRemovalQueue();
    }

    private void processLightRemovalQueue() {
        if (!sunlightRemovalQueue.isEmpty()) {
            Side[] sides = Side.values();

            List<IChunk> changedChunks = new ArrayList<>();
            int[] pos;
            while ((pos = sunlightRemovalQueue.poll()) != null) {
                int x = pos[0];
                int cx = x & (CHUNK_SIZE-1);
                int y = pos[1];
                int z = pos[2];
                int cz = z & (CHUNK_SIZE-1);
                IChunk posChunk = getChunk(x, z);
                if (posChunk == null) {
                    continue;
                }
                int ll;
                try {
                    ll = posChunk.getSunlight(x & (CHUNK_SIZE-1), y, z & (CHUNK_SIZE-1));

                    // Spread off to each side
                    for(Side s : sides) {
                        int sx = x; // Side x coord
                        int sy = y; // Side y coord
                        int sz = z; // Side z coord
                        int scx = cx; // Chunk-relative side x coord
                        int scz = cz; // Chunk-relative side z coord
                        IChunk sChunk = posChunk;

                        // Offset values based on side
                        switch (s) {
                            case TOP:
                                sy += 1;
                                break;
                            case BOTTOM:
                                sy -= 1;
                                break;
                            case WEST:
                                sx -= 1;
                                scx -= 1;
                                break;
                            case EAST:
                                sx += 1;
                                scx += 1;
                                break;
                            case NORTH:
                                sz += 1;
                                scz += 1;
                                break;
                            case SOUTH:
                                sz -= 1;
                                scz -= 1;
                                break;
                        }
                        if (sy < 0)
                            continue;
                        if (sy > WORLD_HEIGHT - 1)
                            continue;

                        // Select the correct chunk
                        if(scz < 0) {
                            scz += CHUNK_SIZE;
                            sChunk = getChunk(sx, sz);
                        } else if(scz > CHUNK_SIZE-1) {
                            scz -= CHUNK_SIZE;
                            sChunk = getChunk(sx, sz);
                        }
                        if(scx < 0) {
                            scx += CHUNK_SIZE;
                            sChunk = getChunk(sx, sz);
                        } else if(scx > CHUNK_SIZE-1) {
                            scx -= CHUNK_SIZE;
                            sChunk = getChunk(sx, sz);
                        }

                        if(sChunk == null)
                            continue;

                        Block sBlock = sChunk.getBlock(scx, sy, scz);
                        int sSunlight = sChunk.getSunlight(scx, sy, scz);
                        if (sBlock == null || sBlock.doesLightPassThrough() || !sBlock.decreasesLight()) {
                            if ((sSunlight < ll || s == Side.BOTTOM) && sSunlight != 0) { // Reset lighting for blocks affected by this block
                                addToSunlightRemovalQueue(sx, sy, sz);
                            } else if (sSunlight >= ll && s != Side.BOTTOM) { // Add surrounding blocks to queue to restore lighting of the reset blocks
                                addToSunlightQueue(sx, sy, sz);
                            }
                        }
                    }

                    // Reset lighting for the block
                    posChunk.setSunlight(cx, y, cz, 0);
                    changedChunks.add(posChunk);
                } catch (CoordinatesOutOfBoundsException e) {
                    e.printStackTrace();
                }
            }
            changedChunks.forEach(IChunk::finishChangingSunlight);
        }
    }

    @Override
    public void cleanup() {
        chunkMap.values().forEach(IChunk::dispose);
        modelBatch.dispose();
        modelBatch = null;
        skyboxTexture.dispose();
        skyboxTexture = null;
        skyboxModel.dispose();
        skyboxModel = null;
    }

    @Override
    public void rerenderChunks() {
        // If the chunk is in range, rerender it
        for(IChunk c : chunkMap.values()) {
            if(RadixClient.getInstance().getPlayer().getPosition().planeDistance(c.getStartPosition().x, c.getStartPosition().z)
                    <= RadixClient.getInstance().getSettingsManager().getVisualSettings().getViewDistance() * CHUNK_SIZE) {
                rerenderChunk(c);
            }
        }
    }

    @Override
    public void rmChunk(IChunk chunk) {
        if(chunk == null)
            return;

        removeChunkFromMap(chunk.getStartPosition());
    }

    @Override
    public void addToMeshQueue(Runnable updateFaces) {
        chunkMeshQueue.add(updateFaces);
        synchronized (chunkMeshQueue) {
            chunkMeshQueue.notify();
        }
    }

    @Override
    public void addToChunkUploadQueue(Runnable upload) {
        chunkUploadQueue.add(upload);
    }

    private ModelInstance createSkybox() {
        if(skyboxModel != null) {
            skyboxModel.dispose();
        }
        if(skyboxTexture == null) {
            skyboxTexture = new Texture(Gdx.files.internal("textures/world/skybox.png"));
        }

        MeshBuilder builder = new MeshBuilder();
        builder.begin(VertexAttributes.Usage.Position | VertexAttributes.Usage.TextureCoordinates, GL20.GL_TRIANGLES);

        int x1 = -256;
        int y1 = -256;
        int z1 = -256;
        int x2 = 256;
        int y2 = 256;
        int z2 = 256;
        float sideU1 = 0;
        float sideU2 = 1/3f;
        float sideV1 = 0;
        float sideV2 = 1;
        float topU1 = sideU2;
        float topU2 = topU1+sideU2;
        float bottomU1 = topU2+sideU2;
        float bottomU2 = bottomU1+sideU2;
        float topV1 = 0;
        float topV2 = 1;
        float bottomV1 = 0;
        float bottomV2 = 1;
        MeshPartBuilder.VertexInfo bottomLeftBack = new MeshPartBuilder.VertexInfo().setPos(x1, y1, z1).setUV(sideU1, sideV2);
        MeshPartBuilder.VertexInfo bottomRightBack = new MeshPartBuilder.VertexInfo().setPos(x2, y1, z1).setUV(sideU2, sideV2);
        MeshPartBuilder.VertexInfo bottomRightFront = new MeshPartBuilder.VertexInfo().setPos(x2, y1, z2).setUV(sideU1, sideV2);
        MeshPartBuilder.VertexInfo bottomLeftFront = new MeshPartBuilder.VertexInfo().setPos(x1, y1, z2).setUV(sideU2, sideV2);
        MeshPartBuilder.VertexInfo topLeftBack = new MeshPartBuilder.VertexInfo().setPos(x1, y2, z1).setUV(sideU1, sideV1);
        MeshPartBuilder.VertexInfo topRightBack = new MeshPartBuilder.VertexInfo().setPos(x2, y2, z1).setUV(sideU2, sideV1);
        MeshPartBuilder.VertexInfo topRightFront = new MeshPartBuilder.VertexInfo().setPos(x2, y2, z2).setUV(sideU1, sideV1);
        MeshPartBuilder.VertexInfo topLeftFront = new MeshPartBuilder.VertexInfo().setPos(x1, y2, z2).setUV(sideU2, sideV1);
        // Negative Z
        builder.rect(bottomLeftBack, bottomRightBack, topRightBack, topLeftBack);
        // Positive Z
        builder.rect(topLeftFront, topRightFront, bottomRightFront, bottomLeftFront);
        // Negative X
        builder.rect(bottomLeftBack, topLeftBack, topLeftFront, bottomLeftFront);
        // Positive X
        builder.rect(bottomRightFront, topRightFront, topRightBack, bottomRightBack);
        // Positive Y
        builder.rect(topLeftBack.setUV(topU1, topV1),
                topRightBack.setUV(topU2, topV1),
                topRightFront.setUV(topU2, topV2),
                topLeftFront.setUV(topU1, topV2));
        // Negative Y
        builder.rect(bottomLeftFront.setUV(bottomU1, bottomV2),
                bottomRightFront.setUV(bottomU2, bottomV2),
                bottomRightBack.setUV(bottomU2, bottomV1),
                bottomLeftBack.setUV(bottomU1, bottomV1));
        Mesh skybox = builder.end();

        ModelBuilder modelBuilder = new ModelBuilder();
        modelBuilder.begin();
        modelBuilder.part("skybox", skybox, GL20.GL_TRIANGLES, new Material(
                TextureAttribute.createDiffuse(skyboxTexture)));
        return new ModelInstance(skyboxModel = modelBuilder.end());
    }

    private void setupLighting(IChunk c) {
        try {
            int y = WORLD_HEIGHT - 1;
            for (int z = 0; z < CHUNK_SIZE; z++) {
                for (int x = 0; x < CHUNK_SIZE; x++) {
                    if (c.getBlockId(x, y, z) > 0) {
                        break;
                    }
                    sunlightQueue.add(new int[]{c.getStartPosition().x + x, y, c.getStartPosition().z + z});
                    c.setSunlight(x, y, z, c.getMaxLightLevel());

                    for (int cy = 0; cy < getHeight(); cy++) {
                        int id = c.getBlockId(x, cy, z);
                        if (id > 0) {
                            Block blk = RadixAPI.instance.getBlock(id);
                            if (blk.getLightValue() > 0) {
                                c.setBlocklight(x, cy, z, blk.getLightValue());
                                addToBlocklightQueue(c.getStartPosition().x + x, c.getStartPosition().y + cy, c.getStartPosition().z + z);
                            }
                        }
                    }
                }
            }
            synchronized (sunlightQueue) {
                sunlightQueue.notifyAll();
            }
            synchronized (blocklightQueue) {
                blocklightQueue.notifyAll();
            }
        } catch (CoordinatesOutOfBoundsException ex) {
            ex.printStackTrace();
        }
    }

    private void processChunkUploadQueue() {
        while(!chunkUploadQueue.isEmpty()) {
            chunkUploadQueue.poll().run();
            if(RadixClient.getInstance().getSettingsManager().getVisualSettings().getSmoothChunkLoad().getValue())
                break; // distribute chunk uploads across frames (one chunkload per frame)
        }
    }

} class CoordinatesOutOfBoundsException extends Exception {} public interface IChunk extends Serializable, Disposable {

    /**
     * Redraws all of the blocks in the chunk
     */
    void rerender();

    /**
     * Renders the chunk
     */
    void render(ModelBatch batch);

    void renderTranslucent(ModelBatch modelBatch);

    /**
     * Gets the starting position of the chunk
     */
    Vec3i getStartPosition();

    /**
     * @return The y-value of the highest block in the chunk
     */
    int getHighestPoint();

    int getBlockId(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Get the block at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    Block getBlock(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Remove the block at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    void removeBlock(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Set the block at the specified position
     * @param block New block to set to
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    void setBlock(int block, int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Set the block at the specified position
     * @param block New block to set to
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     * @param updateSunlight Whether to update sunlight values for the new block
     */
    void setBlock(int block, int x, int y, int z, boolean updateSunlight) throws CoordinatesOutOfBoundsException;

    /**
     * Set the metadata for the block at the specified position
     * @param meta Metadata value to set to
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    void setMeta(short meta, int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Get the metadata value at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    short getMeta(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Gets the light value at the specified location
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     */
    float getLightLevel(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Get sunlight at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     * @return Sunlight value, between 0 and 15
     */
    int getSunlight(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Set the sunlight at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     * @param level Value between 0 and 15 for sunlight
     */
    void setSunlight(int x, int y, int z, int level) throws CoordinatesOutOfBoundsException;

    /**
     * Get the blocklight at the specified position
     * @param x
     * @param y
     * @param z
     * @return Blocklight value, between 0 and 15
     */
    int getBlocklight(int x, int y, int z) throws CoordinatesOutOfBoundsException;

    /**
     * Set the blocklight at the specified position
     * @param x X value, relative to the chunk. 0->(chunk size - 1)
     * @param y Y value, relative to the chunk. 0->(chunk height - 1)
     * @param z Z value, relative to the chunk. 0->(chunk size - 1)
     * @param level Value between 0 and 15 for blocklight
     */
    void setBlocklight(int x, int y, int z, int level) throws CoordinatesOutOfBoundsException;

    void eachBlock(EachBlockCallee action);

    /**
     * Update the state of the chunk to represent changed sunlight
     */
    void finishChangingSunlight();

    /**
     * Get the parent world of the chunk
     */
    IWorld getWorld();

    /**
     * Get the biome type of the chunk
     */
    Biome getBiome();

    /**
     * Whether the chunk as obtained its initial sunlight from World
     */
    boolean hasInitialSun();

    /**
     * Called after the chunk as finished getting initial sunlight fromm world
     */
    void finishAddingSun();

    /**
     * Returns true if the chunk is waiting for a light update to finish before rerendering
     */
    boolean waitingOnLightFinish();

    int getMaxLightLevel();

    float getBrightness(int lightLevel);

    interface EachBlockCallee {
        void call(Block block, int x, int y, int z);
    }

}
import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Touchpad;
import com.badlogic.gdx.scenes.scene2d.ui.Touchpad.TouchpadStyle;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.viewport.FitViewport;
import org.spacehq.mc.protocol.data.game.entity.metadata.ItemStack;
import org.spacehq.mc.protocol.data.game.entity.metadata.Position;
import org.spacehq.mc.protocol.data.game.entity.player.Hand;
import org.spacehq.mc.protocol.data.game.entity.player.PlayerAction;
import org.spacehq.mc.protocol.data.game.world.block.BlockFace;
import org.spacehq.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
import org.spacehq.mc.protocol.packet.ingame.client.player.ClientPlayerPlaceBlockPacket;
import org.spacehq.mc.protocol.packet.ingame.client.player.ClientPlayerSwingArmPacket;
import pw.oxcafebabe.marcusant.eventbus.EventListener;
import pw.oxcafebabe.marcusant.eventbus.Priority;
import pw.oxcafebabe.marcusant.eventbus.exceptions.InvalidListenerException;
import sx.lambda.voxel.api.BuiltInBlockIds;
import sx.lambda.voxel.api.RadixAPI;
import sx.lambda.voxel.api.events.EventEarlyInit;
import sx.lambda.voxel.api.events.EventWorldStart;
import sx.lambda.voxel.api.events.register.EventRegisterBiomes;
import sx.lambda.voxel.api.events.register.EventRegisterBlockRenderers;
import sx.lambda.voxel.api.events.register.EventRegisterItems;
import sx.lambda.voxel.block.Block;
import sx.lambda.voxel.client.gui.GuiScreen;
import sx.lambda.voxel.client.gui.SceneTheme;
import sx.lambda.voxel.client.gui.screens.ChatGUI;
import sx.lambda.voxel.client.gui.screens.IngameHUD;
import sx.lambda.voxel.client.gui.screens.MainMenu;
import sx.lambda.voxel.client.gui.transition.SlideUpAnimation;
import sx.lambda.voxel.client.gui.transition.TransitionAnimation;
import sx.lambda.voxel.entity.EntityModel;
import sx.lambda.voxel.entity.EntityPosition;
import sx.lambda.voxel.entity.EntityRotation;
import sx.lambda.voxel.entity.player.Player;
import sx.lambda.voxel.net.mc.client.MinecraftClientConnection;
import sx.lambda.voxel.render.NotInitializedException;
import sx.lambda.voxel.render.Renderer;
import sx.lambda.voxel.render.game.GameRenderer;
import sx.lambda.voxel.settings.SettingsManager;
import sx.lambda.voxel.tasks.EntityUpdater;
import sx.lambda.voxel.tasks.MovementHandler;
import sx.lambda.voxel.tasks.RepeatedTask;
import sx.lambda.voxel.tasks.WorldLoader;
import sx.lambda.voxel.util.PlotCell3f;
import sx.lambda.voxel.util.Vec3i;
import sx.lambda.voxel.world.IWorld;
import sx.lambda.voxel.world.World;
import sx.lambda.voxel.world.chunk.BlockStorage.CoordinatesOutOfBoundsException;
import sx.lambda.voxel.world.chunk.IChunk;
import javax.swing.*;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import static com.badlogic.gdx.graphics.GL20.GL_COLOR_BUFFER_BIT;
import static com.badlogic.gdx.graphics.GL20.GL_DEPTH_BUFFER_BIT;
package sx.lambda.voxel;




/**
 * Primary class for an instance of a Radix client.
 */
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)};