in this code, add an inspector button that when pressed, exports all your chunks under a parent as a prefab. make sure the materials work properly in the prefab and can be drag and dropped to any scene

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
using System;
using PimDeWitte.UnityMainThreadDispatcher;
using System.Collections;

public class BlockFetcher : MonoBehaviour
{
    private string serverUrl = "http://localhost:8000/blocks";
    public string texturePath = "Assets/Textures";
    public Material blockMaterial;
    public string defaultTextureName = "missing_texture";
    private const int MAX_VERTICES_PER_CHUNK = 65000;
    private List<MeshChunk> meshChunks = new();
    public string[] woodTypes;
    public Mesh slabMesh, fenceMesh, stairMesh;

    private class MeshChunk
    {
        public List<Vector3> vertices = new(MAX_VERTICES_PER_CHUNK);
        public List<int> triangles = new();
        public List<Vector2> uvs = new();
        public bool isFull => vertices.Count + 4 >= MAX_VERTICES_PER_CHUNK;
    }

    [Serializable]
    public class BlockTextureConfig
    {
        public string blockName, top, bottom, front, back, left, right, side;
        public bool isBillboard;
        public Mesh customMesh;
        public void SetDefaults(string defaultName)
        {
            top ??= defaultName;
            bottom ??= defaultName;
            if (!string.IsNullOrEmpty(side))
                front = back = left = right = side;
            else
            {
                front ??= defaultName;
                back ??= defaultName;
                left ??= defaultName;
                right ??= defaultName;
            }
        }
    }

    [Serializable]
    public class BlockTintConfig
    {
        public string blockName;
        public Color tintColor;
        public string maskName;
        public BlockTintConfig(string blockName, Color tintColor, string maskName = null)
        {
            this.blockName = blockName;
            this.tintColor = tintColor;
            this.maskName = maskName;
        }
    }

    public BlockTextureConfig[] blockConfigs;
    public BlockTintConfig[] blockTintConfigs;

    private Texture2D textureAtlas;
    private Rect[] atlasRects;
    private Dictionary<string, int> blockNameToTextureIndex = new(StringComparer.OrdinalIgnoreCase);
    private Dictionary<string, BlockTextureConfig> blockConfigMap = new(StringComparer.OrdinalIgnoreCase);
    private int defaultTextureIndex;
    private Dictionary<int, Vector2[]> uvCache = new();

    [Serializable]
    public class BlockData
    {
        public string block_name;
        public int x, y, z;
        public string direction;
    }

    private static readonly Vector3[,] faceVertices = {
        { new(-0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, -0.5f), new(0.5f, 0.5f, -0.5f), new(-0.5f, 0.5f, -0.5f) },
        { new(-0.5f, -0.5f, 0.5f), new(0.5f, -0.5f, 0.5f), new(0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, 0.5f) },
        { new(-0.5f, 0.5f, -0.5f), new(0.5f, 0.5f, -0.5f), new(0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, 0.5f) },
        { new(-0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, 0.5f), new(-0.5f, -0.5f, 0.5f) },
        { new(-0.5f, -0.5f, -0.5f), new(-0.5f, -0.5f, 0.5f), new(-0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, -0.5f) },
        { new(0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, 0.5f), new(0.5f, 0.5f, 0.5f), new(0.5f, 0.5f, -0.5f) }
    };

    private static readonly Vector3[][] billboardVertices = {
        new[] { new Vector3(-0.5f, -0.5f, -0.5f), new(0.5f, -0.5f, 0.5f), new(0.5f, 0.5f, 0.5f), new(-0.5f, 0.5f, -0.5f) },
        new[] { new Vector3(-0.5f, -0.5f, 0.5f), new(0.5f, -0.5f, -0.5f), new(0.5f, 0.5f, -0.5f), new(-0.5f, 0.5f, 0.5f) }
    };

    void Start()
    {
        InitializeBlockConfigs();
        BeginInitialization();
    }

    async void BeginInitialization()
    {
        await CreateTextureAtlasAsync();
        await FetchAndProcessBlocksAsync();
    }

