using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; namespace TKS.EditorTools { /// /// UI 테마 임포트(PD #1100) — Assets/Data/UI/ui_theme.csv(SOT) → Assets/Resources/UI/UITheme.asset. /// 경로 = 팩 Components 기준 상대 경로(Assets/ 로 시작하면 그대로) · 오류가 있으면 자산을 바꾸지 않는다. /// 신규 제작 납품(Assets/UI/Samguk/**/<id>.png)은 같은 id 의 스킨·아이콘을 치환하고 표에 없는 id 는 아이콘으로 추가한다(코드 변경 없이 교체). /// public static class UIThemeImporter { public const string CsvPath = "Assets/Data/UI/ui_theme.csv"; public const string AssetPath = "Assets/Resources/UI/UITheme.asset"; public const string PackRoot = "Assets/UI/GUIPro/ResourcesData/Sprites/Components/"; public const string SamgukRoot = "Assets/UI/Samguk"; [MenuItem("TKS/Import UI Theme (UI 테마 임포트)")] public static void Menu() { Import(); } public static bool Import() { var errors = new List(); var theme = ScriptableObject.CreateInstance(); if (!File.Exists(CsvPath)) { Debug.LogError("[TKS] UI 테마 표 없음: " + CsvPath); return false; } var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n'); for (int n = 1; n < lines.Length; n++) { var line = lines[n]; if (string.IsNullOrWhiteSpace(line)) continue; var f = line.Split(','); if (f.Length < 8) { errors.Add((n + 1) + "행: 열 부족"); continue; } string kind = f[0].Trim(), id = f[1].Trim(), path = f[2].Trim(), overlay = f[3].Trim(), color = f[7].Trim(); switch (kind) { case "color": { Color c; if (!ColorUtility.TryParseHtmlString(color, out c)) errors.Add((n + 1) + "행 " + id + ": 색 형식 " + color); else theme.colors.Add(new UITheme.ColorEntry { id = id, color = c }); break; } case "skin": { var s = new UITheme.SkinEntry { id = id, pack = LoadSprite(path, n, errors) }; if (overlay.Length > 0) s.overlay = LoadSprite(overlay, n, errors); s.ppu = ParseF(f[4], 1f); s.inset = ParseInset(f[5]); s.overlayAlpha = ParseF(f[6], 1f); theme.skins.Add(s); break; } case "icon": { var e = new UITheme.IconEntry { id = id, pack = LoadSprite(path, n, errors) }; Color c; if (color.Length > 0 && ColorUtility.TryParseHtmlString(color, out c)) e.tint = c; theme.icons.Add(e); break; } case "font": if (id == "title") { theme.titleFont = AssetDatabase.LoadAssetAtPath(path); if (theme.titleFont == null) errors.Add((n + 1) + "행: 폰트 없음 " + path); } break; default: errors.Add((n + 1) + "행: 알 수 없는 kind " + kind); break; } } if (errors.Count > 0) { foreach (var e in errors) Debug.LogError("[TKS] UI 테마 표 오류 · " + e); Debug.LogError("[TKS] UI 테마 임포트 중단(오류 " + errors.Count + ") · 자산 변경 없음"); Object.DestroyImmediate(theme); return false; } // 신규 제작 치환(파일 이름 = id) var samguk = SamgukSprites(); int replaced = 0, added = 0; var used = new HashSet(); foreach (var s in theme.skins) { Sprite sp; if (samguk.TryGetValue(s.id, out sp)) { s.samguk = sp; used.Add(s.id); replaced++; } } foreach (var e in theme.icons) { Sprite sp; if (samguk.TryGetValue(e.id, out sp)) { e.samguk = sp; used.Add(e.id); replaced++; } } foreach (var kv in samguk) { if (used.Contains(kv.Key)) continue; theme.icons.Add(new UITheme.IconEntry { id = kv.Key, samguk = kv.Value }); added++; } if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources"); if (!AssetDatabase.IsValidFolder("Assets/Resources/UI")) AssetDatabase.CreateFolder("Assets/Resources", "UI"); var asset = AssetDatabase.LoadAssetAtPath(AssetPath); if (asset == null) { AssetDatabase.CreateAsset(theme, AssetPath); asset = theme; } else { EditorUtility.CopySerialized(theme, asset); Object.DestroyImmediate(theme); } asset.Rebuild(); EditorUtility.SetDirty(asset); AssetDatabase.SaveAssets(); UITheme.Use(asset); Debug.Log("[TKS] UI 테마 임포트 · 색 " + asset.colors.Count + " · 스킨 " + asset.skins.Count + " · 아이콘 " + asset.icons.Count + " · 신규 제작 치환 " + replaced + " · 추가 " + added + " → " + AssetPath); // 테마가 쓰는 스프라이트 = UI 아틀라스 구성(PD 2026-11-10 · 새 스프라이트도 자동으로 묶임 · 구성이 같으면 그대로) UIAtlasBuilder.Build(asset); return true; } /// Assets/UI/Samguk/**/<id>.png → 스프라이트(같은 id 가 둘이면 경로 순 마지막). public static Dictionary SamgukSprites() { var map = new Dictionary(); if (!AssetDatabase.IsValidFolder(SamgukRoot)) return map; var guids = AssetDatabase.FindAssets("t:Sprite", new[] { SamgukRoot }); var paths = new List(); foreach (var g in guids) paths.Add(AssetDatabase.GUIDToAssetPath(g)); paths.Sort(System.StringComparer.Ordinal); foreach (var p in paths) { var sp = AssetDatabase.LoadAssetAtPath(p); if (sp != null) map[Path.GetFileNameWithoutExtension(p)] = sp; } return map; } static Sprite LoadSprite(string path, int n, List errors) { if (string.IsNullOrEmpty(path)) { errors.Add((n + 1) + "행: 경로 없음"); return null; } string full = path.StartsWith("Assets/") ? path : PackRoot + path; var sp = AssetDatabase.LoadAssetAtPath(full); if (sp == null) errors.Add((n + 1) + "행: 스프라이트 없음 " + full); return sp; } static float ParseF(string s, float def) { float v; return float.TryParse(s.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) ? v : def; } static Vector4 ParseInset(string s) { s = s.Trim(); if (s.Length == 0) return Vector4.zero; var p = s.Split(';'); if (p.Length == 1) { float v = ParseF(p[0], 0f); return new Vector4(v, v, v, v); } return new Vector4(ParseF(p[0], 0f), p.Length > 1 ? ParseF(p[1], 0f) : 0f, p.Length > 2 ? ParseF(p[2], 0f) : 0f, p.Length > 3 ? ParseF(p[3], 0f) : 0f); } } /// 신규 제작 UI(Assets/UI/Samguk) · 로비 배경(Assets/UI/Lobby) 임포트 설정 = Sprite(2D) · 납품 파일이 들어오면 테마 치환 테이블 자동 갱신. public class UIThemeAssetPostprocessor : AssetPostprocessor { static bool pending; static bool Ours(string path) { return path.StartsWith(UIThemeImporter.SamgukRoot + "/") || path.StartsWith("Assets/UI/Lobby/") || path.StartsWith("Assets/UI/Symbols/"); // Symbols = 병과 단색 심볼(Tools/class_symbols.py · PD 2026-11-09) } void OnPreprocessTexture() { if (!Ours(assetPath)) return; var ti = (TextureImporter)assetImporter; ti.textureType = TextureImporterType.Sprite; ti.spriteImportMode = SpriteImportMode.Single; ti.alphaIsTransparency = true; ti.mipmapEnabled = false; ti.spritePixelsPerUnit = 100f; ti.maxTextureSize = assetPath.StartsWith("Assets/UI/Lobby/") ? 2048 : 1024; } static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom) { bool hit = false; foreach (var p in imported) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png"); foreach (var p in deleted) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png"); if (!hit || pending) return; pending = true; EditorApplication.delayCall += () => { pending = false; UIThemeImporter.Import(); }; } } }