Files
test/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md
T
2026-04-01 16:47:02 +07:00

16 KiB

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:

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:

// 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):

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.

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.

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):

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):

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:

// 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)

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)

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:

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:

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++:

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):

const string l_colorWhite = "<color=#ffffff>";
const string l_colorRed = "<color=#ff0000>";
const string l_colorClose = "</color>";

string strSp = string.Format(GetStringFromTable(11402), needSp);

if (spOK) {
    strSp = l_colorWhite + strSp + l_colorClose;
} else {
    strSp = l_colorRed + strSp + l_colorClose;
}

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 <link> tag): In Unity's TextMeshPro, we can use the standard <link> tag to define an interactive region of text. By parsing the legacy &PlayerName& format using Regex, we can inject a <link> tag that Unity's event system can interact with.

  1. Format the string with Regex replacing delimiters (e.g., in EC_GameUIMan.cs):
if (parsedMsg.Contains("&"))
{
    // Convert &PlayerName& into <color=#HexColor><u><link="PlayerName">PlayerName</link></u></color>
    parsedMsg = System.Text.RegularExpressions.Regex.Replace(
        parsedMsg,
        @"&([^&]+)&",
        $"<color=#{colorHex}><u><link=\"$1\">$1</link></u></color>"
    );
}
  1. 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 <link> markup.
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 <link> 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

// 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:

string template = "Cost: %d gold";  // C++ style
string message = AUIDialog.FormatPrintf(template, 1000);  // "Cost: 1000 gold"

Option 2: Convert to C# format strings:

string template = "Cost: {0} gold";  // C# style
string message = string.Format(template, 1000);  // "Cost: 1000 gold"

Wrong: Forgetting to capture variables in lambda

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:

// 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

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 <color=#RRGGBB>)
  • 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 <color=#ffffff>...</color>
Auto-close pMsgBox->SetLife(seconds) autoCloseTime: seconds

The C# version is generally more concise and type-safe thanks to lambda expressions and closure capture!