From a82337f61787cddb7dfafba972e5841da446ed27 Mon Sep 17 00:00:00 2001
From: CuongNV <>
Date: Wed, 1 Apr 2026 16:47:02 +0700
Subject: [PATCH] update skill for chat
---
.agents/skills/chat_system_csharp/SKILL.md | 26 ++++++
.../CONVERSION_GUIDE_STRING_FORMAT.md | 83 +++++++++++++++++--
2 files changed, 102 insertions(+), 7 deletions(-)
create mode 100644 .agents/skills/chat_system_csharp/SKILL.md
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/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"