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/Dialogs/DlgProduce.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs index 561c03662c..936bbfbb21 100644 --- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs +++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs @@ -28,7 +28,7 @@ namespace BrewMonster [SerializeField] private GameObject itemPb; [Header("Quantity")] - [SerializeField] private List quantityText; + [SerializeField] private TextMeshProUGUI quantityText; [SerializeField] private Button quantityIncreaseBtn; [SerializeField] private Button quantityDecreaseBtn; [SerializeField] private Button quantityMaxBtn; @@ -42,7 +42,7 @@ namespace BrewMonster [SerializeField] private List materialSlots = new List(); [Header("Result Slot")] - [SerializeField] private Transform itemResult; + [SerializeField] private Image itemResult; [Header("Item Info Panel")] public Transform itemInfoRoot; @@ -56,6 +56,9 @@ namespace BrewMonster [SerializeField] private TextMeshProUGUI weponDescInfoText; [SerializeField] private TextMeshProUGUI weponExtraInfoText; + [Header("Default")] + [SerializeField] private Sprite khung_item; + private NPC_MAKE_SERVICE? cachedMakeService = null; private int currentTabIndex = 0; private uint selectedRecipeId = 0; // Track the currently selected recipe @@ -79,8 +82,7 @@ namespace BrewMonster public override void Start() { - quantityText[0].text = currentQuantity.ToString(); - quantityText[1].text = currentQuantity.ToString(); + quantityText.text = currentQuantity.ToString(); quantityDecreaseBtn.onClick.AddListener(OnClickDecreaseBtn); quantityIncreaseBtn.onClick.AddListener(OnClickIncreaseBtn); quantityMaxBtn.onClick.AddListener(OnClickMaxBtn); @@ -309,8 +311,15 @@ namespace BrewMonster btn.onClick.RemoveAllListeners(); btn.onClick.AddListener(() => { - ShowItemInfoByRecipe(recipeId); + bool isNewRecipe = selectedRecipeId != recipeId; + selectedRecipeId = recipeId; + if (isNewRecipe) + { + currentQuantity = 1; + UpdateQuantityText(currentQuantity); + } ShowRecipeMaterials(recipeId); + ShowItemInfoByRecipe(recipeId); }); } @@ -331,14 +340,12 @@ namespace BrewMonster { if (slot == null) continue; - slot.gameObject.SetActive(false); - Transform iconTf = slot.Find("item"); if (iconTf != null) { Image img = iconTf.GetComponent(); if (img != null) - img.sprite = null; + img.sprite = khung_item; } Transform qtyTf = slot.Find("text_quantity"); @@ -352,30 +359,14 @@ namespace BrewMonster if (itemResult != null) { - itemResult.gameObject.SetActive(false); - - Transform iconTf = itemResult.Find("item"); - if (iconTf != null) - { - Image img = iconTf.GetComponent(); - if (img != null) - img.sprite = null; - } - - Transform qtyTf = itemResult.Find("text_quantity"); - if (qtyTf != null) - { - TextMeshProUGUI txt = qtyTf.GetComponent(); - if (txt != null) - txt.text = ""; - } + itemResult.sprite = khung_item; } } public void ShowRecipeMaterials(uint recipeId) { - selectedRecipeId = recipeId; // Track the selected recipe + selectedRecipeId = recipeId; ClearMaterialSlots(); var edm = ElementDataManProvider.GetElementDataMan(); @@ -395,32 +386,20 @@ namespace BrewMonster { uint outputItemId = recipe.targets[0].id_to_make; - itemResult.gameObject.SetActive(true); - - Transform iconTf = itemResult.Find("item"); - if (iconTf != null) + if (itemResult != null) { - Image img = iconTf.GetComponent(); - if (img != null) + if (itemResult != null) { Sprite sp = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)outputItemId); if (sp != null) { - img.sprite = sp; - img.enabled = true; - img.preserveAspect = true; + itemResult.sprite = sp; + itemResult.enabled = true; + itemResult.preserveAspect = true; } } } - Transform qtyTf = itemResult.Find("text_quantity"); - if (qtyTf != null) - { - TextMeshProUGUI txt = qtyTf.GetComponent(); - if (txt != null) - txt.text = "1"; - } - Button resultBtn = itemResult.GetComponent