Files
test/Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs
2026-04-09 11:56:36 +07:00

103 lines
4.7 KiB
C#

using System.Text.RegularExpressions;
using CSNetwork;
using UnityEngine;
namespace BrewMonster.Scripts.Chat
{
/// <summary>
/// Chat 表情与内联富文本 — Emotion set + TMP conversion for chat (ported from CECGameUIMan::AddChatMessage).
/// Keeps channel emotion filtering and Unmarshal/Convert-to-TMP in one place.
/// </summary>
public sealed class ChatEmotionDisplayPipeline
{
/// <summary>
/// Khi thiếu ký tự PUA trước payload, Unmarshal không chạy nhưng vẫn còn &lt;0&gt;&lt;set:index&gt; (Serialize).
/// When PUA prefix is missing, Unmarshal skips but &lt;0&gt;&lt;set:index&gt; (Serialize) may remain.
/// </summary>
private static readonly Regex LooseMarshaledEmotionRegex = new Regex(
@"<0><(\d+):(\d+)>",
RegexOptions.Compiled);
private IEmotionSpriteMap _spriteMap = new StubEmotionSpriteMap();
/// <summary>
/// Wire 正文(服务器 / 输入缓冲)→ TMP 显示用富文本 — 与 CECGameUIMan.AddChatMessage 内转换一致(不含 FilterBadWords)。
/// Wire body (server / input buffer) → TMP rich text for display — same as CECGameUIMan.AddChatMessage (without FilterBadWords).
/// </summary>
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();
}
/// <summary>C++: FilterEmotionSet — map inline emotion codes to the correct set for this channel.</summary>
public string ApplyChannelEmotionFilter(string pszMsg, int cEmotion)
{
return AUICommon.FilterEmotionSet(pszMsg, cEmotion);
}
/// <summary>
/// TMP: Unmarshal edit-box item codes and convert emotion / coord / inventory links to TMP rich text.
/// Call after <see cref="CECUIManager.FilterBadWords"/> when the message is from a player channel.
/// </summary>
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);
}
/// <summary>
/// Fallback: wire emotion chỉ còn dạng Serialize &lt;0&gt;&lt;set:index&gt; (không qua PUA) → TMP &lt;sprite&gt;.
/// Fallback: emotion wire as Serialize-only &lt;0&gt;&lt;set:index&gt; (no PUA) → TMP &lt;sprite&gt;.
/// </summary>
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;
});
}
}
}