using System; using System.Text; using System.Text.RegularExpressions; using CSNetwork; namespace BrewMonster.Scripts.Chat { /// /// Chat 输入/协议:wire(MarshalEditBoxText)↔ TMP <sprite> 显示。 /// Chat input/protocol: wire (MarshalEditBoxText) ↔ TMP <sprite> display. /// public static class ChatWireTmpCodec { /// /// 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"). /// private static readonly Regex InnerSpriteTagRegex = new Regex(@"]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// /// 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. /// private static readonly Regex EmotionSizedBlockRegex = new Regex(@"\s*]*>\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex OrphanSpriteFragmentRegex = new Regex(@"^\s*sprite\s[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// /// 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. /// 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); } /// /// TMP 正文(无频道前缀)→ wire marshal(用于发送)。 /// TMP body text (no channel prefix) → marshaled wire (for sending). /// 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(); } /// /// Đ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). /// 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); } } /// /// 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). /// 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; } /// /// 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). /// 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; } /// /// Wire → TMP 富文本(FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp)。 /// Wire → TMP rich text (FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp). /// public static string WireBodyToTmpForDisplay(string wireBody, IEmotionSpriteMap map, int cEmotion) { return ChatEmotionDisplayPipeline.ConvertWireBodyToTmpDisplay(wireBody, map, cEmotion); } } }