From 9e81951f1250c531d1c010c00b22892b766ff2ba Mon Sep 17 00:00:00 2001
From: CuongNV <>
Date: Wed, 1 Apr 2026 10:41:22 +0700
Subject: [PATCH 1/6] remove log and change ErrorChatHandler
---
.../Scripts/Chat/UI/ServerErrorChatHandler.cs | 2 +-
.../Scripts/Network/CSNetwork/GameSession.cs | 10 +---------
.../Scripts/UI/GamePlay/EC_GameUIMan.cs | 13 ++++++-------
Assets/Scripts/ChatInputHandler.cs | 2 --
Assets/Scripts/EC_GameRun.cs | 4 ++--
Assets/Scripts/EC_GameRunChat.cs | 10 ++++------
6 files changed, 14 insertions(+), 27 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs
index 9dc5e3ff99..f3116fe94a 100644
--- a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs
@@ -31,7 +31,7 @@ namespace BrewMonster.Scripts.ChatUI
errorMsg = $"Lỗi không xác định";
}
- string coloredMsg = $"[System Error {e.ErrorCode}] {errorMsg}";
+ string coloredMsg = $"{errorMsg}";
EventBus.Publish(new GameSession.ChatMessageEvent(
coloredMsg,
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
index 1c91a90cf4..6fe9391a70 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
@@ -2094,11 +2094,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;
}
@@ -2111,7 +2109,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;
}
@@ -2160,18 +2157,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))
@@ -2179,7 +2173,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);
@@ -2189,7 +2182,6 @@ namespace CSNetwork
}
else
{
- Debug.Log("[Cuong] Other 1");
char[] szText = new char[80];
AUICommon.AUI_ConvertChatString(ref szName,ref szText, false);
@@ -2210,7 +2202,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)
diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
index b538a03e37..04891faf0d 100644
--- a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
+++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
@@ -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
diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs
index 8eec819abf..3e1750c16a 100644
--- a/Assets/Scripts/ChatInputHandler.cs
+++ b/Assets/Scripts/ChatInputHandler.cs
@@ -503,8 +503,6 @@ 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)
diff --git a/Assets/Scripts/EC_GameRun.cs b/Assets/Scripts/EC_GameRun.cs
index f4fb44f9cb..0932be7aba 100644
--- a/Assets/Scripts/EC_GameRun.cs
+++ b/Assets/Scripts/EC_GameRun.cs
@@ -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
diff --git a/Assets/Scripts/EC_GameRunChat.cs b/Assets/Scripts/EC_GameRunChat.cs
index 5ba9947eaf..5f480a64b1 100644
--- a/Assets/Scripts/EC_GameRunChat.cs
+++ b/Assets/Scripts/EC_GameRunChat.cs
@@ -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
///
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}");
}
}
}
From a82337f61787cddb7dfafba972e5841da446ed27 Mon Sep 17 00:00:00 2001
From: CuongNV <>
Date: Wed, 1 Apr 2026 16:47:02 +0700
Subject: [PATCH 2/6] update skill for chat
---
.agents/skills/chat_system_csharp/SKILL.md | 26 ++++++
.../CONVERSION_GUIDE_STRING_FORMAT.md | 83 +++++++++++++++++--
2 files changed, 102 insertions(+), 7 deletions(-)
create mode 100644 .agents/skills/chat_system_csharp/SKILL.md
diff --git a/.agents/skills/chat_system_csharp/SKILL.md b/.agents/skills/chat_system_csharp/SKILL.md
new file mode 100644
index 0000000000..d8725dfd91
--- /dev/null
+++ b/.agents/skills/chat_system_csharp/SKILL.md
@@ -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 {...})`.
diff --git a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md
index 7435b3639a..6db6b8bb73 100644
--- a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md
+++ b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md
@@ -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 `` 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 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"
From 56923e1b404c9150d1b974a92c2982198e62ef97 Mon Sep 17 00:00:00 2001
From: CuongNV <>
Date: Wed, 1 Apr 2026 16:50:38 +0700
Subject: [PATCH 3/6] update logic chatsystem
---
.../Scripts/Chat/UI/ChatMessageView.cs | 41 ++++++++++++++++++-
.../Scripts/Network/CSNetwork/GameSession.cs | 4 +-
.../Scripts/UI/Dialogs/DlgPlayerOptions.cs | 6 +--
.../Scripts/UI/GamePlay/EC_GameUIMan.cs | 4 +-
Assets/Scripts/ChatInputHandler.cs | 2 +-
5 files changed, 48 insertions(+), 9 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs
index c83e5943df..b49d91a3ec 100644
--- a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs
@@ -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().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));
+ }
+ }
+ }
}
}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
index 6fe9391a70..4433c48713 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
@@ -2132,7 +2132,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
@@ -2252,7 +2252,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);
diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
index 1254ab6dc8..9240ef47eb 100644
--- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
@@ -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()
diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
index 04891faf0d..d11b434cc6 100644
--- a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
+++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs
@@ -516,11 +516,11 @@ namespace BrewMonster.UI
string parsedMsg = pszMsg;
if (parsedMsg.Contains("&"))
{
- // Convert &Cuong& into Cuong
+ // Convert &Cuong& into Cuong
parsedMsg = System.Text.RegularExpressions.Regex.Replace(
parsedMsg,
@"&([^&]+)&",
- $"$1"
+ $"$1"
);
}
else
diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs
index 3e1750c16a..c44b26a58d 100644
--- a/Assets/Scripts/ChatInputHandler.cs
+++ b/Assets/Scripts/ChatInputHandler.cs
@@ -429,7 +429,7 @@ 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.
From aad2789f58df554f2ef933e06f7d02a8ee0ab3ca Mon Sep 17 00:00:00 2001
From: CuongNV <>
Date: Thu, 2 Apr 2026 11:42:34 +0700
Subject: [PATCH 4/6] add logic whisper
---
.../PerfectWorld/Scripts/UI/Chat/DlgChat.cs | 22 +++++-
Assets/Scripts/ChatInputHandler.cs | 73 ++++++++++++++++---
2 files changed, 82 insertions(+), 13 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs
index 236e8b1530..ed432b3e49 100644
--- a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs
@@ -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";
///
/// Formats a message with TextMeshPro color tags based on the channel.
@@ -55,10 +59,22 @@ namespace BrewMonster.UI
///
/// 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).
///
+ /// Chat channel
+ /// Sender/recipient role ID — used to check friend status for GP_CHAT_WHISPER
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";
diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs
index c44b26a58d..9204c57da9 100644
--- a/Assets/Scripts/ChatInputHandler.cs
+++ b/Assets/Scripts/ChatInputHandler.cs
@@ -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
@@ -433,11 +434,54 @@ namespace BrewMonster.Scripts.ChatUI
// [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 = $"{target}: {msg}";
- 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;
}
// =====================================================
@@ -507,14 +551,23 @@ namespace BrewMonster.Scripts.ChatUI
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"
};
}
From 569d987dcf16e30e265ff0775af187d86327c159 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Thu, 2 Apr 2026 17:32:51 +0700
Subject: [PATCH 5/6] Add buying stackable item, not stackable. Fixing cant buy
item from 2nd tabs
---
.../Scripts/UI/NPCShopUIManager.cs | 278 +++++++++++++-----
Assets/Prefabs/UI/DialogNPCShop.prefab | 6 +-
2 files changed, 209 insertions(+), 75 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs b/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs
index 37e9c69ec1..94820c8658 100644
--- a/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs
+++ b/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs
@@ -29,9 +29,9 @@ public class NPCShopUIManager : AUIDialog
[Header("Texts")]
public TextMeshProUGUI itemDetailNameText;
- public TextMeshProUGUI itemDetailPriceText;
- public TextMeshProUGUI itemsPriceText;
public TextOutlet itemDescriptionText;
+ public TextMeshProUGUI itemsBuyAmountText;
+ public TextMeshProUGUI itemsBuyTotalMoneyText;
[Header("Tabs")]
public Transform tabButtonContainer;
@@ -74,6 +74,10 @@ public class NPCShopUIManager : AUIDialog
private int shopItemIndex;
private Color colorUnActive = new Color(1f, 1f, 1f, 0f);
private Color colorActive = new Color(1f, 1f, 1f, 1f);
+ private int buyCount = 1;
+ private const int BuyCountMin = 1;
+ private const string BuyUiEmptyValue = "0";
+ private const int BuyCountFallbackMax = 99;
/// Current NPC id for this shop session. Send SEVNPC_HELLO with this before buy.
public uint CurrentNPCID => currentNPCID;
@@ -354,11 +358,29 @@ public class NPCShopUIManager : AUIDialog
// Create GShopItem
GShopItem shopItem = CreateShopItemFromGood(good, itemData, itemDataType);
- // Create panel with shop slot index (server expects this in npc_trade_item.index)
- CreateItemPanel(shopItem, i);
+ // Create panel with absolute shop slot index (server validates npc_trade_item.index against full shop list).
+ int absoluteShopSlotIndex = GetAbsoluteShopSlotIndex(sellService, pageIndex, i);
+ CreateItemPanel(shopItem, absoluteShopSlotIndex);
}
}
}
+
+ private static int GetAbsoluteShopSlotIndex(NPC_SELL_SERVICE sellService, int pageIndex, int localIndex)
+ {
+ if (pageIndex <= 0)
+ return localIndex;
+
+ int offset = 0;
+ int safePageIndex = Mathf.Clamp(pageIndex, 0, sellService.pages?.Length ?? 0);
+ for (int p = 0; p < safePageIndex; p++)
+ {
+ var goods = sellService.pages[p].goods;
+ if (goods != null)
+ offset += goods.Length;
+ }
+
+ return offset + localIndex;
+ }
GShopItem CreateShopItemFromGood(NPC_SELL_SERVICE.SellGood good, object itemData, DATA_TYPE itemDataType)
{
@@ -518,6 +540,15 @@ public class NPCShopUIManager : AUIDialog
if (m_btn_buy != null)
m_btn_buy.onClick.AddListener(OnBuyButtonClicked);
+ if (m_btn_reduce != null)
+ m_btn_reduce.onClick.AddListener(OnReduceBuyCountClicked);
+
+ if (m_btn_incre != null)
+ m_btn_incre.onClick.AddListener(OnIncreaseBuyCountClicked);
+
+ if (m_btn_max != null)
+ m_btn_max.onClick.AddListener(OnMaxBuyCountClicked);
+
if (m_btn_sell != null)
m_btn_sell.onClick.AddListener(OnSellButtonClicked);
@@ -542,6 +573,7 @@ public class NPCShopUIManager : AUIDialog
if (currentItem.id == 0)
{
Debug.LogWarning("[NPCShopDetailPanel] Current item ID is 0, skipping display update");
+ ResetBuyUi();
return;
}
@@ -550,23 +582,8 @@ public class NPCShopUIManager : AUIDialog
itemDetailNameText.text = currentItem.name;
}
- uint price = 0;
- if (currentItem.buy != null && currentItem.buy.Length > 0)
- {
- for (int i = 0; i < currentItem.buy.Length; i++)
- {
- if (currentItem.buy[i].price > 0)
- {
- price = currentItem.buy[i].price;
- break;
- }
- }
- }
-
- if(itemDetailPriceText != null)
- {
- itemDetailPriceText.text = price > 0 ? $"Giá: {price}" : "Price: N/A";
- }
+ buyCount = BuyCountMin;
+ UpdateBuyPriceTexts();
// Set item description
if (itemDescriptionText != null)
@@ -585,6 +602,148 @@ public class NPCShopUIManager : AUIDialog
}
}
+ private uint GetCurrentUnitPrice()
+ {
+ if (currentItem.id == 0)
+ return 0;
+
+ if (currentItem.buy == null || currentItem.buy.Length == 0)
+ return 0;
+
+ for (int i = 0; i < currentItem.buy.Length; i++)
+ {
+ if (currentItem.buy[i].price > 0)
+ return currentItem.buy[i].price;
+ }
+
+ return 0;
+ }
+
+ private int GetMaxBuyCountForCurrentItem()
+ {
+ if (currentItem.id == 0)
+ return BuyCountMin;
+
+ int tid = (int)currentItem.id;
+ if (tid <= 0)
+ return BuyCountMin;
+
+ int pileLimit = EC_IvtrItem.GetPileLimit(tid);
+ // In some setups the lightweight lookup returns 1 until the local DB data is loaded.
+ // If it says "1", try to resolve pile limit from a temporary item instance.
+ if (pileLimit <= BuyCountMin)
+ {
+ try
+ {
+ var tmp = EC_IvtrItem.CreateItem(tid, 0, 1);
+ if (tmp != null)
+ {
+ tmp.GetDetailDataFromLocal();
+ int instLimit = tmp.GetPileLimitInstance();
+ if (instLimit > pileLimit)
+ pileLimit = instLimit;
+ }
+ }
+ catch
+ {
+ // Keep the original pileLimit.
+ }
+ }
+ if (pileLimit < BuyCountMin)
+ pileLimit = BuyCountMin;
+
+ return pileLimit;
+ }
+
+ private void SetBuyCount(int newCount)
+ {
+ int max = GetMaxBuyCountForCurrentItem();
+ if (newCount < BuyCountMin) newCount = BuyCountMin;
+ if (newCount > max) newCount = max;
+
+ buyCount = newCount;
+ UpdateBuyPriceTexts();
+ }
+
+ private void UpdateBuyPriceTexts()
+ {
+ // Only update buy UI while buy panel is active.
+ if (contentMidBuy != null && !contentMidBuy.activeInHierarchy)
+ return;
+
+ if (currentItem.id == 0)
+ {
+ ResetBuyUi();
+ return;
+ }
+
+ uint unitPrice = GetCurrentUnitPrice();
+ int max = GetMaxBuyCountForCurrentItem();
+ if (buyCount < BuyCountMin) buyCount = BuyCountMin;
+ if (buyCount > max) buyCount = max;
+
+ // Quantity controls:
+ // - Non-stackable items have max == BuyCountMin, so keep +/-/max disabled.
+ // - Stackable items re-enable controls automatically.
+ bool canChangeQty = max > BuyCountMin;
+ if (m_btn_reduce != null) m_btn_reduce.interactable = canChangeQty && buyCount > BuyCountMin;
+ if (m_btn_incre != null) m_btn_incre.interactable = canChangeQty && buyCount < max;
+ if (m_btn_max != null) m_btn_max.interactable = canChangeQty && buyCount < max;
+
+ long total = unitPrice > 0 ? (long)unitPrice * buyCount : 0;
+ if (total < 0) total = 0;
+
+ if (itemsBuyAmountText != null)
+ {
+ itemsBuyAmountText.text = buyCount.ToString();
+ }
+
+ if (itemsBuyTotalMoneyText != null)
+ {
+ itemsBuyTotalMoneyText.text = total.ToString();
+ }
+
+ if (m_btn_buy != null)
+ {
+ m_btn_buy.interactable = unitPrice > 0 && buyCount >= BuyCountMin;
+ }
+ }
+
+ private void ResetBuyUi()
+ {
+ buyCount = BuyCountMin;
+ if (itemsBuyAmountText != null)
+ itemsBuyAmountText.text = BuyUiEmptyValue;
+ if (itemsBuyTotalMoneyText != null)
+ itemsBuyTotalMoneyText.text = BuyUiEmptyValue;
+ if (m_btn_buy != null)
+ m_btn_buy.interactable = false;
+ if (m_btn_reduce != null) m_btn_reduce.interactable = false;
+ if (m_btn_incre != null) m_btn_incre.interactable = false;
+ if (m_btn_max != null) m_btn_max.interactable = false;
+ }
+
+ private void OnReduceBuyCountClicked()
+ {
+ if (currentItem.id == 0)
+ return;
+ SetBuyCount(buyCount - 1);
+ }
+
+ private void OnIncreaseBuyCountClicked()
+ {
+ if (currentItem.id == 0)
+ return;
+ SetBuyCount(buyCount + 1);
+ }
+
+ private void OnMaxBuyCountClicked()
+ {
+ if (currentItem.id == 0)
+ return;
+ SetBuyCount(GetMaxBuyCountForCurrentItem());
+ }
+
void LoadItemIcon(Image iconImage, int itemId)
{
if (itemId <= 0 || iconImage == null)
@@ -670,35 +829,35 @@ public class NPCShopUIManager : AUIDialog
}
// Get price from item
- uint price = 0;
- if (currentItem.buy != null && currentItem.buy.Length > 0)
+ uint price = GetCurrentUnitPrice();
+ if (price == 0)
{
- for (int i = 0; i < currentItem.buy.Length; i++)
- {
- if (currentItem.buy[i].price > 0)
- {
- price = currentItem.buy[i].price;
- break;
- }
- }
+ Debug.LogWarning($"[NPCShopDetailPanel] Cannot buy item {currentItem.id} with invalid price");
+ return;
}
+ int max = GetMaxBuyCountForCurrentItem();
+ if (buyCount < BuyCountMin) buyCount = BuyCountMin;
+ if (buyCount > max) buyCount = max;
+
// Server requires SEVNPC_HELLO with NPC id before buy, and the correct shop slot index
if (shopManager != null && shopManager.CurrentNPCID != 0)
UnityGameSession.c2s_CmdNPCSevHello((int)shopManager.CurrentNPCID);
- // Create npc_trade_item: tid = template ID, index = shop slot (server validates this), count = quantity
+ // Create npc_trade_item: tid = template ID, index = shop slot (server validates this), count = quantity.
+ // For this server, multiple entries with the same index are rejected (ERROR_MESSAGE 15),
+ // so we must send a single entry and put the desired quantity in npc_trade_item.count.
npc_trade_item[] items = new npc_trade_item[1];
items[0] = new npc_trade_item
{
tid = (int)currentItem.id,
index = (uint)shopItemIndex,
- count = 1
+ count = (uint)buyCount
};
- UnityGameSession.c2s_CmdNPCSevBuy(1, items);
+ UnityGameSession.c2s_CmdNPCSevBuy(items.Length, items);
- Debug.Log($"[NPCShopDetailPanel] Sent buy command for item {currentItem.id}, price {price}");
+ Debug.Log($"[NPCShopDetailPanel] Sent buy command for item {currentItem.id}, unitPrice {price}, count {buyCount}");
}
private void OnSellButtonClicked()
@@ -955,6 +1114,15 @@ public class NPCShopUIManager : AUIDialog
if (m_btn_buy != null)
m_btn_buy.onClick.RemoveListener(OnBuyButtonClicked);
+ if (m_btn_reduce != null)
+ m_btn_reduce.onClick.RemoveListener(OnReduceBuyCountClicked);
+
+ if (m_btn_incre != null)
+ m_btn_incre.onClick.RemoveListener(OnIncreaseBuyCountClicked);
+
+ if (m_btn_max != null)
+ m_btn_max.onClick.RemoveListener(OnMaxBuyCountClicked);
+
if (m_btn_sell != null)
m_btn_sell.onClick.RemoveListener(OnSellButtonClicked);
@@ -1044,6 +1212,7 @@ public class NPCShopUIManager : AUIDialog
contentMidBuy.SetActive(true);
contentRight.SetActive(true);
contentMidSell.SetActive(false);
+ UpdateBuyPriceTexts();
}
private void OnClickTabButtonSell()
@@ -1058,42 +1227,7 @@ public class NPCShopUIManager : AUIDialog
private void UpdateSellTotalPriceText()
{
- if (itemsPriceText == null)
- return;
-
- if (sellSlotToSourceSlot == null || sellSlotToSourceSlot.Count == 0)
- {
- itemsPriceText.text = "0";
- return;
- }
-
- var host = CECGameRun.Instance?.GetHostPlayer();
- var inv = host?.GetInventory(0);
- if (inv == null)
- {
- itemsPriceText.text = "0";
- return;
- }
-
- long total = 0;
- foreach (var pair in sellSlotToSourceSlot)
- {
- int sourceSlot = pair.Value;
- if (sourceSlot < 0 || sourceSlot >= inv.GetSize())
- continue;
-
- var item = inv.GetItem(sourceSlot, false);
- if (item == null || item.m_iCount <= 0)
- continue;
-
- if (!item.IsSellable())
- continue;
-
- item.GetDetailDataFromLocal();
- total += item.GetScaledPrice();
- }
-
- if (total < 0) total = 0;
- itemsPriceText.text = total.ToString();
+ // Sell total text removed from this manager.
+ return;
}
}
diff --git a/Assets/Prefabs/UI/DialogNPCShop.prefab b/Assets/Prefabs/UI/DialogNPCShop.prefab
index f1eacfd38e..7fa77b3efe 100644
--- a/Assets/Prefabs/UI/DialogNPCShop.prefab
+++ b/Assets/Prefabs/UI/DialogNPCShop.prefab
@@ -1368,7 +1368,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
- m_IsActive: 0
+ m_IsActive: 1
--- !u!224 &1538965918058098960
RectTransform:
m_ObjectHideFlags: 0
@@ -12969,11 +12969,11 @@ MonoBehaviour:
contentMidSell: {fileID: 5285943178504563476}
item_info: {fileID: 3637242207861637912}
itemDetailNameText: {fileID: 2529529646566217934}
- itemDetailPriceText: {fileID: 2517942723267868233}
- itemsPriceText: {fileID: 6499688482604231105}
itemDescriptionText:
legacy: {fileID: 0}
tmp: {fileID: 5329995747664012504}
+ itemsBuyAmountText: {fileID: 1188040176770993507}
+ itemsBuyTotalMoneyText: {fileID: 1958673310957352830}
tabButtonContainer: {fileID: 2298715577163083360}
tabButtonPrefab: {fileID: 532136160345846687, guid: 548ae6ac061bc9648b093c9f9d203615, type: 3}
tabButtonTextComponentName: Text
From a33c35722bf21c8e86c4af4a053d6304ab0b32d9 Mon Sep 17 00:00:00 2001
From: VuNgocHaiC7
Date: Thu, 2 Apr 2026 17:57:25 +0700
Subject: [PATCH 6/6] update ui for dialogProduce.prefab
---
.../Scripts/UI/Dialogs/DlgProduce.cs | 106 +-
.../Scripts/UI/ProduceItemPanel.cs | 7 +-
Assets/PerfectWorld/UI/DlgProduce.prefab | 19763 +++-------------
Assets/Prefabs/UI/itemProduce.prefab | 328 +-
4 files changed, 3841 insertions(+), 16363 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs
index 561c03662c..936bbfbb21 100644
--- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs
@@ -28,7 +28,7 @@ namespace BrewMonster
[SerializeField] private GameObject itemPb;
[Header("Quantity")]
- [SerializeField] private List quantityText;
+ [SerializeField] private TextMeshProUGUI quantityText;
[SerializeField] private Button quantityIncreaseBtn;
[SerializeField] private Button quantityDecreaseBtn;
[SerializeField] private Button quantityMaxBtn;
@@ -42,7 +42,7 @@ namespace BrewMonster
[SerializeField] private List materialSlots = new List();
[Header("Result Slot")]
- [SerializeField] private Transform itemResult;
+ [SerializeField] private Image itemResult;
[Header("Item Info Panel")]
public Transform itemInfoRoot;
@@ -56,6 +56,9 @@ namespace BrewMonster
[SerializeField] private TextMeshProUGUI weponDescInfoText;
[SerializeField] private TextMeshProUGUI weponExtraInfoText;
+ [Header("Default")]
+ [SerializeField] private Sprite khung_item;
+
private NPC_MAKE_SERVICE? cachedMakeService = null;
private int currentTabIndex = 0;
private uint selectedRecipeId = 0; // Track the currently selected recipe
@@ -79,8 +82,7 @@ namespace BrewMonster
public override void Start()
{
- quantityText[0].text = currentQuantity.ToString();
- quantityText[1].text = currentQuantity.ToString();
+ quantityText.text = currentQuantity.ToString();
quantityDecreaseBtn.onClick.AddListener(OnClickDecreaseBtn);
quantityIncreaseBtn.onClick.AddListener(OnClickIncreaseBtn);
quantityMaxBtn.onClick.AddListener(OnClickMaxBtn);
@@ -309,8 +311,15 @@ namespace BrewMonster
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() =>
{
- ShowItemInfoByRecipe(recipeId);
+ bool isNewRecipe = selectedRecipeId != recipeId;
+ selectedRecipeId = recipeId;
+ if (isNewRecipe)
+ {
+ currentQuantity = 1;
+ UpdateQuantityText(currentQuantity);
+ }
ShowRecipeMaterials(recipeId);
+ ShowItemInfoByRecipe(recipeId);
});
}
@@ -331,14 +340,12 @@ namespace BrewMonster
{
if (slot == null) continue;
- slot.gameObject.SetActive(false);
-
Transform iconTf = slot.Find("item");
if (iconTf != null)
{
Image img = iconTf.GetComponent();
if (img != null)
- img.sprite = null;
+ img.sprite = khung_item;
}
Transform qtyTf = slot.Find("text_quantity");
@@ -352,30 +359,14 @@ namespace BrewMonster
if (itemResult != null)
{
- itemResult.gameObject.SetActive(false);
-
- Transform iconTf = itemResult.Find("item");
- if (iconTf != null)
- {
- Image img = iconTf.GetComponent();
- if (img != null)
- img.sprite = null;
- }
-
- Transform qtyTf = itemResult.Find("text_quantity");
- if (qtyTf != null)
- {
- TextMeshProUGUI txt = qtyTf.GetComponent();
- if (txt != null)
- txt.text = "";
- }
+ itemResult.sprite = khung_item;
}
}
public void ShowRecipeMaterials(uint recipeId)
{
- selectedRecipeId = recipeId; // Track the selected recipe
+ selectedRecipeId = recipeId;
ClearMaterialSlots();
var edm = ElementDataManProvider.GetElementDataMan();
@@ -395,32 +386,20 @@ namespace BrewMonster
{
uint outputItemId = recipe.targets[0].id_to_make;
- itemResult.gameObject.SetActive(true);
-
- Transform iconTf = itemResult.Find("item");
- if (iconTf != null)
+ if (itemResult != null)
{
- Image img = iconTf.GetComponent();
- if (img != null)
+ if (itemResult != null)
{
Sprite sp = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)outputItemId);
if (sp != null)
{
- img.sprite = sp;
- img.enabled = true;
- img.preserveAspect = true;
+ itemResult.sprite = sp;
+ itemResult.enabled = true;
+ itemResult.preserveAspect = true;
}
}
}
- Transform qtyTf = itemResult.Find("text_quantity");
- if (qtyTf != null)
- {
- TextMeshProUGUI txt = qtyTf.GetComponent();
- if (txt != null)
- txt.text = "1";
- }
-
Button resultBtn = itemResult.GetComponent