145 lines
10 KiB
Markdown
145 lines
10 KiB
Markdown
# 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.Chat` và `BrewMonster.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.Awake` → `gameUI.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).
|
||
- `onValueChanged` → `TmpBodyToWire` 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.Value` là `null` → `pItem.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 **có** PUA emotion thì kích hoạt ngay.
|
||
- **Đã làm:** Đổi sang `while (it.MoveNext()) { ... }` và `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).*
|