# C++ to C# Unity String Format Conversion Guide ## Overview This guide explains how to convert the C++ string formatting pattern used in Perfect World Online to C# Unity. --- ## The Original C++ Code From `DlgSkillSubListItem.cpp`, line 290: ```cpp int needMoney = CECHostSkillModel::Instance().GetSkillMoney(m_skillID, m_curLevel + 1); int needSp = CECHostSkillModel::Instance().GetSkillSp(m_skillID, m_curLevel + 1); ACString str; str.Format(GetStringFromTable(11326), needMoney, needSp); GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox); pMsgBox->SetData(m_skillID); ``` ### What This Code Does: 1. **Gets skill upgrade costs**: Retrieves the money and skill points needed for the next level 2. **Formats localized string**: Uses `GetStringFromTable(11326)` to get a template string, then formats it with the cost values 3. **Shows confirmation dialog**: Displays a message box with OK/Cancel buttons 4. **Attaches data**: Associates the skill ID with the dialog --- ## The C# Unity Conversion From `CDlgSkillSubListItem.cs`: ```csharp // Get the required money and skill points for the next level int needMoney = CECHostSkillModel.Instance.GetSkillMoney(m_skillID, m_curLevel + 1); int needSp = CECHostSkillModel.Instance.GetSkillSp(m_skillID, m_curLevel + 1); // Format the confirmation message string // C++ equivalent: str.Format(GetStringFromTable(11326), needMoney, needSp); string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp); // Show confirmation dialog with OK/Cancel buttons uiManager.ShowConfirmDialog( "Game_LearnSkill", confirmMessage, onConfirm: () => { // Player confirmed - learn the skill CECHostSkillModel.Instance.LearnSkill(m_skillID); }, onCancel: null, skillData: m_skillID ); ``` --- ## Key Conversion Patterns ### 1. String Formatting **C++ (ACString)**: ```cpp ACString str; str.Format(GetStringFromTable(11326), needMoney, needSp); ``` **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 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 ### 2. Message Box with Callbacks **C++ (Pointer-based)**: ```cpp PAUIDIALOG pMsgBox; GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox); pMsgBox->SetData(m_skillID); // Later, in message handler: void OnMessageBox_OK(const char* szName) { if (strcmp(szName, "Game_LearnSkill") == 0) { PAUIDIALOG pMsgBox = GetGameUIMan()->GetDialog(szName); int skillID = pMsgBox->GetData(); // Learn skill... } } ``` **C# (Callback-based)**: ```csharp uiManager.ShowConfirmDialog( "Game_LearnSkill", confirmMessage, onConfirm: () => { // This code runs when player clicks OK CECHostSkillModel.Instance.LearnSkill(m_skillID); }, onCancel: () => { // This code runs when player clicks Cancel (optional) }, skillData: m_skillID ); ``` **Key Differences**: - C++ uses pointers and manual dialog lookup - C# uses lambda callbacks (closures) that capture variables - C# is more straightforward - no need to check dialog names or retrieve data manually ### 3. Localized String Table **Both versions use the same approach**: ```csharp // Get localized string template from string table // String ID 11326 might contain: "学习此技能需要 {0} 金钱和 {1} 技能点,确定学习吗?" // English: "Learning this skill requires {0} money and {1} skill points, confirm?" string template = GetStringFromTable(11326); // Format with actual values string message = string.Format(template, needMoney, needSp); // Result: "Learning this skill requires 2480 money and 34200 skill points, confirm?" ``` --- ## Complete Example Comparison ### C++ Version (Original) ```cpp void CDlgSkillSubListItem::OnCommand_Upgrade(const char* szCommand) { CECHostPlayer* player = GetHostPlayer(); PAUIDIALOG pMsgBox; // Check player state... if (player->IsDead() || player->IsSitting() || /* ... */) { GetGameUIMan()->MessageBox("", GetGameUIMan()->GetStringFromTable(11327), MB_OK, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox); return; } // Check learning conditions... int nCondition = CECHostSkillModel::Instance().CheckLearnCondition(m_skillID); int nCheckCode = 0; if (1 == nCondition) { nCheckCode = 270; } else if (6 == nCondition) { nCheckCode = 527; } // ... more conditions ... if (nCheckCode == 0) { // Show confirmation dialog int needMoney = CECHostSkillModel::Instance().GetSkillMoney(m_skillID, m_curLevel + 1); int needSp = CECHostSkillModel::Instance().GetSkillSp(m_skillID, m_curLevel + 1); ACString str; str.Format(GetStringFromTable(11326), needMoney, needSp); GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox); pMsgBox->SetData(m_skillID); if (!GetGameUIMan()->m_pDlgSkillAction->IsReceivedNPCGreeting()) { CECHostSkillModel::Instance().SendHelloToSkillLearnNPC(); } } else { // Show error message GetGameUIMan()->MessageBox("", GetGameUIMan()->GetStringFromTable(nCheckCode), MB_OK, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox); pMsgBox->SetLife(3); } } // Separate message handler void CDlgSkillSubListItem::OnMessageBox_OK(const char* szName) { if (strcmp(szName, "Game_LearnSkill") == 0) { PAUIDIALOG pMsgBox = GetGameUIMan()->GetDialog(szName); int skillID = pMsgBox->GetData(); // Send learn skill command to server CECHostSkillModel::Instance().LearnSkill(skillID); } } ``` ### C# Unity Version (Converted) ```csharp private void OnCommand_Upgrade() { CECHostPlayer player = GetHostPlayer(); if (player == null) { return; } var uiManager = CECUIManager.Instance; var gameUIMan = uiManager.GetInGameUIMan(); // Check player state... if (player.IsDead() || player.IsSitting() || player.IsChangingFace() || player.IsTrading() || player.GetBoothState() != 0 || player.IsRooting() || player.IsHangerOn() || player.GetCurSkill() != null || player.IsFighting()) { uiManager.ShowMessageBox("MessageBox", gameUIMan.GetStringFromTable(11327)); return; } // Check learning conditions... int nCondition = CECHostSkillModel.Instance.CheckLearnCondition(m_skillID); int nCheckCode = 0; if (1 == nCondition) { nCheckCode = 270; } else if (6 == nCondition) { nCheckCode = 527; } // ... more conditions ... if (nCheckCode == 0) { // Get the required costs int needMoney = CECHostSkillModel.Instance.GetSkillMoney(m_skillID, m_curLevel + 1); int needSp = CECHostSkillModel.Instance.GetSkillSp(m_skillID, m_curLevel + 1); // Format the confirmation message string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp); // Show confirmation dialog with inline callback uiManager.ShowConfirmDialog( "Game_LearnSkill", confirmMessage, onConfirm: () => { // This runs when player clicks OK // No need to look up dialog or retrieve data - it's captured in closure CECHostSkillModel.Instance.LearnSkill(m_skillID); }, onCancel: null, skillData: m_skillID ); // 如果打开对话框时NPC发送的Hello消息没有收到回复,再次发送 // If the NPC Hello message hasn't been received when opening dialog, send again if (!gameUIMan.IsReceivedNPCGreeting()) { CECHostSkillModel.Instance.SendHelloToSkillLearnNPC(); } } else { // Show error message with auto-close after 3 seconds uiManager.ShowMessageBox("", gameUIMan.GetStringFromTable(nCheckCode), autoCloseTime: 3f); } } ``` --- ## String Format Examples ### Example 1: Simple Integer Formatting **C++ String Table Entry (ID 11326)**: ``` "学习此技能需要 %d 金钱和 %d 技能点,确定学习吗?" ``` **C# String Table Entry (ID 11326)** (if you convert to C# format): ``` "学习此技能需要 {0} 金钱和 {1} 技能点,确定学习吗?" ``` **Usage**: ```csharp string message = string.Format(GetStringFromTable(11326), 2480, 34200); // Result: "学习此技能需要 2480 金钱和 34200 技能点,确定学习吗?" // English: "Learning this skill requires 2480 money and 34200 skill points, confirm?" ``` ### Example 2: Mixed Types **String Table**: ``` "Skill: {0}, Level: {1}, Cost: {2:N0} gold" ``` **Usage**: ```csharp string skillName = "Fireball"; int level = 5; int cost = 10000; string message = string.Format(GetStringFromTable(123), skillName, level, cost); // Result: "Skill: Fireball, Level: 5, Cost: 10,000 gold" ``` ### Example 3: With Colors (Unity TextMeshPro) **C++**: ```cpp ACString strSp; strSp.Format(GetStringFromTable(11402), needSp); if (spOK) { strSp = l_colorWhite + strSp; // l_colorWhite = "^ffffff" } else { strSp = l_colorRed + strSp; // l_colorRed = "^ff0000" } ``` **C# (Unity TextMeshPro)**: ```csharp const string l_colorWhite = ""; const string l_colorRed = ""; const string l_colorClose = ""; string strSp = string.Format(GetStringFromTable(11402), needSp); if (spOK) { strSp = l_colorWhite + strSp + l_colorClose; } else { strSp = l_colorRed + strSp + l_colorClose; } ``` ### 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 with `string.Format` ```csharp // This won't work if the string uses C++ format specifiers string template = "Cost: %d gold"; // C++ style string message = string.Format(template, 1000); // Error! ``` ### ✅ 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" ``` ### ❌ Wrong: Forgetting to capture variables in lambda ```csharp void ShowDialog(int skillID) { int needMoney = GetMoney(skillID); // Wrong: using skillID parameter instead of captured value ShowConfirmDialog("Learn", "Confirm?", onConfirm: () => { LearnSkill(skillID); // This captures the parameter, which is fine } ); } ``` This is actually correct, but be careful with loop variables: ```csharp // Dangerous in loops! for (int i = 0; i < skills.Length; i++) { int skillID = skills[i]; ShowButton(skillID, () => { LearnSkill(skillID); // Captures skillID, which changes each iteration }); } ``` ### ✅ Correct: Capture loop variable properly ```csharp for (int i = 0; i < skills.Length; i++) { int currentSkillID = skills[i]; // Create a copy for this iteration ShowButton(currentSkillID, () => { LearnSkill(currentSkillID); // Captures the copy, safe! }); } ``` --- ## Implementation Checklist When converting C++ string formatting to C# Unity: - [ ] Replace `ACString` with `string` - [ ] Replace `str.Format(...)` with `string.Format(...)` - [ ] Convert format specifiers if needed (`%d` → `{0}`) - [ ] Replace pointer-based message boxes with callback-based dialogs - [ ] Use lambda expressions to capture context - [ ] Update color codes for Unity (TextMeshPro uses ``) - [ ] Replace `pMsgBox->SetLife(seconds)` with `autoCloseTime` parameter - [ ] Remove manual dialog lookup code - [ ] Keep Chinese comments and add English translations side by side --- ## Summary The key difference between C++ and C# Unity for string formatting: | Aspect | C++ | C# Unity | |--------|-----|----------| | **String type** | `ACString` | `string` | | **Format method** | `str.Format(template, args)` | `string.Format(template, args)` | | **Format specifiers** | `%d`, `%s`, `%f` | `{0}`, `{1}`, `{2}` | | **Message box** | Pointer + callback lookup | Lambda callbacks | | **Data passing** | `pMsgBox->SetData(data)` | Closure captures | | **Colors** | `^ffffff` | `...` | | **Auto-close** | `pMsgBox->SetLife(seconds)` | `autoCloseTime: seconds` | The C# version is generally more concise and type-safe thanks to lambda expressions and closure capture!