#include "raylib.h"
#include "math.h"
#include "raymath.h"
#include <stdint.h>
#include <stdbool.h>
#include "common.hpp"
#include <stdio.h>
#include "rlgl.h"
#include <unordered_set>
#include <iostream>
#include <stack>
#include <cstring>
#include <algorithm>
#include <vector>
#include <list>
#include <climits>
#include <queue>
#include <thread>
#include <atomic>
#define MAX_RAY_DISTANCE 8.0f
struct MessageData {
    std::string message;
    float lifetime;
};
static inline int imax(int a, int b) {
    if(a > b) { return a; }
    return b;
}
extern std::vector<Item> registry;
void Debug_Write(const std::string &stuff);
Vector2 cameraMovementSmooth;
static float camYaw = 0.0f;
static float camPitch = -45.0f;
static const float acceleration = 48.0f;
static const float drag = 6.0f; // tuned for feel
Camera camera;
bool online = false;
bool flying = false;

unsigned long long worldTime;

static float entUpdateCounter;
int selectedBlockType = 1;

bool hide_ui = false;
bool taking_panorama = false;
int panorama_step;

Music currentMusic;
static float tilNextMusic;

Font fnt;
Texture2D terrain;
Texture2D water;
Texture2D white_pixel;
Entity* ent;
const bool ROTATE_PLAYER_WITH_CAMERA = true;
static Sound wood_place;
static Sound stone_place;
static Sound dirt_place;
static Sound grass_place;
bool tick_entities;
const std::string LocaleGet(std::string loc);
bool InputGetKeyDown(const std::string name);
bool InputGetKeyPressed(const std::string name);
extern std::list<Entity> entities;
std::bitset<WORLD_SIZE_BLOCKS*WORLD_SIZE_BLOCKS/8/8> clouds;
std::queue<Vector3I> tickedBlocks;
struct MeshUnit {
    Model model;
    int vertices;
};
struct ProfilerPart {
    std::string id;
    double time;
    Color color;
};
std::vector<ProfilerPart> profParts;
static void AddProfilerPart(std::string id, double time, Color color) {
    ProfilerPart part = {};
    part.id = id;
    part.time = time;
    part.color = color;
    profParts.push_back(part);
}
static Vector3 CameraForwardFromYawPitch(float yaw, float pitch)
{
    Vector3 f;
    f.x = cosf(pitch) * cosf(yaw);
    f.y = sinf(pitch);
    f.z = cosf(pitch) * sinf(yaw);
    return Vector3Normalize(f);
}
Chunk world[MAX_WORLD_AREA];
static MeshUnit rendering[MAX_WORLD_AREA];
static MeshUnit renderingT[MAX_WORLD_AREA];

GameState state = GAME_STATE_MAINMENU;
bool loaderParam_remeshOnly = false;
int worldCreationProgress = 0;
int worldCreationStep = 0;
static bool IsBlockSolid(int bx, int by, int bz)
{
    return GetBlock(world, bx, by, bz) != 0;
}
static bool IsBlockWater(int bx, int by, int bz)
{
    return GetBlock(world, bx, by, bz) == BLOCK_WATERS;
}

typedef struct Plane {
    float a, b, c, d;
} Plane;

typedef struct Frustum {
    Plane planes[6];
} Frustum;

static Frustum ExtractFrustumPlanes(Camera camera, float aspect)
{
    Frustum frustum;
    
    // Get current Projection and View matrices from raylib
    Matrix proj = MatrixPerspective(camera.fovy * DEG2RAD, aspect, 0.01f, 1000.0f);
    Matrix view = MatrixLookAt(camera.position, camera.target, camera.up);
    Matrix mat = MatrixMultiply(view, proj); // View-Projection Matrix

    // Left Plane
    frustum.planes[0] = (Plane){ mat.m3 + mat.m0, mat.m7 + mat.m4, mat.m11 + mat.m8, mat.m15 + mat.m12 };
    // Right Plane
    frustum.planes[1] = (Plane){ mat.m3 - mat.m0, mat.m7 - mat.m4, mat.m11 - mat.m8, mat.m15 - mat.m12 };
    // Bottom Plane
    frustum.planes[2] = (Plane){ mat.m3 + mat.m1, mat.m7 + mat.m5, mat.m11 + mat.m9, mat.m15 + mat.m13 };
    // Top Plane
    frustum.planes[3] = (Plane){ mat.m3 - mat.m1, mat.m7 - mat.m5, mat.m11 - mat.m9, mat.m15 - mat.m13 };
    // Near Plane
    frustum.planes[4] = (Plane){ mat.m3 + mat.m2, mat.m7 + mat.m6, mat.m11 + mat.m10, mat.m15 + mat.m14 };
    // Far Plane
    frustum.planes[5] = (Plane){ mat.m3 - mat.m2, mat.m7 - mat.m6, mat.m11 - mat.m10, mat.m15 - mat.m14 };

    // Normalize planes
    for (int i = 0; i < 6; i++) {
        float length = sqrtf(frustum.planes[i].a * frustum.planes[i].a +
                             frustum.planes[i].b * frustum.planes[i].b +
                             frustum.planes[i].c * frustum.planes[i].c);
        frustum.planes[i].a /= length;
        frustum.planes[i].b /= length;
        frustum.planes[i].c /= length;
        frustum.planes[i].d /= length;
    }

    return frustum;
}

static bool IsBoxInFrustum(Frustum frustum, BoundingBox box)
{
    for (int i = 0; i < 6; i++) {
        // Find the positive vertex (farthest along the plane normal)
        Vector3 p = box.min;
        if (frustum.planes[i].a >= 0) p.x = box.max.x;
        if (frustum.planes[i].b >= 0) p.y = box.max.y;
        if (frustum.planes[i].c >= 0) p.z = box.max.z;

        // Calculate dot product of plane equation
        float dot = (frustum.planes[i].a * p.x) + 
                    (frustum.planes[i].b * p.y) + 
                    (frustum.planes[i].c * p.z) + 
                    frustum.planes[i].d;

        // If the positive vertex is behind the plane, the box is outside
        if (dot < 0) return false;
    }
    return true;
}

static bool IsChunkInFrustum(Frustum frustum, Chunk chunk, int x, int y) {
    float highestBlock = 0;
    for (int i = 0; i < CHUNK_SIZE*CHUNK_SIZE; i++)
    {
        if(highestBlock < chunk.highestBlock[i]) highestBlock = chunk.highestBlock[i];
    }
    BoundingBox bb = {};
    bb.min = (Vector3){x, 0, y};
    bb.max = (Vector3){x+CHUNK_SIZE, highestBlock, y+CHUNK_SIZE};
    return IsBoxInFrustum(frustum, bb);
}