    private void InitializeBlockConfigs()
    {
        blockConfigMap.Clear();
        foreach (var config in blockConfigs)
        {
            config.SetDefaults(defaultTextureName); // Use default texture name
            blockConfigMap[config.blockName] = config;
        }
    }

    private async Task CreateTextureAtlasAsync()
    {
        var maskTextures = LoadMaskTextures();

        // Load all textures on the main thread to avoid Unity exceptions
        var textureList = await LoadAllTexturesAsync(maskTextures);

        textureAtlas = new Texture2D(2048, 2048, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
        atlasRects = textureAtlas.PackTextures(textureList.ToArray(), 0, 2048);
        textureAtlas.Apply(false, true);
        blockMaterial.mainTexture = textureAtlas;
    }

    private Dictionary<string, Texture2D> LoadMaskTextures()
    {
        var maskTextures = new Dictionary<string, Texture2D>(StringComparer.OrdinalIgnoreCase);
        string maskPath = Path.Combine(texturePath, "Masks");
        if (!Directory.Exists(maskPath)) return maskTextures;

        foreach (string maskFile in Directory.GetFiles(maskPath, "*.png"))
        {
            string maskName = Path.GetFileNameWithoutExtension(maskFile);
            Texture2D maskTexture = new Texture2D(2, 2, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
            maskTexture.LoadImage(File.ReadAllBytes(maskFile));
            maskTextures[maskName] = maskTexture;
        }
        return maskTextures;
    }

    private async Task<List<Texture2D>> LoadAllTexturesAsync(Dictionary<string, Texture2D> maskTextures)
    {
        List<Texture2D> texturesList = new();
        string[] textureFiles = Directory.GetFiles(texturePath, "*.png");

        // Load default texture
        Texture2D defaultTexture = await LoadDefaultTextureAsync();
        texturesList.Add(defaultTexture);
        blockNameToTextureIndex[defaultTextureName] = 0;
        defaultTextureIndex = 0;

        int textureIndex = 1;
        foreach (string textureFile in textureFiles)
        {
            string fileName = Path.GetFileNameWithoutExtension(textureFile);
            if (fileName.Equals(defaultTextureName, StringComparison.OrdinalIgnoreCase)) continue;
            Texture2D texture = await LoadAndProcessTextureAsync(textureFile, fileName, maskTextures);
            texturesList.Add(texture);
            blockNameToTextureIndex[fileName] = textureIndex++;
        }

        return texturesList;
    }

    private async Task<Texture2D> LoadDefaultTextureAsync()
    {
        string defaultTexturePath = Path.Combine(texturePath, defaultTextureName + ".png");
        if (File.Exists(defaultTexturePath))
        {
            byte[] data = await ReadFileAsync(defaultTexturePath);
            return await LoadTextureOnMainThreadAsync(data);
        }
        else
        {
            Debug.LogError($"Default texture {defaultTexturePath} not found! Creating fallback texture.");
            return CreateFallbackTexture();
        }
    }

    private async Task<Texture2D> LoadAndProcessTextureAsync(string textureFile, string textureName, Dictionary<string, Texture2D> maskTextures)
    {
        byte[] fileData = await ReadFileAsync(textureFile);

        // Load texture on main thread to avoid Unity exceptions
        Texture2D texture = await LoadTextureOnMainThreadAsync(fileData);

        var tintConfig = blockTintConfigs.FirstOrDefault(c => c.blockName.Equals(textureName, StringComparison.OrdinalIgnoreCase));
        if (tintConfig != null)
        {
            ApplyTintToTexture(texture, tintConfig, maskTextures);
        }
        return texture;
    }

    private async Task<Texture2D> LoadTextureOnMainThreadAsync(byte[] data)
    {
        TaskCompletionSource<Texture2D> tcs = new();
        UnityMainThreadDispatcher.Instance().Enqueue(() =>
        {
            Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
            texture.LoadImage(data);
            tcs.SetResult(texture);
        });
        return await tcs.Task;
    }

    private void ApplyTintToTexture(Texture2D texture, BlockTintConfig config, Dictionary<string, Texture2D> maskTextures)
    {
        Texture2D maskTexture = null;
        if (!string.IsNullOrEmpty(config.maskName))
        {
            maskTextures.TryGetValue(config.maskName, out maskTexture);
        }

        Color[] pixels = texture.GetPixels();
        Color[] maskPixels = maskTexture?.GetPixels();

        for (int i = 0; i < pixels.Length; i++)
        {
            if (maskPixels != null)
            {
                float maskIntensity = maskPixels[i].grayscale;
                pixels[i] = Color.Lerp(pixels[i], pixels[i] * config.tintColor, maskIntensity);
            }
            else
            {
                pixels[i] *= config.tintColor;
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();
    }

    private Task<byte[]> ReadFileAsync(string filePath)
    {
        return Task.Run(() => File.ReadAllBytes(filePath));
    }

    private Texture2D CreateFallbackTexture()
    {
        Texture2D texture = new Texture2D(16, 16, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
        Color[] colors = Enumerable.Range(0, 256).Select(i => (i / 16 + i % 16) % 2 == 0 ? Color.magenta : Color.black).ToArray();
        texture.SetPixels(colors);
        texture.Apply();
        return texture;
    }

    private async Task FetchAndProcessBlocksAsync()
    {
        string jsonResponse = await GetBlocksDataAsync();
        var blockDataList = JsonConvert.DeserializeObject<List<BlockData>>(jsonResponse);

        // Process blocks on the main thread using a coroutine
        UnityMainThreadDispatcher.Instance().Enqueue(() => StartCoroutine(ProcessBlocksCoroutine(blockDataList)));
    }

    private async Task<string> GetBlocksDataAsync()
    {
        using UnityWebRequest webRequest = UnityWebRequest.Get(serverUrl);
        var asyncOp = webRequest.SendWebRequest();

        while (!asyncOp.isDone)
            await Task.Yield();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error fetching block data: " + webRequest.error);
            return null;
        }
        else
        {
            return webRequest.downloadHandler.text;
        }
    }

    private IEnumerator ProcessBlocksCoroutine(List<BlockData> blockDataList)
    {
        int batchSize = 1000; // Adjust as needed
        int totalBlocks = blockDataList.Count;
        int processedBlocks = 0;

        while (processedBlocks < totalBlocks)
        {
            int blocksToProcess = Mathf.Min(batchSize, totalBlocks - processedBlocks);
            for (int i = 0; i < blocksToProcess; i++)
            {
                AddBlockFaces(blockDataList[processedBlocks + i]);
            }
            processedBlocks += blocksToProcess;

            // Optional: Update a progress indicator here

            yield return null; // Wait for the next frame
        }

        CreateMesh();
    }

    private void AddBlockFaces(BlockData block)
    {
        Vector3 blockPosition = new(block.x, block.y, block.z);
        if (!blockConfigMap.TryGetValue(block.block_name, out var config))
        {
            config = new BlockTextureConfig { blockName = block.block_name };
            config.SetDefaults(block.block_name); // Use block's own name
        }

        var customMeshes = new Dictionary<string, Mesh>
        {
            { "_slab", slabMesh },
            { "_fence", fenceMesh },
            { "_stairs", stairMesh }
        };

        Mesh selectedMesh = config.customMesh;
        string replace = null, add = null;
        if (selectedMesh == null)
        {
            foreach (var kvp in customMeshes)
            {
                if (config.blockName.Contains(kvp.Key))
                {
                    selectedMesh = kvp.Value;
                    replace = kvp.Key;
                    if (woodTypes.Any(w => config.front.Contains(w)))
                        add = "_planks";
                    break;
                }
            }
        }

        if (selectedMesh != null)
            AddCustomMeshToChunk(blockPosition, selectedMesh, config.front, replace, add);
        else if (config.isBillboard)
            AddBillboardFacesToChunk(blockPosition, config);
        else
            AddDefaultCubeFacesToChunk(blockPosition, config);
    }

    private MeshChunk GetCurrentChunk()
    {
        if (meshChunks.Count == 0 || meshChunks[^1].isFull)
            meshChunks.Add(new MeshChunk());
        return meshChunks[^1];
    }

    private void AddCustomMeshToChunk(Vector3 blockPosition, Mesh customMesh, string textureName, string replace, string add)
    {
        if (replace != null) textureName = textureName.Replace(replace, "") + add;
        var chunk = GetCurrentChunk();
        var textureUVs = GetUVsForTexture(textureName);
        int vertCount = chunk.vertices.Count;
        int uvIndex = 0;

        foreach (var vertex in customMesh.vertices)
        {
            chunk.vertices.Add(vertex + blockPosition);
            chunk.uvs.Add(textureUVs[uvIndex % 4]);
            uvIndex++;
        }
        foreach (var triangle in customMesh.triangles)
            chunk.triangles.Add(vertCount + triangle);
    }

    private void AddBillboardFacesToChunk(Vector3 blockPosition, BlockTextureConfig config)
    {
        var textureUVs = GetUVsForTexture(config.front);
        foreach (var plane in billboardVertices)
        {
            var chunk = GetCurrentChunk();
            int vertCount = chunk.vertices.Count;
            for (int i = 0; i < 4; i++)
            {
                chunk.vertices.Add(plane[i] + blockPosition);
                chunk.uvs.Add(textureUVs[i]);
            }
            chunk.triangles.AddRange(new[] { vertCount, vertCount + 2, vertCount + 1, vertCount, vertCount + 3, vertCount + 2 });
        }
    }

    private void AddDefaultCubeFacesToChunk(Vector3 blockPosition, BlockTextureConfig config)
    {
        string[] faceTextures = { config.front, config.back, config.top, config.bottom, config.left, config.right };
        for (int faceIndex = 0; faceIndex < 6; faceIndex++)
        {
            var chunk = GetCurrentChunk();
            var textureUVs = GetUVsForTexture(faceTextures[faceIndex]);
            int vertCount = chunk.vertices.Count;
            for (int i = 0; i < 4; i++)
            {
                chunk.vertices.Add(faceVertices[faceIndex, i] + blockPosition);
                chunk.uvs.Add(textureUVs[i]);
            }
            chunk.triangles.AddRange(new[] { vertCount, vertCount + 2, vertCount + 1, vertCount, vertCount + 3, vertCount + 2 });
        }
    }

    private int GetTextureIndexForTextureName(string textureName)
    {
        if (string.IsNullOrEmpty(textureName))
            return defaultTextureIndex;
        return blockNameToTextureIndex.TryGetValue(textureName, out int index) ? index : defaultTextureIndex;
    }

    private Vector2[] GetUVsForTexture(string textureName)
    {
        int textureIndex = GetTextureIndexForTextureName(textureName);

        if (textureIndex >= atlasRects.Length || textureIndex < 0)
        {
            Debug.LogWarning($"Invalid texture index: {textureIndex} for texture: {textureName}. Using default texture.");
            textureIndex = defaultTextureIndex;
        }

        if (!uvCache.TryGetValue(textureIndex, out Vector2[] uvs))
        {
            Rect uvRect = atlasRects[textureIndex];
            uvs = new[]
            {
                new Vector2(uvRect.xMin, uvRect.yMin),
                new Vector2(uvRect.xMax, uvRect.yMin),
                new Vector2(uvRect.xMax, uvRect.yMax),
                new Vector2(uvRect.xMin, uvRect.yMax)
            };
            uvCache[textureIndex] = uvs;
        }
        return uvs;
    }

    private void CreateMesh()
    {
        GameObject chunksParent = new("ChunkedMeshes");
        for (int i = 0; i < meshChunks.Count; i++)
        {
            var chunk = meshChunks[i];
            GameObject chunkObject = new($"MeshChunk_{i}") { transform = { parent = chunksParent.transform } };
            Mesh mesh = new() { indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 };
            mesh.SetVertices(chunk.vertices);
            mesh.SetTriangles(chunk.triangles, 0);
            mesh.SetUVs(0, chunk.uvs);
            mesh.RecalculateNormals();
            chunkObject.AddComponent<MeshFilter>().mesh = mesh;
            chunkObject.AddComponent<MeshRenderer>().material = blockMaterial;
            chunkObject.AddComponent<MeshCollider>().sharedMesh = mesh;
        }
    }
}