20 KiB
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 SendChatData và OnPrtcChatMessage 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ý:
-
Khởi tạo và đóng gói dữ liệu:
- Tạo gói tin C2S
publicchatvà thiết lập các thông số cơ bản (Channel,Roleidcủa người gửi).
- Tạo gói tin C2S
-
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 racardIdvà chèn byte data lệnhCHAT_GENERALCARD_COLLECTIONvào giao thức. - Vật phẩm trang bị thông thường: Nếu
iPackvàiSlothợp lệ, đóng gói định dạngCHAT_EQUIP_ITEMgồm vị trí túiwherevà vị trí slotindexchèn vào gói tin.
- Vật phẩm thẻ (
-
Gửi gói tin lên Server:
- Tin nhắn chuỗi (
szMsg) được mã hóa sangUTF-16 LE(Unicode trong C#) và lưu vàoMsg(kiểu Octets). - Gọi
SendProtocol(p)để đẩy dữ liệu xuống tầng Network và gửi đi.
- Tin nhắn chuỗi (
-
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.
- Xác định
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ý:
-
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).
- Kiểm tra cấp độ người gửi qua
-
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
ChannellàGP_CHAT_SYSTEMvà 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 theoSrcroleid(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ừ
FixedMsgkế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ọiAddChatMessage(...)đẩy vào hộp thoại Chat và bắn bong bóng đầu tự động.
- Request tên người chơi
-
Nếu là NPC (
ISNPCID):- Tìm kiếm
CECNPCtừ danh sáchNPCMan. - 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.
- Tìm kiếm
- Nếu
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 trongAddChatMessage), hệ thống gọi:EventBus.PublishChannel(p.Srcroleid, new EventChatMessageOnTopPlayer(p.Srcroleid, strMsg));Tương ứng bên kia, trongUIPlayer.cschạy ở hàmStart():EventBus.SubscribeChannel<EventChatMessageOnTopPlayer>(hostplayer.GetCharacterID(), SetChatMessage);Khi event kích hoạt, hàmSetChatMessagesẽ 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:
ChatInputHandlertrên GameObject chat (có thể cùngChatSystemlUI). 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 propertyMaxRecentWhisperTargets).
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 đươngchannelButtons). - Đồ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:
inputFieldkhông nhập; dropdowninteractable = 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— trongUnmarshalEditBoxText, với mỗienumEIEmotionsửa info"set:index"rồiMarshalEditBoxText.- 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).indextrong protocol = thứ tự dòng trong.txt, không phải tọa độ tùy ý. - File
Emotions{N}.txtthườ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
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).ChatPanelUInhận event →ChatMessageView.Bind→ TMP (TextOutlet).AUICommon.cs(CSNetwork): logic rich-text item +EmotionSpriteInfo(hỗ trợUseSpriteName,SpriteName, và tag có chỉ định asset quaEmotionTMPTagBuilder) +IEmotionSpriteMap.CECGameUIMan: field_emotionLibrarySpriteMap. TrongInit(), 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 choMarshalEditBoxTextđầy đủ.EditBoxItemsSet: dùng range PUAAUICommon(AUICOMMON_ITEM_CODE_START/END), không dùng range\u0001…\u0010sai.ChatWireTmpCodec.cs:BuildMarshaledEmotionWire,TmpBodyToWire(parse<sprite…>khớp outputEmotionTMPTagBuilder),WireBodyToTmpForDisplay(ủy quyền pipeline).ChatEmotionDisplayPipeline:ConvertWireBodyToTmpDisplay, cảnh báo khi thiếuspriteMap;ConvertInlineItemsToTmpcó fallback regex<0><(\d+):(\d+)>(LooseMarshaledEmotionRegex) khi thiếu PUA nhưng còn literal serialize — tránh TMP in raw<0><set:index>.ChatInputHandler:_chatWireBodylưu thân tin wire gửi server;onValueChanged→TmpBodyToWire; gửi quaSendChat/SendPrivateChatdùng_chatWireBody; emoji:AppendEmotionWire/InsertEmoji+RefreshInputDisplayFromWire()(Super Far Cry dùngGameSession.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 quaAddChatMessageđã convert — không double-convert. Nhánh NPC (ISNPCID): đã bỏ publishChatMessageEventtrùng sauAddChatMessage(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
EmotionSetSnapshottrongEmotionLibrarySOcóTmpSpriteAssetriêng (một atlas / một TMP asset mỗi bộ). EmotionLibrarySpriteMap.TryGetSpritetrả thông tin + tên asset (SpriteAssetName) đểEmotionTMPTagBuildertạo tag dạng<sprite="Emotions N" name="cell_XXXX">(hoặcanim/index) — tránh trường hợp nhiều asset cùng têncell_XXXXkhiến TMP luôn resolve set 1.- Index sprite:
ParseCellIndextừ têncell_XXXX(thứ tự cắt atlas trái→phải, trên→dưới), giảm phụ thuộc thứ tự trongspriteCharacterTable. - Converter atlas có thể đặt tên namespace
s{N}_cell_XXXXkhi 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ấtEmotions{N}_atlas.png(hoặc slice in-place), Sprite Mode Multiple, sub-spritecell_0000…EmotionAtlasConverterWindow.cs: single set; batch theo slot (Set N, Atlas, TXT), trùngNbáo lỗi; có thể tạoEmotionLibrary.asset.
5.7 Checklist setup Unity (một lần)
- Import PNG +
Emotions{N}.txt→ chạy converter →EmotionLibrary.asset(và/hoặc từngEmotionSetData_*.asset). - Tạo TMP Sprite Asset khớp atlas; tên asset ổn định (ví dụ
Emotions 0,Emotions 1, …) để tag<sprite="…">resolve đúng. - Trong Emotion Library: mỗi snapshot gán
Tmp Sprite Assettương ứng. - 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).
- Gán
EmotionLibrarySpriteMapvàoCECGameUIMan(vàChatInputHandler/CECUIManagertheo luồng wire–TMP). - Trên TextMeshProUGUI chat: gán Sprite Asset / fallback phù hợp để
<sprite>render.
5.8 Lỗi đã xử lý (tham chiếu nhanh)
FilterEmotionSetNRE: enumeratorDictionary— đổiwhile (it.MoveNext()), check null item (AUICommon.cs).UnmarshalEditBoxTextkhông extract item:itemMask→ dùngITEMMASK_ALL = ~0.EditBoxItemsSetrange sai: đồng bộ PUA vớiAUICommon.- 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 trongChatEmotionDisplayPipeline.ConvertInlineItemsToTmp. - Duplicate
ChatMessageEventNPC: bỏ publish thứ hai sauAddChatMessage(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ộngChatMessageView.OnPointerClicknếu khác logic whisper. - Tinh chỉnh FPS animation từ
FrameTickscho 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.csAssets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.csAssets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.csAssets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs,ChatMessageView.csAssets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs,ChatEmotionDisplayPipeline.cs,EmotionTMPTagBuilder.csAssets/PerfectWorld/Scripts/Chat/EmotionData/*Assets/PerfectWorld/Scripts/Editor/EmotionAtlasConverter*.csAssets/Scripts/ChatInputHandler.cs,Assets/Scripts/CECUIManager.csAssets/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.