Files
test/Documentation/Chatsystem/ChatSystem_Flow.md
T
2026-04-10 15:27:36 +07:00

20 KiB
Raw Blame History

Luồng Hoạt Động Của Hệ Thống Chat (C#)

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 SendChatDataOnPrtcChatMessage thuộc lớp GameSession.cs.


1. Luồng Gửi Tin Nhắn (C2S) - Hàm SendChatData

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 iPackiSlot 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.

2. Luồng Nhận Tin Nhắn (S2C) - Hàm OnPrtcChatMessage

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ý:

  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. Phân loại Channel và RoleID để xử lý:

    Hệ thống phân chia theo 3 nhóm luồng chính:

    Nhóm A: Broadcast, System, hoặc System Role (Srcroleid == 0)

    • Nếu ChannelGP_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.

    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.

    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.

3. Phân Tích Khác Biệt Giữa C++ và C# (Triển khai OnPrtcChatMessage)

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<EventChatMessageOnTopPlayer>(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.

4. UI nhập liệu: TMP_Dropdown kênh và MRU Whisper (ChatInputHandler)

Luồng này bổ sung cho ô chat mobile: dropdown gắn với ChatInputHandler (Assets/Scripts/ChatInputHandler.cs).

4.1 Cấu hình Inspector

  • 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).

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 (SendPrivateChatRecordRecentWhisper).
  • 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ũ.

5. Chat / Emotion: từ protocol PC đến TMP (tổng hợp handoff)

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.

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\.

5.2 Luồng Unity: tin nhắn → TMP trên panel

  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 (<sprite …>). 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 <sprite…> khớp output EmotionTMPTagBuilder), WireBodyToTmpForDisplay (ủy quyền pipeline).
  • ChatEmotionDisplayPipeline: ConvertWireBodyToTmpDisplay, cảnh báo khi thiếu spriteMap; ConvertInlineItemsToTmpfallback regex <0><(\d+):(\d+)> (LooseMarshaledEmotionRegex) khi thiếu PUA nhưng còn literal serialize — tránh TMP in raw <0><set:index>.
  • ChatInputHandler: _chatWireBody lưu thân tin wire gửi server; onValueChangedTmpBodyToWire; 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 EmotionLibrarySOTmpSpriteAsset 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 <sprite="Emotions N" name="cell_XXXX"> (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<EmotionSetSnapshot> (+ 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 <sprite="…"> 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 wireTMP).
  6. Trên TextMeshProUGUI chat: gán Sprite Asset / fallback phù hợp để <sprite> 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><set:index> 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.