using System.Text.RegularExpressions; using CSNetwork; using UnityEngine; namespace BrewMonster.Scripts.Chat { /// /// Chat 表情与内联富文本 — Emotion set + TMP conversion for chat (ported from CECGameUIMan::AddChatMessage). /// Keeps channel emotion filtering and Unmarshal/Convert-to-TMP in one place. /// public sealed class ChatEmotionDisplayPipeline { /// /// Khi thiếu ký tự PUA trước payload, Unmarshal không chạy nhưng vẫn còn <0><set:index> (Serialize). /// When PUA prefix is missing, Unmarshal skips but <0><set:index> (Serialize) may remain. /// private static readonly Regex LooseMarshaledEmotionRegex = new Regex( @"<0><(\d+):(\d+)>", RegexOptions.Compiled); private IEmotionSpriteMap _spriteMap = new StubEmotionSpriteMap(); /// /// Wire 正文(服务器 / 输入缓冲)→ TMP 显示用富文本 — 与 CECGameUIMan.AddChatMessage 内转换一致(不含 FilterBadWords)。 /// Wire body (server / input buffer) → TMP rich text for display — same as CECGameUIMan.AddChatMessage (without FilterBadWords). /// public static string ConvertWireBodyToTmpDisplay(string wireBody, IEmotionSpriteMap map, int cEmotion) { if (string.IsNullOrEmpty(wireBody)) return ""; if (map == null) return wireBody; var p = new ChatEmotionDisplayPipeline(map); string filtered = p.ApplyChannelEmotionFilter(wireBody, cEmotion); return p.ConvertInlineItemsToTmp(filtered); } public ChatEmotionDisplayPipeline(IEmotionSpriteMap spriteMap = null) { if (spriteMap != null) _spriteMap = spriteMap; else BMLogger.LogWarning("[ChatEmotionDisplayPipeline] spriteMap is null — using StubEmotionSpriteMap. Call SetSpriteMap() or assign EmotionLibrarySpriteMap SO on CECUIManager."); } public void SetSpriteMap(IEmotionSpriteMap spriteMap) { if (spriteMap == null) BMLogger.LogWarning("[ChatEmotionDisplayPipeline] SetSpriteMap() called with null — EmotionLibrarySpriteMap SO chưa được gán trên CECUIManager Inspector."); _spriteMap = spriteMap ?? new StubEmotionSpriteMap(); } /// C++: FilterEmotionSet — map inline emotion codes to the correct set for this channel. public string ApplyChannelEmotionFilter(string pszMsg, int cEmotion) { return AUICommon.FilterEmotionSet(pszMsg, cEmotion); } /// /// TMP: Unmarshal edit-box item codes and convert emotion / coord / inventory links to TMP rich text. /// Call after when the message is from a player channel. /// public string ConvertInlineItemsToTmp(string pszMsgAfterBadWordFilter) { EditBoxItemsSet itemsSet = new EditBoxItemsSet(); string displayText = AUICommon.UnmarshalEditBoxText(pszMsgAfterBadWordFilter, itemsSet); string tmpText; if (itemsSet.GetItemCount() <= 0) tmpText = pszMsgAfterBadWordFilter; else { tmpText = displayText; tmpText = AUICommon.ConvertEmotionsToTMP(tmpText, itemsSet, _spriteMap); tmpText = AUICommon.ConvertCoordsToTMP(tmpText, itemsSet); tmpText = AUICommon.ConvertIvtrItemsToTMP(tmpText, itemsSet); tmpText = AUICommon.StripRemainingItemCodes(tmpText); } return ReplaceLooseMarshaledEmotionTags(tmpText); } /// /// Fallback: wire emotion chỉ còn dạng Serialize <0><set:index> (không qua PUA) → TMP <sprite>. /// Fallback: emotion wire as Serialize-only <0><set:index> (no PUA) → TMP <sprite>. /// private string ReplaceLooseMarshaledEmotionTags(string text) { if (string.IsNullOrEmpty(text) || _spriteMap == null) return text; return LooseMarshaledEmotionRegex.Replace(text, match => { int set = int.Parse(match.Groups[1].Value); int index = int.Parse(match.Groups[2].Value); if (_spriteMap.TryGetSprite(set, index, out EmotionSpriteInfo info)) return EmotionTMPTagBuilder.BuildSpriteTag(info); return match.Value; }); } } }