using UnityEngine;
namespace BrewMonster.Scripts.Chat.EmotionData
{
///
/// Ánh xạ (emotionSet, emotionIndex) từ sang tag TMP.
/// Maps (emotionSet, emotionIndex) from EmotionLibrarySO to TMP rich-text tag.
///
/// Atlas được cắt trái→phải, trên→dưới, sub-sprite đặt tên cell_XXXX.
/// Số XXXX trong tên chính là index tuyến tính → không cần tra spriteCharacterTable.
/// Atlas is sliced left-to-right, top-to-bottom, named cell_XXXX.
/// The number XXXX in the name IS the linear index → no spriteCharacterTable lookup needed.
///
/// Cách dùng:
/// 1) Tạo EmotionLibrarySO bằng Emotion Atlas Converter.
/// 2) Gán TMP_SpriteAsset cho từng set vào EmotionSetSnapshot.TmpSpriteAsset trong Library
/// (chỉ cần cho emoji động nhiều frame hoặc khi PreferSpriteNameTag = false).
/// OnValidate / SetEmotionSpriteMap đồng bộ TmpSpriteAssetName từ asset — English: sync cached name so network thread never calls GetName().
/// 3) Kéo SO này vào field trên GameObject có CECUIManager (Awake gọi SetEmotionSpriteMap).
///
[CreateAssetMenu(fileName = "EmotionLibrarySpriteMap", menuName = "Perfect World/Chat/Emotion Library Sprite Map", order = 2)]
public class EmotionLibrarySpriteMap : ScriptableObject, IEmotionSpriteMap
{
[Tooltip("Dữ liệu emotion đã convert. Emotion data converted by the atlas tool.")]
public EmotionLibrarySO Library;
private void OnValidate()
{
EnsureCachedTmpSpriteAssetNames();
}
///
/// 主线程:把 Library 里各 TmpSpriteAsset.name 写入快照字段,供 TryGetSprite 在任意线程读取。
/// Main thread: copy TmpSpriteAsset.name into snapshot fields so TryGetSprite can run off the main thread.
///
public void EnsureCachedTmpSpriteAssetNames()
{
Library?.SyncCachedTmpSpriteAssetNamesFromObjects();
}
[Tooltip("Nếu true và emoji chỉ 1 frame: dùng thay vì . " +
"If true and single-frame: use instead of .")]
public bool PreferSpriteNameTag = true;
[Tooltip("FPS mặc định cho khi không suy ra được từ FrameTicks. " +
"Default FPS for when it cannot be derived from FrameTicks.")]
public int DefaultAnimFps = 10;
// ─────────────────────────────────────────────────────────────────────
// IEmotionSpriteMap
// ─────────────────────────────────────────────────────────────────────
public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info)
{
info = default;
if (Library == null)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Library == null. Gán EmotionLibrarySO vào field Library.");
return false;
}
EmotionSetSnapshot set = Library.GetSetOrNull(emotionSet);
if (set == null)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Không tìm thấy set index={emotionSet}. " +
$"Sets hiện có: [{string.Join(", ", Library.Sets?.ConvertAll(s => s?.EmotionSetIndex.ToString() ?? "null") ?? new System.Collections.Generic.List())}]");
return false;
}
// 禁止 TmpSpriteAsset.name:GetName() 仅主线程(网络收包线程会调 TryGetSprite)。
// Never use TmpSpriteAsset.name here — GetName() is main-thread-only (network receive may call TryGetSprite).
string spriteAssetName = set.TmpSpriteAssetName ?? string.Empty;
if (emotionIndex < 0 || emotionIndex >= set.Entries.Count)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: emotionIndex={emotionIndex} ngoài range [0, {set.Entries.Count - 1}].");
return false;
}
EmotionEntryData entry = set.Entries[emotionIndex];
Sprite[] frames = entry.FrameSprites;
if (frames == null || frames.Length == 0)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: entry.FrameSprites null/empty. Chạy lại Emotion Atlas Converter để populate FrameSprites.");
return false;
}
// Dùng StartPos/NumFrames từ txt (khớp EmotionAtlasConverterCore: cellIndex = StartPos + f).
// Use StartPos/NumFrames from txt (matches EmotionAtlasConverterCore: cellIndex = StartPos + f).
// Không đọc Sprite.name — GetName() chỉ được gọi trên main thread (vd. gói chat từ network thread).
// Do not read Sprite.name — GetName() is main-thread-only (e.g. chat packets on network thread).
int startCell = entry.StartPos;
int frameCount = frames.Length;
if (startCell < 0 || frameCount < 1)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: StartPos={startCell} hoặc không có frame.");
return false;
}
// ── Single frame ──────────────────────────────────────────────────
if (frameCount == 1)
{
if (frames[0] == null)
{
Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frames[0] == null.");
return false;
}
if (PreferSpriteNameTag)
{
// — TMP tra tên trong SpriteAsset được gán trên component.
// — TMP looks up the name in the SpriteAsset on the component.
info = new EmotionSpriteInfo
{
SpriteAssetName = spriteAssetName,
UseSpriteName = true,
SpriteName = FormatCellSpriteName(startCell),
IsAnimated = false
};
return true;
}
info = new EmotionSpriteInfo
{
SpriteAssetName = spriteAssetName,
SpriteIndex = startCell,
IsAnimated = false
};
return true;
}
// ── Multi-frame (animated) ────────────────────────────────────────
int endCell = startCell + frameCount - 1;
int fps = EstimateFps(entry.FrameTicks, frameCount);
info = new EmotionSpriteInfo
{
SpriteAssetName = spriteAssetName,
SpriteIndex = startCell,
IsAnimated = true,
AnimEndFrame = endCell,
AnimFPS = fps > 0 ? fps : DefaultAnimFps
};
return true;
}
// ─────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────
///
/// Tên ô giống tool atlas: "cell_XXXX" (4 chữ số). Không đọc Unity Sprite.name.
/// Same cell naming as atlas tool: "cell_XXXX" (4 digits). Does not read Unity Sprite.name.
///
private static string FormatCellSpriteName(int cellIndex) => $"cell_{cellIndex:D4}";
private static int EstimateFps(int[] frameTicks, int numFrames)
{
if (frameTicks == null || numFrames < 2 || frameTicks.Length < numFrames)
return 0;
int span = frameTicks[numFrames - 1] - frameTicks[0];
if (span <= 0)
return 0;
return Mathf.Max(1, Mathf.RoundToInt((numFrames - 1) * 60f / span));
}
}
}