static void ResolveCollisions(float dt)
{
    float minY = player_position.y;
    float maxY = player_position.y + PLAYER_HEIGHT;
    int minX = floorf(player_position.x - PLAYER_RADIUS);
    int maxX = floorf(player_position.x + PLAYER_RADIUS);
    int minZ = floorf(player_position.z - PLAYER_RADIUS);
    int maxZ = floorf(player_position.z + PLAYER_RADIUS);
    int minB = floorf(minY);
    int maxB = floorf(maxY);

    for (int bx = minX; bx <= maxX; bx++)
    {
        for (int bz = minZ; bz <= maxZ; bz++)
        {
            for (int by = minB; by <= maxB; by++)
            {
                if (!IsBlockSolid(bx, by, bz)) continue;
                if(IsBlockWater(bx, by, bz)) {
                    if(player_velocity.y < -5) {
                        player_velocity.y = -5;
                    }
                    if(player_velocity.y > 5) {
                        player_velocity.y = 5;
                    }
                    continue;
                }
                player_velocity.x *= fmaxf(0.0f, 1.0f - drag * dt);
                player_velocity.z *= fmaxf(0.0f, 1.0f - drag * dt);
                float bx0 = (float)bx;
                float by0 = (float)by;
                float bz0 = (float)bz;
                float bx1 = bx0 + 1.0f;
                float by1 = by0 + 1.0f;
                float bz1 = bz0 + 1.0f;

                float px0 = player_position.x - PLAYER_RADIUS;
                float py0 = player_position.y;
                float pz0 = player_position.z - PLAYER_RADIUS;
                float px1 = player_position.x + PLAYER_RADIUS;
                float py1 = player_position.y + PLAYER_HEIGHT;
                float pz1 = player_position.z + PLAYER_RADIUS;

                float ix = fminf(px1, bx1) - fmaxf(px0, bx0);
                float iy = fminf(py1, by1) - fmaxf(py0, by0);
                float iz = fminf(pz1, bz1) - fmaxf(pz0, bz0);

                if (ix > 0 && iy > 0 && iz > 0)
                {
                    // find smallest penetration axis and push out along it
                    if (ix < iy && ix < iz)
                    {
                        // push on X
                        if (player_position.x < bx0) player_position.x -= ix;
                        else player_position.x += ix;
                        player_velocity.x = 0;
                    }
                    else if (iy < ix && iy < iz)
                    {
                        // push on Y
                        if (player_position.y < by0) player_position.y -= iy;
                        else player_position.y += iy;
                        if (player_velocity.y > 0) player_velocity.y = 0;
                        else player_velocity.y = 0;
                    }
                    else
                    {
                        // push on Z
                        if (player_position.z < bz0) player_position.z -= iz;
                        else player_position.z += iz;
                        player_velocity.z = 0;
                    }
                }
            }
        }
    }
}

bool loadedChunks[MAX_WORLD_AREA] = { false };
bool dirtyChunks[MAX_WORLD_AREA] = { false };

void Debug_EndMeasure(const std::string &stuff);
void Debug_EndMeasureLoops(const std::string &stuff, int loops);
static void RemeshChunk(int x, int y) {
    int idx = x + y * MAX_WORLD_SIZE;
    if(loadedChunks[idx]) {
        UnloadModel(renderingT[idx].model);
        UnloadModel(rendering[idx].model);
    }
    Chunk chunk = world[idx];
    RebuildOpaqueMask(&chunk);
    
    MeshData renderDataOpq = GenerateChunkMesh(GenerateCRDOpaque(chunk, world), chunk, x, y, 0xFF);
    MeshData renderDataTsl = GenerateChunkMesh(GenerateCRDTranslucent(chunk, world), chunk, x, y, 0xFF);
    
    Model model = LoadModelFromMesh(GenChunkMesh(renderDataOpq));
    model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = terrain;
    MeshUnit munit0;
    munit0.model = model;
    munit0.vertices += renderDataOpq.vertexCount;
    rendering[idx] = munit0;
    model = LoadModelFromMesh(GenChunkMesh(renderDataTsl));
    model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = terrain;
    MeshUnit munit1;
    munit1.model = model;
    munit1.vertices += renderDataTsl.vertexCount;
    renderingT[idx] = munit1;
    loadedChunks[idx] = true;
    
}

static bool RaycastBlock(Vector3 origin, Vector3 dir, float maxDist, Vector3 *outBlock, Vector3 *outPrev)
{
    float t = 0.0f;
    Vector3 pos = origin;
    Vector3 step = Vector3Scale(dir, 0.1f); // step resolution (smaller => more accurate)
    Vector3 last = origin;
    while (t < maxDist)
    {
        pos = Vector3Add(origin, Vector3Scale(dir, t));
        int bx = floorf(pos.x);
        int by = floorf(pos.y);
        int bz = floorf(pos.z);
        if (bx < 0 || by < 0 || bz < 0 || bx >= WORLD_SIZE_BLOCKS || by >= WORLD_HEIGHT || bz >= WORLD_SIZE_BLOCKS) { t += 0.1f; continue; }
        if (IsBlockSolid(bx, by, bz) && !IsBlockWater(bx, by, bz))
        {
            // hit block at bx,by,bz; last is previous pos (empty)
            if (outBlock) *outBlock = (Vector3){ (float)bx, (float)by, (float)bz };
            if (outPrev) *outPrev = (Vector3){ (float)floorf(last.x), (float)floorf(last.y), (float)floorf(last.z) };
            return true;
        }
        last = pos;
        t += 0.1f;
    }
    return false;
}
static Entity* RaycastEntity(Vector3 origin, Vector3 dir, float maxDist)
{
    float t = 0.0f;
    Vector3 pos = origin;
    Vector3 step = Vector3Scale(dir, 0.1f);
    Vector3 last = origin;
    while (t < maxDist)
    {
        pos = Vector3Add(origin, Vector3Scale(dir, t));
        int bx = floorf(pos.x);
        int by = floorf(pos.y);
        int bz = floorf(pos.z);
        if (bx < 0 || by < 0 || bz < 0 || bx >= WORLD_SIZE_BLOCKS || by >= WORLD_HEIGHT || bz >= WORLD_SIZE_BLOCKS) { t += 0.1f; continue; }

        for(Entity &ent : entities) {
            if(Vector3Distance(pos, ent.position) < 0.5f) {
                return &ent;
            }
        }
        last = pos;
        t += 0.1f;
    }
    return nullptr;
}
static inline Vector3 BlockToWorldCenter(int bx, int by, int bz)
{
    return (Vector3){ bx , by , bz };
}

static inline void RemeshChunkForBlock(int bx, int by, int bz)
{
    int cx = bx / CHUNK_SIZE;
    int cz = bz / CHUNK_SIZE;
    if (cx >= 0 && cx < MAX_WORLD_SIZE && cz >= 0 && cz < MAX_WORLD_SIZE) RemeshChunk(cx, cz);
}

static void AddNekit() {
    Entity temp = NewEntity();
    temp.type = 2;
    temp.health = 50;
    temp.velocity = {0, 0, 0};
    temp.position = player_position + (Vector3){0, 10, 0};
    AddEntity(temp);
}

ENetPeer *peer;
ENetHost* client;
static std::stack<std::vector<uint8_t>> worldPacketStack;
static int fetchedChunks;

int plrId = -1;

