199 lines
8.2 KiB
C#
199 lines
8.2 KiB
C#
using System;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using CSNetwork;
|
||
|
||
namespace BrewMonster.Scripts.Chat
|
||
{
|
||
/// <summary>
|
||
/// Chat 输入/协议:wire(MarshalEditBoxText)↔ TMP <sprite> 显示。
|
||
/// Chat input/protocol: wire (MarshalEditBoxText) ↔ TMP <sprite> display.
|
||
/// </summary>
|
||
public static class ChatWireTmpCodec
|
||
{
|
||
/// <summary>
|
||
/// Thẻ <sprite …> nội bộ (TMP cho phép <sprite="asset" …> không có khoảng sau "sprite").
|
||
/// English: Inner <sprite …> tag (TMP allows <sprite="asset" …> with no space after "sprite").
|
||
/// </summary>
|
||
private static readonly Regex InnerSpriteTagRegex = new Regex(@"<sprite[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||
|
||
/// <summary>
|
||
/// Một emoji hiển thị trong input = <size> + sprite + </size> (EmotionTMPTagBuilder).
|
||
/// English: One in-game emoji in input = size wrapper + sprite + closing size tag.
|
||
/// </summary>
|
||
private static readonly Regex EmotionSizedBlockRegex =
|
||
new Regex(@"<size=\d+%>\s*<sprite[^>]*>\s*</size>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||
|
||
private static readonly Regex OrphanSpriteFragmentRegex = new Regex(@"^\s*sprite\s[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||
|
||
/// <summary>
|
||
/// Tạo một đoạn wire marshal cho một emotion (set:index) — gửi server đúng protocol.
|
||
/// Build one marshaled wire segment for a single emotion (set:index) — correct server protocol.
|
||
/// </summary>
|
||
public static string BuildMarshaledEmotionWire(int emotionSet, int emotionIndex)
|
||
{
|
||
var item = new EditBoxItemBase(AUICommon.EditboxItemType.enumEIEmotion);
|
||
item.SetName("W");
|
||
item.SetInfo(AUICommon.MarshalEmotionInfo(emotionSet, emotionIndex));
|
||
var items = new EditBoxItemsSet();
|
||
char c = items.AppendItem(item);
|
||
if (c == '\0')
|
||
return "";
|
||
|
||
string display = c.ToString();
|
||
return AUICommon.MarshalEditBoxText(display, items);
|
||
}
|
||
|
||
/// <summary>
|
||
/// TMP 正文(无频道前缀)→ wire marshal(用于发送)。
|
||
/// TMP body text (no channel prefix) → marshaled wire (for sending).
|
||
/// </summary>
|
||
public static string TmpBodyToWire(string tmpBody, IEmotionSpriteMap map)
|
||
{
|
||
if (string.IsNullOrEmpty(tmpBody))
|
||
return "";
|
||
if (map == null)
|
||
return tmpBody;
|
||
|
||
var sb = new StringBuilder(tmpBody.Length);
|
||
int last = 0;
|
||
foreach (Match block in EmotionSizedBlockRegex.Matches(tmpBody))
|
||
{
|
||
AppendTmpSegmentToWire(sb, tmpBody, last, block.Index - last, map);
|
||
last = block.Index + block.Length;
|
||
|
||
Match inner = InnerSpriteTagRegex.Match(block.Value);
|
||
if (inner.Success && TryMatchSpriteTagToEmotion(map, inner.Value, out int es, out int ei))
|
||
sb.Append(BuildMarshaledEmotionWire(es, ei));
|
||
else
|
||
sb.Append(block.Value);
|
||
}
|
||
|
||
AppendTmpSegmentToWire(sb, tmpBody, last, tmpBody.Length - last, map);
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Đoạn TMP (không gồm khối <size>…</size> đã xử lý ở ngoài): chữ thường + <sprite> rời.
|
||
/// English: TMP segment: plain text plus bare sprite tags (outside size-wrapped emotion blocks).
|
||
/// </summary>
|
||
private static void AppendTmpSegmentToWire(StringBuilder sb, string tmpBody, int start, int length, IEmotionSpriteMap map)
|
||
{
|
||
if (length <= 0)
|
||
return;
|
||
|
||
string gap = tmpBody.Substring(start, length);
|
||
int last = 0;
|
||
foreach (Match m in InnerSpriteTagRegex.Matches(gap))
|
||
{
|
||
AppendSanitizedPlainText(sb, gap, last, m.Index - last);
|
||
string tag = m.Value;
|
||
if (TryMatchSpriteTagToEmotion(map, tag, out int es, out int ei))
|
||
sb.Append(BuildMarshaledEmotionWire(es, ei));
|
||
else
|
||
sb.Append(tag);
|
||
last = m.Index + m.Length;
|
||
|
||
var orphan = OrphanSpriteFragmentRegex.Match(gap, last);
|
||
if (orphan.Success)
|
||
last += orphan.Length;
|
||
}
|
||
|
||
AppendSanitizedPlainText(sb, gap, last, gap.Length - last);
|
||
}
|
||
|
||
private static void AppendSanitizedPlainText(StringBuilder sb, string source, int start, int length)
|
||
{
|
||
if (length <= 0)
|
||
return;
|
||
|
||
int end = start + length;
|
||
for (int i = start; i < end; i++)
|
||
{
|
||
char ch = source[i];
|
||
if (!AUICommon.IsEditboxItemCode(ch))
|
||
sb.Append(ch);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Nếu charIndex nằm trong một emoji TMP (khối <size>… hoặc thẻ <sprite> rời), trả khoảng xóa [start, end).
|
||
/// English: If charIndex is inside one TMP emoji unit, returns delete span [start, end).
|
||
/// </summary>
|
||
public static bool TryGetSpriteTagRangeContainingCharacterIndex(string text, int charIndex, out int tagStart, out int tagEndExclusive)
|
||
{
|
||
tagStart = 0;
|
||
tagEndExclusive = 0;
|
||
if (string.IsNullOrEmpty(text) || charIndex < 0 || charIndex >= text.Length)
|
||
return false;
|
||
|
||
foreach (Match m in EmotionSizedBlockRegex.Matches(text))
|
||
{
|
||
int end = m.Index + m.Length;
|
||
if (charIndex >= m.Index && charIndex < end)
|
||
{
|
||
tagStart = m.Index;
|
||
tagEndExclusive = end;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
foreach (Match m in InnerSpriteTagRegex.Matches(text))
|
||
{
|
||
int end = m.Index + m.Length;
|
||
if (charIndex >= m.Index && charIndex < end)
|
||
{
|
||
tagStart = m.Index;
|
||
tagEndExclusive = end;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Khớp tag <sprite…> với EmotionTMPTagBuilder — so sánh phần sprite bên trong <size>…</size>.
|
||
/// English: Match inner <sprite…> to EmotionTMPTagBuilder output (full tag includes size wrapper).
|
||
/// </summary>
|
||
public static bool TryMatchSpriteTagToEmotion(IEmotionSpriteMap map, string spriteTag, out int emotionSet, out int emotionIndex)
|
||
{
|
||
emotionSet = 0;
|
||
emotionIndex = 0;
|
||
if (map == null || string.IsNullOrEmpty(spriteTag))
|
||
return false;
|
||
|
||
string normalized = spriteTag.Trim();
|
||
for (int s = 0; s < AUICommon.AUIMANAGER_MAX_EMOTIONGROUPS; s++)
|
||
{
|
||
for (int e = 0; e < 512; e++)
|
||
{
|
||
if (!EmotionTMPTagBuilder.TryBuildEmotionTag(map, s, e, out string built))
|
||
continue;
|
||
|
||
Match builtSprite = InnerSpriteTagRegex.Match(built);
|
||
if (!builtSprite.Success)
|
||
continue;
|
||
if (string.Equals(builtSprite.Value, normalized, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
emotionSet = s;
|
||
emotionIndex = e;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Wire → TMP 富文本(FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp)。
|
||
/// Wire → TMP rich text (FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp).
|
||
/// </summary>
|
||
public static string WireBodyToTmpForDisplay(string wireBody, IEmotionSpriteMap map, int cEmotion)
|
||
{
|
||
return ChatEmotionDisplayPipeline.ConvertWireBodyToTmpDisplay(wireBody, map, cEmotion);
|
||
}
|
||
}
|
||
}
|