Files
test/CONVERSION_GUIDE_STRING_FORMAT.md
2026-01-13 10:55:51 +07:00

13 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# (string):

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:
    • %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;
}

Common Pitfalls

Wrong: Using C++ format specifiers in C#

// 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: Convert to C# format

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!