UnityException: SupportsTextureFormatNative can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.

using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Threading.Tasks;
using UnityEngine.Networking;

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 MaxVerticesPerChunk = 65000;
    private List<MeshChunk> meshChunks = new List<MeshChunk>();

    public string[] woodTypes;
    public Mesh slabMesh;
    public Mesh fenceMesh;
    public Mesh stairMesh;

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

    [System.Serializable]
    public class BlockTextureConfig
    {
        public string blockName;
        public string top;
        public string bottom;
        public string front;
        public string back;
        public string left;
        public string right;
        public string side;
        public bool isBillboard;
        public Mesh customMesh;

        public void SetDefaults(string defaultName)
        {
            top = string.IsNullOrEmpty(top) ? defaultName : top;
            bottom = string.IsNullOrEmpty(bottom) ? defaultName : bottom;
            if (!string.IsNullOrEmpty(side))
            {
                front = back = left = right = side;
            }
            else
            {
                front = string.IsNullOrEmpty(front) ? defaultName : front;
                back = string.IsNullOrEmpty(back) ? defaultName : back;
                left = string.IsNullOrEmpty(left) ? defaultName : left;
                right = string.IsNullOrEmpty(right) ? defaultName : right;
            }
        }
    }

    [System.Serializable]
    public class BlockTintConfig
    {
        public string blockName;
        public Color tintColor;
        public string maskName;
    }

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

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

    [System.Serializable]
    public struct BlockData
    {
        public string block_name;
        public int x;
        public int y;
        public int z;
        public string direction;
    }

    private static readonly Vector3[,] faceVertices = new Vector3[6, 4]
    {
        // Front
        {
            new Vector3(-0.5f, -0.5f, -0.5f),
            new Vector3( 0.5f, -0.5f, -0.5f),
            new Vector3( 0.5f,  0.5f, -0.5f),
            new Vector3(-0.5f,  0.5f, -0.5f)
        },
        // Back
        {
            new Vector3(-0.5f, -0.5f, 0.5f),
            new Vector3( 0.5f, -0.5f, 0.5f),
            new Vector3( 0.5f,  0.5f, 0.5f),
            new Vector3(-0.5f,  0.5f, 0.5f)
        },
        // Top
        {
            new Vector3(-0.5f,  0.5f, -0.5f),
            new Vector3( 0.5f,  0.5f, -0.5f),
            new Vector3( 0.5f,  0.5f,  0.5f),
            new Vector3(-0.5f,  0.5f,  0.5f)
        },
        // Bottom
        {
            new Vector3(-0.5f, -0.5f, -0.5f),
            new Vector3( 0.5f, -0.5f, -0.5f),
            new Vector3( 0.5f, -0.5f,  0.5f),
            new Vector3(-0.5f, -0.5f,  0.5f)
        },
        // Left
        {
            new Vector3(-0.5f, -0.5f, -0.5f),
            new Vector3(-0.5f, -0.5f,  0.5f),
            new Vector3(-0.5f,  0.5f,  0.5f),
            new Vector3(-0.5f,  0.5f, -0.5f)
        },
        // Right
        {
            new Vector3( 0.5f, -0.5f, -0.5f),
            new Vector3( 0.5f, -0.5f,  0.5f),
            new Vector3( 0.5f,  0.5f,  0.5f),
            new Vector3( 0.5f,  0.5f, -0.5f)
        }
    };

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

    private async void Start()
    {
        InitializeBlockConfigs();
        await Task.WhenAll(CreateTextureAtlasAsync(), FetchBlocksAsync());
    }

    private void InitializeBlockConfigs()
    {
        blockConfigMap.Clear();
        foreach (var config in blockConfigs)
        {
            config.SetDefaults(config.blockName);
            blockConfigMap[config.blockName] = config;
        }
    }

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

        string defaultTexturePath = Path.Combine(texturePath, defaultTextureName + ".png");
        Texture2D defaultTexture = File.Exists(defaultTexturePath) ? await LoadTextureAsync(defaultTexturePath) : CreateFallbackTexture();

        texturesList.Add(defaultTexture);
        blockNameToTextureIndex[defaultTextureName] = 0;
        defaultTextureIndex = 0;

        var textureTasks = new List<Task>();
        var loadedTextures = new Dictionary<string, Texture2D>();

        foreach (var file in textureFiles)
        {
            string fileName = Path.GetFileNameWithoutExtension(file);
            if (fileName.Equals(defaultTextureName, System.StringComparison.OrdinalIgnoreCase)) continue;

            textureTasks.Add(Task.Run(async () =>
            {
                Texture2D tex = await LoadTextureAsync(file);
                var tintConfig = GetTintConfig(fileName);
                if (tintConfig != null)
                    ApplyTint(tex, tintConfig, maskTextures);
                lock (loadedTextures)
                {
                    loadedTextures[fileName] = tex;
                }
            }));
        }

        await Task.WhenAll(textureTasks);

        int textureIndex = 1;
        foreach (var kvp in loadedTextures)
        {
            texturesList.Add(kvp.Value);
            blockNameToTextureIndex[kvp.Key] = textureIndex++;
        }

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

        foreach (var maskTexture in maskTextures.Values)
            Destroy(maskTexture);
    }

    private async Task<Dictionary<string, Texture2D>> LoadMaskTexturesAsync()
    {
        var maskTextures = new Dictionary<string, Texture2D>();
        string maskPath = Path.Combine(texturePath, "Masks");
        if (Directory.Exists(maskPath))
        {
            var maskFiles = Directory.GetFiles(maskPath, "*.png");
            var maskTasks = new List<Task>();
            foreach (var maskFile in maskFiles)
            {
                maskTasks.Add(Task.Run(async () =>
                {
                    string maskName = Path.GetFileNameWithoutExtension(maskFile);
                    Texture2D maskTexture = await LoadTextureAsync(maskFile);
                    lock (maskTextures)
                    {
                        maskTextures[maskName] = maskTexture;
                    }
                }));
            }
            await Task.WhenAll(maskTasks);
        }
        return maskTextures;
    }

    private BlockTintConfig GetTintConfig(string fileName)
    {
        foreach (var config in blockTintConfigs)
        {
            if (config.blockName.Equals(fileName, System.StringComparison.OrdinalIgnoreCase))
                return config;
        }
        return null;
    }

    private async Task<Texture2D> LoadTextureAsync(string filePath)
    {
        byte[] fileData = await File.ReadAllBytesAsync(filePath);
        Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
        texture.LoadImage(fileData);
        return texture;
    }

    private void ApplyTint(Texture2D texture, BlockTintConfig config, Dictionary<string, Texture2D> maskTextures)
    {
        Color[] pixels = texture.GetPixels();
        if (!string.IsNullOrEmpty(config.maskName) && maskTextures.TryGetValue(config.maskName, out Texture2D maskTexture))
        {
            Color[] maskPixels = maskTexture.GetPixels();
            for (int i = 0; i < pixels.Length; i++)
                pixels[i] = Color.Lerp(pixels[i], pixels[i] * config.tintColor, maskPixels[i].grayscale);
        }
        else
        {
            for (int i = 0; i < pixels.Length; i++)
            {
                pixels[i].r *= config.tintColor.r;
                pixels[i].g *= config.tintColor.g;
                pixels[i].b *= config.tintColor.b;
            }
        }
        texture.SetPixels(pixels);
        texture.Apply();
    }

    private Texture2D CreateFallbackTexture()
    {
        Texture2D fallback = new Texture2D(16, 16, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point };
        Color pink = new Color(1f, 0f, 1f, 1f);
        Color black = Color.black;
        for (int y = 0; y < 16; y++)
            for (int x = 0; x < 16; x++)
                fallback.SetPixel(x, y, ((x + y) % 2 == 0) ? pink : black);
        fallback.Apply();
        return fallback;
    }

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

    private Vector2[] GetUVsForTexture(string textureName)
    {
        int textureIndex = GetTextureIndexForTextureName(textureName);
        if (!uvCache.TryGetValue(textureIndex, out Vector2[] uvs))
        {
            Rect uvRect = atlasRects[textureIndex];
            uvs = new Vector2[4]
            {
                new Vector2(uvRect.x, uvRect.y),
                new Vector2(uvRect.x + uvRect.width, uvRect.y),
                new Vector2(uvRect.x + uvRect.width, uvRect.y + uvRect.height),
                new Vector2(uvRect.x, uvRect.y + uvRect.height)
            };
            uvCache[textureIndex] = uvs;
        }
        return uvs;
    }

    private async Task FetchBlocksAsync()
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(serverUrl))
        {
            await webRequest.SendWebRequest();
            if (webRequest.result == UnityWebRequest.Result.Success)
            {
                blockDataList = JsonUtility.FromJson<BlockDataList>("{\"blocks\":" + webRequest.downloadHandler.text + "}").blocks;
                Parallel.ForEach(blockDataList, block => AddBlockFaces(block));
                CreateMesh();
            }
            else
            {
                Debug.LogError("Error fetching block data: " + webRequest.error);
            }
        }
    }

    [System.Serializable]
    private class BlockDataList
    {
        public List<BlockData> blocks;
    }

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

        Mesh selectedMesh = config.customMesh ?? GetCustomMesh(config.blockName, ref config.front);
        if (selectedMesh != null)
            AddCustomMeshToChunk(blockPosition, selectedMesh, config.front);
        else if (config.isBillboard)
            AddBillboardToChunk(blockPosition, config.front);
        else
            AddCubeToChunk(blockPosition, config);
    }

    private Mesh GetCustomMesh(string blockName, ref string textureName)
    {
        if (slabMesh != null && blockName.Contains("_slab"))
        {
            textureName = AdjustTextureName(textureName, "_slab", "_planks");
            return slabMesh;
        }
        if (fenceMesh != null && blockName.Contains("_fence"))
        {
            textureName = AdjustTextureName(textureName, "_fence", "_planks");
            return fenceMesh;
        }
        if (stairMesh != null && blockName.Contains("_stairs"))
        {
            textureName = AdjustTextureName(textureName, "_stairs", "_planks");
            return stairMesh;
        }
        return null;
    }

    private string AdjustTextureName(string textureName, string oldValue, string newValue)
    {
        return textureName.Replace(oldValue, "") + newValue;
    }

    private void AddCustomMeshToChunk(Vector3 position, Mesh mesh, string textureName)
    {
        MeshChunk chunk = GetCurrentChunk();
        Vector2[] uv = GetUVsForTexture(textureName);
        int vertCount = chunk.vertices.Count;

        foreach (var vertex in mesh.vertices)
            chunk.vertices.Add(vertex + position);

        for (int i = 0; i < mesh.uv.Length; i++)
            chunk.uvs.Add(uv[i % 4]);

        foreach (var triangle in mesh.triangles)
            chunk.triangles.Add(vertCount + triangle);
    }

    private void AddBillboardToChunk(Vector3 position, string textureName)
    {
        Vector2[] uv = GetUVsForTexture(textureName);
        for (int planeIndex = 0; planeIndex < 2; planeIndex++)
        {
            MeshChunk chunk = GetCurrentChunk();
            int vertCount = chunk.vertices.Count;

            for (int i = 0; i < 4; i++)
            {
                chunk.vertices.Add(billboardVertices[planeIndex][i] + position);
                chunk.uvs.Add(uv[i]);
            }

            chunk.triangles.AddRange(new int[] { vertCount, vertCount + 2, vertCount + 1, vertCount, vertCount + 3, vertCount + 2 });

            vertCount = chunk.vertices.Count;

            for (int i = 0; i < 4; i++)
            {
                chunk.vertices.Add(billboardVertices[planeIndex][i] + position);
                chunk.uvs.Add(uv[i]);
            }

            chunk.triangles.AddRange(new int[] { vertCount, vertCount + 1, vertCount + 2, vertCount, vertCount + 2, vertCount + 3 });
        }
    }

    private void AddCubeToChunk(Vector3 position, BlockTextureConfig config)
    {
        string[] faceTextures = { config.front, config.back, config.top, config.bottom, config.left, config.right };
        for (int faceIndex = 0; faceIndex < 6; faceIndex++)
        {
            MeshChunk chunk = GetCurrentChunk();
            Vector2[] uv = GetUVsForTexture(faceTextures[faceIndex]);
            int vertCount = chunk.vertices.Count;

            for (int i = 0; i < 4; i++)
            {
                chunk.vertices.Add(faceVertices[faceIndex, i] + position);
                chunk.uvs.Add(uv[i]);
            }

            chunk.triangles.AddRange(new int[] { vertCount, vertCount + 2, vertCount + 1, vertCount, vertCount + 3, vertCount + 2 });
        }
    }

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

    private void CreateMesh()
    {
        GameObject chunksParent = new GameObject("ChunkedMeshes");
        chunksParent.transform.position = Vector3.zero;

        foreach (var chunk in meshChunks)
        {
            GameObject chunkObject = new GameObject("MeshChunk");
            chunkObject.transform.parent = chunksParent.transform;
            chunkObject.transform.localPosition = Vector3.zero;

            MeshFilter meshFilter = chunkObject.AddComponent<MeshFilter>();
            MeshRenderer meshRenderer = chunkObject.AddComponent<MeshRenderer>();
            Mesh mesh = new Mesh { indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 };

            mesh.SetVertices(chunk.vertices);
            mesh.SetTriangles(chunk.triangles, 0);
            mesh.SetUVs(0, chunk.uvs);
            mesh.RecalculateNormals();

            meshFilter.mesh = mesh;
            meshRenderer.material = blockMaterial;

            chunkObject.AddComponent<MeshCollider>().sharedMesh = mesh;
        }
    }
}