using System; using System.Collections.Generic; using System.Linq; using System.Text; using BrewMonster.Scripts.Chat; using CSNetwork; namespace CSNetwork { public static class AUICommon { public const char AUICOMMON_ITEM_CODE_START = (char)0xE000; public const char AUICOMMON_ITEM_CODE_END = (char)0xE3FF; public const int AUIMANAGER_MAX_EMOTIONGROUPS = 32; public const int MAX_EDITBOX_ITEM_NUM = AUICOMMON_ITEM_CODE_END - AUICOMMON_ITEM_CODE_START + 1; public const int MAXNUM_CUSTOM_ITEM = 255; public enum EditboxItemType { enumEIEmotion = 0, enumEIIvtrlItem, enumEICoord, enumEIImage, enumEIScriptItem, enumEICustom, enumEINum = enumEICustom + MAXNUM_CUSTOM_ITEM } public static void AUI_ConvertChatString(ref string pszChat, ref char[] pszConv, bool bName) { if (string.IsNullOrEmpty(pszChat) || pszConv == null) return; int nLen = 0; for (int i = 0; i < pszChat.Length; i++) { char c = pszChat[i]; if (c == '^') { if (nLen + 1 < pszConv.Length) { pszConv[nLen] = '^'; pszConv[nLen + 1] = '^'; nLen += 2; } } else if (c == '&') { if (nLen + 1 < pszConv.Length) { pszConv[nLen] = '^'; pszConv[nLen + 1] = '&'; nLen += 2; } } else { if (nLen < pszConv.Length) { pszConv[nLen] = c; nLen++; } } } // kết thúc chuỗi bằng ký tự null if (nLen < pszConv.Length) pszConv[nLen] = '\0'; } public static string FilterEmotionSet(string szText, int nEmotionSet) { EditBoxItemsSet itemsSet = new EditBoxItemsSet(); string strOrgText = UnmarshalEditBoxText(szText, itemsSet); int nCount = itemsSet.GetItemCount(); if (nCount == 0) return szText; var it = itemsSet.GetItemIterator(); while (it.MoveNext()) { EditBoxItemBase pItem = it.Current.Value; if (pItem == null) continue; if (pItem.GetType() == EditboxItemType.enumEIEmotion) { int nSet = 0; int nIndex = 0; UnmarshalEmotionInfo(pItem.GetInfo(), ref nSet, ref nIndex); pItem.SetInfo(MarshalEmotionInfo(nEmotionSet, nIndex)); } } return MarshalEditBoxText(strOrgText, itemsSet); } public static string MarshalEmotionInfo(int nEmotionSet, int nIndex) { return nEmotionSet.ToString() + ":" + nIndex.ToString(); } public static void UnmarshalEmotionInfo(string szText, ref int nEmotionSet, ref int nIndex) { if (string.IsNullOrEmpty(szText)) return; var parts = szText.Split(':'); if (parts.Length >= 2) { int.TryParse(parts[0], out nEmotionSet); int.TryParse(parts[1], out nIndex); } // a_ClampFloor(nEmotionSet, 0); if (nEmotionSet < 0) nEmotionSet = 0; // a_ClampFloor(nIndex, 0); if (nIndex < 0) nIndex = 0; if (nEmotionSet >= AUIMANAGER_MAX_EMOTIONGROUPS) nEmotionSet = 0; } public static string MarshalEditBoxText(string text, EditBoxItemsSet itemsSet) { var sb = new StringBuilder(text.Length * 2); int start = 0; for (int i = 0; i < text.Length; i++) { char ch = text[i]; if (IsEditboxItemCode(ch)) { sb.Append(text, start, i - start + 1); var item = itemsSet.GetItemByChar(ch); if (item != null) sb.Append(item.Serialize()); start = i + 1; } } if (start < text.Length) sb.Append(text, start, text.Length - start); return sb.ToString(); } public static bool IsEditboxItemCode(char ch) { return ch >= AUICOMMON_ITEM_CODE_START && ch <= AUICOMMON_ITEM_CODE_END; } public static string UnmarshalEditBoxText(string szText, EditBoxItemsSet itemsSet) { return UnmarshalEditBoxText( szText, itemsSet, 0, null, 0, 0, null, 0, false, false, 0 ); } public static string UnmarshalEditBoxText( string? sztext, EditBoxItemsSet itemsSet, int msgIndex, string ivtrItem, uint clIvtrItem, int itemMask, EditboxScriptItem[]? scriptItems, int scriptItemCount, bool underLine, bool sameColor, uint clUnderLine) { if (sztext == null) return ""; var scriptInfo = new AUI_UNMARSH_SCRIPTITEM_INFO { ScriptItems = scriptItems, ScriptItemCount = scriptItemCount }; var underlineInfo = new AUI_UNMARSH_UNDERLINE_INFO { UnderLine = underLine, SameColor = sameColor, UnderLineColor = clUnderLine }; return UnmarshalEditBoxTextEx( sztext, itemsSet, msgIndex, ivtrItem, clIvtrItem, itemMask, scriptInfo, underlineInfo ); } public static string UnmarshalEditBoxTextEx( string text, EditBoxItemsSet itemsSet, int msgIndex, string ivtrItem, uint clIvtrItem, int itemMask, AUI_UNMARSH_SCRIPTITEM_INFO? scriptInfo, AUI_UNMARSH_UNDERLINE_INFO? underlineInfo) { if (text == null) return ""; int start = 0; int i = 0; int curScriptIndex = 0; var sb = new StringBuilder(); while (i < text.Length) { char ch = text[i]; if (IsEditboxItemCode(ch)) { if (i > start) sb.Append(text, start, i - start); i++; EditBoxItemBase? item = EditBoxItemBase.Unserialize(text, ref i); start = i; if (item != null) { if ((itemMask & (1 << (int)item.GetType())) != 0) { char newChar = itemsSet.AppendItem(item); if (newChar != '\0') { sb.Append(newChar); item.SetMsgIndex(msgIndex); if (underlineInfo != null) { item.SetUnderLine( underlineInfo.UnderLine, underlineInfo.SameColor, underlineInfo.UnderLineColor); } switch (item.GetType()) { case EditboxItemType.enumEIIvtrlItem: item.SetName(ivtrItem); item.SetColor(clIvtrItem); break; case EditboxItemType.enumEIScriptItem: if (scriptInfo != null && curScriptIndex < scriptInfo.ScriptItemCount) { var sItem = scriptInfo.ScriptItems![curScriptIndex]; item.SetName(sItem.Name); item.SetColor(sItem.Color); var data = sItem.Name; if (data != null) item.SetExtraData(sItem.Data, sItem.GetDataSize()); curScriptIndex++; } break; } } } } } else { i++; } } if (i > start) sb.Append(text, start, i - start); return sb.ToString(); } /// /// [Port] CECGameUIMan::FilterInvalidTags (EC_GameUIMan.cpp:6424) /// Lọc bỏ các tag đặc biệt không hợp lệ trong nội dung chat nhận từ server. /// - Emotion (biểu cảm): luôn giữ lại. /// - IvtrItem (link vật phẩm): giữ hoặc loại tùy bFilterItem. /// - Tất cả loại khác (image, coord...): luôn bị loại bỏ (thay bằng text thuần). /// public static string FilterInvalidTags(string szText, bool bFilterItem) { return AUI_FilterEditboxItem(szText, (EditBoxItemBase pItem) => { var type = pItem.GetType(); if (type == EditboxItemType.enumEIEmotion) return false; if (type == EditboxItemType.enumEIIvtrlItem) { pItem.SetInfo(""); return bFilterItem; } return true; }); } /// /// [Port] CECGameUIMan::AUI_FilterEditboxItem (EC_GameUIMan.cpp:6477) /// Duyệt qua các EditboxItem trong text, filter trả về true thì item bị loại /// (thay bằng tên hiển thị dạng text thuần), false thì giữ nguyên. /// public static string AUI_FilterEditboxItem(string szText, Func filter) { EditBoxItemsSet itemsSet = new EditBoxItemsSet(); string strText = UnmarshalEditBoxText(szText, itemsSet); int nCount = itemsSet.GetItemCount(); if (nCount == 0) return szText; var it = itemsSet.GetItemIterator(); int i = 0; var itemsToFilter = new List>(); while (it.MoveNext() && i < nCount) { EditBoxItemBase pItem = it.Current.Value; if (pItem != null && filter(pItem)) { itemsToFilter.Add(new KeyValuePair(it.Current.Key, pItem.GetName())); } i++; } foreach (var kv in itemsToFilter) { strText = AUI_ReplaceEditboxItem(strText, kv.Key, kv.Value); } return MarshalEditBoxText(strText, itemsSet); } /// /// [Port] CECGameUIMan::AUI_ReplaceEditboxItem (EC_GameUIMan.cpp:6431) /// Thay thế ký tự EditboxItem code trong chuỗi bằng một đoạn text thay thế. /// public static string AUI_ReplaceEditboxItem(string szText, char cItem, string szSubText) { if (string.IsNullOrEmpty(szText) || !IsEditboxItemCode(cItem)) return szText ?? ""; var sb = new StringBuilder(szText.Length); for (int i = 0; i < szText.Length; i++) { if (szText[i] == cItem) sb.Append(szSubText ?? ""); else sb.Append(szText[i]); } return sb.ToString(); } // ===================================================================== // TMP Rich Text Converters — 将内部item格式转换为TextMeshPro标签 // Pipeline: displayText + EditBoxItemsSet // → ConvertEmotionsToTMP (表情 → ) // → ConvertCoordsToTMP (坐标 → ) // → ConvertIvtrItemsToTMP (物品 → ) // → StripRemainingItemCodes(清理残留) // ===================================================================== /// /// [TMP] 将表情item替换为TMP sprite标签 — Replace emotion items with TMP animated sprite tags. /// Input: displayText từ UnmarshalEditBoxText (chứa PUA placeholder chars). /// Các item không phải Emotion được giữ nguyên cho converter tiếp theo xử lý. /// public static string ConvertEmotionsToTMP(string displayText, EditBoxItemsSet itemsSet, IEmotionSpriteMap spriteMap) { if (string.IsNullOrEmpty(displayText) || spriteMap == null) return displayText ?? ""; var sb = new StringBuilder(displayText.Length); for (int i = 0; i < displayText.Length; i++) { char ch = displayText[i]; if (IsEditboxItemCode(ch)) { var item = itemsSet.GetItemByChar(ch); if (item != null && item.GetType() == EditboxItemType.enumEIEmotion) { int nSet = 0, nIndex = 0; UnmarshalEmotionInfo(item.GetInfo(), ref nSet, ref nIndex); if (spriteMap.TryGetSprite(nSet, nIndex, out var info)) sb.Append(EmotionTMPTagBuilder.BuildSpriteTag(info)); } else { sb.Append(ch); } } else { sb.Append(ch); } } return sb.ToString(); } /// /// [TMP] 将坐标item替换为TMP可点击link标签 — Replace coord items with TMP link tags. /// Output: <link="coord:info"><color=#RRGGBB>name</color></link> /// public static string ConvertCoordsToTMP(string displayText, EditBoxItemsSet itemsSet) { if (string.IsNullOrEmpty(displayText)) return displayText ?? ""; var sb = new StringBuilder(displayText.Length); for (int i = 0; i < displayText.Length; i++) { char ch = displayText[i]; if (IsEditboxItemCode(ch)) { var item = itemsSet.GetItemByChar(ch); if (item != null && item.GetType() == EditboxItemType.enumEICoord) { string colorHex = A3DColorToHex(item.GetColor()); string name = item.GetName() ?? ""; string info = item.GetInfo() ?? ""; sb.AppendFormat("{2}", info, colorHex, name); } else { sb.Append(ch); } } else { sb.Append(ch); } } return sb.ToString(); } /// /// [TMP] 将物品item替换为TMP可点击link标签 — Replace inventory item links with TMP link tags. /// Output: <link="item:info"><color=#RRGGBB><u>name</u></color></link> /// public static string ConvertIvtrItemsToTMP(string displayText, EditBoxItemsSet itemsSet) { if (string.IsNullOrEmpty(displayText)) return displayText ?? ""; var sb = new StringBuilder(displayText.Length); for (int i = 0; i < displayText.Length; i++) { char ch = displayText[i]; if (IsEditboxItemCode(ch)) { var item = itemsSet.GetItemByChar(ch); if (item != null && item.GetType() == EditboxItemType.enumEIIvtrlItem) { string colorHex = A3DColorToHex(item.GetColor()); string name = item.GetName() ?? ""; string info = item.GetInfo() ?? ""; sb.AppendFormat("{2}", info, colorHex, name); } else { sb.Append(ch); } } else { sb.Append(ch); } } return sb.ToString(); } /// /// [TMP] 移除剩余的PUA item code字符 — Safety net: strip any remaining PUA item codes /// not handled by the above converters (e.g. enumEIScriptItem, enumEIImage, custom types). /// public static string StripRemainingItemCodes(string displayText) { if (string.IsNullOrEmpty(displayText)) return displayText ?? ""; var sb = new StringBuilder(displayText.Length); for (int i = 0; i < displayText.Length; i++) { if (!IsEditboxItemCode(displayText[i])) sb.Append(displayText[i]); } return sb.ToString(); } /// /// A3DCOLOR (uint, ARGB format) → "RRGGBB" hex string cho TMP color tag. /// public static string A3DColorToHex(uint color) { byte r = (byte)((color >> 16) & 0xFF); byte g = (byte)((color >> 8) & 0xFF); byte b = (byte)(color & 0xFF); return $"{r:X2}{g:X2}{b:X2}"; } } } public class AUI_UNMARSH_SCRIPTITEM_INFO { public EditboxScriptItem[]? ScriptItems; public int ScriptItemCount; } public class AUI_UNMARSH_UNDERLINE_INFO { public bool UnderLine; public bool SameColor; public uint UnderLineColor; } public class EditBoxItemsSet { protected Dictionary m_Items = new(); protected int[] m_ItemsCount = new int[(int)AUICommon.EditboxItemType.enumEINum]; protected char m_cNextItemChar; public EditBoxItemsSet() { Array.Clear(m_ItemsCount, 0, m_ItemsCount.Length); m_cNextItemChar = AUICommon.AUICOMMON_ITEM_CODE_START; } public EditBoxItemsSet(EditBoxItemsSet itemsset) { this.Assign(itemsset); } public void Assign(EditBoxItemsSet src) { m_Items.Clear(); foreach (var kv in src.m_Items) { m_Items[kv.Key] = new EditBoxItemBase(kv.Value); } Array.Copy(src.m_ItemsCount, m_ItemsCount, m_ItemsCount.Length); m_cNextItemChar = src.m_cNextItemChar; } public void Release() { m_Items.Clear(); Array.Clear(m_ItemsCount, 0, m_ItemsCount.Length); m_cNextItemChar = AUICommon.AUICOMMON_ITEM_CODE_START; } public int GetItemCount() { return m_Items.Count; } public Dictionary.Enumerator GetItemIterator() { return m_Items.GetEnumerator(); } public EditBoxItemBase? GetItemByChar(char ch) { if (!IsEditboxItemCode(ch)) throw new Exception("Invalid editbox item char"); if (m_Items.TryGetValue(ch, out var item)) return item; return null; } public bool IsEditboxItemCode(char ch) { return AUICommon.IsEditboxItemCode(ch); } public int GetItemCountByType(AUICommon.EditboxItemType type) { return m_ItemsCount[(int)type]; } public void DelItemByChar(char ch) { if (m_Items.TryGetValue(ch, out var item)) { m_ItemsCount[(int)item.GetType()]--; m_Items.Remove(ch); } } public char AppendItem(EditBoxItemBase pItem) { if (m_Items.Count >= AUICommon.MAX_EDITBOX_ITEM_NUM) return '\0'; char cur = m_cNextItemChar; do { if (m_Items.ContainsKey(m_cNextItemChar)) { m_cNextItemChar = EditboxGetNextChar(m_cNextItemChar); } else { m_Items[m_cNextItemChar] = pItem; m_ItemsCount[(int)pItem.GetType()]++; char ret = m_cNextItemChar; m_cNextItemChar = EditboxGetNextChar(m_cNextItemChar); return ret; } } while (m_cNextItemChar != cur); return '\0'; } public char EditboxGetNextChar(char cur) { if (cur >= AUICommon.AUICOMMON_ITEM_CODE_END) return AUICommon.AUICOMMON_ITEM_CODE_START; else return (char)(cur + 1); } public char AppendItem(AUICommon.EditboxItemType type, uint cl, string szName, string szInfo) { // Implement theo logic C++ gốc throw new NotImplementedException(); } public int GetTotalExtraDataSize() { int sz = 0; foreach (var item in m_Items.Values) { sz += item.GetExtraDataSize(); } return sz; } } public class EditBoxItemBase { protected AUICommon.EditboxItemType m_type; protected uint m_dwColor; protected string m_strName = ""; protected string m_strInfo = ""; protected int m_nMsgIndex; protected int m_nImageIndex; protected int m_nImageFrame; protected float m_fImageScale; protected byte[]? m_pExtraData; protected int m_uExtraDataSize; protected bool m_bUnderLine; protected bool m_bSameColor; protected uint m_dwUnderLineColor; protected static EditBoxItemBase?[] m_mapCustomType = new EditBoxItemBase[AUICommon.MAXNUM_CUSTOM_ITEM]; public EditBoxItemBase(AUICommon.EditboxItemType type) { m_type = type; m_dwColor = 0xffffffff; m_nMsgIndex = 0; m_nImageIndex = 0; m_nImageFrame = 0; m_fImageScale = 1.0f; m_bUnderLine = false; m_bSameColor = true; m_dwUnderLineColor = 0; RegisterCustomType(type); } public EditBoxItemBase(EditBoxItemBase src) { Assign(src); } public bool UnserializeContent(string text, ref int index) { int szNext; int szEnd = text.IndexOf("<", index); if (szEnd == -1) return false; if (m_type == AUICommon.EditboxItemType.enumEICoord) { szNext = szEnd + 1; if (!TryParseInt(text, szNext, out int color)) return false; SetColor((uint)color); szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szNext = szEnd + 2; if (!TryParseInt(text, szNext, out int underline)) return false; m_bUnderLine = underline != 0; szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szNext = szEnd + 2; if (!TryParseInt(text, szNext, out int underlineColor)) return false; m_dwUnderLineColor = (uint)underlineColor; szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; m_bSameColor = (m_dwUnderLineColor == m_dwColor); szNext = szEnd + 2; szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; SetName(text.Substring(szNext, szEnd - szNext)); szEnd += 1; } else if (m_type == AUICommon.EditboxItemType.enumEIImage) { szNext = szEnd + 1; if (!TryParseUInt(text, szNext, out uint color)) return false; SetColor(color); szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szNext = szEnd + 2; if (!TryParseInt(text, szNext, out int imageIndex)) return false; SetImageIndex(imageIndex); szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szNext = szEnd + 2; if (!TryParseInt(text, szNext, out int frame)) return false; SetImageFrame(frame); szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szNext = szEnd + 2; if (!float.TryParse(text.Substring(szNext), out float f)) return false; SetImageScale(f); szEnd = text.IndexOf("><", szNext); if (szEnd == -1) return false; szEnd += 1; } else if (m_type == AUICommon.EditboxItemType.enumEIEmotion) { SetName("W"); } szNext = szEnd + 1; szEnd = text.IndexOf('>', szNext); if (szEnd == -1) return false; SetInfo(text.Substring(szNext, szEnd - szNext)); index = szEnd + 1; return true; } private bool TryParseInt(string text, int start, out int value) { int end = start; while (end < text.Length && char.IsDigit(text[end])) end++; return int.TryParse(text.Substring(start, end - start), out value); } private bool TryParseUInt(string text, int start, out uint value) { int end = start; while (end < text.Length && char.IsDigit(text[end])) end++; return uint.TryParse(text.Substring(start, end - start), out value); } protected void RegisterCustomType(AUICommon.EditboxItemType type) { if (type >= AUICommon.EditboxItemType.enumEICustom && type < AUICommon.EditboxItemType.enumEINum) { int index = (int)type - (int)AUICommon.EditboxItemType.enumEICustom; if (m_mapCustomType[index] == null) m_mapCustomType[index] = this; } } protected virtual EditBoxItemBase Create() { return new EditBoxItemBase(m_type); } protected static EditBoxItemBase? GetCustomItemFromType(AUICommon.EditboxItemType type) { if (type >= AUICommon.EditboxItemType.enumEICustom && type < AUICommon.EditboxItemType.enumEINum) { return m_mapCustomType[(int)type - (int)AUICommon.EditboxItemType.enumEICustom]; } return null; } public uint GetColor() => m_dwColor; public AUICommon.EditboxItemType GetType() => m_type; public string GetName() => m_strName; public string GetInfo() => m_strInfo; public int GetMsgIndex() => m_nMsgIndex; public int GetImageIndex() => m_nImageIndex; public int GetImageFrame() => m_nImageFrame; public float GetImageScale() => m_fImageScale; public bool GetUnderLine() => m_bUnderLine; public bool GetSameColor() => m_bSameColor; public uint GetUnderLineColor() => m_dwUnderLineColor; public void SetColor(uint cl) => m_dwColor = cl; public void SetName(string name) => m_strName = name; public void SetInfo(string info) => m_strInfo = info; public void SetMsgIndex(int n) => m_nMsgIndex = n; public void SetImageIndex(int n) => m_nImageIndex = n; public void SetImageFrame(int n) => m_nImageFrame = n; public void SetImageScale(float f) => m_fImageScale = f; public byte[]? GetExtraData() => m_pExtraData; public int GetExtraDataSize() => m_uExtraDataSize; public void SetUnderLine(bool underline, bool sameColor = true, uint underlineColor = 0) { m_bUnderLine = underline; m_bSameColor = sameColor; m_dwUnderLineColor = underlineColor; } public void SetExtraData(byte[] data, int size) { m_pExtraData = new byte[size]; Array.Copy(data, 0, m_pExtraData, 0, size); m_uExtraDataSize = size; } /// /// [Port] EditBoxItemBase::Serialize — AUICommon.cpp — wire payload sau mỗi ký tự placeholder PUA. /// [Port] EditBoxItemBase::Serialize — AUICommon.cpp — wire payload after each PUA placeholder character. /// public virtual string Serialize() { if (m_type == AUICommon.EditboxItemType.enumEICoord) { uint ul = m_bSameColor ? m_dwColor : m_dwUnderLineColor; return $"<{(int)m_type}><{m_dwColor}><{(m_bUnderLine ? 1 : 0)}><{ul}><{m_strName}><{m_strInfo}>"; } if (m_type == AUICommon.EditboxItemType.enumEIImage) { return $"<{(int)m_type}><{m_dwColor}><{m_nImageIndex}><{m_nImageFrame}><{m_fImageScale.ToString(System.Globalization.CultureInfo.InvariantCulture)}><{m_strInfo}>"; } return $"<{(int)m_type}><{m_strInfo}>"; } public static EditBoxItemBase Unserialize(string text, ref int index) { int start = text.IndexOf('<', index); if (start == -1) return null; start++; int endType = text.IndexOf('>', start); if (endType == -1) return null; string typeStr = text.Substring(start, endType - start); if (!int.TryParse(typeStr, out int type)) return null; if (type < 0 || type >= (int)AUICommon.EditboxItemType.enumEINum) return null; index = endType + 1; EditBoxItemBase pItem = EditBoxItemBase.GetCustomItemFromType((AUICommon.EditboxItemType)type); EditBoxItemBase pItemNew; if (pItem == null) { pItemNew = new EditBoxItemBase((AUICommon.EditboxItemType)type); } else { pItemNew = pItem.Create(); } if (!pItemNew.UnserializeContent(text, ref index)) { return null; } return pItemNew; } public void Assign(EditBoxItemBase src) { m_type = src.m_type; m_dwColor = src.m_dwColor; m_strName = src.m_strName; m_strInfo = src.m_strInfo; m_nMsgIndex = src.m_nMsgIndex; m_nImageIndex = src.m_nImageIndex; m_nImageFrame = src.m_nImageFrame; m_fImageScale = src.m_fImageScale; if (src.m_pExtraData != null) { SetExtraData(src.m_pExtraData, src.m_uExtraDataSize); } else { m_pExtraData = null; m_uExtraDataSize = 0; } RegisterCustomType(m_type); } } public class EditboxScriptItem { public string Name { get; set; } = ""; public uint Color { get; set; } public byte[]? Data { get; private set; } public void SetData(byte[] data) { Data = data.ToArray(); // deep copy } public int GetDataSize() { return Data?.Length ?? 0; } } /// /// 表情sprite信息 — Describes how one emotion maps to a TMP_SpriteAsset sprite (or animation). /// public struct EmotionSpriteInfo { /// /// Tên TMP_SpriteAsset cần dùng cho emoji này. Rỗng/null = dùng Sprite Asset mặc định trên text component. /// TMP_SpriteAsset name to target for this emoji. Empty/null = use text component default sprite asset. /// public string SpriteAssetName; /// TMP sprite index (frame đầu tiên nếu có animation). public int SpriteIndex; /// True nếu emotion này là icon động (nhiều frame). public bool IsAnimated; /// Index frame cuối (inclusive). Chỉ dùng khi IsAnimated = true. public int AnimEndFrame; /// Frames per second. Chỉ dùng khi IsAnimated = true. public int AnimFPS; /// /// Nếu true: dùng <sprite name="SpriteName"> (sprite phải có trong Sprite Asset gán trên TMP). /// If true: emit <sprite name="SpriteName"> (sprite must exist on the TextMeshPro sprite asset). /// public bool UseSpriteName; /// Tên sub-sprite Unity (vd. cell_0012) — khớp TMP Sprite Asset. public string SpriteName; } /// /// Interface ánh xạ (emotionSet, emotionIndex) → TMP sprite info. /// Implement interface này khi đã có TMP_SpriteAsset (atlas emotion icons). /// /// Ví dụ sử dụng: /// /// public class MyEmotionMap : IEmotionSpriteMap /// { /// public bool TryGetSprite(int set, int index, out EmotionSpriteInfo info) /// { /// int spriteStart = set * 30 + index * 4; // 30 emotions/set, 4 frames mỗi emotion /// info = new EmotionSpriteInfo /// { /// SpriteIndex = spriteStart, /// IsAnimated = true, /// AnimEndFrame = spriteStart + 3, /// AnimFPS = 10 /// }; /// return true; /// } /// } /// /// public interface IEmotionSpriteMap { /// /// Tra bảng (emotionSet, emotionIndex) → EmotionSpriteInfo. /// Trả về false nếu không tìm thấy (emotion sẽ bị bỏ qua khi render). /// bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info); }