using System.Collections.Generic; using BrewMonster.Scripts.Chat.EmotionData; using TMPro; using UnityEditor; using UnityEngine; namespace BrewMonster.Scripts.Editor { /// /// Tool: atlas + txt → Sprite Mode Multiple (một PNG/bộ) + SO; batch linh hoạt số slot. /// public class EmotionAtlasConverterWindow : EditorWindow { private Texture2D _sourceAtlas; private TextAsset _txtAsset; private int _emotionSetIndex; private int _cellW = 32; private int _cellH = 32; private string _outputFolder = "Assets/PerfectWorld/UI/Chat/GeneratedEmotions"; private bool _sliceInPlace; private List _batchSlots = new List(); private Vector2 _batchScroll; private string _libraryAssetPath = "Assets/PerfectWorld/UI/Chat/GeneratedEmotions/EmotionLibrary.asset"; // ── TMP Sprite Asset ────────────────────────────────────────────────── private bool _genTmpOnConvert = true; private string _tmpSpriteFolder = "Assets/TextMesh Pro/Resources/Sprite Assets"; private float _tmpCellSizePt = 0f; // 0 = lấy từ cellH private bool _showTmpSection = true; // Snapshot cuối cùng được convert (cho nút gen TMP standalone) // Last converted snapshot (for standalone TMP gen button) private EmotionSetSnapshot _lastSnapshot; [MenuItem("Tools/Perfect World/ChatSystem/Emotion Atlas Converter…")] public static void Open() { GetWindow(true, "Emotion Atlas Converter", true); } private void OnEnable() { if (_batchSlots == null) _batchSlots = new List(); if (_batchSlots.Count == 0) { for (int i = 0; i < 8; i++) _batchSlots.Add(new EmotionBatchSlot { EmotionSetIndex = i }); } } private void OnGUI() { EditorGUILayout.LabelField("Chung / Shared", EditorStyles.boldLabel); _cellW = EditorGUILayout.IntField("Cell width (W)", _cellW); _cellH = EditorGUILayout.IntField("Cell height (H)", _cellH); _outputFolder = EditorGUILayout.TextField("Output folder", _outputFolder); _sliceInPlace = EditorGUILayout.ToggleLeft( "Slice tại asset nguồn (ghi đè import Multiple lên atlas đang chọn) — Slice in-place on source asset", _sliceInPlace); if (_sliceInPlace) { EditorGUILayout.HelpBox( "Cảnh báo: Unity sẽ đổi import của file atlas gốc thành Sprite Multiple full lưới.\n" + "Warning: overwrites source texture import settings.", MessageType.Warning); } else { EditorGUILayout.HelpBox( "Mặc định: copy atlas vào Output/Emotions{N}/Emotions{N}_atlas.png rồi Multiple slice (giữ nguyên file nguồn).\n" + "Default: copy atlas to output folder then slice (source unchanged).", MessageType.None); } EditorGUILayout.Space(8f); EditorGUILayout.LabelField("Một bộ / Single set", EditorStyles.boldLabel); _sourceAtlas = (Texture2D)EditorGUILayout.ObjectField("Atlas", _sourceAtlas, typeof(Texture2D), false); _txtAsset = (TextAsset)EditorGUILayout.ObjectField("EmotionsN.txt", _txtAsset, typeof(TextAsset), false); _emotionSetIndex = EditorGUILayout.IntField("Emotion set index (N)", _emotionSetIndex); EditorGUILayout.Space(); if (GUILayout.Button("Convert → EmotionSetData + Atlas (Multiple)")) { RunConvertSingle(); } EditorGUILayout.Space(12f); EditorGUILayout.LabelField("Batch → EmotionLibrary", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("+ Thêm slot", GUILayout.Width(100))) _batchSlots.Add(new EmotionBatchSlot { EmotionSetIndex = NextSuggestedSetIndex() }); if (GUILayout.Button("− Xóa slot cuối", GUILayout.Width(120)) && _batchSlots.Count > 0) _batchSlots.RemoveAt(_batchSlots.Count - 1); EditorGUILayout.LabelField($"Số slot: {_batchSlots.Count}"); EditorGUILayout.EndHorizontal(); _batchScroll = EditorGUILayout.BeginScrollView(_batchScroll, GUILayout.MinHeight(160f)); for (int i = 0; i < _batchSlots.Count; i++) { var slot = _batchSlots[i]; EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"#{i}", GUILayout.Width(24)); slot.EmotionSetIndex = EditorGUILayout.IntField("Set N", slot.EmotionSetIndex, GUILayout.Width(120)); slot.Atlas = (Texture2D)EditorGUILayout.ObjectField(slot.Atlas, typeof(Texture2D), false); slot.Txt = (TextAsset)EditorGUILayout.ObjectField(slot.Txt, typeof(TextAsset), false); if (GUILayout.Button("×", GUILayout.Width(22))) { _batchSlots.RemoveAt(i); i--; EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); continue; } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } EditorGUILayout.EndScrollView(); _libraryAssetPath = EditorGUILayout.TextField("Library .asset path", _libraryAssetPath); EditorGUILayout.Space(); if (GUILayout.Button("Convert batch → EmotionLibrary.asset")) { RunConvertLibrary(); } EditorGUILayout.Space(12f); DrawTMPSection(); EditorGUILayout.Space(8f); EditorGUILayout.HelpBox( "Mỗi bộ: một texture **Sprite Multiple**, tên sub-sprite `cell_0000` … theo chỉ số ô (giống C++).\n" + "Each set: one **Sprite Multiple** texture; sub-sprite names `cell_0000` … by cell index.", MessageType.Info); } // ───────────────────────────────────────────────────────────────────── // TMP Sprite Asset section // ───────────────────────────────────────────────────────────────────── private void DrawTMPSection() { _showTmpSection = EditorGUILayout.Foldout(_showTmpSection, "TMP Sprite Asset", true, EditorStyles.foldoutHeader); if (!_showTmpSection) return; EditorGUI.indentLevel++; _tmpSpriteFolder = EditorGUILayout.TextField( new GUIContent("Sprite Assets folder", "Thư mục lưu TMP_SpriteAsset (.asset). Mặc định: Assets/TextMesh Pro/Resources/Sprite Assets\n" + "Folder for TMP_SpriteAsset files. Default: Assets/TextMesh Pro/Resources/Sprite Assets"), _tmpSpriteFolder); _tmpCellSizePt = EditorGUILayout.FloatField( new GUIContent("Cell size (pt)", "Chiều cao glyph theo đơn vị TMP point. 0 = dùng Cell height (pixel).\n" + "Glyph height in TMP points. 0 = use Cell height in pixels."), _tmpCellSizePt); _genTmpOnConvert = EditorGUILayout.ToggleLeft( "Tự động gen TMP Sprite Asset sau mỗi lần Convert — Auto-generate TMP Sprite Asset after Convert", _genTmpOnConvert); EditorGUILayout.HelpBox( "Output: /Emotions {N}.asset + Emotions {N} Material.mat\n" + "Tên sub-sprite khớp với atlas: cell_0000 … (dùng cho trong TMP).\n" + "Sub-sprite names match atlas: cell_0000 … (use in TMP).", MessageType.Info); EditorGUILayout.Space(4f); // Nút gen standalone từ snapshot cuối — Standalone gen button from last snapshot EditorGUI.BeginDisabledGroup(_lastSnapshot == null); if (GUILayout.Button("Gen TMP Sprite Asset từ lần Convert cuối — from last Convert")) RunGenTMPSingle(_lastSnapshot); EditorGUI.EndDisabledGroup(); if (_lastSnapshot == null) EditorGUILayout.HelpBox("Chạy Convert single ít nhất một lần để kích hoạt nút này.\nRun Convert (single) once to enable this button.", MessageType.None); EditorGUI.indentLevel--; } private int NextSuggestedSetIndex() { int max = -1; foreach (var s in _batchSlots) { if (s.EmotionSetIndex > max) max = s.EmotionSetIndex; } return max + 1; } private void RunConvertSingle() { if (!EmotionAtlasConverterCore.ConvertOneSet( _sourceAtlas, _txtAsset, _emotionSetIndex, _cellW, _cellH, _outputFolder, _sliceInPlace, out var snapshot, out string err)) { EditorUtility.DisplayDialog("Error", err, "OK"); return; } _lastSnapshot = snapshot; var so = ScriptableObject.CreateInstance(); EmotionAtlasConverterCore.ApplySnapshotToSetSO(so, snapshot); string setFolder = $"{_outputFolder}/Emotions{_emotionSetIndex}"; string soPath = $"{setFolder}/EmotionSetData_{_emotionSetIndex}.asset"; AssetDatabase.CreateAsset(so, soPath); AssetDatabase.SaveAssets(); string msg = $"Đã tạo:\n{soPath}\nAtlas (Multiple) trong thư mục set."; if (_genTmpOnConvert) { TMP_SpriteAsset tmpAsset = RunGenTMPSingle(snapshot); if (tmpAsset != null) msg += $"\nTMP Sprite Asset: {AssetDatabase.GetAssetPath(tmpAsset)}"; } EditorUtility.DisplayDialog("Done", msg, "OK"); EditorGUIUtility.PingObject(so); } /// /// Gen TMP_SpriteAsset cho một snapshot. Trả về asset nếu thành công, null nếu thất bại. /// Generate TMP_SpriteAsset for one snapshot. Returns asset on success, null on failure. /// private TMP_SpriteAsset RunGenTMPSingle(EmotionSetSnapshot snapshot) { if (snapshot == null) return null; string outPath = EmotionAtlasConverterCore.DefaultTMPSpriteAssetPath(_tmpSpriteFolder, snapshot.EmotionSetIndex); if (!EmotionAtlasConverterCore.CreateTMPSpriteAsset(snapshot, outPath, _tmpCellSizePt, out var tmpAsset, out string err)) { EditorUtility.DisplayDialog("TMP Gen Error", $"Set {snapshot.EmotionSetIndex}: {err}", "OK"); return null; } EditorGUIUtility.PingObject(tmpAsset); return tmpAsset; } private void RunConvertLibrary() { if (!EmotionAtlasConverterCore.TryValidateBatchIndices(_batchSlots, out string dupErr)) { EditorUtility.DisplayDialog("Error", dupErr, "OK"); return; } var library = ScriptableObject.CreateInstance(); library.Sets.Clear(); int ok = 0; int total = _batchSlots.Count; for (int i = 0; i < total; i++) { var slot = _batchSlots[i]; if (slot.Atlas == null && slot.Txt == null) continue; if (slot.Atlas == null || slot.Txt == null) { EditorUtility.DisplayDialog("Error", $"Slot #{i} (Set N={slot.EmotionSetIndex}): cần cả Atlas và TXT.", "OK"); Object.DestroyImmediate(library); return; } EditorUtility.DisplayProgressBar("Emotion Library", $"Set {slot.EmotionSetIndex}…", (float)i / Mathf.Max(1, total)); if (!EmotionAtlasConverterCore.ConvertOneSet( slot.Atlas, slot.Txt, slot.EmotionSetIndex, _cellW, _cellH, _outputFolder, _sliceInPlace, out var snapshot, out string err)) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("Error", $"Set {slot.EmotionSetIndex}: {err}", "OK"); Object.DestroyImmediate(library); return; } library.Sets.Add(snapshot); ok++; } EditorUtility.ClearProgressBar(); if (ok == 0) { EditorUtility.DisplayDialog("Error", "Không có cặp Atlas+TXT hợp lệ.", "OK"); Object.DestroyImmediate(library); return; } string dir = System.IO.Path.GetDirectoryName(_libraryAssetPath)?.Replace('\\', '/') ?? _outputFolder; if (!string.IsNullOrEmpty(dir)) EmotionAtlasConverterCore.EnsureFolder(dir); AssetDatabase.CreateAsset(library, _libraryAssetPath); AssetDatabase.SaveAssets(); int tmpOk = 0; if (_genTmpOnConvert) { for (int i = 0; i < library.Sets.Count; i++) { var snap = library.Sets[i]; EditorUtility.DisplayProgressBar("TMP Sprite Assets", $"Set {snap.EmotionSetIndex}…", (float)i / Mathf.Max(1, library.Sets.Count)); string outPath = EmotionAtlasConverterCore.DefaultTMPSpriteAssetPath(_tmpSpriteFolder, snap.EmotionSetIndex); if (EmotionAtlasConverterCore.CreateTMPSpriteAsset(snap, outPath, _tmpCellSizePt, out _, out string tmpErr)) tmpOk++; else Debug.LogWarning($"[EmotionConverter] TMP gen Set {snap.EmotionSetIndex}: {tmpErr}"); } EditorUtility.ClearProgressBar(); } string doneMsg = $"EmotionLibrary: {ok} bộ.\n{_libraryAssetPath}"; if (_genTmpOnConvert) doneMsg += $"\nTMP Sprite Assets: {tmpOk}/{ok} bộ → {_tmpSpriteFolder}"; EditorUtility.DisplayDialog("Done", doneMsg, "OK"); EditorGUIUtility.PingObject(library); } } }