Загрузка данных


using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using MSCLoader;
using UnityEngine;

namespace MapExporter
{
    public class MapExporterMod : Mod
    {
        public override string ID { get { return "MapExporter"; } }
        public override string Name { get { return "Map Exporter"; } }
        public override string Author { get { return "me"; } }
        public override string Version { get { return "2.0"; } }

        public override void ModSetup()
        {
            SetupFunction(Setup.OnLoad, Mod_OnLoad);
        }

        private void Mod_OnLoad()
        {
            GameObject go = new GameObject("MapExporterRunner");
            go.AddComponent<MapExporterBehaviour>();
            UnityEngine.Object.DontDestroyOnLoad(go);
        }
    }

    // F8 - пробный прогон, F9 - выгрузка, F10 - сбор объектов по ходу игры
    public class MapExporterBehaviour : MonoBehaviour
    {
        private const float ZoneSize = 200f;
        private const int MaxTextureSize = 2048;
        private const int TerrainMaxGrid = 513;
        private const int TerrainTexSize = 2048;
        private const float TrackInterval = 3f;

        private class Entry
        {
            public MeshFilter mf;
            public int zx;
            public int zz;
        }

        private string status = "";
        private float statusUntil;
        private float hintUntil;
        private bool busy;
        private bool tracking;
        private float nextTrack;

        private Dictionary<int, MeshFilter> tracked = new Dictionary<int, MeshFilter>();

        private string outDir;
        private string texDir;
        private Dictionary<int, string> texNames = new Dictionary<int, string>();
        private Dictionary<int, string> matNames = new Dictionary<int, string>();
        private StringBuilder mtl = new StringBuilder();

        private int dupeCount;
        private int skippedMeshes;

        void Start()
        {
            hintUntil = Time.realtimeSinceStartup + 25f;
        }

        void Update()
        {
            if (busy) return;

            if (Input.GetKeyDown(KeyCode.F8))
            {
                StartCoroutine(DryRun());
            }
            else if (Input.GetKeyDown(KeyCode.F9))
            {
                StartCoroutine(Export());
            }
            else if (Input.GetKeyDown(KeyCode.F10))
            {
                tracking = !tracking;
                nextTrack = 0f;
                SetStatus("MapExporter: сбор объектов " + (tracking ? "ВКЛ" : "ВЫКЛ") + " (собрано: " + tracked.Count + ")", 6f);
            }

            if (tracking && Time.realtimeSinceStartup >= nextTrack)
            {
                nextTrack = Time.realtimeSinceStartup + TrackInterval;
                MeshFilter[] all = UnityEngine.Object.FindObjectsOfType<MeshFilter>();
                for (int i = 0; i < all.Length; i++)
                {
                    MeshFilter mf = all[i];
                    if (mf == null || !mf.gameObject.activeInHierarchy) continue;
                    int id = mf.GetInstanceID();
                    if (!tracked.ContainsKey(id)) tracked[id] = mf;
                }
            }
        }

        void OnGUI()
        {
            float now = Time.realtimeSinceStartup;
            float y = 10f;
            if (status.Length > 0 && (busy || now < statusUntil))
            {
                GUI.Label(new Rect(10, y, 1000, 25), status);
                y += 22f;
            }
            if (tracking)
            {
                GUI.Label(new Rect(10, y, 1000, 25), "MapExporter: сбор ВКЛ, объектов: " + tracked.Count);
                y += 22f;
            }
            if (now < hintUntil)
            {
                GUI.Label(new Rect(10, y, 1000, 25), "MapExporter: F8 пробный прогон | F9 выгрузка | F10 сбор объектов");
            }
        }

        private void SetStatus(string s, float seconds)
        {
            status = s;
            statusUntil = Time.realtimeSinceStartup + seconds;
        }

        private static string F(float v)
        {
            return v.ToString("0.#####", CultureInfo.InvariantCulture);
        }

