Files
test/Docs/chat-emotion-refactor-summary.md
T
2026-05-25 10:33:40 +07:00

10 KiB
Raw Blame History

Tóm tắt refactor Chat / Emotion (session)

Tài liệu này ghi lại các thay đổi code liên quan Emotion → TMP, ChatInput / Emoji picker, và EmotionLibrarySpriteMap trong kho perfect-world-unity.


1. EmotionTMPTagBuilder — nhập liệu chat

Mục đích: Chèn tag TMP (<sprite name=…>, <sprite index=…>, <sprite anim=…>) vào TMP_InputField khi người chơi chọn emoji.

Đã làm:

  • Assets/Scripts/ChatInputHandler.cs

    • Thêm using BrewMonster.Scripts.ChatBrewMonster.Scripts.Chat.EmotionData.
    • Thêm [SerializeField] EmotionLibrarySpriteMap _spriteMap.
    • InsertEmoji → gọi AppendEmotionWire (buffer wire + refresh TMP) — xem mục 7.
  • Assets/PerfectWorld/Scripts/Chat/UI/EmojiPickerUI.cs

    • Bỏ Reflection gọi InsertEmoji qua MethodInfo.
    • Gọi ChatInputHandler.InsertEmoji (nội bộ dùng wire + TMP display).

Ghi chú Inspector: Gán EmotionLibrarySpriteMap vào ChatInputHandler nếu dùng InsertEmoji; picker vẫn dùng _emotionSpriteMap riêng trên EmojiPickerUI.


2. ChatEmotionDisplayPipeline — cảnh báo spriteMap null

Vấn đề: Constructor luôn log warning dù spriteMap có giá trị (vì Debug.LogWarning nằm ngoài nhánh else).

Đã làm:

  • Chỉ log khi spriteMap == null (dùng StubEmotionSpriteMap).
  • SetSpriteMap(null) ghi log rõ: cần gán EmotionLibrarySpriteMap trên CECUIManager.

Luồng đúng: CECUIManager.AwakegameUI.SetEmotionSpriteMap(_emotionLibrarySpriteMap) — nếu field không gán trên Inspector thì map vẫn null và fallback stub.


3. EmotionLibrarySpriteMap — nhiều bộ atlas / một TMP mỗi bộ

Vấn đề ban đầu: Chỉ có một TMP_SpriteAsset → tra index sai khi có nhiều bộ (ví dụ 9 atlas).

Tiến hóa thiết kế:

  1. Tạm thời: Thêm list PerSetTmpSpriteAssets (SetIndex → TMP_SpriteAsset) + fallback.
  2. Chốt: Di chuyển TMP_SpriteAsset vào EmotionSetSnapshot trong EmotionLibrarySO — mỗi snapshot một atlas + một TmpSpriteAsset cùng chỗ dữ liệu.
  3. Xóa list trung gian trên EmotionLibrarySpriteMap; chỉ còn Library, PreferSpriteNameTag, DefaultAnimFps.

File: Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs — field TmpSpriteAsset trên EmotionSetSnapshot.


4. Index sprite: không còn tra spriteCharacterTable theo tên

Yêu cầu: Atlas cắt trái → phải, trên → dưới; sub-sprite đặt tên cell_XXXX.

Đã làm:

  • Thay FindSpriteIndex(TMP_SpriteAsset, name) (duyệt spriteCharacterTable) bằng ParseCellIndex(string) — parse số sau prefix cell_ → index tuyến tính khớp thứ tự cắt.
  • Áp dụng cho:
    • PreferSpriteNameTag == false (một frame): <sprite index=N>.
    • Emoji động (nhiều frame): start / end từ frame đầu và frame cuối.

Hệ quả: Không phụ thuộc thứ tự entry trong TMP_SpriteAsset.spriteCharacterTable để khớp index; vẫn cần gán đúng TMP Sprite Asset trên TextMeshProUGUI chat để hiển thị.


5. Đường dẫn file chính

File Vai trò
Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs Build tag (dùng để khớp tag khi TMP → wire)
Assets/Scripts/ChatInputHandler.cs _chatWireBody, AppendEmotionWire / InsertEmoji, đồng bộ input
Assets/PerfectWorld/Scripts/Chat/UI/EmojiPickerUI.cs Gọi InsertEmoji khi chọn emoji
Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs Cảnh báo / SetSpriteMap / ConvertWireBodyToTmpDisplay
Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs Wire ↔ TMP body: marshal emotion, TmpBodyToWire
Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs EmotionSetSnapshot.TmpSpriteAsset
Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs TryGetSprite, ParseCellIndex
Assets/PerfectWorld/Scripts/Network/CSNetwork/AUICommon.cs EditBoxItemBase.Serialize (port C++), EditBoxItemsSet + PUA
Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs ConvertWireChatBodyForDisplay
Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs ConvertWireBodyForChatPanel (EventBus system)

7. Chat wire (raw) vs TMP — gửi server & hiển thị (cập nhật)

Mục đích: Giữ chuỗi wire đúng protocol PC (MarshalEditBoxText / PUA + payload <type><…>) khi gửi; ô nhập chỉ hiển thị bản TMP (<sprite>). Khi nhận, tin từ server vẫn là wire; chuyển sang TMP ở pipeline hiển thị (và bổ sung chỗ chỉ đi EventBus).