static bool pauseMenuActive;
void _LoadWorld(const char* path) {
    Debug_Write("Loading world");
    LoadWorld(world, path);
    Debug_Write("World loaded");
    state = GAME_STATE_LOADING;
    worldCreationProgress = 0;
    worldCreationStep = 2;
}
void PlayRandomMusic() {
    currentMusic = LoadMusicStream(Pathify((std::string("music/calm0")+std::to_string(GetRandomValue(1,3))+std::string(".ogg")).c_str()));
    PlayMusicStream(currentMusic);
}
static int _blockPlacedC = 0;
void SetBlockNetwork(Chunk world[], int bx, int by, int bz, int t) {
    float pitch = GetRandomValue(-100, 100) / 100.0f * 0.25f + 1.0f;
    int sound = B_Stone;
    tickedBlocks.push({bx, by, bz});
    tickedBlocks.push({bx-1, by, bz});
    tickedBlocks.push({bx+1, by, bz});
    tickedBlocks.push({bx, by-1, bz});
    tickedBlocks.push({bx, by+1, bz});
    tickedBlocks.push({bx, by, bz-1});
    tickedBlocks.push({bx, by, bz+1});
    if(t != 0) {
        sound = BLOCKS[t].type;
    }
    else {
        int id = GetBlock(world, bx, by, bz);
        BlockDef block = BLOCKS[id];
        if(id != 0) {
            sound = block.type;
            for (int x = 0; x < 4; x++)
            {
                for (int z = 0; z < 4; z++)
                {
                    for (int y = 0; y < 4; y++)
                    {
                        if(GetRandomValue(0, 100) < 25) {
                            Particle part = NewParticle({bx + x / 4.0f - 0.375f, by + y / 4.0f - 0.375f, bz + z / 4.0f - 0.375f});
                            part.data[0] = block.textureSide;
                            if(y == 3) {
                                part.data[0] = block.textureTop;
                            }
                            part.velocity = Vector3({(x <= 1) ? -1.0f : 1.0f, (y <= 1) ? -1.0f : 1.0f, (z <= 1) ? -1.0f : 1.0f}) * 1.5f;
                            AddParticle(part);
                            part.lifetime = GetRandomValue(0, 100);
                        }
                    }
                }
            }
        }
        else {
            return;
        }
    }
    switch (sound)
    {
        case B_Dirt:
            MS_PlaySound(dirt_place, pitch, 1, (Vector3){bx, by, bz});
            break;
        case B_Grass:
            MS_PlaySound(grass_place, pitch, 1, (Vector3){bx, by, bz});
            break;
        case B_Wood:
            MS_PlaySound(wood_place, pitch, 1, (Vector3){bx, by, bz});
            break;
        default:
            MS_PlaySound(stone_place, pitch, 1, (Vector3){bx, by, bz});
            break;
    }
    if(online) {
        char data[5];
        data[0] = NET_ARG_BLOCKDELTA;
        data[1] = (uint8_t)bx;
        data[2] = (uint8_t)by;
        data[3] = (uint8_t)bz;
        data[4] = (uint8_t)t;
        SendPacketR(peer, data, sizeof(data));
    }
    else {
        SetBlock(world, bx, by, bz, t);
        int cx = bx / CHUNK_SIZE;
        int cz = bz / CHUNK_SIZE;
        int lx = bx - cx * CHUNK_SIZE;
        int lz = bz - cz * CHUNK_SIZE;

        dirtyChunks[GetIndexWorld(cx, cz)] = true;
        if (lx == 0)                 dirtyChunks[GetIndexWorld(cx - 1, cz)] = true;
        if (lx == CHUNK_SIZE - 1)    dirtyChunks[GetIndexWorld(cx + 1, cz)] = true;
        if (lz == 0)                 dirtyChunks[GetIndexWorld(cx, cz - 1)] = true;
        if (lz == CHUNK_SIZE - 1)    dirtyChunks[GetIndexWorld(cx, cz + 1)] = true;
    }
    _blockPlacedC++;
}

static void TickAvailableBlocks() {
    if(tickedBlocks.size() > 0) {
        Vector3I pos = tickedBlocks.front();
        if(CanTick(GetBlock(world, pos.x, pos.y, pos.z))) {
            TickBlock(world, pos.x, pos.y, pos.z);
            tickedBlocks.pop();
        }
        else {
            tickedBlocks.pop();
            TickAvailableBlocks();
        }
    }
}
std::atomic_bool serverStarted;
int TryConnect(const char* hostname) {
    client = StartClient();

    if(client == NULL)
    {
        fprintf(stderr, "An error occurred while trying to create an ENet client host!\n");
        return NET_ERROR_INTERNAL;
    }

    ENetAddress address;
    ENetEvent event;
    
    
    int s = enet_address_set_host (& address, hostname);
    address.port = PORT;
    
    peer = enet_host_connect(client, &address, 1, 0);    
    
    if (peer == NULL)
    {
        fprintf (stderr, 
                    "No available peers for initiating an ENet connection.\n");
        return NET_ERROR_NOTFOUND;
    }
    
    if (enet_host_service (client, & event, 5000) > 0 &&
        event.type == ENET_EVENT_TYPE_CONNECT)
    {
        puts ("Connection succeeded.");
        char data[1];
        data[0] = NET_ARG_REQWORLD;
        SendPacketR(peer, data, 1);
        online = true;
        state = GAME_STATE_LOADING;
        return 0;
    }
    else
    {
        enet_peer_reset (peer);
    
        return NET_ERROR_FAILED;
    }
    return -1;
}

//std::thread serverThread;
std::atomic_bool stop_server = false;
void StartInternalServer(std::atomic_bool& started);
/*void ServerThread(std::atomic_bool& started) {
    StartInternalServer(started);
    stop_server = false;
    while(true) {
        if (stop_server)
            return;
        ServerNetworkUpdate();
    }
}*/
/*void InitSingleplayer() {
    online = true;
    serverThread = std::thread(ServerThread, std::ref(serverStarted));
    int result = TryConnect("localhost");
    if(result == NET_ERROR_FAILED) {
        state = GAME_STATE_MAINMENU;
    }
}*/
std::vector<MessageData> messages;
bool survival = false;
bool chatOpen = false;
float oxygen = 100;
float blockBreakProgress;
void TakeDamage(float amount) {
    player_health -= amount;
}
int currentlyTickedChunk = 0;
Vector3 highlightBlock = {-1, -1, -1};

