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

508 lines
16 KiB
Markdown

# 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 = "<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;
}
```
### 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 `<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`)**:
```csharp
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>"
);
}
```
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 `<link>` 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 <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`
```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 `<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!