338 lines
15 KiB
C#
338 lines
15 KiB
C#
using System.Collections.Generic;
|
||
using BrewMonster.Scripts.Chat.EmotionData;
|
||
using TMPro;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
namespace BrewMonster.Scripts.Editor
|
||
{
|
||
/// <summary>
|
||
/// Tool: atlas + txt → Sprite Mode Multiple (một PNG/bộ) + SO; batch linh hoạt số slot.
|
||
/// </summary>
|
||
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<EmotionBatchSlot> _batchSlots = new List<EmotionBatchSlot>();
|
||
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<EmotionAtlasConverterWindow>(true, "Emotion Atlas Converter", true);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (_batchSlots == null)
|
||
_batchSlots = new List<EmotionBatchSlot>();
|
||
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: <Sprite Assets folder>/Emotions {N}.asset + Emotions {N} Material.mat\n" +
|
||
"Tên sub-sprite khớp với atlas: cell_0000 … (dùng cho <sprite name=\"cell_XXXX\"> trong TMP).\n" +
|
||
"Sub-sprite names match atlas: cell_0000 … (use <sprite name=\"cell_XXXX\"> 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<EmotionSetDataSO>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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<EmotionLibrarySO>();
|
||
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);
|
||
}
|
||
}
|
||
}
|