static void GameUpdate(Camera camera, float &time) {
    if (player_position.y < 0 || 
        player_position.x < 0 || 
        player_position.z < 0 || 
        player_position.z > WORLD_SIZE_BLOCKS || 
        player_position.x > WORLD_SIZE_BLOCKS) {
        player_position = { MAX_WORLD_SIZE / 2 * CHUNK_SIZE, 128.0f, MAX_WORLD_SIZE / 2 * CHUNK_SIZE };
        player_velocity = { 0 };
    }
    bool highlightValid = false;
    
    float dt = GetFrameTime();
    if (dt <= 0) dt = DELTA_SMOOTH;
    if (dt > 0.1f) dt = 0.1f;
    time += dt;
    Vector3 forward = Vector3Normalize({ cosf(camYaw), 0.0f, sinf(camYaw) });
    Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));

    camera.position = (Vector3){ player_position.x - 0.5f, player_position.y + 1.25f, player_position.z - 0.5f};
    camera.target = Vector3Add(camera.position, CameraForwardFromYawPitch(camYaw, camPitch));
    camera.up = (Vector3){0,1,0};
    float dt_faux = dt / 4.0f;
    if(IsKeyPressed(KEY_ESCAPE)) {
        if(chatOpen) chatOpen = false;
        else if (inventoryOpen) CloseInventory();
        else {
            pauseMenuActive = !pauseMenuActive;
            if(!pauseMenuActive) {
                DisableCursor();
            }
            else {
                EnableCursor();
            }
        }
    }
    /*if(IsKeyPressed(KEY_F2)) {
        taking_panorama = true;
        panorama_step = -1;
    }
    if(taking_panorama) {
        if(panorama_step == 6) {
            taking_panorama = false;
        }
        else {
            camYaw = (90 * panorama_step) * DEG2RAD;
            if(panorama_step < 3) {
                camPitch = 0;
            }
            else {
                camPitch = 89.99f * DEG2RAD;
                if(panorama_step == 4) {
                    camPitch = -89.99f * DEG2RAD;
                }
                camYaw = 0;
            }
        }
    }*/
    if(InputGetKeyPressed("chat") && !pauseMenuActive) {
        chatOpen = !chatOpen;
        if(!chatOpen) {
            DisableCursor();
        }
        else {
            EnableCursor();
        }
    }
    for (int _ = 0; _ < 4; _++)
    {
        player_position.x += player_velocity.x * dt_faux;
        player_position.y += player_velocity.y * dt_faux;
        player_position.z += player_velocity.z * dt_faux;
        if(!flying) {
            player_velocity.y -= GRAVITY * dt_faux;
        }
        ResolveCollisions(dt_faux);
    }
    if(!pauseMenuActive && !chatOpen && !inventoryOpen) {
        float speedMod = 1;
        if(InputGetKeyPressed("fly")) {
            flying = !flying;
        }

        Vector3 wish = {0,0,0};
        Vector2 walkAxis = InputGetWalkAxis();

        wish = Vector3Add(Vector3Scale(forward, walkAxis.y),
                        Vector3Scale(right,   walkAxis.x));

        float inputMag = Vector3Length((Vector3){wish.x, 0, wish.z});
        Vector3 wishDir = (inputMag > 0.0001f) ? Vector3Scale(wish, 1.0f / inputMag)
                                            : (Vector3){0,0,0};

        if (flying) {
            speedMod = InputGetKeyDown("speed_up") ? 6.0f : 3.0f;
            float w_y = 0;
            if (InputGetKeyDown("jump"))  w_y += 1.0f;
            if (InputGetKeyDown("shift")) w_y -= 1.0f;
            player_velocity.y = w_y * MOVE_SPEED * speedMod;
        }

        if (inputMag > 0.0001f) {
            player_velocity.x += wishDir.x * acceleration * inputMag * speedMod * dt;
            player_velocity.z += wishDir.z * acceleration * inputMag * speedMod * dt;
        }

        float hSpeed = sqrtf(player_velocity.x*player_velocity.x + player_velocity.z*player_velocity.z);
        float maxH = MOVE_SPEED * speedMod;
        if (hSpeed > maxH) {
            float s = maxH / hSpeed;
            player_velocity.x *= s;
            player_velocity.z *= s;
        }

        if(!flying) {
            bool onGround = false;
            int belowY = floorf(player_position.y - 0.05f);
            for (int bx = floorf(player_position.x - PLAYER_RADIUS); bx <= floorf(player_position.x + PLAYER_RADIUS); bx++)
                for (int bz = floorf(player_position.z - PLAYER_RADIUS); bz <= floorf(player_position.z + PLAYER_RADIUS); bz++)
                    if (IsBlockSolid(bx, belowY, bz) && !IsBlockWater(bx, belowY, bz)) onGround = true;

            if (InputGetKeyPressed("jump") && onGround) player_velocity.y = JUMP_SPEED;
            if (InputGetKeyDown("jump") && IsBlockWater(player_position.x, player_position.y, player_position.z)) player_velocity.y += dt * 30;
        }
        
        Vector2 md = InputGetLookAxis();
        camYaw += md.x * MOUSE_SENS;
        camPitch -= md.y * MOUSE_SENS;
        cameraMovementSmooth += md * dt / 8.0f;
        cameraMovementSmooth -= cameraMovementSmooth * dt * 16.0f;
        if (camPitch < PITCH_MIN && !taking_panorama) camPitch = PITCH_MIN;
        if (camPitch > PITCH_MAX && !taking_panorama) camPitch = PITCH_MAX;

        Vector3 camForward = CameraForwardFromYawPitch(camYaw, camPitch);
        Vector3 rayOrigin = (Vector3){ player_position.x, player_position.y + 1.7f, player_position.z};;

        Vector3 hitBlock, prevBlock;
        if(IsKeyDown(KEY_N) && !online) {
            AddNekit();
        }
        if(IsKeyPressed(KEY_F10)) {
            tick_entities = !tick_entities;
        }
        if(IsKeyPressed(KEY_B) && !online) {
            Entity temp = NewEntity();
            temp.type = 3;
            temp.position = camera.position;
            temp.velocity = (camera.target - camera.position) * 4.0f;
            AddEntity(temp);
        }
        if (RaycastBlock(rayOrigin, Vector3Normalize(camForward), 5, &hitBlock, &prevBlock))
        {
            if((int)hitBlock.x != (int)highlightBlock.x || (int)hitBlock.y != (int)highlightBlock.y || (int)hitBlock.z != (int)highlightBlock.z)
                blockBreakProgress = 0.0f;
            highlightBlock = hitBlock;
            highlightValid = true;
        }
        else
        {
            blockBreakProgress = 0.0f;
            highlightValid = false;
        }

        if (InputGetLMBDown())
        {
            player_attack_time += dt * 2;
            if(player_attack_time > 1) {
                player_attack_time = 0;
            }
            if(highlightValid) {
                int bx = (int)highlightBlock.x;
                int by = (int)highlightBlock.y;
                int bz = (int)highlightBlock.z;
                float speed = 1.0f;
                int tier = 0;
                Item* selected_item = HotbarGetSelected().item;
                if(selected_item != nullptr) {
                    if(selected_item->type == IType_Mining) {
                        speed += selected_item->tier;
                        tier = selected_item->tier;
                    }
                }
                BlockDef block = BLOCKS[GetBlock(world, bx, by, bz)];
                float hardnessMultiplier = block.hardness / 4.0f;
                if(player_break_parts_time >= 0.05f) {
                    Vector3 v = {0, 1, 0};
                    int side = GetRandomValue(0, 3);
                    switch (side)
                    {
                        case 1:
                            v = {1, GetRandomValue(0, 15) / 16.0f, GetRandomValue(0, 15) / 16.0f};
                            break;
                        
                        case 2:
                            v = {GetRandomValue(0, 15) / 16.0f, GetRandomValue(0, 15) / 16.0f, 1};
                            break;
                        
                        default:
                            v = {GetRandomValue(0, 15) / 16.0f, 1, GetRandomValue(0, 15) / 16.0f};
                            break;
                    }
                    Particle part = NewParticle({bx + v.x - 0.375f, by + v.y - 0.375f, bz + v.z - 0.375f});
                    part.data[0] = block.textureSide;
                    part.velocity = {0};
                    AddParticle(part);
                    part.lifetime = GetRandomValue(0, 100);
                    player_break_parts_time = 0;
                }
                player_break_parts_time += dt;
                if(block.min_tier <= tier)
                    blockBreakProgress+=dt*speed/hardnessMultiplier;
                if(blockBreakProgress > 1.0f) {
                    HotbarAddItem(&registry[0], GetBlock(world, bx, by, bz));
                    SetBlockNetwork(world, bx, by, bz, 0);
                    blockBreakProgress = 0.0f;
                }
            }
        }
        else {
            player_attack_time += (0.5f - player_attack_time) * dt * 16.0f;
            blockBreakProgress = 0.0f;
        }

        if (InputGetLMB())
        {
            Entity* ent = RaycastEntity(rayOrigin, Vector3Normalize(camForward), 5);
            int tier = 0;
            Item* selected_item = HotbarGetSelected().item;
            if(selected_item != nullptr) {
                if(selected_item->type == IType_Weapon) {
                    tier = selected_item->tier;
                }
            }
            if(ent != nullptr) {
                ent->health -= 5.0f + tier * 5;
                ent->damage_flash = 50;
                ent->velocity += (camera.target - camera.position) * 50;
            }
        }
        
        if (InputGetRMB())
        {
            InventoryItem selected = HotbarGetSelected();
            if(selected.item != nullptr) {
                std::cout << selected.item->type << std::endl;
                switch (selected.item->type)
                {
                    case IType_Edible:
                        HotbarRemoveSelected();
                        player_health += 10.0f;
                        break;
                    case IType_Block:
                        if(highlightValid) {
                            int px, py, pz;
                            px = (int)prevBlock.x;
                            py = (int)prevBlock.y;
                            pz = (int)prevBlock.z;

                            if (px >= 0 && py >= 0 && pz >= 0 && px < WORLD_SIZE_BLOCKS && py < WORLD_HEIGHT && pz < WORLD_SIZE_BLOCKS)
                            {
                                if (!(abs(px - player_position.x + 0.5f) < PLAYER_RADIUS * 2 && abs(pz - player_position.z + 0.5f) < PLAYER_RADIUS * 2 && abs(py - player_position.y) < PLAYER_HEIGHT - 0.2f))
                                {
                                    SetBlockNetwork(world, px, py, pz, selected.damage);
                                    HotbarRemoveSelected();
                                    player_place_time = 1.0f;
                                }
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
        }
        if(player_place_time > 0) {
            player_place_time -= dt * 3.0f;
        }
        if(player_place_time < 0) {
            player_place_time = 0;
        }
        float y = GetMouseWheelMove();
        bool changed = abs(y) > 0.5f;
        if(y > 0.5f) {
            selectedBlockType++;
        }
        if(y < -0.5f) {
            selectedBlockType--;
        }
        if(selectedBlockType > MAX_BLOCK_ID) {
            selectedBlockType = 1;
        }
        if(selectedBlockType < 1) {
            selectedBlockType = MAX_BLOCK_ID;
        }
    }
    int bestIdx = -1;
    int bestDist2 = INT_MAX;
    int c = 0;
    for (bool l : loadedChunks) {
        int x, y;
        GetXZFromIndex(c, &x, &y);
        int dx = x - ((int)(player_position.x) >> 4);
        int dy = y - ((int)(player_position.z) >> 4);
        int dist2 = dx*dx + dy*dy;
        if (dist2 > RENDER_DISTANCE_SQUARED) {
            loadedChunks[c] = false;
        }
        else {
            loadedChunks[c] = true;
        }
        c++;
    }
    c = 0;
    for (bool b : dirtyChunks) {
        if(b) {
            int x, y;
            GetXZFromIndex(c, &x, &y);
            int dx = x - ((int)(player_position.x) >> 4);
            int dy = y - ((int)(player_position.z) >> 4);
            int dist2 = dx*dx + dy*dy;
            if (dist2 <= RENDER_DISTANCE_SQUARED && dist2 < bestDist2) {
                bestDist2 = dist2;
                bestIdx = c;
            }
        }
        c++;
    }
    double PROFCOUNT = GetTime();
    if (bestIdx != -1) {
        RebuildOpaqueMask(&world[bestIdx]);
        int bx, by;
        GetXZFromIndex(bestIdx, &bx, &by);
        RemeshChunk(bx, by);
        dirtyChunks[bestIdx] = false;
    }
    AddProfilerPart("rebuild", GetTime()-PROFCOUNT, BLUE);
    

    PROFCOUNT = GetTime();
    if(tick_entities) {
        entUpdateCounter += dt;
        if(entUpdateCounter > TPS_DIV) {
            while (entUpdateCounter > 0)
            {
                UpdateEntities(world, TPS_DIV / 2.0f);
                entUpdateCounter-= TPS_DIV;
            }
            TickAvailableBlocks();
            entUpdateCounter = 0.0f;
        }
    }
    AddProfilerPart("entities", GetTime()-PROFCOUNT, MAGENTA);

    PROFCOUNT = GetTime();
    ParticlesUpdate(dt, world);
    AddProfilerPart("particles", GetTime()-PROFCOUNT, ORANGE);
    MS_Update(dt, camera);
    UpdateMusicStream(currentMusic);
    std::stack<std::string> messages_local;
    for(MessageData &msg : messages) {
        if(msg.lifetime < 10.0f) {
            messages_local.push(msg.message);
        }
        msg.lifetime += dt;
    }
    
    UpdateCamera(&camera, CAMERA_CUSTOM);

    unsigned int debug_verts_opaque = 0;
    unsigned int debug_verts_transparent = 0;
    UpdateHotbar();
    Frustum frustum = ExtractFrustumPlanes(camera, (float)GetScreenWidth() / (float)GetScreenHeight());
    BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), BLUE);

        BeginMode3D(camera);

            PROFCOUNT = GetTime();
            double chunkRenderTime = 0;
            for (int x = 0; x < MAX_WORLD_SIZE * MAX_WORLD_SIZE; x++)
            {
                int _x, _z;
                GetXZFromIndex(x, &_x, &_z);
                _x *= CHUNK_SIZE;
                _z *= CHUNK_SIZE;

                if(!loadedChunks[x]) continue;
                if(!IsChunkInFrustum(frustum, world[x], _x, _z)) continue;
                DrawModel(rendering[x].model, {0, 0, 0}, 1, WHITE);
                debug_verts_opaque += rendering[x].vertices;
            }
            AddProfilerPart("chunk render opq", GetTime()-PROFCOUNT, MAROON);

            DrawEntities(dt);
            ParticlesDraw();
            //Transparent shit
            rlDisableDepthMask();
            rlBegin(RL_QUADS);
            rlSetTexture(white_pixel.id);
            const int cloudMapLength = WORLD_SIZE_BLOCKS/8;
            
            PROFCOUNT = GetTime();
            for (int x = 0; x < cloudMapLength; x++)
            {
                for (int z = 0; z < cloudMapLength; z++)
                {
                    rlColor4f(255, 255, 255, 255);
                    if(clouds.test(x * cloudMapLength + z)) { //TODO: Normal frustum culling
                        Vector3 A = Vector3Normalize((Vector3){x*8, 70, z*8}-(Vector3){camera.position.x, camera.position.y, camera.position.z});
                        float dot = Vector3DotProduct(A, camera.target - camera.position);
                        if(dot > cosf(FOV*DEG2RAD)) {
                            int ex_x = (imax(-x*8, 0) + imax(x*8-WORLD_SIZE_BLOCKS, 0)) >> 5;
                            int ex_z = (imax(-z*8, 0) + imax(z*8-WORLD_SIZE_BLOCKS, 0)) >> 5;
                            rlVertex3f(x*8, 70, z*8);
                            rlVertex3f(x*8+8, 70, z*8);
                            rlVertex3f(x*8+8, 70, z*8+8);
                            rlVertex3f(x*8, 70, z*8+8);
                        }
                    }
                }
            }
            rlSetTexture(water.id);
            for (int x = -8; x < MAX_WORLD_SIZE+8; x++)
            {
                for (int z = -8; z < MAX_WORLD_SIZE+8; z++)
                {
                    if((x < 0 || x >= MAX_WORLD_SIZE) || (z < 0 || z >= MAX_WORLD_SIZE)) {
                        Vector3 A = Vector3Normalize((Vector3){x * CHUNK_SIZE, 32, z * CHUNK_SIZE}-(Vector3){camera.position.x, camera.position.y, camera.position.z});
                        float dot = Vector3DotProduct(A, camera.target - camera.position);
                        if(dot > cosf(FOV*DEG2RAD)) {
                            Vector3 A = Vector3Normalize((Vector3){x, 70, z}-(Vector3){camera.position.x, camera.position.y, camera.position.z});
                            float dot = Vector3DotProduct(A, camera.target - camera.position);
                            int ex_x = (imax(-x, 0) + imax(x-MAX_WORLD_SIZE, 0));
                            int ex_z = (imax(-z, 0) + imax(z-MAX_WORLD_SIZE, 0));
                            rlColor4ub(255, 255, 255, 255); //(255 >> ex_x) >> ex_z
                            rlTexCoord2f(0, CHUNK_SIZE);rlVertex3f(x*CHUNK_SIZE - 0.5f, 32 + 0.5f, (z+1)*CHUNK_SIZE - 0.5f);
                            rlTexCoord2f(CHUNK_SIZE, CHUNK_SIZE);rlVertex3f((x+1)*CHUNK_SIZE - 0.5f, 32 + 0.5f, (z+1)*CHUNK_SIZE - 0.5f);
                            rlTexCoord2f(CHUNK_SIZE, 0);rlVertex3f((x+1)*CHUNK_SIZE - 0.5f, 32 + 0.5f, z*CHUNK_SIZE - 0.5f);
                            rlTexCoord2f(0, 0);rlVertex3f(x*CHUNK_SIZE - 0.5f, 32 + 0.5f, z*CHUNK_SIZE - 0.5f);
                        }
                    }
                }
            }
            rlEnd();
            rlEnableDepthMask();
            AddProfilerPart("fancy", GetTime()-PROFCOUNT, BLACK);

            PROFCOUNT = GetTime();
            std::vector<std::pair<float,int>> distances;
            distances.reserve(MAX_WORLD_SIZE * MAX_WORLD_SIZE);

            for (int i = 0; i < MAX_WORLD_SIZE * MAX_WORLD_SIZE; ++i) {
                if (!loadedChunks[i]) continue;
                int cx, cz;
                GetXZFromIndex(i, &cx, &cz);
                if (!IsChunkInFrustum(frustum, world[i], cx*CHUNK_SIZE, cz*CHUNK_SIZE)) continue;
                float wx = cx * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
                float wz = cz * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
                float dx = wx - camera.position.x;
                float dz = wz - camera.position.y;
                float dist2 = dx*dx + dz*dz;
                distances.emplace_back(dist2, i);
            }
            std::sort(distances.begin(), distances.end(),
                    [](auto &a, auto &b){ return a.first > b.first; });

            for (auto &p : distances) {
                int idx = p.second;
                DrawModel(renderingT[idx].model, {0, 0, 0}, 1.0f, WHITE);
                debug_verts_transparent += renderingT[idx].vertices;
            }
            AddProfilerPart("chunk render tsl", GetTime()-PROFCOUNT, RED);
            if (highlightValid)
            {
                Vector3 center = BlockToWorldCenter((int)highlightBlock.x, (int)highlightBlock.y, (int)highlightBlock.z);
                DrawCubeWires(center, 1.01f, 1.01f, 1.01f, BLACK);
                DrawCube(center, 1.01f, 1.01f, 1.01f, Fade(WHITE, blockBreakProgress * 0.6f));
            }
        EndMode3D();
        Vector3 camPos = camera.position;
        Vector3 camTarget = camera.target;
        camera.position = {0, 0, 0};
        
        camera.target = {-cameraMovementSmooth.x, -cameraMovementSmooth.y, 1};

        BeginMode3D(camera);
            rlDisableDepthMask();
            rlDisableDepthTest();
            DrawSelectedItem();
            rlEnableDepthTest();
            rlEnableDepthMask();
        EndMode3D();

        camera.position = camPos;
        camera.target = camTarget;

        if(IsBlockWater(camera.position.x, camera.position.y+0.5f, camera.position.z)) {
            oxygen -= dt * 10.0f;
            DrawTexturePro(water, {0, 0, 16, 16}, {0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()}, {0, 0}, 0, WHITE);
        }
        else {
            oxygen += dt * 20.0f;
            oxygen = Clamp(oxygen, 0, MAX_HP);
        }
        if(!hide_ui && !taking_panorama) {
            char text[128];
            sprintf(text, "%s %s", GAME_NAME, GAME_VERSION);
            DrawTextB(text, 20, 20, DEFAULT_FONT_SIZE, WHITE);
            sprintf(text, "%.0f FPS", 1 / dt);
            DrawTextB(text, 20, 40, DEFAULT_FONT_SIZE, WHITE);
            sprintf(text, "Player %d %.1f %.1f %.1f", plrId, player_position.x, player_position.y, player_position.z);
            DrawTextB(text, 20, 60, DEFAULT_FONT_SIZE, WHITE);

            MemInfo m;
            Debug_GetProcessMemory(m);
            float residentmem = m.resident / 1024.0f / 1024.0f;
            float virtualmem = m.virtualSize / 1024.0f / 1024.0f;
            sprintf(text, "Mem %.2fMiB/%.2fMiB", residentmem, virtualmem);
            DrawTextB(text, 20, 80, DEFAULT_FONT_SIZE, WHITE);
            sprintf(text, "Drawing %d verts", debug_verts_opaque+debug_verts_transparent);
            DrawTextB(text, 120, 40, DEFAULT_FONT_SIZE, WHITE);
            DrawRectangle(GetScreenWidth()/2-3, GetScreenHeight()/2-3, 6, 6, BLACK);
            DrawRectangle(GetScreenWidth()/2-2, GetScreenHeight()/2-2, 4, 4, WHITE);

            int idx = 0;
            while (messages_local.size() > 0)
            {
                std::string msg = messages_local.top();
                DrawTextB(msg.c_str(), 24, 96 + idx * 24, DEFAULT_FONT_SIZE, WHITE);
                idx++;
                messages_local.pop();
            }
            
            DrawRectangle(4-1, GetScreenHeight()-13, 2*(MAX_HP+2), 12, BLACK);
            DrawRectangle(4+1, GetScreenHeight()-11, 2*player_health, 8, RED);
            if(oxygen < MAX_HP) {
                DrawRectangle(4-1, GetScreenHeight()-13-14, 2*(MAX_HP+2), 12, BLACK);
                DrawRectangle(4+1, GetScreenHeight()-11-14, 2*oxygen, 8, BLUE);
            }
            DrawHotbar();

            double totalTime = 0.0;
            for (auto &p : profParts) totalTime += p.time;
            if (totalTime <= 0.0) return;

            float startAngle = -PI/2.0f;
            const int segmentsPerSlice = 24;

            const float cx = GetScreenWidth()-64;
            const float cy = 64;
            const float radius = 32;
            for (auto &p : profParts) {
                float frac = float(p.time / totalTime);
                float sweep = frac * 2.0f * PI;
                float endAngle = startAngle + sweep;
                float angleA = startAngle;
                for (int s = 1; s <= segmentsPerSlice; ++s) {
                    float angleB = startAngle + (s / (float)segmentsPerSlice) * sweep;
                    float x1 = cx;
                    float y1 = cy;
                    float x2 = cx + cosf(angleA) * radius;
                    float y2 = cy + sinf(angleA) * radius;
                    float x3 = cx + cosf(angleB) * radius;
                    float y3 = cy + sinf(angleB) * radius;
                    DrawTriangle({x3, y3}, {x2, y2}, {x1, y1}, p.color);
                    angleA = angleB;
                }
                /*float mid = (startAngle + endAngle) * 0.5f;
                float lx = cx + cosf(mid) * (radius * 0.6f);
                float ly = cy + sinf(mid) * (radius * 0.6f);
                char label[128];
                sprintf(label, "%s (%.1f ms)", p.id.c_str(), p.time * 1000.0f);
                DrawText(label, lx - 20, ly - 6, 10, WHITE);*/

                startAngle = endAngle;
            }
            int T = 0;
            std::sort(profParts.begin(), profParts.end(),
                    [](auto &a, auto &b){ return a.time > b.time; });
            for (auto &p : profParts) {
                char label[128];
                sprintf(label, "%s (%.1f ms) (%.1f%%)", p.id.c_str(), p.time * 1000.0f, p.time / totalTime*100);
                DrawText(label, cx-radius-180+1, cy+T*8-radius+1, 8, BLACK);
                DrawText(label, cx-radius-180, cy+T*8-radius, 8, p.color);
                T++;
            };
            profParts.clear();
        }
        
        if(pauseMenuActive) {
            DrawPauseMenu();
        }

        if(chatOpen) {
            char* msg = DrawChat();
            if(msg != nullptr) {
                chatOpen = false;
                DisableCursor();
                char data[1+MAX_MESSAGE_LENGTH];
                data[0] = NET_ARG_MESSAGEC;
                memcpy(data+1, msg, MAX_MESSAGE_LENGTH);
                SendPacketR(peer, data, 1+MAX_MESSAGE_LENGTH);
                memset(msg, 0, MAX_MESSAGE_LENGTH);
            }
        }
    EndDrawing();
    if(oxygen < 0.1f) {
        player_health -= dt * 20.0f;
    }
    if(taking_panorama) {
        if(panorama_step >= 0) {
            char name[16];
            sprintf(name, "%d.png", panorama_step);
            TakeScreenshot(name);
        }
        panorama_step++;
    }
}
static void GameStart() {
    const int cloudMapSize = MAX_WORLD_SIZE*CHUNK_SIZE/8;
    for (int x = 0; x < cloudMapSize; x++)
    {
        for (int y = 0; y < cloudMapSize; y++) {
            clouds.set(y * cloudMapSize + x, GetCloud(x*8, y*8));
        }
    }
    LoadPlayerData();
    //PlayRandomMusic();
    char data[1];
    data[0] = NET_ARG_REQPLAYERS;
    SendPacketR(peer, data, 1);
    
    camera = { 0 };
    camera.position = (Vector3){ MAX_WORLD_SIZE / 2 * CHUNK_SIZE, 12.0f, MAX_WORLD_SIZE / 2 * CHUNK_SIZE};
    camera.target = (Vector3){camera.position.x, camera.position.y, camera.position.z + 1};
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = FOV;
    camera.projection = CAMERA_PERSPECTIVE;

    int center = MAX_WORLD_SIZE / 2 * CHUNK_SIZE;
    player_position = (Vector3){ MAX_WORLD_SIZE / 2 * CHUNK_SIZE, 128.0f, MAX_WORLD_SIZE / 2 * CHUNK_SIZE};
    player_position.y = world[GetIndexWorld(center / 16, center / 16)].highestBlock[0] + 2;
    player_velocity = (Vector3){ 0, 0, 0 };

    DisableCursor();
    InitHotbar();
    SetTargetFPS(-1);

    wood_place = LoadSound(Pathify("sound/block_wood.wav"));
    stone_place = LoadSound(Pathify("sound/block_stone.wav"));
    dirt_place = LoadSound(Pathify("sound/block_dirt.wav"));
    grass_place = LoadSound(Pathify("sound/block_grass.wav"));
}

static void CreateNetPlayer(int id) {
    Entity temp = NewEntity();
    temp.type = 1;
    temp.position = player_position;
    temp.data[0] = id;
    temp.data[1] = 0;
    AddEntity(temp);
}
static void RemovePlayerEntity(int id) {
    auto match = [&](const Entity ent){
        return ent.data[0] == id && ent.type == 1;
    };
    auto it = std::find_if(entities.begin(), entities.end(), match);
    if (it != entities.end()) {
        entities.erase(it);
    }
}
static void ParseMessagePacket(std::vector<unsigned char> &a_data) {
    std::string output;
    if(a_data[1] == 255) {
        output += std::string("[Server] ");
    }
    for (int i = 0; i < a_data[2]; ++i) {        
        output.push_back(static_cast<char>(a_data[3 + i]));    
    }
    MessageData msg;
    msg.lifetime = 0;
    msg.message = output;
    messages.push_back(msg);
}
static void ParseData(ENetPacket* packet) {
    int len = packet->dataLength;
    std::vector<unsigned char> a_data;
    std::cout << "Processing packet: " << len << "b ;";
    for (int i = 0; i < len; i++)
    {
        a_data.push_back(packet->data[i]);
    }
    switch (a_data[0])
    {
        case NET_ARG_CHUNKF:
            puts("Server sent a chunk, caching for later use");
            worldPacketStack.push(a_data);
            break;
        case NET_ARG_PLRID:
            plrId = a_data[1];
            std::cout << "Server assigned ID:" << plrId << std::endl;
            break;
        case NET_ARG_PLRCON:
            puts("Player connected");
            if(a_data[1] != plrId) {
                CreateNetPlayer(a_data[1]);
            }
            break;
        case NET_ARG_PLRDCN:
            puts("Player disconnected");
            RemovePlayerEntity(a_data[1]);
            break;
        case NET_ARG_ECHOMOVE:
            std::cout << "Server requested to sync pos " << (int)(a_data[1]) << std::endl;
            if(a_data[1] == plrId) {
                float x = DeserializeFloat(a_data[2],a_data[3]);
                float y = DeserializeFloat(a_data[4],a_data[5]);
                float z = DeserializeFloat(a_data[6],a_data[7]);
                player_position.x = x;
                player_position.y = y;
                player_position.z = z;
            }
            for (Entity &plr : entities)
            {
                std::cout << (int)(plr.data[0]) << " " << (int)(plr.type) << std::endl;
                if(plr.data[0] == a_data[1] && plr.type == 1) {
                    float x = DeserializeFloat(a_data[2],a_data[3]);
                    float y = DeserializeFloat(a_data[4],a_data[5]);
                    float z = DeserializeFloat(a_data[6],a_data[7]);
                    plr.position.x = x;
                    plr.position.y = y;
                    plr.position.z = z;
                    //plr.yaw = (short)floorf(a_data[8] / 255.0f* 65536);
                    plr.data[1] = 255;
                    std::cout << x << " " << y << " " << z << " " << (float)(plr.yaw) << std::endl;
                }
            }
            break;
        case NET_ARG_ECHOYAW:
            std::cout << "Server requested to sync yaw " << (int)(a_data[1]) << std::endl;
            for (Entity &plr : entities)
            {
                if(plr.data[0] == a_data[1] && plr.type == 1) {
                    plr.yaw = (short)floorf(a_data[2] / 255.0f * 65536);
                }
            }
            break;
        case NET_ARG_PLRREG:
            puts("Got a connected player");
            if(a_data[1] != plrId) {
                CreateNetPlayer(a_data[1]);
            }
            for (Entity &plr : entities)
            {
                if(plr.data[0] == a_data[1] && plr.type == 1) {
                    float x = DeserializeFloat(a_data[2],a_data[3]);
                    float y = DeserializeFloat(a_data[4],a_data[5]);
                    float z = DeserializeFloat(a_data[6],a_data[7]);
                    plr.position.x = x;
                    plr.position.y = y;
                    plr.position.z = z;
                    plr.yaw = (short)floorf(a_data[8] / 255.0f * 65536);
                }
            }
            break;
        case NET_ARG_BLOCK:
            SetBlock(world, a_data[2], a_data[3], a_data[4], a_data[5]);
            dirtyChunks[GetIndexWorld(a_data[2] / 16, a_data[4] / 16)] = true;
            if(a_data[2] == 0) {
                dirtyChunks[GetIndexWorld(a_data[2] / 16-1, a_data[4] / 16)] = true;
            }
            if(a_data[2] == CHUNK_SIZE-1) {
                dirtyChunks[GetIndexWorld(a_data[2] / 16+1, a_data[4] / 16)] = true;
            }
            if(a_data[4] == 0) {
                dirtyChunks[GetIndexWorld(a_data[2] / 16, a_data[4] / 16-1)] = true;
            }
            if(a_data[4] == CHUNK_SIZE-1) {
                dirtyChunks[GetIndexWorld(a_data[2] / 16, a_data[4] / 16+1)] = true;
            }
            break;
        case NET_ARG_MESSAGEE:
            ParseMessagePacket(a_data);
            break;
        default:
            puts("Malformed data?");
            break;
    }
}
Vector3 player_positionPrev;
float yawPrev;
void NetworkUpdate() {
    if(!online) return;
    if(state == GAME_STATE_GAME) {
        float dist = Vector3Distance(player_positionPrev, player_position);
        if(dist > 0.05f) {
            char data[8];
            data[0] = NET_ARG_PLAYERMOVE;
            SerializeFloat2Data(player_position.x, &data, 1);
            SerializeFloat2Data(player_position.y, &data, 3);
            SerializeFloat2Data(player_position.z, &data, 5);
            float deg = camYaw * RAD2DEG;
            int wrapped = (int)(deg + 180) % 360;
            float scaled = wrapped / 360.0f * 255.0f; 
            int iv = (int)floorf(scaled + 0.5f);
            if (iv < 0) iv = 0;
            if (iv > 255) iv = 255;
            data[7] = (uint8_t)iv;

            SendPacket(peer, &data, sizeof(data));
            player_positionPrev = player_position;
        }
        float yawDist = camYaw - yawPrev;
        if(abs(yawDist) > 0.25f) {
            char data[2];
            data[0] = NET_ARG_PLAYERYAW;
            float deg = camYaw * RAD2DEG;
            int wrapped = (int)(deg + 180) % 360;
            float scaled = wrapped / 360.0f * 255.0f; 
            int iv = (int)floorf(scaled + 0.5f);
            iv &= 0xFF;
            data[1] = (uint8_t)iv;
            SendPacket(peer, &data, sizeof(data));
            yawPrev = camYaw;
        }
    }
    ENetEvent event;
    while(enet_host_service(client, &event, 2) > 0)
    {
        switch(event.type)
        {
            case ENET_EVENT_TYPE_RECEIVE:
                ParseData(event.packet);
                enet_packet_destroy(event.packet);
                break;
            case ENET_EVENT_TYPE_DISCONNECT:
                puts("Disconnection succeeded.");
                break;
        }
    }
}

void GameModeCleanupFull() {
    if(online) {

        enet_host_destroy(client);
        online = false;
    }

    stop_server = true;
    //serverThread.join();

    for (int idx = 0; idx < MAX_WORLD_AREA; idx++)
    {
        if(loadedChunks[idx]) {
            //UnloadModel(renderingT[idx]);
            //UnloadModel(rendering[idx]);
        }
    }
    memset(loadedChunks, 0, sizeof(loadedChunks));
    memset(dirtyChunks, 0, sizeof(dirtyChunks));
    entities.clear();
}

void GameModeCleanup() {
    state = GAME_STATE_TRANSITION;
    SaveWorld(world, "tempworld.data");
    SavePlayerData();
    pauseMenuActive = false;
    EnableCursor();
    GameModeCleanupFull();
    state = GAME_STATE_MAINMENU;
    worldCreationProgress = 0;
    worldCreationStep = 0;
}

extern void InitMenu();
int main(void)
{
    SetTraceLogLevel(LOG_WARNING);
    NetworkingStart();
    SetConfigFlags(FLAG_WINDOW_RESIZABLE);
    InitWindow(WIDTH, HEIGHT, GAME_NAME);
    InitWorldgen(0);
    InitAudioDevice();
    InitMenu();
    InputInit();
    EntitiesInit();
    LocaleUpdate();
    LoadRecipes();

    camera = { 0 };
    camera.position = (Vector3){0, 0, 0};
    camera.target = (Vector3){camera.position.x, camera.position.y, camera.position.z + 1};
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 90;
    camera.projection = CAMERA_PERSPECTIVE;

    fnt = LoadFontEx(Pathify("font.ttf"), 32, NULL, 1024*8);
    terrain = LoadTexture(Pathify("terrain.png"));
    GenTextureMipmaps(&terrain);
    Image temp = GenImageColor(1, 1, WHITE);
    white_pixel = LoadTextureFromImage(temp);
    Image src = LoadImageFromTexture(terrain);

    UVCorners uvs = GetUVTex(14);
    Rectangle src0;
    src0.x = uvs.corner2.x * terrain.width;
    src0.y = uvs.corner2.y * terrain.height;
    src0.width = (uvs.corner3.x - uvs.corner2.x) * terrain.width;
    src0.height = (uvs.corner1.y - uvs.corner2.y) * terrain.height;
    Image copied = ImageCopy(src);
    ImageCrop(&copied, src0);
    
    water = LoadTextureFromImage(copied);
    SetTextureWrap(water, TEXTURE_WRAP_REPEAT);
    SetTargetFPS(-1);
    float time;

    SetExitKey(KEY_NULL);

    while (!WindowShouldClose())
    {
        NetworkUpdate();
        InputUpdate();

        while (worldPacketStack.size() > 0)
        {
            std::vector<unsigned char> data = worldPacketStack.top();

            int idx = (int)(data[1]);
            std::cout << "Processing chunk " << (int)idx << std::endl;
            Chunk c;
            int x, z;
            GetXZFromIndex(idx, &x, &z);
            c.x = (uint8_t)x;
            c.z = (uint8_t)z;
            std::cout << data.size() << std::endl;
            for (int i = 2; i < CHUNK_DATA_SIZE+2; i++)
            {
                c.blocks[i-2] = data[i];
            }
            for (int i = CHUNK_DATA_SIZE+2; i < CHUNK_DATA_SIZE+2+CHUNK_SIZE*CHUNK_SIZE; i++)
            {
                c.highestBlock[i-CHUNK_DATA_SIZE-2] = data[i];
            }
            world[idx] = c;
            dirtyChunks[idx] = true;
            worldPacketStack.pop();
            fetchedChunks++;
        }

        std::string text0 = LocaleGet("step_0");
        char text[64];
        switch (state)
        {
            case GAME_STATE_GAME:
                GameUpdate(camera, time);
                break;
        
            case GAME_STATE_LOADING:
                // World creation crap
                
                BeginDrawing();
                    DrawBackground();

                    if(worldCreationStep == 1) {
                        text0 = LocaleGet("step_1");
                    }
                    if(worldCreationStep == 2) {
                        text0 = LocaleGet("step_2");
                    }
                    if(worldCreationStep == 3) {
                        text0 = LocaleGet("step_3");
                    }
                    DrawTextB(text0.c_str(), GetScreenWidth()/2 - 64, GetScreenHeight()/2 - 32, DEFAULT_FONT_SIZE,  WHITE);
                    if(worldCreationStep != 1) {
                        DrawRectangle( GetScreenWidth()/2-64, GetScreenHeight()/2, 128, 16, BLACK);
                        DrawRectangle( GetScreenWidth()/2-62, GetScreenHeight()/2 + 2, 124 * ((float)worldCreationProgress / static_cast<float>(MAX_WORLD_AREA)), 12, GREEN);
                        sprintf(text, LocaleGet("chunk_status").c_str(), worldCreationProgress, MAX_WORLD_AREA);
                        DrawTextB(text, GetScreenWidth()/2 - 32, GetScreenHeight()/2 + 32, DEFAULT_FONT_SIZE, WHITE);
                    }
                EndDrawing();

                // * Well, there was a loop here.
                if(worldCreationProgress < MAX_WORLD_AREA) {
                    for (int _ = 0; _ < 16; _++)
                    {
                        int x = worldCreationProgress % MAX_WORLD_SIZE;
                        int y = worldCreationProgress / MAX_WORLD_SIZE;
                        int idx = x + y * MAX_WORLD_SIZE;
                        if(worldCreationStep == 0 && !online) {
                            Chunk chunk = GenerateChunk(x, y);
                            world[idx] = chunk;
                        }
                        if(worldCreationStep == 1 && !online) {
                            if(worldCreationProgress > 0) {
                                worldCreationStep++;
                            }
                            else {
                                GenerateWorldAdditional(world);
                            }
                        }
                        if (worldCreationStep == 2)                        
                        {
                            //RebuildOpaqueMask(&world[idx]);
                        }
                        if (worldCreationStep == 3) {
                            dirtyChunks[idx] = true;
                        }
                        if(online)
                            worldCreationProgress = fetchedChunks+1;
                    }
                }
                else {
                    if(worldCreationStep <= 2) {
                        worldCreationProgress = 0;
                        worldCreationStep++;
                    }
                    else {
                        state = GAME_STATE_GAME;
                        GameStart();
                        break;
                    }
                }
                break;
            case GAME_STATE_MAINMENU:
                DrawMainMenu();
                break;
            case GAME_STATE_WORLDSELECT:
                DrawWorldSelect();
                break;
            case GAME_STATE_MULTIPLAYERSELECT:
                DrawMultiplayer();
                break;
            case GAME_STATE_TRANSITION:
                DrawLogoCenter();
                break;
            case GAME_STATE_OPTIONS:
                DrawControls();
                break;
        }
    }

    enet_deinitialize();
    GameModeCleanupFull();

    UnloadTexture(terrain);
    CloseWindow();
    return 0;
}