completely overhaul this code by removing special block arrays, and dont store textures locally. fetch textures and custom block models and other info from https://github.com/PrismarineJS/minecraft-assets/tree/master/data/1.20.2 . textures are stored in https://github.com/PrismarineJS/minecraft-assets/tree/master/data/1.20.2/blocks . block texture mappings are in https://github.com/PrismarineJS/minecraft-assets/blob/master/data/1.20.2/blocks_textures.json . Block models/directional texture mappings are in https://github.com/PrismarineJS/minecraft-assets/blob/master/data/1.20.2/blocks_models.json .

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 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;
    }
}
}