Đã làm:

  1. EditBoxItemBase.Serialize() (AUICommon.cs) — port từ AUICommon.cpp (coord / image / default gồm emotion). Trước đó trả rỗng → MarshalEditBoxText không nối payload sau ký tự PUA.
  2. EditBoxItemsSet.IsEditboxItemCode — dùng AUICommon.IsEditboxItemCode (PUA \uE000…) thay khoảng \u0001…\u0010 lệch với AppendItem.
  3. ChatWireTmpCodec.cs (mới)
    • BuildMarshaledEmotionWire(set, index) — một đoạn emotion đã marshal.
    • TmpBodyToWire — parse <sprite…>, khớp tag với output của EmotionTMPTagBuilder (brute-force set/index trong giới hạn).
    • WireBodyToTmpForDisplay — ủy quyền ChatEmotionDisplayPipeline.ConvertWireBodyToTmpDisplay.
  4. ChatEmotionDisplayPipeline.ConvertWireBodyToTmpDisplay (static) — FilterEmotionSet + ConvertInlineItemsToTmp (dùng cho input và helper).
  5. ChatInputHandler
    • Field _chatWireBody: nội dung thân tin nhắn ở dạng wire gửi server (sau FilterBadWords vẫn cập nhật lại).
    • onValueChangedTmpBodyToWire trên phần thân (sau prefix kênh / whisper).
    • ParseAndSendMessage / whisper → SendChat / SendPrivateChat dùng _chatWireBody, không lấy từ chuỗi TMP.
    • Emoji: AppendEmotionWire / InsertEmoji — nối wire + RefreshInputDisplayFromWire() (prefix + WireBodyToTmpForDisplay, SuperFarCry dùng GameSession.SUPER_FAR_CRY_EMOTION_SET).
  6. CECGameUIMan.ConvertWireChatBodyForDisplay — wire → TMP tái sử dụng cho UI.
  7. GameSession
    • ConvertWireBodyForChatPanel: wire → TMP cho các nhánh chỉ EventBus.Publish(ChatMessageEvent) (system/broadcast), trước đây có thể đẩy raw wire lên panel.
    • OnPrtcWorldChat / OnPrtcPrivateChat / player OnPrtcChatMessage: tin người chơi vẫn qua AddChatMessage — wire→TMP đã có trong pipeline; không gọi thêm convert trên body (tránh double conversion). Comment trong code ghi rõ.

Ghi chú: Cần gán EmotionLibrarySpriteMap trên ChatInputHandler để TMP ↔ wire khớp emoji; không gán thì phần thân gần như text thường (emoji marshal không đầy đủ).


6. Việc cần làm trên Editor (một lần)

  • Gán EmotionLibrarySpriteMap (hoặc EmotionLibrarySO) đúng vào CECUIManager và các chỗ SerializeField liên quan.
  • Trong EmotionLibrary asset: mỗi EmotionSetSnapshot trong Sets → gán Tmp Sprite Asset tương ứng từng atlas (nếu dùng cho tooling / tài liệu; logic index hiện tại dựa trên tên cell_XXXX).
  • Trên TextMeshProUGUI chat: gán Sprite Asset khớp atlas đang dùng.

8. Hiển thị wire <0><set:index> & crash khi nhận tin (cập nhật phiên tháng 4/2026)

8.1 Vấn đề hiển thị literal <0><0:23> thay vì TMP

  • Nguyên nhân: Chuỗi wire đúng chuẩn PC dùng ký tự PUA (\uE000\uE3FF) + payload Serialize() dạng <0><set:index>. Nếu thiếu PUA (server/encoding, hoặc chỉ còn phần serialize), UnmarshalEditBoxText không tạo item → ConvertInlineItemsToTmp trả về nguyên chuỗi → TMP in literal.
  • Đã làm: Trong ChatEmotionDisplayPipeline.ConvertInlineItemsToTmp, sau bước unmarshal/TMP thông thường, thêm fallback regex <0><(\d+):(\d+)>: thay bằng tag TMP qua IEmotionSpriteMap.TryGetSprite + EmotionTMPTagBuilder.BuildSpriteTag (cùng logic với emotion đã marshal đầy đủ).
  • Vị trí file: Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs (LooseMarshaledEmotionRegex, ReplaceLooseMarshaledEmotionTags).
  • Phương án kiến trúc: Ưu tiên sửa một chỗ trong pipeline (ChatEmotionDisplayPipeline / CECGameUIMan.ConvertWireChatBodyForDisplay), không chỉ trong ChatMessageView.Bind, để mini chat và panel chung một luồng.

8.2 Trùng ChatMessageEvent nhánh NPC

  • Vấn đề: OnPrtcChatMessage (NPC) gọi AddChatMessage (đã publish bản wire→TMP), sau đó còn EventBus.Publish(new ChatMessageEvent(message)) với message vẫn chứa thân wire trong format string → trùng tin / tin thứ hai chưa convert.
  • Đã làm: Xóa publish thứ hai; giữ comment giải thích.
  • Vị trí file: Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs (nhánh ISNPCID).

8.3 Crash khi nhận tin có emoji (NullReferenceException)

  • Nguyên nhân: Trong AUICommon.FilterEmotionSet, vòng lặp dùng Dictionary enumerator truy cập it.Current.Value trước it.MoveNext() — trước lần MoveNext() đầu tiên, Current.ValuenullpItem.GetType() crash. Tin không có emoji (GetItemCount() == 0) không vào vòng lặp nên bug ẩn; tin nhận từ server PUA emotion thì kích hoạt ngay.
  • Đã làm: Đổi sang while (it.MoveNext()) { ... }if (pItem == null) continue; (cùng pattern với AUI_FilterEditboxItem).
  • Vị trí file: Assets/PerfectWorld/Scripts/Network/CSNetwork/AUICommon.cs — hàm FilterEmotionSet.

Cập nhật: tháng 4/2026 — mục 7 (chat wire vs TMP); mục 8 (fallback loose tag, NPC event, fix FilterEmotionSet).
Last update: April 2026 — added section 8 (loose tag fallback, NPC duplicate event, FilterEmotionSet iterator fix).