        private static string Clean(string s)
        {
            if (string.IsNullOrEmpty(s)) return "unnamed";
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                if (char.IsLetterOrDigit(c) || c == '_' || c == '-') sb.Append(c);
                else sb.Append('_');
            }
            return sb.ToString();
        }

        private List<Entry> Collect()
        {
            List<Entry> list = new List<Entry>();
            Dictionary<int, bool> seenIds = new Dictionary<int, bool>();
            Dictionary<string, bool> seenSig = new Dictionary<string, bool>();
            dupeCount = 0;

            MeshFilter[] active = UnityEngine.Object.FindObjectsOfType<MeshFilter>();
            for (int i = 0; i < active.Length; i++)
            {
                Accept(active[i], true, seenIds, seenSig, list);
            }
            foreach (KeyValuePair<int, MeshFilter> kv in tracked)
            {
                Accept(kv.Value, false, seenIds, seenSig, list);
            }

            list.Sort(delegate (Entry a, Entry b)
            {
                int c = a.zx.CompareTo(b.zx);
                if (c != 0) return c;
                return a.zz.CompareTo(b.zz);
            });
            return list;
        }

        private void Accept(MeshFilter mf, bool requireActive, Dictionary<int, bool> seenIds,
                            Dictionary<string, bool> seenSig, List<Entry> list)
        {
            if (mf == null) return;
            if (mf.sharedMesh == null) return;
            Renderer r = mf.GetComponent<Renderer>();
            if (r == null) return;
            if (requireActive && (!mf.gameObject.activeInHierarchy || !r.enabled)) return;

            int id = mf.GetInstanceID();
            if (seenIds.ContainsKey(id)) return;
            seenIds[id] = true;

            Transform t = mf.transform;
            Vector3 p = t.position;
            Vector3 e = t.eulerAngles;
            Vector3 s = t.lossyScale;
            string sig = mf.sharedMesh.GetInstanceID() + "|"
                + Mathf.Round(p.x * 100f) + "," + Mathf.Round(p.y * 100f) + "," + Mathf.Round(p.z * 100f) + "|"
                + Mathf.Round(e.x * 10f) + "," + Mathf.Round(e.y * 10f) + "," + Mathf.Round(e.z * 10f) + "|"
                + Mathf.Round(s.x * 100f) + "," + Mathf.Round(s.y * 100f) + "," + Mathf.Round(s.z * 100f);
            if (seenSig.ContainsKey(sig))
            {
                dupeCount++;
                return;
            }
            seenSig[sig] = true;

            Entry en = new Entry();
            en.mf = mf;
            en.zx = Mathf.FloorToInt(p.x / ZoneSize);
            en.zz = Mathf.FloorToInt(p.z / ZoneSize);
            list.Add(en);
        }

