Files
2026-04-13 13:48:16 +07:00

169 lines
8.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
namespace BrewMonster.Scripts.Chat.EmotionData
{
/// <summary>
/// Ánh xạ (emotionSet, emotionIndex) từ <see cref="EmotionLibrarySO"/> 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).
/// </summary>
[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();
}
/// <summary>
/// 主线程:把 Library 里各 TmpSpriteAsset.name 写入快照字段,供 TryGetSprite 在任意线程读取。
/// Main thread: copy TmpSpriteAsset.name into snapshot fields so TryGetSprite can run off the main thread.
/// </summary>
public void EnsureCachedTmpSpriteAssetNames()
{
Library?.SyncCachedTmpSpriteAssetNamesFromObjects();
}
[Tooltip("Nếu true và emoji chỉ 1 frame: dùng <sprite name=\"cell_XXXX\"> thay vì <sprite index=N>. " +
"If true and single-frame: use <sprite name=\"cell_XXXX\"> instead of <sprite index=N>.")]
public bool PreferSpriteNameTag = true;
[Tooltip("FPS mặc định cho <sprite anim> khi không suy ra được từ FrameTicks. " +
"Default FPS for <sprite anim> 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<string>())}]");
return false;
}
// 禁止 TmpSpriteAsset.nameGetName() 仅主线程(网络收包线程会调 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)
{
// <sprite name="cell_XXXX"> — TMP tra tên trong SpriteAsset được gán trên component.
// <sprite name="cell_XXXX"> — 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
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Tên ô giống tool atlas: "cell_XXXX" (4 chữ số). Không đọc Unity <c>Sprite.name</c>.
/// Same cell naming as atlas tool: "cell_XXXX" (4 digits). Does not read Unity Sprite.name.
/// </summary>
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));
}
}
}