diff --git a/Documentation/Chatsystem/ChatSystem_Flow.md b/Documentation/Chatsystem/ChatSystem_Flow.md index 4ad3a6266d..ca310cae4a 100644 --- a/Documentation/Chatsystem/ChatSystem_Flow.md +++ b/Documentation/Chatsystem/ChatSystem_Flow.md @@ -1,248 +1,138 @@ -# Luồng Hoạt Động Của Hệ Thống Chat (C#) +# Chat System Flow — Luồng hệ thống chat -Tài liệu này tổng hợp luồng xử lý của hệ thống Chat S2C và C2S trong client, dựa trên hai hàm cốt lõi `SendChatData` và `OnPrtcChatMessage` thuộc lớp `GameSession.cs`. +Tài liệu tổng hợp luồng chat từ mạng → UI (TMP), đồng bộ main thread, và xử lý emoji — **English summary inline where useful**. --- -## 1. Luồng Gửi Tin Nhắn (C2S) - Hàm `SendChatData` +## 1. Tổng quan (Overview) -Hàm này được gọi khi người chơi nhập tin nhắn ở Local và muốn gửi lên Server (hoặc sử dụng vật phẩm liên quan đến Chat). Đặc biệt, **Server sẽ KHÔNG tự động gửi lại (echo) tin nhắn cho người gửi**, do đó client phải chủ động tự hiển thị tin nhắn của chính mình lên UI. - -**Các bước xử lý:** - -1. **Khởi tạo và đóng gói dữ liệu:** - - Tạo gói tin C2S `publicchat` và thiết lập các thông số cơ bản (`Channel`, `Roleid` của người gửi). - -2. **Xử lý đính kèm vật phẩm (nếu có):** - - **Vật phẩm thẻ (`GENERALCARD_PACK`):** Nếu tin nhắn được gửi cùng một thẻ (card), hệ thống sẽ tìm trong túi đồ client (Client Pack) theo slot (`iSlot`), lấy ra `cardId` và chèn byte data lệnh `CHAT_GENERALCARD_COLLECTION` vào giao thức. - - **Vật phẩm trang bị thông thường:** Nếu `iPack` và `iSlot` hợp lệ, đóng gói định dạng `CHAT_EQUIP_ITEM` gồm vị trí túi `where` và vị trí slot `index` chèn vào gói tin. - -3. **Gửi gói tin lên Server:** - - Tin nhắn chuỗi (`szMsg`) được mã hóa sang `UTF-16 LE` (Unicode trong C#) và lưu vào `Msg` (kiểu Octets). - - Gọi `SendProtocol(p)` để đẩy dữ liệu xuống tầng Network và gửi đi. - -4. **Hiển thị tin nhắn Local ngay lập tức:** - - Xác định `nEmotionSet` (nếu có), ví dụ kênh **Super Far Cry** sẽ có hiệu ứng bong bóng đầu đặc biệt (trim 8 character cuối của text). - - **Bong bóng đầu (Head Bubble):** Áp dụng nếu kênh thuộc dạng lân cận (`GP_CHAT_LOCAL`, `GP_CHAT_FARCRY`, `GP_CHAT_SUPERFARCRY`, `GP_CHAT_BATTLE`, `GP_CHAT_COUNTRY`). - - Lấy tên Host Player, ghép chuỗi với định dạng text của hệ thống (sử dụng cố định `FIXMSG_CHAT`). - - Gọi `EC_Game.GetGameRun().AddChatMessage(...)`: Hàm này sẽ đẩy dữ liệu lên UI Chat Box và tự động xử lý bong bóng trên đầu nhân vật. +- Gói chat S2C được xử lý trong `GameSession` (luồng nhận của `NetworkManager`, **không phải** main thread của Unity). +- Mọi thao tác dùng API Unity (`UnityEngine.Object.name`, TMP, `EventBus` → UI, v.v.) phải chạy trên **main thread**. +- Hai lớp bảo vệ: + 1. **Marshal UI chat** qua `EnqueueChatUI` → `SynchronizationContext` → `ChatThreadDispatcher` (cùng hàng đợi với `ChatSystemlUI` / `MiniChatUI`). + 2. **Cache tên TMP sprite asset** trong dữ liệu SO (`TmpSpriteAssetName`) để `TryGetSprite` không gọi `GetName()` từ luồng lạ. --- -## 2. Luồng Nhận Tin Nhắn (S2C) - Hàm `OnPrtcChatMessage` +## 2. Luồng mạng → hiển thị (Network → display) -Hàm này nhận giao thức `chatmessage` từ Server và xử lý việc hiển thị lên kênh chat, bong bóng đầu player/NPC. -Vì hàm OnPrtcChatMessage đang quá lớn nên tôi chia ra thêm 1 class Chat_GameSession để xử lý các logic nhỏ. -**Các bước xử lý:** +```mermaid +flowchart LR + subgraph net [Network thread] + NM[NetworkManager.ProcessReceivedData] + GS[GameSession.OnProtocolReceived] + H[Chat handlers decode + PolicyResolver] + end + subgraph main [Main thread] + Ctx[SynchronizationContext.Post] + CTD[ChatThreadDispatcher.Update] + ACM[AddChatMessage / EventBus] + TMP[TMP / Chat panel] + end + NM --> GS --> H + H -->|EnqueueChatUI| Ctx --> CTD --> ACM --> TMP +``` -1. **Tiền xử lý và Lọc (Filter):** - - Kiểm tra cấp độ người gửi qua `Chat_GameSession.ShouldBlockByLevel()`. Nếu bị chặn, hàm ngắt ngang và bỏ qua tin nhắn. - - Tạo `EC_IvtrItem` (Chat Item) từ dải byte đính kèm (nếu có vật phẩm). - - Lọc các tag mã hóa không hợp lệ thông qua `AUICommon.FilterInvalidTags`. - - Chạy qua các policy chặn lọc hệ thống (`Chat_GameSession.PolicyResolver()`) để ra chuỗi tinh chỉnh cuối cùng (`szMsg`). +### 2.1 Điểm vào gói chat (Protocol entry points) -2. **Phân loại Channel và RoleID để xử lý:** - - Hệ thống phân chia theo 3 nhóm luồng chính: +| Gói / hàm | File | Ghi chú | +|-----------|------|---------| +| `PROTOCOL_CHATMESSAGE` | `GameSession.OnPrtcChatMessage` | Decode + filter + `PolicyResolver` có thể **giữ trên luồng mạng** khi cần `return false` (pending tên). | +| `PROTOCOL_WORLDCHAT` | `GameSession.OnPrtcWorldChat` | Copy chuỗi → `EnqueueChatUI` → `OnPrtcWorldChat_Apply`. | +| `PROTOCOL_PRIVATECHAT` | `GameSession.OnPrtcPrivateChat` | Copy chuỗi → `EnqueueChatUI` → `OnPrtcPrivateChat_Apply`. | +| Echo local sau gửi | `GameSession.SendChatData` | Sau `SendProtocol` → `EnqueueChatUI` → `SendChatData_ApplyLocalEchoToChatPanel`. | - ### Nhóm A: Broadcast, System, hoặc System Role (Srcroleid == 0) - - Nếu `Channel` là `GP_CHAT_SYSTEM` và có `Srcroleid > 0`, tin nhắn thuộc các thông báo riêng biệt của cục bộ tính năng, được chia nhánh theo `Srcroleid` (Hardcode ID): - - **ID = 1,2,3,4,6,7:** Bản tin Chiến trường (Battle Message). - - **ID = 18..22:** Bản tin Đấu giá (Auction Message). Gọi `ChatMessageEvent` đưa lên kênh system. - - **ID = 24:** Bản tin Nhiệm vụ (Task Message). - - **ID = 29..45:** Bản tin Pháo đài, Lãnh thổ (Fortress Message). - - **ID = 46..49:** Bản tin Quốc chiến (Country Battle). - - **ID = 50..59:** Bản tin Vua (King Chat). - - **ID = 60..64:** Bản tin PVP Bang hội (Faction PVP). - *(Lưu ý: Một số hàm của luồng C++ đang bị ẩn/ẩn implementation, nếu cần data pending chưa có sẽ hoãn và chờ lần gọi lại (`bCalledagain`).)* - - Nếu không thuộc các ID đặc biệt trên, hệ thống đơn giản là phát ra `ChatMessageEvent` đẩy thẳng tin nhắn lên luồng EventBus. +### 2.2 `EnqueueChatUI` — cơ chế đồng bộ (Marshaling) - ### Nhóm B: Instance Channel (`GP_CHAT_INSTANCE` & `Srcroleid == 1`) - - Tin nhắn riêng của Phó bản, hiện tại được thiết kế để ném vào luồng `AddHeartBeatHint` ở UIMan. +**File:** `Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs` - ### Nhóm C: Dành cho người chơi khác (Player) hoặc NPC - *Sử dụng chuỗi định dạng lấy từ `FixedMsg` kết hợp tên người / NPC.* - - - **Nếu là người chơi (`ISPLAYERID`):** - - Request tên người chơi `GetPlayerName`. - - **Thiếu Data:** Nếu không tìm thấy tên player trong cache, tin nhắn yêu cầu Server lấy tên (`AddChatPlayerID`) và **treo** giao thức chat này vào Pending Protoco (`AddElemForPendingProtocols`). Giao thức này sẽ tự kích hoạt lại ở Tick sau khi Server trả về tên. - - **Có Data:** Xây dựng câu (kiểu `[Kênh] Player: Message`), gọi `AddChatMessage(...)` đẩy vào hộp thoại Chat và bắn bong bóng đầu tự động. - - - **Nếu là NPC (`ISNPCID`):** - - Tìm kiếm `CECNPC` từ danh sách `NPCMan`. - - Dựng chuỗi bằng format `FIXMSG_CHAT2`. - - Gọi `AddChatMessage()` cho chat panel và `EventBus.Publish(ChatMessageEvent)` cho UI liên quan. - - Lọc riêng Name Flag trên đầu NPC, rồi gán nội dung bằng hàm `pNPC.SetLastSaidWords(szMsg)` để hiện Bubble Text trên mô hình 3D NPC. +1. **`GameSession.Context`** được gán trong `UnityGameSession.Awake` từ `SynchronizationContext.Current` (main thread). +2. `EnqueueChatUI(action)`: + - Nếu `Context == null` → log lỗi, **không** chạy UI (tránh hỏng thread). + - Ngược lại: `Context.Post(_ => ChatThreadDispatcher.Instance.Post(action), null)`. + +**Vì sao hai bước (Context rồi ChatThreadDispatcher)?** + +- Luồng mạng **không** được gọi `ChatThreadDispatcher.Instance` trực tiếp nếu singleton chưa tồn tại: `MonoSingleton` có thể `FindFirstObjectByType` / tạo `GameObject` — đó là API chỉ an toàn trên main thread. +- `Context.Post` đưa lệnh lên main thread; từ đó `Instance.Post` an toàn và **cùng queue** với các UI chat khác đã dùng `ChatThreadDispatcher`. + +**Khởi tạo sớm singleton (warmup):** + +**File:** `Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs` + +- Trong `Awake`, sau khi gán `GameSession.Context`: `_ = ChatThreadDispatcher.Instance;` + — Đảm bảo dispatcher đã tạo trên main **trước** khi luồng mạng bắt đầu post. + +### 2.3 Việc vẫn chạy trên luồng mạng (Stays on network thread) + +Cần giữ **đồng bộ** để `return false` / pending đúng nghĩa: + +- `Chat_GameSession.ShouldBlockByLevel` +- `AUICommon.FilterInvalidTags` (sau khi decode; nếu sau này có Unity API trong đây cần xem lại) +- `Chat_GameSession.PolicyResolver` — khi trả về `false` (chờ tên / policy), thêm `pProtocol` vào pending trên luồng xử lý gói +- Nhánh battle: `OnBattleChatMessage` có thể `return false` (giữ nguyên logic cũ) + +### 2.4 Việc chạy trên main thread (qua `EnqueueChatUI`) + +- `EventBus.Publish(ChatMessageEvent(...))` cho system / broadcast / auction (sau khi copy `strTemp`, `channel`, `emotion` vào closure). +- `EC_Game.GetGameRun().AddChatMessage(...)` (player có tên, NPC, echo local). +- `CECGameUIMan.AddChatMessage`, `ConvertWireBodyForChatPanel`, `ConvertWireBodyForHeadBubble`, bubble `EventChatMessageOnTopPlayer`. + +Các helper apply: `OnPrtcWorldChat_Apply`, `OnPrtcPrivateChat_Apply`, `OnPrtcChatMessage_ApplyNpcBranch`, `SendChatData_ApplyLocalEchoToChatPanel`. + +### 2.5 `ChatThreadDispatcher` — vai trò (Role) + +**File:** `Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs` + +- `ConcurrentQueue` + `Update()` dequeue trên main thread. +- Các màn như `ChatSystemlUI`, `MiniChatUI`, `UIPlayer` đã dùng `ChatThreadDispatcher.Instance.Post` từ **main**; luồng chat từ `GameSession` giờ **đồng nhất** queue sau khi qua `Context.Post`. --- -## 3. Phân Tích Khác Biệt Giữa C++ và C# (Triển khai `OnPrtcChatMessage`) +## 3. Wire → TMP trong game (In-game pipeline) -Trong quá trình chuyển đổi (port) hệ thống Chat từ C++ sang C# (Unity), kiến trúc đã được thay đổi từ **mô hình liên kết chặt chẽ (Tight Coupling)** sang **mô hình hướng sự kiện (Event-Driven)** nhằm tách biệt hoàn toàn tầng Network và tầng UI/Logic Game. - -Dưới đây là các điểm khác biệt chính: - -| Tiêu chí | C++ (`EC_GameSession.cpp`) | C# Unity (`GameSession.cs` & `UIPlayer.cs`) | -| :--- | :--- | :--- | -| **Kiến trúc liên kết (Giao tiếp với mô hình 3D)** | Tầng Network gọi trực tiếp vào hệ thống quản lý Game World. Code chủ động tìm kiếm player: `CECPlayer *pPlayer = GetWorld()->GetPlayerMan()->GetPlayer(...)` rồi gọi trực tiếp hàm `pPlayer->SetLastSaidWords(strTemp, p->emotion, pItem);`. | Gọi thông qua kiến trúc kênh Pub/Sub: `EventBus.PublishChannel(p.Srcroleid, new EventChatMessageOnTopPlayer(p.Srcroleid, strMsg));`. Tầng Network không cần biết đối tượng 3D có tồn tại hay không. | -| **Bong bóng Chat trên đầu (Head Bubble)** | Đối tượng `CECPlayer` tự tính toán, lưu trữ Text và dựa vào Tick update trung tâm C++ để hiển thị/tắt text sau một khoảng thời gian. | Giao diện hiển thị `UIPlayer.cs` (gắn trên Prefab nhân vật) tự đăng ký sự kiện (`EventBus.SubscribeChannel`). Khi nhận event, tự đổi chuỗi `TextMeshProUGUI`, hiện Text và gọi một `UniTask` (`HideChatAsync`) độc lập để tự động tắt sau 5 giây. | -| **Xử lý Thread/Đa luồng** | Toàn bộ Network và Logic xử lý trên chung một luồng chính đồng bộ. | Tầng Network (nhận Packet) và tầng UI hiển thị Unity có thể bất đồng bộ. Vì vậy C# dùng `ChatThreadDispatcher.Instance.Post(...)` trong `UIPlayer.SetChatMessage` để đồng bộ an toàn việc gán giá trị UI lên luồng chính của Unity. | -| **Bảo trì / Mở rộng** | Gắn chặt UI với Data Network. Nếu UI thay đổi, sửa Network code. | Decoupled (Giảm kết dính). Việc hiển thị UI chat (khung Chat Panel lẫn Head Bubble trên đầu nhân vật) hoạt động độc lập và chỉ "lắng nghe" dữ liệu. | - -**Ví Dụ Chi Tiết (Luồng Head Bubble Player):** -* **C++:** Trong `OnPrtcChatMessage`, code thực hiện: - `pPlayer->SetLastSaidWords(strTemp, p->emotion, pItem);` - Tức là bắt ép đối tượng thực hiện cập nhật UI. -* **C#:** Trong `GameSession.cs` (hoặc bên trong `AddChatMessage`), hệ thống gọi: - `EventBus.PublishChannel(p.Srcroleid, new EventChatMessageOnTopPlayer(p.Srcroleid, strMsg));` - Tương ứng bên kia, trong `UIPlayer.cs` chạy ở hàm `Start()`: - `EventBus.SubscribeChannel(hostplayer.GetCharacterID(), SetChatMessage);` - Khi event kích hoạt, hàm `SetChatMessage` sẽ nhận Context và gỡ nó ra gán vào Text Mesh UI một cách an toàn. +- **`CECGameUIMan.AddChatMessage`** (`EC_GameUIMan.cs`): filter kênh emotion → bad words (kênh player) → `ChatEmotionDisplayPipeline.ConvertInlineItemsToTmp` → màu / kênh → `EventBus` (`ChatMessageEvent`) và bubble nếu kênh hỗ trợ. +- **`ConvertWireBodyForChatPanel`**: gọi `CECGameUIMan.ConvertWireChatBodyForDisplay` — **chỉ** nên gọi từ code đã chạy trên main (hiện được gọi trong lambda đã `EnqueueChatUI`). --- -## 4. UI nhập liệu: TMP_Dropdown kênh và MRU Whisper (`ChatInputHandler`) +## 4. Emoji / TMP — tránh `GetName()` ngoài main thread -Luồng này bổ sung cho ô chat mobile: dropdown gắn với `ChatInputHandler` (`Assets/Scripts/ChatInputHandler.cs`). +**Vấn đề:** `TMP_SpriteAsset.name` (và mọi `UnityEngine.Object.name`) gọi native `GetName()` — **chỉ main thread**. Trước đây `EmotionLibrarySpriteMap.TryGetSprite` đọc `set.TmpSpriteAsset.name` khi xử lý chat, dễ lỗi nếu vẫn còn path nào đó gọi pipeline ngoài main. -### 4.1 Cấu hình Inspector +**Cách xử lý:** -- **Component:** `ChatInputHandler` trên GameObject chat (có thể cùng `ChatSystemlUI`). -- **`recentWhisperDropdown`:** kéo **TMP_Dropdown** từ Hierarchy. -- **`maxRecentWhisperTargets`:** giới hạn MRU (mặc định **5**, tối thiểu áp dụng **1** qua property `MaxRecentWhisperTargets`). +| Thành phần | File | Mô tả | +|------------|------|--------| +| `EmotionSetSnapshot.TmpSpriteAssetName` | `EmotionLibrarySO.cs` | Chuỗi cache khớp `TmpSpriteAsset.name`. | +| `EmotionLibrarySO.SyncCachedTmpSpriteAssetNamesFromObjects()` | cùng file | Ghi cache từ reference (chỉ gọi trên main / editor `OnValidate`). | +| `EmotionLibrarySpriteMap.EnsureCachedTmpSpriteAssetNames()` | `EmotionLibrarySpriteMap.cs` | Gọi sync trên library. | +| `CECGameUIMan.SetEmotionSpriteMap` | `EC_GameUIMan.cs` | Nếu map là `EmotionLibrarySpriteMap` → `EnsureCachedTmpSpriteAssetNames()` trước khi gán pipeline. | +| `ChatInputHandler.Awake` | `ChatInputHandler.cs` | `_spriteMap?.EnsureCachedTmpSpriteAssetNames()` nếu map gán riêng trên input. | -### 4.2 Hai chế độ nội dung dropdown - -**A. Không ở kênh Whisper** (`m_currentChannel != GP_CHAT_WHISPER`) - -Dropdown liệt kê **4 kênh** cố định theo thứ tự: - -| Thứ tự | `ChatChannel` | Nhãn hiển thị | -|--------|-------------------|---------------| -| 0 | `GP_CHAT_LOCAL` | Local | -| 1 | `GP_CHAT_TEAM` | Team | -| 2 | `GP_CHAT_FACTION` | Faction | -| 3 | `GP_CHAT_FARCRY` | World | - -- Chọn dòng → `OnCommand_speakmode(kênh)` (tương đương `channelButtons`). -- Đồng bộ selection: nếu kênh hiện tại **không** thuộc bốn kênh trên (ví dụ Trade), caption mặc định về **Local** (index 0) cho đến khi người chọn lại. - -**B. Kênh Whisper** (`GP_CHAT_WHISPER`) - -- Dòng đầu: ký tự **—** (placeholder), không ép chọn MRU / giữ target đang gõ. -- Các dòng sau: tên người **MRU** (mới chat gần nhất lên trên). -- MRU chỉ cập nhật sau khi gửi whisper thành công (`SendPrivateChat` → `RecordRecentWhisper`). -- Khi đang ở kênh khác, MRU vẫn lưu trong bộ nhớ; chuyển sang Whisper sẽ thấy danh sách trong dropdown. - -### 4.3 Tương tác và sự kiện - -- **Kênh System:** `inputField` không nhập; dropdown **`interactable = false`**. -- **Các kênh khác:** dropdown **interactable** (kể cả 4 kênh public hoặc Whisper). -- **`WhisperPlayerEvent` / `SetWhisperTarget`:** vẫn chuyển sang Whisper và đồng bộ dropdown (MRU). - -### 4.4 Code tham chiếu - -- `ChatDropdownChannelOrder`, `GetChannelDropdownLabel(ChatChannel)`, `RebuildChatDropdownOptions()`, `RecordRecentWhisper`. - -### 4.5 Lưu ý - -- MRU **không** persist (PlayerPrefs / file) — chỉ trong phiên chơi. -- Tên field SerializeField vẫn là `recentWhisperDropdown` để tương thích prefab cũ. +`TryGetSprite` chỉ đọc `set.TmpSpriteAssetName`, **không** đọc `TmpSpriteAsset.name` tại runtime lookup. --- -## 5. Chat / Emotion: từ protocol PC đến TMP (tổng hợp handoff) +## 5. Đường dẫn file tham chiếu nhanh (Quick file index) -Phần này tương ứng **mục 9** trong `claude.md` và các tóm tắt `Docs/chat-*-summary.md`: luồng rich text (emoji, item, coord), wire format PC, và hiển thị TextMeshPro. +| Nội dung | Đường dẫn | +|----------|-----------| +| Marshal chat UI, handlers | `Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs` | +| Gán `Context`, warmup dispatcher | `Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs` | +| Hàng đợi chat UI | `Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs` | +| AddChatMessage + pipeline TMP | `Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs` | +| Policy / pending chat | `Assets/PerfectWorld/Scripts/Chat/Chat_GameSession.cs` | +| SO emotion + cache tên | `Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs`, `EmotionLibrarySpriteMap.cs` | -### 5.1 Tham chiếu C++ (chỉ đối chiếu) +--- -- **`FilterEmotionSet`:** `AUInterface2/AUICommon.cpp` — trong `UnmarshalEditBoxText`, với mỗi `enumEIEmotion` sửa info `"set:index"` rồi `MarshalEditBoxText`. -- **Atlas + txt:** `AUInterface2/AUIManager.cpp` (`Init`): `Surfaces\InGame\Emotions{l}.dds`, lưới **32×32**, `Surfaces\InGame\Emotions{l}.txt` — mỗi dòng: `nStartPos`, `nNumFrames`, hint, `nFrameTick[]` (tối đa 20). **`index` trong protocol** = thứ tự dòng trong `.txt`, không phải tọa độ tùy ý. -- File **`Emotions{N}.txt`** thường **không** có trong repo source — lấy từ client PC: `Surfaces\InGame\`. +## 6. Lưu ý vận hành (Operational notes) -### 5.2 Luồng Unity: tin nhắn → TMP trên panel +- **Độ trễ:** Tin có thể tới UI chậm thêm khoảng **một vài frame** so với xử lý trực tiếp trên main (Context post + queue `ChatThreadDispatcher` + `Update`). +- **Mobile / IL2CPP:** `SynchronizationContext`, `ConcurrentQueue`, và cache `string` trên SO là an toàn cho build thiết bị. +- **Nếu rút gọn một hop:** Có thể chỉ `Context.Post(action)` trực tiếp (bỏ `ChatThreadDispatcher` ở giữa) để giảm trễ, nhưng sẽ **không** cùng queue với `ChatSystemlUI` / `MiniChatUI` trừ khi refactor thêm. -1. **`EC_GameUIMan.AddChatMessage`** (`Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs`): xử lý king bit (Country) → **`AUICommon.FilterEmotionSet`** → FilterBadWords → **`UnmarshalEditBoxText`** + **`ConvertEmotionsToTMP`** / **`ConvertCoordsToTMP`** / **`ConvertIvtrItemsToTMP`** / **`StripRemainingItemCodes`** → format màu/kênh → **`GameSession.ChatMessageEvent`** (hoặc luồng tương đương). -2. **`ChatPanelUI`** nhận event → **`ChatMessageView.Bind`** → TMP (`TextOutlet`). -3. **`AUICommon.cs` (`CSNetwork`):** logic rich-text item + `EmotionSpriteInfo` (hỗ trợ `UseSpriteName`, `SpriteName`, và tag có chỉ định asset qua `EmotionTMPTagBuilder`) + `IEmotionSpriteMap`. -4. **`CECGameUIMan`:** field **`_emotionLibrarySpriteMap`**. Trong **`Init()`**, nếu gán SO thì dùng map đó, không thì **`StubEmotionSpriteMap`**. +--- -### 5.3 Wire (raw) ↔ TMP: gửi và nhận - -**Mục tiêu:** Giữ **chuỗi wire** đúng chuẩn PC (PUA `\uE000`–`\uE3FF` + payload `MarshalEditBoxText` / `EditBoxItemBase.Serialize`) khi **gửi**; ô nhập hiển thị bản **TMP** (``). Tin từ server là wire; chuyển TMP ở pipeline hiển thị. - -**Các thành phần chính:** - -- **`EditBoxItemBase.Serialize()`** (`AUICommon.cs`): port từ C++ — emotion / coord / image; cần cho `MarshalEditBoxText` đầy đủ. -- **`EditBoxItemsSet`:** dùng range PUA `AUICommon` (`AUICOMMON_ITEM_CODE_START/END`), không dùng range `\u0001…\u0010` sai. -- **`ChatWireTmpCodec.cs`:** `BuildMarshaledEmotionWire`, `TmpBodyToWire` (parse `` khớp output `EmotionTMPTagBuilder`), `WireBodyToTmpForDisplay` (ủy quyền pipeline). -- **`ChatEmotionDisplayPipeline`:** `ConvertWireBodyToTmpDisplay`, cảnh báo khi thiếu `spriteMap`; **`ConvertInlineItemsToTmp`** có **fallback regex** `<0><(\d+):(\d+)>` (`LooseMarshaledEmotionRegex`) khi thiếu PUA nhưng còn literal serialize — tránh TMP in raw `<0>`. -- **`ChatInputHandler`:** **`_chatWireBody`** lưu thân tin wire gửi server; `onValueChanged` → `TmpBodyToWire`; gửi qua `SendChat` / `SendPrivateChat` dùng **`_chatWireBody`**; emoji: `AppendEmotionWire` / `InsertEmoji` + `RefreshInputDisplayFromWire()` (Super Far Cry dùng `GameSession.SUPER_FAR_CRY_EMOTION_SET`). -- **`CECGameUIMan.ConvertWireChatBodyForDisplay`:** tái sử dụng wire → TMP cho UI. -- **`GameSession.ConvertWireBodyForChatPanel`:** wire → TMP cho nhánh chỉ `EventBus.Publish(ChatMessageEvent)` (system/broadcast). Nhánh player qua **`AddChatMessage`** đã convert — **không** double-convert. Nhánh **NPC (`ISNPCID`):** đã bỏ publish `ChatMessageEvent` trùng sau `AddChatMessage` (tránh tin thứ hai còn wire). - -**Gán `EmotionLibrarySpriteMap` trên `ChatInputHandler`** nếu cần TMP ↔ wire khớp emoji; thiếu thì phần thân gần như text thường. - -### 5.4 Nhiều bộ emoji (9 set) và tag TMP - -- Mỗi **`EmotionSetSnapshot`** trong **`EmotionLibrarySO`** có **`TmpSpriteAsset`** riêng (một atlas / một TMP asset mỗi bộ). -- **`EmotionLibrarySpriteMap.TryGetSprite`** trả thông tin + **tên asset** (`SpriteAssetName`) để **`EmotionTMPTagBuilder`** tạo tag dạng **``** (hoặc `anim` / `index`) — tránh trường hợp nhiều asset cùng tên `cell_XXXX` khiến TMP luôn resolve set 1. -- Index sprite: **`ParseCellIndex`** từ tên `cell_XXXX` (thứ tự cắt atlas trái→phải, trên→dưới), giảm phụ thuộc thứ tự trong `spriteCharacterTable`. -- Converter atlas có thể đặt tên namespace **`s{N}_cell_XXXX`** khi gom nhiều pack; cần đồng bộ với TMP asset và tag builder. - -### 5.5 Dữ liệu và ScriptableObject - -| File / type | Vai trò | -|-------------|--------| -| `Chat/EmotionData/EmotionEntryData.cs` | Một emotion: StartPos, NumFrames, Hint, FrameTicks, FrameSprites | -| `Chat/EmotionData/EmotionSetDataSO.cs` | Một bộ `EmotionsN` | -| `Chat/EmotionData/EmotionLibrarySO.cs` | Nhiều bộ: `List` (+ `TmpSpriteAsset` trên snapshot) | -| `Chat/EmotionData/EmotionLibrarySpriteMap.cs` | SO implement `IEmotionSpriteMap`: Library + tùy chọn PreferSpriteNameTag, DefaultAnimFps | -| `Chat/StubEmotionSpriteMap.cs` | Fallback khi chưa gán map đầy đủ | -| `Chat/EmotionTMPTagBuilder.cs` | Build tag TMP thống nhất cho input và `ConvertEmotionsToTMP` | - -### 5.6 Tool Editor (atlas + txt) - -- Menu: **Perfect World → Chat → Emotion Atlas Converter…** -- **`EmotionAtlasConverterCore.cs`:** mỗi bộ xuất **`Emotions{N}_atlas.png`** (hoặc slice in-place), **Sprite Mode Multiple**, sub-sprite **`cell_0000` …** -- **`EmotionAtlasConverterWindow.cs`:** single set; batch theo slot (**Set N**, Atlas, TXT), trùng `N` báo lỗi; có thể tạo **`EmotionLibrary.asset`**. - -### 5.7 Checklist setup Unity (một lần) - -1. Import PNG + `Emotions{N}.txt` → chạy converter → **`EmotionLibrary.asset`** (và/hoặc từng `EmotionSetData_*.asset`). -2. Tạo **TMP Sprite Asset** khớp atlas; tên asset ổn định (ví dụ `Emotions 0`, `Emotions 1`, …) để tag `` resolve đúng. -3. Trong **Emotion Library**: mỗi snapshot gán **`Tmp Sprite Asset`** tương ứng. -4. Tạo asset **Create → Perfect World → Chat → Emotion Library Sprite Map**: gán Library + tùy chọn **Prefer Sprite Name Tag** (emoji 1 frame). -5. Gán **`EmotionLibrarySpriteMap`** vào **`CECGameUIMan`** (và `ChatInputHandler` / `CECUIManager` theo luồng wire–TMP). -6. Trên **TextMeshProUGUI** chat: gán **Sprite Asset** / fallback phù hợp để `` render. - -### 5.8 Lỗi đã xử lý (tham chiếu nhanh) - -- **`FilterEmotionSet` NRE:** enumerator `Dictionary` — đổi `while (it.MoveNext())`, check null item (`AUICommon.cs`). -- **`UnmarshalEditBoxText` không extract item:** `itemMask` → dùng `ITEMMASK_ALL = ~0`. -- **`EditBoxItemsSet` range sai:** đồng bộ PUA với `AUICommon`. -- **Hiển thị raw wire trên TMP:** luồng convert TMP ↔ wire; tag có chỉ định sprite asset theo set. -- **Literal `<0>` khi nhận:** fallback regex trong `ChatEmotionDisplayPipeline.ConvertInlineItemsToTmp`. -- **Duplicate `ChatMessageEvent` NPC:** bỏ publish thứ hai sau `AddChatMessage` (`GameSession.cs`). - -### 5.9 Việc còn mở / tùy chọn - -- Test end-to-end tin có emoji từ server. -- Link **`coord:`** / **`item:`** trong TMP: mở rộng `ChatMessageView.OnPointerClick` nếu khác logic whisper. -- Tinh chỉnh FPS animation từ `FrameTicks` cho sát PC. -- Test nhanh local: `Assets/Scripts/ChatEmojiQuickTest.cs` (mô phỏng TMP → wire → TMP). - -### 5.10 Đường dẫn file nhanh - -- `Assets/PerfectWorld/Scripts/Network/CSNetwork/AUICommon.cs` -- `Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs` -- `Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs` -- `Assets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs`, `ChatMessageView.cs` -- `Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs`, `ChatEmotionDisplayPipeline.cs`, `EmotionTMPTagBuilder.cs` -- `Assets/PerfectWorld/Scripts/Chat/EmotionData/*` -- `Assets/PerfectWorld/Scripts/Editor/EmotionAtlasConverter*.cs` -- `Assets/Scripts/ChatInputHandler.cs`, `Assets/Scripts/CECUIManager.cs` -- `Assets/PerfectWorld/Scripts/Chat/UI/EmojiPickerUI.cs` - -*Tài liệu bổ sung: `Docs/chat-channel-whisper-dropdown-summary.md`, `Docs/chat-emoji-progress-summary.md`, `Docs/chat-emotion-refactor-summary.md`, `claude.md` §9.* +*Tài liệu phản ánh trạng thái codebase tại thời điểm thêm luồng `EnqueueChatUI` + cache `TmpSpriteAssetName`. Cập nhật khi thay đổi handler hoặc kiến trúc thread.*