diff --git a/.agents/skills/chat_system_csharp/SKILL.md b/.agents/skills/chat_system_csharp/SKILL.md new file mode 100644 index 0000000000..d8725dfd91 --- /dev/null +++ b/.agents/skills/chat_system_csharp/SKILL.md @@ -0,0 +1,26 @@ +--- +name: C# Chat System Formatting +description: Hướng dẫn viết code C# cho hệ thống chat dùng AUIDialog.FormatPrintf và pGameUI.GetStringFromTable. +--- + +# C# Chat System Formatting + +Skill này cung cấp hướng dẫn khi viết code C# cho hệ thống chat (ví dụ như khi chuyển đổi code từ C++ sang C# trong Unity). + +## Quy tắc lấy chuỗi và format + +Khi cần lấy một chuỗi từ bảng ngôn ngữ (Table) và format chuỗi đó với các tham số (thay thế cho %s, %d,...), hãy sử dụng kết hợp `pGameUI.GetStringFromTable` và `AUIDialog.FormatPrintf`. + +Trong phiên bản C++ gốc (ví dụ trong `EC_GameUIMan.cpp`), việc lấy chuỗi thường dùng `GetStringFromTable` và format bằng `swprintf` hoặc các hàm tương tự. Ở phiên bản C#, chúng ta thực hiện như sau: + +```csharp +// Ví dụ Table 9343: chuỗi dạng "Thời gian đăng nhập trước: %s" +// AUIDialog.FormatPrintf sẽ thực hiện kết hợp giữa pGameUI.GetStringFromTable(9343) và chuỗi timeStr +string textTime = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9343), timeStr); +``` + +## Các bước thực hiện +1. Đảm bảo đã lấy được referent tới `pGameUI`, ví dụ qua `CECUIManager.Instance?.GetInGameUIMan()`. +2. Lấy chuỗi format gốc từ bảng ngôn ngữ: `pGameUI.GetStringFromTable(ID)`. +3. Dùng `AUIDialog.FormatPrintf` để thay thế các placeholder (như `%s`, `%d`) trong chuỗi lấy được bằng các giá trị thực tế. +4. Gửi chuỗi đã được format đến hệ thống chat, ví dụ qua `EventBus.Publish(new GameSession.ChatMessageEvent {...})`. diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs index c83e5943df..b49d91a3ec 100644 --- a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs @@ -3,10 +3,13 @@ using BrewMonster.Scripts.UI; using TMPro; using UnityEngine; using UnityEngine.UI; +using UnityEngine.EventSystems; +using BrewMonster.Network; +using CSNetwork.GPDataType; namespace BrewMonster.Scripts.ChatUI { - public class ChatMessageView : MonoBehaviour + public class ChatMessageView : MonoBehaviour, IPointerClickHandler { public Image iconImage; public EC_UIUtility.TextOutlet messageText; @@ -24,5 +27,41 @@ namespace BrewMonster.Scripts.ChatUI messageText.Set(message); GetComponent().RefreshLayout(); } + + public void OnPointerClick(PointerEventData eventData) + { + if (messageText == null || messageText.tmp == null) return; + + int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera); + if (linkIndex != -1) + { + TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex]; + string linkId = linkInfo.GetLinkID(); + if (!string.IsNullOrEmpty(linkId)) + { + string playerName = linkId; + int characterId = 0; + + if (linkId.Contains("|")) + { + string[] parts = linkId.Split('|'); + if (parts.Length == 2) + { + int.TryParse(parts[0], out characterId); + playerName = parts[1]; + } + } + + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host != null && characterId > 0 && characterId != host.GetCharacterID() && GPDataTypeHelper.ISPLAYERID(characterId)) + { + CECUIManager.Instance?.ShowPlayerOptionsDialog(characterId, eventData.position + new Vector2(0, 400)); + } + + Debug.Log("[Cuong] OnWhisper from Chat: name: " + playerName); + EventBus.Publish(new WhisperPlayerEvent(playerName)); + } + } + } } } diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs index 9dc5e3ff99..f3116fe94a 100644 --- a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs @@ -31,7 +31,7 @@ namespace BrewMonster.Scripts.ChatUI errorMsg = $"Lỗi không xác định"; } - string coloredMsg = $"[System Error {e.ErrorCode}] {errorMsg}"; + string coloredMsg = $"{errorMsg}"; EventBus.Publish(new GameSession.ChatMessageEvent( coloredMsg, diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 692157ce80..e7b0fe974f 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -2099,11 +2099,9 @@ namespace CSNetwork chatmessage p = (chatmessage)pProtocol; //var channel = (ChatChannel)p.Channel; - Debug.Log("[Cuong] reciver chat channel: " + p.Channel); if (Chat_GameSession.ShouldBlockByLevel(p)) { - Debug.Log("[Cuong] 1"); return true; } @@ -2116,7 +2114,6 @@ namespace CSNetwork strTemp = AUICommon.FilterInvalidTags(strTemp, pItem == null); if (!Chat_GameSession.PolicyResolver(pProtocol, p, ref strTemp, out szMsg)) { - Debug.Log("[Cuong] 2"); return false; } @@ -2140,7 +2137,7 @@ namespace CSNetwork break; case 18: case 19: case 20: case 21: case 22: // Auction Message // pGameUI.AddSysAuctionMessage(...) - Debug.Log("[Auction] " + strTemp); + Debug.Log("[Cuong] Auction " + strTemp); EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); break; case 24: // Task Message @@ -2165,18 +2162,15 @@ namespace CSNetwork } else { - Debug.Log("[Cuong] 5"); EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); } }else if (p.Channel == (byte)ChatChannel.GP_CHAT_INSTANCE && p.Srcroleid == 1) { - Debug.Log("[Cuong] 6"); // Chat_GameSession.AUICTranslate trans; // EC_Game.GetGameRun().GetUIManager().GetInGameUIMan().AddHeartBeatHint(trans.Translate(szMsg )); } else { - Debug.Log("[Cuong] Other"); CECStringTab pStrTab = EC_Game.GetFixedMsgs(); if (ISPLAYERID(p.Srcroleid)) @@ -2184,7 +2178,6 @@ namespace CSNetwork string szName = EC_Game.GetGameRun().GetPlayerName(p.Srcroleid, false); if (string.IsNullOrEmpty(szName)) { - Debug.Log("[Cuong] Other 0"); if (!bCalledagain) { Chat_GameSession.AddElemForPendingProtocols(pProtocol); @@ -2194,7 +2187,6 @@ namespace CSNetwork } else { - Debug.Log("[Cuong] Other 1"); char[] szText = new char[80]; AUICommon.AUI_ConvertChatString(ref szName,ref szText, false); @@ -2215,7 +2207,7 @@ namespace CSNetwork } else if(ISNPCID(p.Srcroleid)) { - Debug.Log("[Cuong] ISNPCID " + strTemp); + BMLogger.Log("[Cuong] ISNPCID " + strTemp); CECNPC pNPC = EC_Game.GetGameRun().GetWorld().GetNPCMan().GetNPC(p.Srcroleid); if (pNPC != null) @@ -2265,7 +2257,7 @@ namespace CSNetwork { // Format: "[Name] whispers to [You]: [Message]" string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT1)); - + BMLogger.Log($"[Cuong] OnPrtcPrivateChat {fmt}"); string formatted; try { formatted = string.Format(fmt, strSrcName, strMsg); diff --git a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs index 236e8b1530..ed432b3e49 100644 --- a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs +++ b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using BrewMonster.Network; using CSNetwork.GPDataType; namespace BrewMonster.UI @@ -29,8 +30,11 @@ namespace BrewMonster.UI { ChatChannel.GP_CHAT_COUNTRY, "FFFFFF" } }; - public const string NPC_COLOR = "C8FF64"; - public const string KING_COLOR = "8A2BE2"; + public const string NPC_COLOR = "C8FF64"; + public const string KING_COLOR = "8A2BE2"; + // [Port] C++: CDlgChat::m_pszWhisperFriendColor = "^FF4AB0" + // Màu hồng cho whisper từ/tới bạn bè trong danh sách + public const string WHISPER_FRIEND_COLOR = "FF4AB0"; /// /// Formats a message with TextMeshPro color tags based on the channel. @@ -55,10 +59,22 @@ namespace BrewMonster.UI /// /// Returns the hex color string for a given channel. /// Mirrors CDlgChat::GetChatColor in C++. - /// idPlayer is reserved for future per-player coloring (e.g. friend highlight). /// + /// Chat channel + /// Sender/recipient role ID — used to check friend status for GP_CHAT_WHISPER public static string GetChatColor(ChatChannel channel, int idPlayer = -1) { + // [Port] C++: DlgChat.cpp dòng 142-157 + // if (iChannel == GP_CHAT_WHISPER && pFriendMan->GetFriendByID(idPlayer)) + // return m_pszWhisperFriendColor; // "^FF4AB0" (hồng) + if (channel == ChatChannel.GP_CHAT_WHISPER && idPlayer > 0) + { + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + var friendMan = host?.GetFriendMan(); + if (friendMan != null && friendMan.GetFriendByID(idPlayer).HasValue) + return WHISPER_FRIEND_COLOR; + } + if (ChannelColors.TryGetValue(channel, out string hexColor)) return hexColor; return "FFFFFF"; diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs index 1254ab6dc8..9240ef47eb 100644 --- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs +++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs @@ -91,9 +91,9 @@ namespace BrewMonster.UI void OnWhisper(int characterId) { string name = EC_ManMessageMono.Instance?.GetECManPlayer?.GetElsePlayer(characterId)?.GetName() ?? ""; - Debug.Log("OnWhisper: " + characterId + " name: " + name); - - EventBus.Publish(new BrewMonster.Scripts.ChatUI.WhisperPlayerEvent(name)); + Debug.Log("[Cuong] OnWhisper: " + characterId + " name: " + name); + + EventBus.Publish(new Scripts.ChatUI.WhisperPlayerEvent(name)); } void PositionAtMouse() diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs index b538a03e37..d11b434cc6 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs @@ -516,11 +516,11 @@ namespace BrewMonster.UI string parsedMsg = pszMsg; if (parsedMsg.Contains("&")) { - // Convert &Cuong& into Cuong + // Convert &Cuong& into Cuong parsedMsg = System.Text.RegularExpressions.Regex.Replace( parsedMsg, @"&([^&]+)&", - $"$1" + $"$1" ); } else @@ -534,7 +534,6 @@ namespace BrewMonster.UI // C++: Write to m_pDlgChatWhisper, Chat1, Chat2, SuperFarCry // Unity equivalent: Dispatch to EventBus for ChatPanelUI to handle - Debug.Log($"[Cuong][{cChannel}] {formattedMsg}"); EventBus.Publish(new GameSession.ChatMessageEvent(formattedMsg, (byte)cChannel)); // C++: AddChatMessage also handles head bubble via pPlayer->SetLastSaidWords @@ -635,7 +634,7 @@ namespace BrewMonster.UI // m_vecIconList.push_back( m_pA2DSpriteIcons[h] ); // add for imaged hints // } - + // SetImageList(&m_vecIconList); // add for imaged hints // for( h = 0; h < ICONS_MAX; h++ ) @@ -643,7 +642,7 @@ namespace BrewMonster.UI // bval = m_pA2DSpriteIcons[h]->ResetItems( // a_nCountX[h] * a_nCountY[h], a_rc[h]); // a_free(a_rc[h]); - // if( !bval ) return AUI_ReportError(__LINE__, __FILE__); + // if( !bval ) return AUI_ReportError(__LINE__, __FILE__); // } // m_pA2DSpriteMask = new A2DSprite; @@ -658,7 +657,7 @@ namespace BrewMonster.UI // if( !m_pA2DSpriteItemExpire ) return AUI_ReportError(__LINE__, __FILE__); // bval = m_pA2DSpriteItemExpire->Init(m_pA3DDevice, "InGame\\IconItemExpire.dds", AUI_COLORKEY); // if( !bval ) - // { + // { // delete m_pA2DSpriteItemExpire; // m_pA2DSpriteItemExpire = NULL; // AUI_ReportError(__LINE__, "CECGameUIMan::LoadIconSet(), failed to load InGame\\IconItemExpire.dds"); @@ -720,7 +719,7 @@ namespace BrewMonster.UI // // ����ռλ�� // m_pA2DSpriteImage.push_back(NULL); // } - + // PAUIDIALOG pShow = GetDialog("Win_Popface"); // pShow->SetData(AUIMANAGER_MAX_EMOTIONGROUPS); // pShow = GetDialog("Win_Popface01"); @@ -760,10 +759,10 @@ namespace BrewMonster.UI // m_pSpriteIconSysModule.push_back(pSprite); // } - + // return true; // } - + } public enum EC_GAMEUI_ICONS : byte diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs index 8eec819abf..9204c57da9 100644 --- a/Assets/Scripts/ChatInputHandler.cs +++ b/Assets/Scripts/ChatInputHandler.cs @@ -4,6 +4,7 @@ using TMPro; using CSNetwork.GPDataType; using System.Collections.Generic; using BrewMonster.Network; +using BrewMonster.UI; using CSNetwork; namespace BrewMonster.Scripts.ChatUI @@ -429,15 +430,58 @@ namespace BrewMonster.Scripts.ChatUI private void SendPrivateChat(string target, string msg, int pack, int slot) { - Debug.Log($"Whisper to {target}: {msg}"); + BMLogger.Log($"[Cuong] Whisper to {target}: {msg}"); // [Port] C++: CDlgChat::OnCommand_speak → privatechat branch // Gửi tin nhắn mật (whisper) lên server qua GameSession. - UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, 0, pack, slot); + // Tra cứu ID của người nhận từ danh sách bạn bè (giống C++: pFriend ? pFriend->id : -1) + int idTarget = GetCharacterIdByName(target); + UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, idTarget, pack, slot); - // Server không echo whisper lại cho chính mình → hiện local echo - string localEcho = $"{target}: {msg}"; - EventBus.Publish(new GameSession.ChatMessageEvent(localEcho, (byte)ChatChannel.GP_CHAT_WHISPER)); + // [Port] C++: DlgChat.cpp dòng 1531-1532 + // Server KHÔNG echo whisper lại cho người gửi → phải hiển thị local. + // C++ dùng: a_sprintf(szMsg, GetStringFromTable(233), szName, szText) + // → format "对&Name&说:msg" — dấu & được thêm bởi FORMAT STRING, không phải bởi caller. + // C# dùng FIXMSG_PRIVATECHAT2 tương ứng; fallback nếu format string lỗi. + // QUAN TRỌNG: chỉ truyền target (không có & bao quanh) vào string.Format. + // Nếu format string đã có &{0}& thì kết quả đúng là "&target&". + // Nếu format string không có & thì AddChatMessage cũng chỉ wrap màu bình thường. + // Tránh truyền "&target&" vì kết quả sẽ là "&&target&&" → regex chỉ match "target" + // nhưng để lại & dư ở ngoài → UI hiển thị "&target&" thay vì link sạch. + CECStringTab pStrTab = EC_Game.GetFixedMsgs(); + string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT2)); + string localMsg; + try + { + localMsg = string.Format(fmt, target, msg); + } + catch + { + // Fallback: dùng &target& để AddChatMessage tạo link — format không có & thì mình thêm + localMsg = $"&{target}&: {msg}"; + } + + CECGameUIMan pGameUI = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); + if (pGameUI != null) + { + // idTarget là ID người NHẬN (hoặc -1 nếu không tìm thấy trong friend list) + // AddChatMessage sẽ convert &target& thành TMP link tag có dạng idTarget|target + pGameUI.AddChatMessage(localMsg, ChatChannel.GP_CHAT_WHISPER, idTarget, null, 0, 0, null, msg); + } + } + + // [Port] C++: pFriendMan->GetFriendByName(name) → pFriend->id + // Tìm character ID của người chơi theo tên từ friend list. + // Trả về -1 nếu không tìm thấy (giống C++ trả về idFriend = -1). + private int GetCharacterIdByName(string name) + { + if (string.IsNullOrEmpty(name)) return -1; + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host == null) return -1; + var friendMan = host.GetFriendMan(); + if (friendMan == null) return -1; + var friend = friendMan.GetFriendByName(name); + return friend.HasValue ? friend.Value.Id : -1; } // ===================================================== @@ -503,20 +547,27 @@ namespace BrewMonster.Scripts.ChatUI // 5. Publish event EventBus.Publish(new GameSession.ChatMessageEvent(finalMsg, (byte)channel)); - - Debug.Log("[Cuong] AddChatMessage" + finalMsg); } private string GetChannelColorHex(ChatChannel channel) { - // Simplified mapping based on common PW colors + // [Port] C++: CDlgChat::m_pszColor[] — DlgChat.cpp dòng 120-136 + // Giữ đúng màu gốc cho các kênh dùng trong local error messages return channel switch { - ChatChannel.GP_CHAT_TEAM => "00FFFF", // Cyan - ChatChannel.GP_CHAT_FACTION => "00FF00", // Green - ChatChannel.GP_CHAT_FARCRY => "FFFF00", // Yellow - ChatChannel.GP_CHAT_WHISPER => "FF00FF", // Magenta - _ => "FFFFFF" // White + ChatChannel.GP_CHAT_LOCAL => "FFFFFF", + ChatChannel.GP_CHAT_FARCRY => "FFE400", + ChatChannel.GP_CHAT_TEAM => "00FF00", + ChatChannel.GP_CHAT_FACTION => "00FFFC", + ChatChannel.GP_CHAT_WHISPER => "0065FE", // C++: "^0065FE" — KHÔNG phải FF00FF + ChatChannel.GP_CHAT_TRADE => "FF742E", + ChatChannel.GP_CHAT_SYSTEM => "BED293", + ChatChannel.GP_CHAT_BROADCAST => "FF3600", + ChatChannel.GP_CHAT_MISC => "9AA6FF", + ChatChannel.GP_CHAT_SUPERFARCRY => "ff9b3e", + ChatChannel.GP_CHAT_BATTLE => "FFFFFF", + ChatChannel.GP_CHAT_COUNTRY => "FFFFFF", + _ => "FFFFFF" }; } diff --git a/Assets/Scripts/EC_GameRun.cs b/Assets/Scripts/EC_GameRun.cs index f4fb44f9cb..0932be7aba 100644 --- a/Assets/Scripts/EC_GameRun.cs +++ b/Assets/Scripts/EC_GameRun.cs @@ -796,7 +796,7 @@ public partial class CECGameRun : ITickable CECGameUIMan pGameUI = m_pUIManager?.GetInGameUIMan(); if (pGameUI != null) { - //pGameUI.AddChatMessage(szFormattedMsg, (int)GP_CHAT.GP_CHAT_SYSTEM); + pGameUI.AddChatMessage(szFormattedMsg, ChatChannel.GP_CHAT_SYSTEM); } else { @@ -888,7 +888,7 @@ public partial class CECGameRun : ITickable else { // Fallback to debug output if UI is not available - Debug.LogError($"[Cuong] [Error] pGameUI == null"); + BMLogger.LogError($"[Cuong] [Error] pGameUI == null"); // Note: In C++ original, pItem is deleted here if UI is not available // In C#, we don't need explicit deletion due to garbage collection diff --git a/Assets/Scripts/EC_GameRunChat.cs b/Assets/Scripts/EC_GameRunChat.cs index 5ba9947eaf..5f480a64b1 100644 --- a/Assets/Scripts/EC_GameRunChat.cs +++ b/Assets/Scripts/EC_GameRunChat.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using BrewMonster; using CSNetwork; using CSNetwork.GPDataType; using BrewMonster.Common; @@ -42,8 +43,6 @@ partial class CECGameRun /// public void ShowAccountLoginInfo() { - Debug.Log("[Cuong] ShowAccountLoginInfo"); - // Kiểm tra cờ xem đã hiển thị thông tin đăng nhập chưa để tránh in lặp lại nhiều lần. if (!m_bAccountLoginInfoShown) { @@ -68,7 +67,7 @@ partial class CECGameRun string textTime = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9343), timeStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textTime, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textTime}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textTime}"); // 2. Định dạng và hiển thị IP đăng nhập lần trước (Last login IP). // Table 9344: chuỗi dạng "IP đăng nhập trước: %s" @@ -76,7 +75,7 @@ partial class CECGameRun string textIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9344), ipStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textIp}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textIp}"); // 3. Định dạng và hiển thị IP đăng nhập hiện tại (Current login IP). // Table 9345: chuỗi dạng "IP đăng nhập hiện tại: %s" @@ -84,7 +83,7 @@ partial class CECGameRun string textCurIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9345), curIpStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textCurIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}"); } } } @@ -106,7 +105,6 @@ partial class CECGameRun { string text = "Hoàn tất thông tin tài khoản..."; // 9347 EventBus.Publish(new GameSession.ChatMessageEvent { context = text, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountInfo {text}"); } } } diff --git a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md index 7435b3639a..6db6b8bb73 100644 --- a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md +++ b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md @@ -66,16 +66,23 @@ ACString str; str.Format(GetStringFromTable(11326), needMoney, needSp); ``` -**C# (string)**: +**C#**: +**Method A: Using `AUIDialog.FormatPrintf` (Recommended for Legacy Strings)** +If the string from the table uses C++ format specifiers (`%d`, `%s`, `%f`), `AUIDialog.FormatPrintf` handles them natively without requiring string conversion. +```csharp +string confirmMessage = AUIDialog.FormatPrintf(GetStringFromTable(11326), needMoney, needSp); +``` + +**Method B: Using `string.Format` (Standard C#)** +If the string table has been updated to use C# format specifiers (`{0}`, `{1}`), you can use the built-in `string.Format`. ```csharp string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp); ``` **Notes**: -- C++'s `ACString::Format()` is a member function -- C#'s `string.Format()` is a static function -- Both use the same placeholder syntax: `%d` (C++) or `{0}`, `{1}` (C#) -- If your string table uses C++ format specifiers, you may need to convert them: +- C++'s `ACString::Format()` is a member function. +- C#'s `string.Format()` is a static function but only understands `{0}`, `{1}` syntax. +- If your string table still uses C++ format specifiers (`%d`, `%s`, `%f`), **you should use `AUIDialog.FormatPrintf`**. Otherwise, you would need to manually convert the placeholders in the string table: - `%d` → `{0}`, `{1}` for integers - `%s` → `{0}`, `{1}` for strings - `%f` → `{0}`, `{1}` for floats @@ -346,11 +353,66 @@ if (spOK) { } ``` +### Example 4: Clickable Text Links (TextMeshPro) + +**C++ (Rich Text and Actions)**: +In C++, player names or interactive elements (like items) in chat are often wrapped with custom delimiters like `&PlayerName&` to be parsed later for coloring and clicking. The click actions are handled by complex UI dialogue components. + +**C# (Unity TextMeshPro `` tag)**: +In Unity's TextMeshPro, we can use the standard `` tag to define an interactive region of text. By parsing the legacy `&PlayerName&` format using Regex, we can inject a `` tag that Unity's event system can interact with. + +1. **Format the string with Regex replacing delimiters (e.g., in `EC_GameUIMan.cs`)**: +```csharp +if (parsedMsg.Contains("&")) +{ + // Convert &PlayerName& into PlayerName + parsedMsg = System.Text.RegularExpressions.Regex.Replace( + parsedMsg, + @"&([^&]+)&", + $"$1" + ); +} +``` + +2. **Handle the Click Event via Unity EventSystems (e.g., in `ChatMessageView.cs`)**: +The UI View script attached to the TextMeshPro text must implement `IPointerClickHandler` to intercept pointer clicks on the `` markup. +```csharp +using UnityEngine.EventSystems; +using TMPro; + +public class ChatMessageView : MonoBehaviour, IPointerClickHandler +{ + public EC_UIUtility.TextOutlet messageText; + + public void OnPointerClick(PointerEventData eventData) + { + if (messageText == null || messageText.tmp == null) return; + + // Check if the pointer click intersects with any tag boundary + int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera); + if (linkIndex != -1) + { + // Retrieve the link ID (which we dynamically set to the player's name) + TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex]; + string linkId = linkInfo.GetLinkID(); + + if (!string.IsNullOrEmpty(linkId)) + { + // Trigger logic, e.g., Whisper the player! + EventBus.Publish(new WhisperPlayerEvent(linkId)); + } + } + } +} +``` + +**Note for specific PW Dialogues**: Legacy classes (such as `DlgNameLink.cs`) may implement a customized command pattern (e.g., `LinkCommand`, `MoveToLinkCommand` alongside `StyledTaskTraceText`) to process complex hyperlink commands recursively. However, for completely rewritten or standalone UI systems like standard Chat or logging panels, utilizing TextMeshPro's native `TMP_TextUtilities.FindIntersectingLink` combined with `IPointerClickHandler` is significantly faster, lightweight, and standard for Unity development. + --- ## Common Pitfalls -### ❌ Wrong: Using C++ format specifiers in C# +### ❌ Wrong: Using C++ format specifiers with `string.Format` ```csharp // This won't work if the string uses C++ format specifiers @@ -358,8 +420,15 @@ string template = "Cost: %d gold"; // C++ style string message = string.Format(template, 1000); // Error! ``` -### ✅ Correct: Convert to C# format +### ✅ Correct: Use `AUIDialog.FormatPrintf` OR convert to C# format +**Option 1: Use AUIDialog.FormatPrintf for C++ styles:** +```csharp +string template = "Cost: %d gold"; // C++ style +string message = AUIDialog.FormatPrintf(template, 1000); // "Cost: 1000 gold" +``` + +**Option 2: Convert to C# format strings:** ```csharp string template = "Cost: {0} gold"; // C# style string message = string.Format(template, 1000); // "Cost: 1000 gold"