169 lines
8.9 KiB
C#
169 lines
8.9 KiB
C#
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.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)
|
||
{
|
||
// <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));
|
||
}
|
||
}
|
||
}
|