Merge pull request 'feature/chat_01' (#282) from feature/chat_01 into develop
Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/282
This commit is contained in:
@@ -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 {...})`.
|
||||
@@ -3,10 +3,13 @@ using BrewMonster.Scripts.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using BrewMonster.Network;
|
||||
using CSNetwork.GPDataType;
|
||||
|
||||
namespace BrewMonster.Scripts.ChatUI
|
||||
{
|
||||
public class ChatMessageView : MonoBehaviour
|
||||
public class ChatMessageView : MonoBehaviour, IPointerClickHandler
|
||||
{
|
||||
public Image iconImage;
|
||||
public EC_UIUtility.TextOutlet messageText;
|
||||
@@ -24,5 +27,41 @@ namespace BrewMonster.Scripts.ChatUI
|
||||
messageText.Set(message);
|
||||
GetComponent<IRefreshLayout>().RefreshLayout();
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (messageText == null || messageText.tmp == null) return;
|
||||
|
||||
int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera);
|
||||
if (linkIndex != -1)
|
||||
{
|
||||
TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex];
|
||||
string linkId = linkInfo.GetLinkID();
|
||||
if (!string.IsNullOrEmpty(linkId))
|
||||
{
|
||||
string playerName = linkId;
|
||||
int characterId = 0;
|
||||
|
||||
if (linkId.Contains("|"))
|
||||
{
|
||||
string[] parts = linkId.Split('|');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
int.TryParse(parts[0], out characterId);
|
||||
playerName = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
var host = EC_Game.GetGameRun()?.GetHostPlayer();
|
||||
if (host != null && characterId > 0 && characterId != host.GetCharacterID() && GPDataTypeHelper.ISPLAYERID(characterId))
|
||||
{
|
||||
CECUIManager.Instance?.ShowPlayerOptionsDialog(characterId, eventData.position + new Vector2(0, 400));
|
||||
}
|
||||
|
||||
Debug.Log("[Cuong] OnWhisper from Chat: name: " + playerName);
|
||||
EventBus.Publish(new WhisperPlayerEvent(playerName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace BrewMonster.Scripts.ChatUI
|
||||
errorMsg = $"Lỗi không xác định";
|
||||
}
|
||||
|
||||
string coloredMsg = $"<color=red>[System Error {e.ErrorCode}] {errorMsg}</color>";
|
||||
string coloredMsg = $"<color=red>{errorMsg}</color>";
|
||||
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent(
|
||||
coloredMsg,
|
||||
|
||||
@@ -2099,11 +2099,9 @@ namespace CSNetwork
|
||||
|
||||
chatmessage p = (chatmessage)pProtocol;
|
||||
//var channel = (ChatChannel)p.Channel;
|
||||
Debug.Log("[Cuong] reciver chat channel: " + p.Channel);
|
||||
|
||||
if (Chat_GameSession.ShouldBlockByLevel(p))
|
||||
{
|
||||
Debug.Log("[Cuong] 1");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2116,7 +2114,6 @@ namespace CSNetwork
|
||||
strTemp = AUICommon.FilterInvalidTags(strTemp, pItem == null);
|
||||
if (!Chat_GameSession.PolicyResolver(pProtocol, p, ref strTemp, out szMsg))
|
||||
{
|
||||
Debug.Log("[Cuong] 2");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2140,7 +2137,7 @@ namespace CSNetwork
|
||||
break;
|
||||
case 18: case 19: case 20: case 21: case 22: // Auction Message
|
||||
// pGameUI.AddSysAuctionMessage(...)
|
||||
Debug.Log("[Auction] " + strTemp);
|
||||
Debug.Log("[Cuong] Auction " + strTemp);
|
||||
EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel));
|
||||
break;
|
||||
case 24: // Task Message
|
||||
@@ -2165,18 +2162,15 @@ namespace CSNetwork
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[Cuong] 5");
|
||||
EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel));
|
||||
}
|
||||
}else if (p.Channel == (byte)ChatChannel.GP_CHAT_INSTANCE && p.Srcroleid == 1)
|
||||
{
|
||||
Debug.Log("[Cuong] 6");
|
||||
// Chat_GameSession.AUICTranslate trans;
|
||||
// EC_Game.GetGameRun().GetUIManager().GetInGameUIMan().AddHeartBeatHint(trans.Translate(szMsg ));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[Cuong] Other");
|
||||
CECStringTab pStrTab = EC_Game.GetFixedMsgs();
|
||||
|
||||
if (ISPLAYERID(p.Srcroleid))
|
||||
@@ -2184,7 +2178,6 @@ namespace CSNetwork
|
||||
string szName = EC_Game.GetGameRun().GetPlayerName(p.Srcroleid, false);
|
||||
if (string.IsNullOrEmpty(szName))
|
||||
{
|
||||
Debug.Log("[Cuong] Other 0");
|
||||
if (!bCalledagain)
|
||||
{
|
||||
Chat_GameSession.AddElemForPendingProtocols(pProtocol);
|
||||
@@ -2194,7 +2187,6 @@ namespace CSNetwork
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[Cuong] Other 1");
|
||||
char[] szText = new char[80];
|
||||
AUICommon.AUI_ConvertChatString(ref szName,ref szText, false);
|
||||
|
||||
@@ -2215,7 +2207,7 @@ namespace CSNetwork
|
||||
}
|
||||
else if(ISNPCID(p.Srcroleid))
|
||||
{
|
||||
Debug.Log("[Cuong] ISNPCID " + strTemp);
|
||||
BMLogger.Log("[Cuong] ISNPCID " + strTemp);
|
||||
CECNPC pNPC = EC_Game.GetGameRun().GetWorld().GetNPCMan().GetNPC(p.Srcroleid);
|
||||
|
||||
if (pNPC != null)
|
||||
@@ -2265,7 +2257,7 @@ namespace CSNetwork
|
||||
{
|
||||
// Format: "[Name] whispers to [You]: [Message]"
|
||||
string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT1));
|
||||
|
||||
BMLogger.Log($"[Cuong] OnPrtcPrivateChat {fmt}");
|
||||
string formatted;
|
||||
try {
|
||||
formatted = string.Format(fmt, strSrcName, strMsg);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using BrewMonster.Network;
|
||||
using CSNetwork.GPDataType;
|
||||
|
||||
namespace BrewMonster.UI
|
||||
@@ -29,8 +30,11 @@ namespace BrewMonster.UI
|
||||
{ ChatChannel.GP_CHAT_COUNTRY, "FFFFFF" }
|
||||
};
|
||||
|
||||
public const string NPC_COLOR = "C8FF64";
|
||||
public const string KING_COLOR = "8A2BE2";
|
||||
public const string NPC_COLOR = "C8FF64";
|
||||
public const string KING_COLOR = "8A2BE2";
|
||||
// [Port] C++: CDlgChat::m_pszWhisperFriendColor = "^FF4AB0"
|
||||
// Màu hồng cho whisper từ/tới bạn bè trong danh sách
|
||||
public const string WHISPER_FRIEND_COLOR = "FF4AB0";
|
||||
|
||||
/// <summary>
|
||||
/// Formats a message with TextMeshPro color tags based on the channel.
|
||||
@@ -55,10 +59,22 @@ namespace BrewMonster.UI
|
||||
/// <summary>
|
||||
/// Returns the hex color string for a given channel.
|
||||
/// Mirrors CDlgChat::GetChatColor in C++.
|
||||
/// idPlayer is reserved for future per-player coloring (e.g. friend highlight).
|
||||
/// </summary>
|
||||
/// <param name="channel">Chat channel</param>
|
||||
/// <param name="idPlayer">Sender/recipient role ID — used to check friend status for GP_CHAT_WHISPER</param>
|
||||
public static string GetChatColor(ChatChannel channel, int idPlayer = -1)
|
||||
{
|
||||
// [Port] C++: DlgChat.cpp dòng 142-157
|
||||
// if (iChannel == GP_CHAT_WHISPER && pFriendMan->GetFriendByID(idPlayer))
|
||||
// return m_pszWhisperFriendColor; // "^FF4AB0" (hồng)
|
||||
if (channel == ChatChannel.GP_CHAT_WHISPER && idPlayer > 0)
|
||||
{
|
||||
var host = EC_Game.GetGameRun()?.GetHostPlayer();
|
||||
var friendMan = host?.GetFriendMan();
|
||||
if (friendMan != null && friendMan.GetFriendByID(idPlayer).HasValue)
|
||||
return WHISPER_FRIEND_COLOR;
|
||||
}
|
||||
|
||||
if (ChannelColors.TryGetValue(channel, out string hexColor))
|
||||
return hexColor;
|
||||
return "FFFFFF";
|
||||
|
||||
@@ -91,9 +91,9 @@ namespace BrewMonster.UI
|
||||
void OnWhisper(int characterId)
|
||||
{
|
||||
string name = EC_ManMessageMono.Instance?.GetECManPlayer?.GetElsePlayer(characterId)?.GetName() ?? "";
|
||||
Debug.Log("OnWhisper: " + characterId + " name: " + name);
|
||||
|
||||
EventBus.Publish(new BrewMonster.Scripts.ChatUI.WhisperPlayerEvent(name));
|
||||
Debug.Log("[Cuong] OnWhisper: " + characterId + " name: " + name);
|
||||
|
||||
EventBus.Publish(new Scripts.ChatUI.WhisperPlayerEvent(name));
|
||||
}
|
||||
|
||||
void PositionAtMouse()
|
||||
|
||||
@@ -516,11 +516,11 @@ namespace BrewMonster.UI
|
||||
string parsedMsg = pszMsg;
|
||||
if (parsedMsg.Contains("&"))
|
||||
{
|
||||
// Convert &Cuong& into <color=#HexColor>Cuong</color>
|
||||
// Convert &Cuong& into <color=#HexColor><link="idPlayer|Cuong">Cuong</link></color>
|
||||
parsedMsg = System.Text.RegularExpressions.Regex.Replace(
|
||||
parsedMsg,
|
||||
@"&([^&]+)&",
|
||||
$"<color=#{colorHex}>$1</color>"
|
||||
$"<color=#{colorHex}><u><link=\"{idPlayer}|$1\">$1</link></u></color>"
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -534,7 +534,6 @@ namespace BrewMonster.UI
|
||||
|
||||
// C++: Write to m_pDlgChatWhisper, Chat1, Chat2, SuperFarCry
|
||||
// Unity equivalent: Dispatch to EventBus for ChatPanelUI to handle
|
||||
Debug.Log($"[Cuong][{cChannel}] {formattedMsg}");
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent(formattedMsg, (byte)cChannel));
|
||||
|
||||
// C++: AddChatMessage also handles head bubble via pPlayer->SetLastSaidWords
|
||||
@@ -635,7 +634,7 @@ namespace BrewMonster.UI
|
||||
|
||||
// m_vecIconList.push_back( m_pA2DSpriteIcons[h] ); // add for imaged hints
|
||||
// }
|
||||
|
||||
|
||||
// SetImageList(&m_vecIconList); // add for imaged hints
|
||||
|
||||
// for( h = 0; h < ICONS_MAX; h++ )
|
||||
@@ -643,7 +642,7 @@ namespace BrewMonster.UI
|
||||
// bval = m_pA2DSpriteIcons[h]->ResetItems(
|
||||
// a_nCountX[h] * a_nCountY[h], a_rc[h]);
|
||||
// a_free(a_rc[h]);
|
||||
// if( !bval ) return AUI_ReportError(__LINE__, __FILE__);
|
||||
// if( !bval ) return AUI_ReportError(__LINE__, __FILE__);
|
||||
// }
|
||||
|
||||
// m_pA2DSpriteMask = new A2DSprite;
|
||||
@@ -658,7 +657,7 @@ namespace BrewMonster.UI
|
||||
// if( !m_pA2DSpriteItemExpire ) return AUI_ReportError(__LINE__, __FILE__);
|
||||
// bval = m_pA2DSpriteItemExpire->Init(m_pA3DDevice, "InGame\\IconItemExpire.dds", AUI_COLORKEY);
|
||||
// if( !bval )
|
||||
// {
|
||||
// {
|
||||
// delete m_pA2DSpriteItemExpire;
|
||||
// m_pA2DSpriteItemExpire = NULL;
|
||||
// AUI_ReportError(__LINE__, "CECGameUIMan::LoadIconSet(), failed to load InGame\\IconItemExpire.dds");
|
||||
@@ -720,7 +719,7 @@ namespace BrewMonster.UI
|
||||
// // ����ռλ��
|
||||
// m_pA2DSpriteImage.push_back(NULL);
|
||||
// }
|
||||
|
||||
|
||||
// PAUIDIALOG pShow = GetDialog("Win_Popface");
|
||||
// pShow->SetData(AUIMANAGER_MAX_EMOTIONGROUPS);
|
||||
// pShow = GetDialog("Win_Popface01");
|
||||
@@ -760,10 +759,10 @@ namespace BrewMonster.UI
|
||||
|
||||
// m_pSpriteIconSysModule.push_back(pSprite);
|
||||
// }
|
||||
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
public enum EC_GAMEUI_ICONS : byte
|
||||
|
||||
@@ -4,6 +4,7 @@ using TMPro;
|
||||
using CSNetwork.GPDataType;
|
||||
using System.Collections.Generic;
|
||||
using BrewMonster.Network;
|
||||
using BrewMonster.UI;
|
||||
using CSNetwork;
|
||||
|
||||
namespace BrewMonster.Scripts.ChatUI
|
||||
@@ -429,15 +430,58 @@ namespace BrewMonster.Scripts.ChatUI
|
||||
|
||||
private void SendPrivateChat(string target, string msg, int pack, int slot)
|
||||
{
|
||||
Debug.Log($"Whisper to {target}: {msg}");
|
||||
BMLogger.Log($"[Cuong] Whisper to {target}: {msg}");
|
||||
|
||||
// [Port] C++: CDlgChat::OnCommand_speak → privatechat branch
|
||||
// Gửi tin nhắn mật (whisper) lên server qua GameSession.
|
||||
UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, 0, pack, slot);
|
||||
// Tra cứu ID của người nhận từ danh sách bạn bè (giống C++: pFriend ? pFriend->id : -1)
|
||||
int idTarget = GetCharacterIdByName(target);
|
||||
UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, idTarget, pack, slot);
|
||||
|
||||
// Server không echo whisper lại cho chính mình → hiện local echo
|
||||
string localEcho = $"<color=#0065FE>{target}: {msg}</color>";
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent(localEcho, (byte)ChatChannel.GP_CHAT_WHISPER));
|
||||
// [Port] C++: DlgChat.cpp dòng 1531-1532
|
||||
// Server KHÔNG echo whisper lại cho người gửi → phải hiển thị local.
|
||||
// C++ dùng: a_sprintf(szMsg, GetStringFromTable(233), szName, szText)
|
||||
// → format "对&Name&说:msg" — dấu & được thêm bởi FORMAT STRING, không phải bởi caller.
|
||||
// C# dùng FIXMSG_PRIVATECHAT2 tương ứng; fallback nếu format string lỗi.
|
||||
// QUAN TRỌNG: chỉ truyền target (không có & bao quanh) vào string.Format.
|
||||
// Nếu format string đã có &{0}& thì kết quả đúng là "&target&".
|
||||
// Nếu format string không có & thì AddChatMessage cũng chỉ wrap màu bình thường.
|
||||
// Tránh truyền "&target&" vì kết quả sẽ là "&&target&&" → regex chỉ match "target"
|
||||
// nhưng để lại & dư ở ngoài → UI hiển thị "&target&" thay vì link sạch.
|
||||
CECStringTab pStrTab = EC_Game.GetFixedMsgs();
|
||||
string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT2));
|
||||
string localMsg;
|
||||
try
|
||||
{
|
||||
localMsg = string.Format(fmt, target, msg);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: dùng &target& để AddChatMessage tạo link — format không có & thì mình thêm
|
||||
localMsg = $"&{target}&: {msg}";
|
||||
}
|
||||
|
||||
CECGameUIMan pGameUI = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan();
|
||||
if (pGameUI != null)
|
||||
{
|
||||
// idTarget là ID người NHẬN (hoặc -1 nếu không tìm thấy trong friend list)
|
||||
// AddChatMessage sẽ convert &target& thành TMP link tag có dạng idTarget|target
|
||||
pGameUI.AddChatMessage(localMsg, ChatChannel.GP_CHAT_WHISPER, idTarget, null, 0, 0, null, msg);
|
||||
}
|
||||
}
|
||||
|
||||
// [Port] C++: pFriendMan->GetFriendByName(name) → pFriend->id
|
||||
// Tìm character ID của người chơi theo tên từ friend list.
|
||||
// Trả về -1 nếu không tìm thấy (giống C++ trả về idFriend = -1).
|
||||
private int GetCharacterIdByName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return -1;
|
||||
var host = EC_Game.GetGameRun()?.GetHostPlayer();
|
||||
if (host == null) return -1;
|
||||
var friendMan = host.GetFriendMan();
|
||||
if (friendMan == null) return -1;
|
||||
var friend = friendMan.GetFriendByName(name);
|
||||
return friend.HasValue ? friend.Value.Id : -1;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
@@ -503,20 +547,27 @@ namespace BrewMonster.Scripts.ChatUI
|
||||
|
||||
// 5. Publish event
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent(finalMsg, (byte)channel));
|
||||
|
||||
Debug.Log("[Cuong] AddChatMessage" + finalMsg);
|
||||
}
|
||||
|
||||
private string GetChannelColorHex(ChatChannel channel)
|
||||
{
|
||||
// Simplified mapping based on common PW colors
|
||||
// [Port] C++: CDlgChat::m_pszColor[] — DlgChat.cpp dòng 120-136
|
||||
// Giữ đúng màu gốc cho các kênh dùng trong local error messages
|
||||
return channel switch
|
||||
{
|
||||
ChatChannel.GP_CHAT_TEAM => "00FFFF", // Cyan
|
||||
ChatChannel.GP_CHAT_FACTION => "00FF00", // Green
|
||||
ChatChannel.GP_CHAT_FARCRY => "FFFF00", // Yellow
|
||||
ChatChannel.GP_CHAT_WHISPER => "FF00FF", // Magenta
|
||||
_ => "FFFFFF" // White
|
||||
ChatChannel.GP_CHAT_LOCAL => "FFFFFF",
|
||||
ChatChannel.GP_CHAT_FARCRY => "FFE400",
|
||||
ChatChannel.GP_CHAT_TEAM => "00FF00",
|
||||
ChatChannel.GP_CHAT_FACTION => "00FFFC",
|
||||
ChatChannel.GP_CHAT_WHISPER => "0065FE", // C++: "^0065FE" — KHÔNG phải FF00FF
|
||||
ChatChannel.GP_CHAT_TRADE => "FF742E",
|
||||
ChatChannel.GP_CHAT_SYSTEM => "BED293",
|
||||
ChatChannel.GP_CHAT_BROADCAST => "FF3600",
|
||||
ChatChannel.GP_CHAT_MISC => "9AA6FF",
|
||||
ChatChannel.GP_CHAT_SUPERFARCRY => "ff9b3e",
|
||||
ChatChannel.GP_CHAT_BATTLE => "FFFFFF",
|
||||
ChatChannel.GP_CHAT_COUNTRY => "FFFFFF",
|
||||
_ => "FFFFFF"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ public partial class CECGameRun : ITickable
|
||||
CECGameUIMan pGameUI = m_pUIManager?.GetInGameUIMan();
|
||||
if (pGameUI != null)
|
||||
{
|
||||
//pGameUI.AddChatMessage(szFormattedMsg, (int)GP_CHAT.GP_CHAT_SYSTEM);
|
||||
pGameUI.AddChatMessage(szFormattedMsg, ChatChannel.GP_CHAT_SYSTEM);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -888,7 +888,7 @@ public partial class CECGameRun : ITickable
|
||||
else
|
||||
{
|
||||
// Fallback to debug output if UI is not available
|
||||
Debug.LogError($"[Cuong] [Error] pGameUI == null");
|
||||
BMLogger.LogError($"[Cuong] [Error] pGameUI == null");
|
||||
|
||||
// Note: In C++ original, pItem is deleted here if UI is not available
|
||||
// In C#, we don't need explicit deletion due to garbage collection
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using BrewMonster;
|
||||
using CSNetwork;
|
||||
using CSNetwork.GPDataType;
|
||||
using BrewMonster.Common;
|
||||
@@ -42,8 +43,6 @@ partial class CECGameRun
|
||||
/// </summary>
|
||||
public void ShowAccountLoginInfo()
|
||||
{
|
||||
Debug.Log("[Cuong] ShowAccountLoginInfo");
|
||||
|
||||
// Kiểm tra cờ xem đã hiển thị thông tin đăng nhập chưa để tránh in lặp lại nhiều lần.
|
||||
if (!m_bAccountLoginInfoShown)
|
||||
{
|
||||
@@ -68,7 +67,7 @@ partial class CECGameRun
|
||||
string textTime = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9343), timeStr);
|
||||
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent { context = textTime, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
|
||||
Debug.Log($"[Cuong] ShowAccountLoginInfo {textTime}");
|
||||
BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textTime}");
|
||||
|
||||
// 2. Định dạng và hiển thị IP đăng nhập lần trước (Last login IP).
|
||||
// Table 9344: chuỗi dạng "IP đăng nhập trước: %s"
|
||||
@@ -76,7 +75,7 @@ partial class CECGameRun
|
||||
string textIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9344), ipStr);
|
||||
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent { context = textIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
|
||||
Debug.Log($"[Cuong] ShowAccountLoginInfo {textIp}");
|
||||
BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textIp}");
|
||||
|
||||
// 3. Định dạng và hiển thị IP đăng nhập hiện tại (Current login IP).
|
||||
// Table 9345: chuỗi dạng "IP đăng nhập hiện tại: %s"
|
||||
@@ -84,7 +83,7 @@ partial class CECGameRun
|
||||
string textCurIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9345), curIpStr);
|
||||
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent { context = textCurIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
|
||||
Debug.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}");
|
||||
BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +105,6 @@ partial class CECGameRun
|
||||
{
|
||||
string text = "Hoàn tất thông tin tài khoản..."; // 9347
|
||||
EventBus.Publish(new GameSession.ChatMessageEvent { context = text, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
|
||||
Debug.Log($"[Cuong] ShowAccountInfo {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `<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 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"
|
||||
|
||||
Reference in New Issue
Block a user