        private IEnumerator DryRun()
        {
            busy = true;
            SetStatus("MapExporter: пробный прогон...", 5f);
            yield return null;

            List<Entry> list = Collect();
            long verts = 0;
            Dictionary<long, bool> zones = new Dictionary<long, bool>();
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].mf == null || list[i].mf.sharedMesh == null) continue;
                verts += list[i].mf.sharedMesh.vertexCount;
                zones[((long)list[i].zx << 32) ^ (long)(uint)list[i].zz] = true;
            }
            Terrain[] terrains = UnityEngine.Object.FindObjectsOfType<Terrain>();
            string tinfo = "";
            for (int i = 0; i < terrains.Length; i++)
            {
                if (terrains[i].terrainData != null)
                    tinfo += " [" + terrains[i].terrainData.heightmapResolution + "x" + terrains[i].terrainData.heightmapResolution + "]";
            }

            long mb = (verts * 60L) / (1024L * 1024L);
            string msg = "MapExporter ПРОБА: объектов " + list.Count + ", дублей пропущено " + dupeCount
                + ", вершин " + verts + " (~" + mb + " МБ), зон-файлов " + zones.Count
                + ", ландшафтов " + terrains.Length + tinfo;
            Debug.Log(msg);
            SetStatus(msg, 25f);
            busy = false;
        }

        private IEnumerator Export()
        {
            busy = true;
            SetStatus("MapExporter: начинаю выгрузку...", 5f);
            yield return null;

            outDir = Path.Combine(Path.GetDirectoryName(Application.dataPath), "MapExport");
            texDir = Path.Combine(outDir, "textures");
            Directory.CreateDirectory(outDir);
            Directory.CreateDirectory(texDir);

            texNames.Clear();
            matNames.Clear();
            mtl.Length = 0;
            skippedMeshes = 0;

            List<Entry> list = Collect();
            int exported = 0;
            int files = 0;

            StreamWriter w = null;
            int curZx = 0, curZz = 0;
            int vOff = 0, vtOff = 0, vnOff = 0;

            for (int i = 0; i < list.Count; i++)
            {
                if ((i % 100) == 0)
                {
                    SetStatus("MapExporter: объекты " + i + " / " + list.Count, 5f);
                    yield return null;
                }

                Entry en = list[i];
                if (en.mf == null || en.mf.sharedMesh == null) continue;

                if (w == null || en.zx != curZx || en.zz != curZz)
                {
                    if (w != null) w.Close();
                    curZx = en.zx;
                    curZz = en.zz;
                    vOff = 0; vtOff = 0; vnOff = 0;
                    w = new StreamWriter(Path.Combine(outDir, "objects_x" + curZx + "_z" + curZz + ".obj"), false, new UTF8Encoding(false));
                    w.WriteLine("# My Summer Car objects, zone " + curZx + "," + curZz);
                    w.WriteLine("mtllib map.mtl");
                    files++;
                }

                if (WriteMesh(w, en.mf, i, ref vOff, ref vtOff, ref vnOff)) exported++;
            }
            if (w != null) w.Close();

            File.WriteAllText(Path.Combine(outDir, "map.mtl"), mtl.ToString());

            Terrain[] terrains = UnityEngine.Object.FindObjectsOfType<Terrain>();
            int terrainsDone = 0;
            for (int ti = 0; ti < terrains.Length; ti++)
            {
                if (terrains[ti] == null || terrains[ti].terrainData == null) continue;
                yield return StartCoroutine(ExportTerrain(terrains[ti], ti));
                terrainsDone++;
            }

            string msg = "MapExporter: ГОТОВО. Объектов: " + exported + " в " + files + " файлах, дублей пропущено: " + dupeCount
                + ", пустых мешей: " + skippedMeshes + ", ландшафтов: " + terrainsDone + ". Папка: " + outDir;
            Debug.Log(msg);
            SetStatus(msg, 30f);
            busy = false;
        }

        private bool WriteMesh(StreamWriter w, MeshFilter mf, int idx, ref int vOff, ref int vtOff, ref int vnOff)
        {
            Mesh mesh = mf.sharedMesh;
            Renderer r = mf.GetComponent<Renderer>();
            Vector3[] verts = mesh.vertices;
            if (verts.Length == 0) { skippedMeshes++; return false; }
            Vector3[] norms = mesh.normals;
            Vector2[] uvs = mesh.uv;
            bool hasN = norms != null && norms.Length == verts.Length;
            bool hasUV = uvs != null && uvs.Length == verts.Length;
            Transform t = mf.transform;

            w.WriteLine("o " + Clean(mf.gameObject.name) + "_" + idx);

            for (int k = 0; k < verts.Length; k++)
            {
                Vector3 p = t.TransformPoint(verts[k]);
                w.WriteLine("v " + F(-p.x) + " " + F(p.y) + " " + F(p.z));
            }
            if (hasN)
            {
                for (int k = 0; k < norms.Length; k++)
                {
                    Vector3 n = t.TransformDirection(norms[k]).normalized;
                    w.WriteLine("vn " + F(-n.x) + " " + F(n.y) + " " + F(n.z));
                }
            }

            Material[] mats = r != null ? r.sharedMaterials : null;
            for (int s = 0; s < mesh.subMeshCount; s++)
            {
                Material m = null;
                if (mats != null && mats.Length > 0) m = mats[Mathf.Min(s, mats.Length - 1)];

                Vector2 sc = Vector2.one;
                Vector2 off = Vector2.zero;
                if (m != null && m.HasProperty("_MainTex"))
                {
                    sc = m.mainTextureScale;
                    off = m.mainTextureOffset;
                }

                int vtBase = vtOff + 1;
                if (hasUV)
                {
                    for (int k = 0; k < uvs.Length; k++)
                    {
                        w.WriteLine("vt " + F(uvs[k].x * sc.x + off.x) + " " + F(uvs[k].y * sc.y + off.y));
                    }
                    vtOff += uvs.Length;
                }

                w.WriteLine("usemtl " + GetMaterialName(m));

                int[] tris = mesh.GetTriangles(s);
                for (int k = 0; k + 2 < tris.Length; k += 3)
                {
                    int a = tris[k];
                    int b = tris[k + 1];
                    int c = tris[k + 2];
                    w.WriteLine("f " + Idx(a, vOff, vtBase, vnOff, hasUV, hasN) + " "
                                     + Idx(c, vOff, vtBase, vnOff, hasUV, hasN) + " "
                                     + Idx(b, vOff, vtBase, vnOff, hasUV, hasN));
                }
            }

            vOff += verts.Length;
            if (hasN) vnOff += norms.Length;
            return true;
        }

        private static string Idx(int i, int vOff, int vtBase, int vnOff, bool hasUV, bool hasN)
        {
            string v = (vOff + i + 1).ToString();
            if (hasUV && hasN) return v + "/" + (vtBase + i) + "/" + (vnOff + i + 1);
            if (hasUV) return v + "/" + (vtBase + i);
            if (hasN) return v + "//" + (vnOff + i + 1);
            return v;
        }

        private IEnumerator ExportTerrain(Terrain terrain, int ti)
        {
            TerrainData td = terrain.terrainData;
            int res = td.heightmapResolution;
            float[,] hgt = td.GetHeights(0, 0, res, res);
            Vector3 size = td.size;
            Vector3 pos = terrain.transform.position;
            string prefix = "terrain_" + ti;

            SetStatus("MapExporter: ландшафт " + ti + " - карта высот", 5f);
            yield return null;

            byte[] raw = new byte[res * res * 2];
            int bi = 0;
            for (int z = 0; z < res; z++)
            {
                for (int x = 0; x < res; x++)
                {
                    int v = Mathf.Clamp(Mathf.RoundToInt(hgt[z, x] * 65535f), 0, 65535);
                    raw[bi++] = (byte)(v & 0xFF);
                    raw[bi++] = (byte)(v >> 8);
                }
            }
            File.WriteAllBytes(Path.Combine(outDir, prefix + "_height_" + res + "x" + res + "_r16.raw"), raw);
            File.WriteAllText(Path.Combine(outDir, prefix + "_info.txt"),
                "position: " + F(pos.x) + " " + F(pos.y) + " " + F(pos.z) + "\n"
                + "size (x y z): " + F(size.x) + " " + F(size.y) + " " + F(size.z) + "\n"
                + "heightmap resolution: " + res + "\n"
                + "format: 16-bit unsigned, little-endian, row 0 = z min\n");

            string colorFile = null;
            yield return StartCoroutine(BakeTerrainTexture(td, size, prefix));
            string bakedName = prefix + "_color.png";
            if (File.Exists(Path.Combine(outDir, bakedName))) colorFile = bakedName;

            int step = Mathf.Max(1, (res - 1) / (TerrainMaxGrid - 1));
            int n = ((res - 1) / step) + 1;

            SetStatus("MapExporter: ландшафт " + ti + " - сетка " + n + "x" + n, 5f);
            yield return null;

            using (StreamWriter w = new StreamWriter(Path.Combine(outDir, prefix + ".obj"), false, new UTF8Encoding(false)))
            {
                w.WriteLine("# My Summer Car terrain " + ti);
                w.WriteLine("mtllib " + prefix + ".mtl");
                w.WriteLine("o Terrain_" + ti);

                for (int zi = 0; zi < n; zi++)
                {
                    for (int xi = 0; xi < n; xi++)
                    {
                        int hx = Mathf.Min(xi * step, res - 1);
                        int hz = Mathf.Min(zi * step, res - 1);
                        float x = pos.x + (hx / (float)(res - 1)) * size.x;
                        float z = pos.z + (hz / (float)(res - 1)) * size.z;
                        float y = pos.y + hgt[hz, hx] * size.y;
                        w.WriteLine("v " + F(-x) + " " + F(y) + " " + F(z));
                    }
                }
                for (int zi = 0; zi < n; zi++)
                {
                    for (int xi = 0; xi < n; xi++)
                    {
                        float u = Mathf.Min(xi * step, res - 1) / (float)(res - 1);
                        float v = Mathf.Min(zi * step, res - 1) / (float)(res - 1);
                        w.WriteLine("vt " + F(u) + " " + F(v));
                    }
                }

                w.WriteLine("usemtl terrain");
                for (int zi = 0; zi < n - 1; zi++)
                {
                    for (int xi = 0; xi < n - 1; xi++)
                    {
                        int a = zi * n + xi + 1;
                        int b = (zi + 1) * n + xi + 1;
                        int c = zi * n + (xi + 1) + 1;
                        int d = (zi + 1) * n + (xi + 1) + 1;
                        w.WriteLine("f " + a + "/" + a + " " + c + "/" + c + " " + b + "/" + b);
                        w.WriteLine("f " + c + "/" + c + " " + d + "/" + d + " " + b + "/" + b);
                    }
                }
            }

            StringBuilder tm = new StringBuilder();
            tm.AppendLine("newmtl terrain");
            tm.AppendLine("Kd 1 1 1");
            if (colorFile != null) tm.AppendLine("map_Kd " + colorFile);
            File.WriteAllText(Path.Combine(outDir, prefix + ".mtl"), tm.ToString());
        }

        private IEnumerator BakeTerrainTexture(TerrainData td, Vector3 size, string prefix)
        {
            SplatPrototype[] protos = td.splatPrototypes;
            if (protos == null || protos.Length == 0) yield break;

            int layers = protos.Length;
            int aw = td.alphamapWidth;
            int ah = td.alphamapHeight;
            float[,,] alpha = td.GetAlphamaps(0, 0, aw, ah);

            Texture2D[] tex = new Texture2D[layers];
            Vector2[] tile = new Vector2[layers];
            for (int i = 0; i < layers; i++)
            {
                tex[i] = ReadableCopy(protos[i].texture, 512);
                tile[i] = protos[i].tileSize;
                if (tile[i].x < 0.01f) tile[i].x = 1f;
                if (tile[i].y < 0.01f) tile[i].y = 1f;
            }

            int S = TerrainTexSize;
            Color32[] pixels = new Color32[S * S];
            for (int py = 0; py < S; py++)
            {
                if ((py % 32) == 0)
                {
                    SetStatus("MapExporter: текстура ландшафта " + (py * 100 / S) + "%", 5f);
                    yield return null;
                }

                float v = (py + 0.5f) / S;
                int ay = Mathf.Min(ah - 1, (int)(v * ah));
                for (int px = 0; px < S; px++)
                {
                    float u = (px + 0.5f) / S;
                    int ax = Mathf.Min(aw - 1, (int)(u * aw));

                    float r = 0f, g = 0f, b = 0f, sum = 0f;
                    for (int l = 0; l < layers; l++)
                    {
                        float a = alpha[ay, ax, l];
                        if (a < 0.003f || tex[l] == null) continue;
                        Color c = tex[l].GetPixelBilinear(u * size.x / tile[l].x, v * size.z / tile[l].y);
                        r += c.r * a;
                        g += c.g * a;
                        b += c.b * a;
                        sum += a;
                    }
                    if (sum > 0.001f) { r /= sum; g /= sum; b /= sum; }
                    else { r = 0.4f; g = 0.45f; b = 0.3f; }
                    pixels[py * S + px] = new Color32((byte)(r * 255f), (byte)(g * 255f), (byte)(b * 255f), 255);
                }
            }

            Texture2D outTex = new Texture2D(S, S, TextureFormat.ARGB32, false);
            outTex.SetPixels32(pixels);
            outTex.Apply();
            File.WriteAllBytes(Path.Combine(outDir, prefix + "_color.png"), outTex.EncodeToPNG());
            UnityEngine.Object.Destroy(outTex);
            for (int i = 0; i < layers; i++)
            {
                if (tex[i] != null) UnityEngine.Object.Destroy(tex[i]);
            }
        }

        private string GetMaterialName(Material m)
        {
            if (m == null) return "default";
            int id = m.GetInstanceID();
            string name;
            if (matNames.TryGetValue(id, out name)) return name;

            name = "mat_" + Clean(m.name) + "_" + Mathf.Abs(id);
            matNames[id] = name;

            mtl.AppendLine("newmtl " + name);
            if (m.HasProperty("_Color"))
            {
                Color c = m.color;
                mtl.AppendLine("Kd " + F(c.r) + " " + F(c.g) + " " + F(c.b));
            }
            else
            {
                mtl.AppendLine("Kd 0.8 0.8 0.8");
            }
            if (m.HasProperty("_MainTex") && m.mainTexture != null)
            {
                string file = ExportTexture(m.mainTexture);
                if (file != null) mtl.AppendLine("map_Kd " + file);
            }
            mtl.AppendLine();
            return name;
        }

        private Texture2D ReadableCopy(Texture tex, int maxSize)
        {
            if (tex == null) return null;
            try
            {
                int tw = tex.width;
                int th = tex.height;
                while (tw > maxSize || th > maxSize)
                {
                    tw /= 2;
                    th /= 2;
                }
                if (tw < 1) tw = 1;
                if (th < 1) th = 1;

                RenderTexture rt = RenderTexture.GetTemporary(tw, th, 0, RenderTextureFormat.ARGB32);
                Graphics.Blit(tex, rt);
                RenderTexture prev = RenderTexture.active;
                RenderTexture.active = rt;
                Texture2D copy = new Texture2D(tw, th, TextureFormat.ARGB32, false);
                copy.ReadPixels(new Rect(0, 0, tw, th), 0, 0);
                copy.Apply();
                copy.wrapMode = TextureWrapMode.Repeat;
                RenderTexture.active = prev;
                RenderTexture.ReleaseTemporary(rt);
                return copy;
            }
            catch (Exception e)
            {
                Debug.Log("MapExporter: не удалось скопировать текстуру " + tex.name + ": " + e.Message);
                return null;
            }
        }

        private string ExportTexture(Texture tex)
        {
            if (tex == null) return null;
            int id = tex.GetInstanceID();
            string existing;
            if (texNames.TryGetValue(id, out existing)) return existing;

            string file = null;
            Texture2D copy = ReadableCopy(tex, MaxTextureSize);
            if (copy != null)
            {
                try
                {
                    string name = Clean(tex.name) + "_" + Mathf.Abs(id) + ".png";
                    File.WriteAllBytes(Path.Combine(texDir, name), copy.EncodeToPNG());
                    file = "textures/" + name;
                }
                catch (Exception e)
                {
                    Debug.Log("MapExporter: не удалось сохранить текстуру " + tex.name + ": " + e.Message);
                }
                UnityEngine.Object.Destroy(copy);
            }
            texNames[id] = file;
            return file;
        }
    }
}