From e32ff4631bdd5fb6ec91f80444eb99b83fd206ca Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Thu, 12 Mar 2026 17:26:24 +0700 Subject: [PATCH] add UI chat --- Assets/PerfectWorld/Scripts/Chat/CHAT_S2C.cs | 31 +- Assets/PerfectWorld/Scripts/Chat/TestChat.cs | 23 -- .../Scripts/Chat/TestChat.cs.meta | 3 - .../Scripts/Chat/UI/ChatPanelUI.cs | 38 +- .../Scripts/Chat/UI/ChatThreadDispatcher.cs | 47 +++ .../Chat/UI/ChatThreadDispatcher.cs.meta | 3 + .../Scripts/Common/EC_C2SCmdCache.cs | 14 +- .../Scripts/Managers/EC_Inventory.cs | 9 +- .../Scripts/Managers/EC_Object.cs | 3 +- .../Scripts/Network/CSNetwork/GameSession.cs | 215 +++++++++-- Assets/PerfectWorld/Scripts/UI/UIPlayer.cs | 57 ++- Assets/Prefabs/ChatSystem/ChatCanvas.prefab | 349 ++++++++++++++++-- .../ChatSystem/ChatThreadDispatcher.prefab | 46 +++ .../ChatThreadDispatcher.prefab.meta | 7 + Assets/Scripts/ChatInputHandler.cs | 221 ++++++++++- 15 files changed, 937 insertions(+), 129 deletions(-) delete mode 100644 Assets/PerfectWorld/Scripts/Chat/TestChat.cs delete mode 100644 Assets/PerfectWorld/Scripts/Chat/TestChat.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs create mode 100644 Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs.meta create mode 100644 Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab create mode 100644 Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab.meta diff --git a/Assets/PerfectWorld/Scripts/Chat/CHAT_S2C.cs b/Assets/PerfectWorld/Scripts/Chat/CHAT_S2C.cs index c817c1b316..07dbde0aed 100644 --- a/Assets/PerfectWorld/Scripts/Chat/CHAT_S2C.cs +++ b/Assets/PerfectWorld/Scripts/Chat/CHAT_S2C.cs @@ -22,21 +22,13 @@ namespace BrewMonster.Scripts.Chat public struct chat_item_base { public short cmd_id; - }; - + } + public struct chat_equip_item { public short cmd_id; - public int type; - public int expire_date; - public int proc_type; - public ushort content_length; - public byte[] content; - - public int LenghtHeader() - { - return Marshal.SizeOf() + (Marshal.SizeOf() * 3) + Marshal.SizeOf(); - } + public char where; + public short index; } struct chat_generalcard_collection @@ -47,6 +39,21 @@ namespace BrewMonster.Scripts.Chat public static class CHAT_S2C { + public struct chat_equip_item + { + public short cmd_id; + public int type; + public int expire_date; + public int proc_type; + public ushort content_length; + public byte[] content; + + public int LenghtHeader() + { + return Marshal.SizeOf() + (Marshal.SizeOf() * 3) + Marshal.SizeOf(); + } + } + public enum EChatS2CCommand : short { CHAT_EQUIP_ITEM, diff --git a/Assets/PerfectWorld/Scripts/Chat/TestChat.cs b/Assets/PerfectWorld/Scripts/Chat/TestChat.cs deleted file mode 100644 index 2b9f6d00e5..0000000000 --- a/Assets/PerfectWorld/Scripts/Chat/TestChat.cs +++ /dev/null @@ -1,23 +0,0 @@ -using UnityEngine; - -namespace BrewMonster.Scripts.Chat -{ - public class TestChat : MonoSingleton - { - public ChatInputHandler testChat; - public string text; - protected override void Awake() - { - base.Awake(); - DontDestroyOnLoad(gameObject); - } - - private void Update() - { - if (Input.GetKeyDown(KeyCode.C)) - { - testChat.Send(text); - } - } - } -} \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Chat/TestChat.cs.meta b/Assets/PerfectWorld/Scripts/Chat/TestChat.cs.meta deleted file mode 100644 index f858494c50..0000000000 --- a/Assets/PerfectWorld/Scripts/Chat/TestChat.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 71c6bd36af2b4627838a10eb95f147b6 -timeCreated: 1772782953 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs index 6c77f341ba..d8557e842b 100644 --- a/Assets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatPanelUI.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using CSNetwork; using UnityEngine; @@ -9,6 +10,7 @@ namespace BrewMonster.Scripts.ChatUI public class ChatPanelUI : MonoBehaviour { [Header("UI")] public ScrollRect scrollRect; + public GameObject chatPanelUIGO; public RectTransform content; public ChatMessageView messagePrefab; //public GameObject newMessageIndicator; @@ -25,8 +27,7 @@ namespace BrewMonster.Scripts.ChatUI void Awake() { - EventBus.Subscribe( - (x) => AddMessage(x.context)); + EventBus.Subscribe(OnChatMessageReceived); _pool = new ObjectPool( CreateItem, OnGetItem, @@ -40,6 +41,21 @@ namespace BrewMonster.Scripts.ChatUI scrollRect.onValueChanged.AddListener(OnScrollChanged); } + private void OnDestroy() + { + EventBus.Unsubscribe(OnChatMessageReceived); + } + + private void OnChatMessageReceived(GameSession.ChatMessageEvent x) + { + Debug.Log("[Cuong] OnChatMessageReceived " + x.context); + + ChatThreadDispatcher.Instance.Post(() => + { + AddMessage(x.context); + }); + } + ChatMessageView CreateItem() { var item = Instantiate(messagePrefab); @@ -84,16 +100,13 @@ namespace BrewMonster.Scripts.ChatUI if (_messages.Count > maxStoredMessages) _messages.RemoveAt(0); + if (!chatPanelUIGO.activeSelf) + return; + AddMessageView(msg); if (_userAtBottom) - { ScrollToBottom(); - } - else - { - //newMessageIndicator.SetActive(true); - } } void AddMessageView(string msg) @@ -150,5 +163,14 @@ namespace BrewMonster.Scripts.ChatUI _visibleViews.Clear(); _messages.Clear(); } + + public void OnHandlerChatButton() + { + bool open = !chatPanelUIGO.activeSelf; + chatPanelUIGO.SetActive(open); + + if (open) + RefreshVisible(); + } } } \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs new file mode 100644 index 0000000000..4714f62af9 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using UnityEngine; + +namespace BrewMonster.Scripts.ChatUI +{ + /// + /// Responsible for switching chat messages from background threads to Unity main thread. + /// Only handles chat related actions. + /// + public class ChatThreadDispatcher : MonoSingleton + { + private static readonly ConcurrentQueue _queue = new ConcurrentQueue(); + + protected override void Awake() + { + base.Awake(); + DontDestroyOnLoad(gameObject); + } + + /// + /// Called from ANY thread (network thread safe) + /// + public void Post(Action action) + { + if (action == null) + return; + + _queue.Enqueue(action); + } + + private void Update() + { + while (_queue.TryDequeue(out var action)) + { + try + { + action?.Invoke(); + } + catch (Exception e) + { + Debug.LogError($"ChatThreadDispatcher error: {e}"); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs.meta b/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs.meta new file mode 100644 index 0000000000..1c00a8022a --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatThreadDispatcher.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 00392315a7ee47e7a9527eda504fb312 +timeCreated: 1773310087 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Common/EC_C2SCmdCache.cs b/Assets/PerfectWorld/Scripts/Common/EC_C2SCmdCache.cs index dc659560ff..1a0901486b 100644 --- a/Assets/PerfectWorld/Scripts/Common/EC_C2SCmdCache.cs +++ b/Assets/PerfectWorld/Scripts/Common/EC_C2SCmdCache.cs @@ -270,7 +270,7 @@ namespace BrewMonster.Common { m_UseItemCmdList.Clear(); - // C2S ʱ + // ���� C2S �����ʱ�� m_CounterMap[(int)CommandID.USE_ITEM].Reset(true); m_EnterSanctuaryList.Clear(); @@ -280,7 +280,7 @@ namespace BrewMonster.Common m_PresentInfoList.Clear(); m_CounterMap[(int)CommandID.PLAYER_GIVE_PRESENT].Reset(true); - // Эʱ + // ����Э���ʱ�� m_GetPlayerBriefInfoList.Clear(); m_CounterMap2[(int)ProtocolType.PROTOCOL_GETPLAYERBRIEFINFO].Reset(true); @@ -377,13 +377,13 @@ namespace BrewMonster.Common getplayerbriefinfo p = m_GetPlayerBriefInfoList[0]; if (p.Playerlist.Count != 0) { - // ȡһidЭ + // ��ȡ��һ�����id�������������Э�� getplayerbriefinfo temp = p; temp.Playerlist.Clear(); temp.Playerlist.Add(p.Playerlist[0]); UnityGameSession.Instance.GameSession.SendProtocol(temp); - // б + // ���б������ p.Playerlist.Remove(p.Playerlist[0]); } @@ -670,12 +670,12 @@ namespace BrewMonster.Common } // Send protocols ... - void SendGetPlayerBriefInfo(int iNumPlayer, int[] aIDs, int iReason) + public void SendGetPlayerBriefInfo(int iNumPlayer, int[] aIDs, int iReason) { if (iNumPlayer == 0 || aIDs == null || aIDs.Length == 0) return; - // 1.ϲӵб + // 1.�ϲ���ӵ��б� getplayerbriefinfo p = new getplayerbriefinfo(); p.Roleid = EC_Game.GetGameRun().GetHostPlayer().GetCharacterID(); p.Reason = (byte)iReason; @@ -687,7 +687,7 @@ namespace BrewMonster.Common if (p.Playerlist.Count > 0) m_GetPlayerBriefInfoList.Add(p); - // 2.鲢 + // 2.��鲢���� SendCachedGetPlayerBriefInfo(); } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs index 5e020ab927..7d113a8309 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs @@ -11,7 +11,8 @@ namespace BrewMonster.Scripts.Managers public class EC_Inventory { // ===== Instance-based inventory (per-pack) ===== - + public static int IVTRTYPE_CLIENT_GENERALCARD_PACK = 1024; // �ͻ��˱��ذ����� ���ڿ���ͼ��Ҫ����ѻ�ÿ���ͨ�����췢�͡�Ϊ��ͳһ�������촰�ڵ���Ʒ�����ӱ��ذ����� + // Item array: index is slot, null means empty. private EC_IvtrItem[] m_aItems = Array.Empty(); @@ -27,12 +28,6 @@ namespace BrewMonster.Scripts.Managers IVTRTYPE_GENERALCARD_BOX = 7; // ���ư��� }; - // ע�� IVTRTYPE_CLIENT_GENERALCARD_PACK ö��ֵ���ܺ������ Inventory type ֵ�ظ����� - public static class IVTRTYPE_PACK_CLIENT_GENERALCAR - { - public const int IVTRTYPE_CLIENT_GENERALCARD_PACK = 1024; // �ͻ��˱��ذ����� ���ڿ���ͼ��Ҫ����ѻ�ÿ���ͨ�����췢�͡�Ϊ��ͳһ�������촰�ڵ���Ʒ�����ӱ��ذ����� - }; - public EC_Inventory() { } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Object.cs b/Assets/PerfectWorld/Scripts/Managers/EC_Object.cs index e622efd239..95f9c347d2 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_Object.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_Object.cs @@ -13,9 +13,8 @@ using UnityEngine; // /////////////////////////////////////////////////////////////////////////// -public class CECObject : MonoBehaviour +public partial class CECObject : MonoBehaviour { - protected static int ALPHA_HASH = Shader.PropertyToID("_Alpha"); protected Quaternion targetRotation; protected Quaternion startRotation; // Store starting rotation for Slerp diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 012c7b93cb..bbb8cafa14 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -1645,6 +1645,8 @@ namespace CSNetwork gamedatasend.Data = C2SCommandFactory.CreateNakeCmd(C2SCommand.CommandID.ENTER_PK_PROTECTED); SendProtocol(gamedatasend); } + + public const int SUPER_FAR_CRY_EMOTION_SET = 6; /// /// Convert string become byte (Encoding.Unicode trong .NET = UTF-16 LE) /// @@ -1654,16 +1656,85 @@ namespace CSNetwork /// public void SendChatData(byte cChannel, in string szMsg, int iPack, int iSlot) { - EventBus.Publish(new EventChatMessageOnTopPlayer(szMsg)); - EventBus.Publish(new ChatMessageEvent(szMsg)); - publicchat publicChat = new publicchat(); - publicChat.Channel = cChannel; - publicChat.Roleid = m_iCharID; + if (string.IsNullOrEmpty(szMsg)) + return; + + publicchat p = new publicchat(); + p.Channel = cChannel; + p.Roleid = m_iCharID; + if (iPack == EC_Inventory.IVTRTYPE_CLIENT_GENERALCARD_PACK) + { + CECHostPlayer pHost = EC_Game.GetGameRun().GetHostPlayer(); + EC_Inventory clientPack = + pHost != null ? pHost.GetPack(EC_Inventory.IVTRTYPE_CLIENT_GENERALCARD_PACK) : null; + if (pHost != null && clientPack != null) + { + EC_IvtrItem pCard = clientPack.GetItem(iSlot); + + if (pCard != null) + { + short cmd = (short)CHAT_S2C.EChatS2CCommand.CHAT_GENERALCARD_COLLECTION; + int cardId = pCard.GetTemplateID(); + + byte[] bytes = new byte[6]; + + BitConverter.GetBytes(cmd).CopyTo(bytes, 0); + BitConverter.GetBytes(cardId).CopyTo(bytes, 2); + Debug.Log($"[Cuong] SendChatData {bytes}"); + p.Data.Replace(bytes); + } + } + } + else if (iPack >= 0 && iSlot >= 0) + { + byte[] bytes = new byte[5]; + + short cmd = (short)CHAT_S2C.EChatS2CCommand.CHAT_EQUIP_ITEM; + short index = (short)iSlot; + byte where = (byte)iPack; + + BitConverter.GetBytes(cmd).CopyTo(bytes, 0); + bytes[2] = where; + BitConverter.GetBytes(index).CopyTo(bytes, 3); + p.Data.Replace(bytes); + } + byte[] unicodeBytes = Encoding.Unicode.GetBytes(szMsg); - publicChat.Msg.Replace(unicodeBytes); - SendProtocol(publicChat); + p.Msg.Replace(unicodeBytes); + SendProtocol(p); + + if (cChannel is (byte)ChatChannel.GP_CHAT_LOCAL + or (byte)ChatChannel.GP_CHAT_FARCRY + or (byte)ChatChannel.GP_CHAT_SUPERFARCRY + or (byte)ChatChannel.GP_CHAT_BATTLE + or (byte)ChatChannel.GP_CHAT_COUNTRY) { + CECHostPlayer pHost = EC_Game.GetGameRun().GetHostPlayer(); + //int nEmotionSet = pHost.GetCurEmotionSet(); + int nEmotionSet = 0; + string strMsg = szMsg; + if (cChannel == (byte)ChatChannel.GP_CHAT_SUPERFARCRY) + { + if (strMsg.Length > 8) + { + strMsg = strMsg.Substring(0, strMsg.Length - 8); + } + nEmotionSet = SUPER_FAR_CRY_EMOTION_SET; + } + + EC_IvtrItem pItem = null; + if (iPack >= 0 && iSlot >= 0) { + EC_Inventory pPack = pHost.GetPack(iPack); + if (pPack != null) { + pItem = pPack.GetItem(iSlot); + } + } + //pHost.SetLastSaidWords(strMsg, nEmotionSet, pItem); + Debug.Log("[Cuong] EventChatMessageOnTopPlayer " + strMsg); + EventBus.Publish(new EventChatMessageOnTopPlayer(pHost.GetCharacterID(),szMsg)); + } } + public void LoadConfigData() { getuiconfig p = new getuiconfig(); @@ -1698,10 +1769,51 @@ namespace CSNetwork private bool OnPrtcWorldChat(Protocol pProtocol, bool bCalledagain) { + worldchat p = (worldchat)pProtocol; + /*if (p.Channel == (byte)ChatChannel.GP_CHAT_FARCRY && p.Roleid == 24) + { + OnTaskChatMessage(p.Msg.RawBuffer, p.Msg.Size); + return true; + }*/ + + // TODO: Porting logic OnPrtcWorldChat hoàn chỉnh từ C++ if needed Debug.Log("[Cuong] OnPrtcWorldChat"); return true; } + /*private void OnTaskChatMessage(byte[] pBuf, int sz) + { + ... logic ẩn ... + }*/ + + private bool OnBattleChatMessage(chatmessage p, List pPendingFactions) + { + // Tương đương OnBattleChatMessage trong C++ + // Hiện tại đơn giản hóa việc hiển thị, thực tế cần parse nội dung Battle cụ thể + Debug.Log($"[Battle Chat] RoleID: {p.Srcroleid}"); + return true; + } + + private bool OnFortressChatMessage(chatmessage p, List pPendingFactions) + { + // Tương đương OnFortressChatMessage trong C++ + Debug.Log($"[Fortress Chat] RoleID: {p.Srcroleid}"); + return true; + } + + private bool OnKingChatMessage(chatmessage p, bool bGetPlayerName = true) + { + // Tương đương OnKingChatMessage trong C++ + Debug.Log($"[King Chat] RoleID: {p.Srcroleid}"); + return true; + } + + private void OnFactionPVPChatMessage(chatmessage p) + { + // Tương đương OnFactionPVPChatMessage trong C++ + Debug.Log($"[FactionPVP Chat] RoleID: {p.Srcroleid}"); + } + private bool OnPrtcChatMessage(Protocol pProtocol, bool bCalledagain) { CECGameUIMan pGameUI = EC_Game.GetGameRun().GetUIManager().GetInGameUIMan(); @@ -1716,20 +1828,20 @@ namespace CSNetwork return true; } - EC_IvtrItem pItem = CHAT_S2C.CreateChatItem(p.Data); + // Todo: Dead Game + ///EC_IvtrItem pItem = CHAT_S2C.CreateChatItem(p.Data); string szMsg = null; string strTemp = Encoding.Unicode.GetString(p.Msg.ToArray(), 0, p.Msg.Length); string strMsgOrigion = strTemp; // Todo: Show Text on Ui Game //strTemp = pGameUI.FilterInvalidTags(strTemp, pItem==NULL); - if (!Chat_GameSession.PolicyResolver(pProtocol, p, ref strTemp)) + /*if (!Chat_GameSession.PolicyResolver(pProtocol, p, ref strTemp)) { Debug.Log("[Cuong] 2"); return false; } - /* if (Chat_GameSession.HanldeGPChatSystem(p, bCalledagain)) { Debug.Log("[Cuong] 3"); @@ -1740,19 +1852,54 @@ namespace CSNetwork { if (p.Channel == (byte)ChatChannel.GP_CHAT_SYSTEM && p.Srcroleid > 0) { - Debug.Log("[Cuong] 4"); + switch (p.Srcroleid) + { + case 1: case 2: case 3: case 4: case 6: case 7: // Battle Message + if (!bCalledagain) + { + List pending = new List(); + if (!OnBattleChatMessage(p, pending)) + { + // Handle pending faction info + return false; + } + } + break; + case 18: case 19: case 20: case 21: case 22: // Auction Message + // pGameUI.AddSysAuctionMessage(...) + Debug.Log("[Auction] " + strTemp); + EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); + break; + case 24: // Task Message + // OnTaskChatMessage(p.Data.RawBuffer, p.Data.Size); + break; + case 29: case 30: case 31: case 32: case 33: case 34: + case 35: case 36: case 37: case 38: case 39: case 40: + case 41: case 42: case 43: case 45: // Fortress Message + OnFortressChatMessage(p, null); + break; + case 46: case 47: case 48: case 49: // Country Battle + // OnCountryChatMessage(p); + break; + case 50: case 51: case 52: case 53: case 54: + case 55: case 56: case 57: case 58: case 59: // King Chat + OnKingChatMessage(p); + break; + case 60: case 61: case 62: case 63: case 64: // Faction PVP + OnFactionPVPChatMessage(p); + break; + } } else { Debug.Log("[Cuong] 5"); - EventBus.Publish(new ChatMessageEvent(strTemp)); - //EC_Game.GetGameRun().AddChatMessage(szMsg, p.Channel, p.Srcroleid, null, 0, p.Emotion); + 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 )); + // Chat_GameSession.AUICTranslate trans; + // EC_Game.GetGameRun().GetUIManager().GetInGameUIMan().AddHeartBeatHint(trans.Translate(szMsg )); } else { @@ -1762,9 +1909,9 @@ namespace CSNetwork if (ISPLAYERID(p.Srcroleid)) { string szName = EC_Game.GetGameRun().GetPlayerName(p.Srcroleid, false); - Debug.Log("[Cuong] Other Msg " + szName); - if (szName == null) + if (string.IsNullOrEmpty(strTemp)) { + Debug.Log("[Cuong] Other 0"); if (!bCalledagain) { Chat_GameSession.AddElemForPendingProtocols(pProtocol); @@ -1774,41 +1921,35 @@ namespace CSNetwork } else { + Debug.Log("[Cuong] Other 1"); char[] szText = new char[80]; AUICommon.AUI_ConvertChatString(szName, szText, false); string str = string.Format( pStrTab.GetWideString((int)FixedMsg.FIXMSG_CHAT), szName, - szMsg + strTemp ); // Convert to EventBus - /*EC_Game.GetGameRun().AddChatMessage(str, p.Channel, p.Srcroleid, - null, 0, p.Emotion, pItem, strMsgOrigion); */ - EventBus.Publish(new ChatMessageEvent(str)); + EC_Game.GetGameRun().AddChatMessage(strTemp, p.Channel, p.Srcroleid, + null, 0, p.Emotion, null, strMsgOrigion); + EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); // Set player's last said words CECPlayer pPlayer = EC_Game.GetGameRun().GetWorld().GetPlayerMan().GetPlayer(p.Srcroleid); - Debug.Log("[Cuong] name " + pPlayer == null ? "null" : "have name"); if (pPlayer != null) { - if (p.Channel == (byte)ChatChannel.GP_CHAT_SUPERFARCRY) { - if (strTemp.Length > 8) - strTemp = strTemp.Substring(0, strTemp.Length - 8); - else - strTemp = ""; - } - //pPlayer.SetLastSaidWords(strTemp, p.Emotion, pItem); - Debug.Log("[Cuong] " + pPlayer); - EventBus.Publish(new EventChatMessageOnTopPlayer(strTemp)); + EventBus.PublishChannel(p.Srcroleid, new EventChatMessageOnTopPlayer(p.Srcroleid, strTemp)); + //pPlayer.SetLastSaidWords(strTemp, p.Emotion); } } } else if(ISNPCID(p.Srcroleid)) { + Debug.Log("[Cuong] ISNPCID " + strTemp); CECNPC pNPC = EC_Game.GetGameRun().GetWorld().GetNPCMan().GetNPC(p.Srcroleid); - if (pNPC) + if (pNPC != null) { string str; string template = pStrTab.GetWideString((int)FixedMsg.FIXMSG_CHAT2); @@ -1816,17 +1957,17 @@ namespace CSNetwork string message = string.Format( template, pNPC.GetName(), - szMsg + strTemp ); - /*EC_Game.GetGameRun().AddChatMessage( + EC_Game.GetGameRun().AddChatMessage( message, p.Channel, p.Srcroleid, null, 0, p.Emotion - );*/ + ); EventBus.Publish(new ChatMessageEvent(message)); CECUIHelper.RemoveNameFlagFromNPCChat(strTemp, out szMsg); @@ -1841,10 +1982,12 @@ namespace CSNetwork public struct ChatMessageEvent { public string context; + public byte channel; - public ChatMessageEvent(string context) + public ChatMessageEvent(string context, byte channel = 0) { this.context = context; + this.channel = channel; } } public void OnPrtcGetConfigRe(Protocol pProtocol) diff --git a/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs b/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs index 3c6420470e..155352d451 100644 --- a/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs +++ b/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs @@ -1,5 +1,8 @@ using System; +using System.Threading; +using BrewMonster.Scripts.ChatUI; using CSNetwork.GPDataType; +using Cysharp.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -16,6 +19,10 @@ namespace BrewMonster.PerfectWorld.Scripts.UI [Header("References")] [SerializeField] private CECPlayer hostplayer; + + [SerializeField] private float chatDisplayDuration = 5f; + + private CancellationTokenSource _chatCts; private const string DefaultLayerName = "Default"; private bool _isVisible; @@ -38,17 +45,19 @@ namespace BrewMonster.PerfectWorld.Scripts.UI SetVisible(true); RefreshName(); - EventBus.Subscribe((x)=> SetChatMessage(x)); + EventBus.SubscribeChannel(hostplayer.GetCharacterID(), SetChatMessage); EventBus.SubscribeChannel(hostplayer.m_PlayerInfo.cid, UpdateHostPlayerInfoUI); } private void OnDestroy() { + _chatCts?.Cancel(); + _chatCts?.Dispose(); if (hostplayer == null) { return; } - EventBus.Unsubscribe(SetChatMessage); + EventBus.UnsubscribeChannel(hostplayer.GetCharacterID(), SetChatMessage); EventBus.UnsubscribeChannel(hostplayer.m_PlayerInfo.cid, UpdateHostPlayerInfoUI); } @@ -127,21 +136,59 @@ namespace BrewMonster.PerfectWorld.Scripts.UI private void SetChatMessage(EventChatMessageOnTopPlayer cxt) { - Debug.Log("[Cuong] SetChatMessage"); if (chatText == null) { BMLogger.LogError("Don't have chatText TMProUI"); return; } - chatText.text = cxt.context; + ChatThreadDispatcher.Instance.Post(() =>{ + SetLastSaidWords(cxt.context); + }); + } + + public void SetLastSaidWords(string message, float duration = -1f) + { + if (chatText == null || string.IsNullOrEmpty(message)) + return; + + chatText.text = message; + chatText.gameObject.SetActive(true); + + // cancel timer cũ + _chatCts?.Cancel(); + _chatCts = new CancellationTokenSource(); + + float time = duration > 0 ? duration : chatDisplayDuration; + + HideChatAsync(time, _chatCts.Token).Forget(); + } + + private async UniTaskVoid HideChatAsync(float time, CancellationToken token) + { + try + { + await UniTask.Delay( + TimeSpan.FromSeconds(time), + cancellationToken: token + ); + + if (chatText != null) + chatText.gameObject.SetActive(false); + } + catch (OperationCanceledException) + { + // chat mới đến → timer cũ bị cancel + } } } public struct EventChatMessageOnTopPlayer { + public int roleId; public string context; - public EventChatMessageOnTopPlayer(string context) + public EventChatMessageOnTopPlayer(int roleId, string context) { + this.roleId = roleId; this.context = context; } } diff --git a/Assets/Prefabs/ChatSystem/ChatCanvas.prefab b/Assets/Prefabs/ChatSystem/ChatCanvas.prefab index 6220b2c21c..5d125222b4 100644 --- a/Assets/Prefabs/ChatSystem/ChatCanvas.prefab +++ b/Assets/Prefabs/ChatSystem/ChatCanvas.prefab @@ -36,6 +36,142 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: -20, y: -20} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &964780034169504921 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7133716985767026273} + - component: {fileID: 7030320337240937342} + - component: {fileID: 6160464800274915712} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7133716985767026273 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964780034169504921} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4454076196230765805} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7030320337240937342 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964780034169504921} + m_CullTransparentMesh: 1 +--- !u!114 &6160464800274915712 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964780034169504921} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Chat + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 1 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &1002556096278408428 GameObject: m_ObjectHideFlags: 0 @@ -291,6 +427,42 @@ MonoBehaviour: m_EditorClassIdentifier: m_Padding: {x: -8, y: -5, z: -8, w: -5} m_Softness: {x: 0, y: 0} +--- !u!1 &2806664982819397903 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4963429530816417249} + m_Layer: 5 + m_Name: Panel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4963429530816417249 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2806664982819397903} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 4454076196230765805} + m_Father: {fileID: 6199635200021499044} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &4190358850504021446 GameObject: m_ObjectHideFlags: 0 @@ -302,7 +474,6 @@ GameObject: - component: {fileID: 1983722419643715407} - component: {fileID: 5213722908587404148} - component: {fileID: 343348515878574983} - - component: {fileID: 7869768243714293468} - component: {fileID: 4104602819061927609} m_Layer: 5 m_Name: Chat_PanelUI @@ -369,23 +540,6 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 ---- !u!114 &7869768243714293468 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 4190358850504021446} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 5790dcd71bafcec4697f10b5366bec2c, type: 3} - m_Name: - m_EditorClassIdentifier: - scrollRect: {fileID: 5786096855155260472} - content: {fileID: 6422370174043654984} - messagePrefab: {fileID: 1976417251556044024, guid: dcc75569599675f46a99bc66a87efc9a, type: 3} - maxVisibleMessages: 30 - maxStoredMessages: 2000 --- !u!114 &4104602819061927609 MonoBehaviour: m_ObjectHideFlags: 0 @@ -399,7 +553,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: inputField: {fileID: 2725480406471239880} - sendButton: {fileID: 0} + maxChatLength: 256 --- !u!1 &4425286880128952433 GameObject: m_ObjectHideFlags: 0 @@ -946,6 +1100,139 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_baseMaterial: {fileID: 0} m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &6847709244521616304 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4454076196230765805} + - component: {fileID: 5054963699385999060} + - component: {fileID: 8898467712372273739} + - component: {fileID: 8167470961996667984} + m_Layer: 5 + m_Name: Button + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4454076196230765805 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6847709244521616304} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 7133716985767026273} + m_Father: {fileID: 4963429530816417249} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -746.7, y: 304} + m_SizeDelta: {x: 160, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5054963699385999060 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6847709244521616304} + m_CullTransparentMesh: 1 +--- !u!114 &8898467712372273739 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6847709244521616304} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &8167470961996667984 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6847709244521616304} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 8898467712372273739} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 5696601840968024233} + m_TargetAssemblyTypeName: BrewMonster.Scripts.ChatUI.ChatPanelUI, Assembly-CSharp + m_MethodName: OnHandlerChatButton + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 --- !u!1 &7245224818833188742 GameObject: m_ObjectHideFlags: 0 @@ -958,6 +1245,7 @@ GameObject: - component: {fileID: 1112302134216474585} - component: {fileID: 4776314709450025773} - component: {fileID: 7685278791772274093} + - component: {fileID: 5696601840968024233} m_Layer: 5 m_Name: ChatCanvas m_TagString: Untagged @@ -977,6 +1265,7 @@ RectTransform: m_LocalScale: {x: 0, y: 0, z: 0} m_ConstrainProportionsScale: 0 m_Children: + - {fileID: 4963429530816417249} - {fileID: 1983722419643715407} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -1048,6 +1337,24 @@ MonoBehaviour: m_BlockingMask: serializedVersion: 2 m_Bits: 4294967295 +--- !u!114 &5696601840968024233 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7245224818833188742} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5790dcd71bafcec4697f10b5366bec2c, type: 3} + m_Name: + m_EditorClassIdentifier: + scrollRect: {fileID: 5786096855155260472} + chatPanelUIGO: {fileID: 4190358850504021446} + content: {fileID: 6422370174043654984} + messagePrefab: {fileID: 1976417251556044024, guid: dcc75569599675f46a99bc66a87efc9a, type: 3} + maxVisibleMessages: 30 + maxStoredMessages: 2000 --- !u!1 &7942261885143240354 GameObject: m_ObjectHideFlags: 0 @@ -1252,8 +1559,8 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 15} + m_SizeDelta: {x: 0, y: -30} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &7307691078876554531 CanvasRenderer: diff --git a/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab b/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab new file mode 100644 index 0000000000..272eda8b10 --- /dev/null +++ b/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab @@ -0,0 +1,46 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1735791580658873054 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1508221814777778566} + - component: {fileID: 5062616109990871891} + m_Layer: 0 + m_Name: ChatThreadDispatcher + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1508221814777778566 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1735791580658873054} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &5062616109990871891 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1735791580658873054} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00392315a7ee47e7a9527eda504fb312, type: 3} + m_Name: + m_EditorClassIdentifier: diff --git a/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab.meta b/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab.meta new file mode 100644 index 0000000000..b73fe6626d --- /dev/null +++ b/Assets/Prefabs/ChatSystem/ChatThreadDispatcher.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 820475c3b63a5de4b9b311fe9e0de9b5 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs index 8b5bc34d73..d274305e5e 100644 --- a/Assets/Scripts/ChatInputHandler.cs +++ b/Assets/Scripts/ChatInputHandler.cs @@ -1,19 +1,41 @@ -using UnityEngine; +using UnityEngine; using TMPro; +using BrewMonster; using BrewMonster.Network; using CSNetwork.GPDataType; -using EditorAttributes; +using System; +using System.Collections.Generic; +using System.Text; +using BrewMonster.Scripts.Task; namespace BrewMonster.Scripts.ChatUI { + /// + /// Handles chat input from TMP_InputField and provides task-sharing functionality. + /// Converted from CDlgTaskTrace::OnCommand_Chat (DlgTaskTrace.cpp line 888). + /// public class ChatInputHandler : MonoBehaviour { - [Header("UI References")] + [Header("UI References")] public TMP_InputField inputField; // Ô gõ text + [Header("Chat Settings")] + [Tooltip("Giới hạn số ký tự tối đa cho mỗi tin nhắn chat")] + public int maxChatLength = 256; + + // ===== Constants from C++ source ===== + private const string INDENTATION = " "; + private const int MAX_ITEM_WANTED = 10; + private const int MAX_MONSTER_WANTED = 3; + private const int MAX_PLAYER_WANTED = MAX_MONSTER_WANTED; + private void Start() { inputField.onSubmit.AddListener(OnSubmit); + + // Giới hạn ký tự trực tiếp trên InputField + if (inputField.characterLimit <= 0 || inputField.characterLimit > maxChatLength) + inputField.characterLimit = maxChatLength; } private void OnDestroy() @@ -21,7 +43,7 @@ namespace BrewMonster.Scripts.ChatUI inputField.onSubmit.RemoveListener(OnSubmit); } - // Khi nhấn Enter + // ===== Khi nhấn Enter ===== private void OnSubmit(string text) { if (string.IsNullOrWhiteSpace(text)) @@ -30,9 +52,15 @@ namespace BrewMonster.Scripts.ChatUI HandleUserInput(text); } - // Hàm xử lý input (gửi server) + // ===== Hàm xử lý input (gửi server) ===== private void HandleUserInput(string text) { + // Áp dụng giới hạn ký tự + text = TruncateText(text, maxChatLength); + + // Lọc từ cấm (bad words filter) + CECUIManager.Instance?.FilterBadWords(ref text); + UnityGameSession.SendChatData( (byte)ChatChannel.GP_CHAT_LOCAL, text, @@ -52,5 +80,188 @@ namespace BrewMonster.Scripts.ChatUI HandleUserInput(text); } + + // ================================================================= + // SECTION: OnCommand_Chat - Chia sẻ trạng thái nhiệm vụ lên chat + // Converted from CDlgTaskTrace::OnCommand_Chat (DlgTaskTrace.cpp) + // ================================================================= + + /// + /// Builds a chat message describing task progress and sends it to the appropriate channel. + /// Gọi hàm này khi người chơi bấm nút "Chia sẻ nhiệm vụ" trên giao diện Task Trace. + /// + /// ID của nhiệm vụ cần chia sẻ. + public void OnCommand_Chat(uint idTask) + { + // TODO: Replace with actual EC_Game Unity Wrapper when available + // CECHostPlayer pHost = EC_Game.GetGameRun()?.GetHostPlayer(); + // CECTaskInterface pTask = pHost.GetTaskInterface(); + // ATaskTempl pTemp = pTask.GetTaskTemplMan().GetTaskTemplByID(idTask); + + // --- MOCK DATA FOR COMPILATION --- + bool bActiveTask = true; + StringBuilder sb = new StringBuilder(); + + // string strName = pTemp.GetName(); + string strName = $"[Task_{idTask}]"; + strName = DeleteColorStr(strName); + sb.Append(strName); + + /* + // --- Lấy thông tin trạng thái task --- + Task_State_info tsi = new Task_State_info(); + pTask.GetTaskStateInfo(idTask, ref tsi, bActiveTask); + + // --- NPC trao thưởng (nếu có thể hoàn thành task) --- + int nANPC = (int)pTemp.GetAwardNPC(); + if (nANPC > 0 && pTask.CanFinishTask(idTask)) + ... + */ + + // --- MOCK LOGIC TO APPEND PROGRESS --- + sb.Append(INDENTATION); + sb.AppendFormat(GetStringFromTable(248), GetItemName(123), 2, 10); // Mock item progress + + // --- Cắt bớt trailing '\r' --- + string strText = sb.ToString().TrimEnd('\r'); + + // --- Áp dụng giới hạn ký tự --- + strText = TruncateText(strText, maxChatLength); + + // --- Xác định kênh chat: Team nếu có đội, Local nếu không --- + byte channel = (byte)ChatChannel.GP_CHAT_LOCAL; + + // --- Gửi lên server --- + UnityGameSession.SendChatData(channel, strText, 0, 0); + + Debug.Log($"[ChatInputHandler] OnCommand_Chat: Sent mock task {idTask} to channel {channel}, length={strText.Length}"); + } + + // ================================================================= + // SECTION: Helper methods + // ================================================================= + + /// + /// Cắt chuỗi nếu vượt quá giới hạn ký tự, thêm dấu "..." ở cuối. + /// + private static string TruncateText(string text, int maxLength) + { + if (string.IsNullOrEmpty(text) || maxLength <= 0) + return text; + + if (text.Length <= maxLength) + return text; + + // Trừ 3 ký tự để chèn "..." + if (maxLength > 3) + return text.Substring(0, maxLength - 3) + "..."; + else + return text.Substring(0, maxLength); + } + + /// + /// Xóa các mã màu "^xxxxxx" ra khỏi chuỗi. + /// Tương đương CDlgTaskTrace::DeleteColorStr trong C++. + /// + private static string DeleteColorStr(string str) + { + if (string.IsNullOrEmpty(str)) + return str; + + StringBuilder result = new StringBuilder(str.Length); + for (int i = 0; i < str.Length; i++) + { + if (str[i] == '^' && i + 6 < str.Length) + { + // Kiểm tra 6 ký tự tiếp theo có phải hex không + bool isColorCode = true; + for (int j = 1; j <= 6; j++) + { + char c = str[i + j]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) + { + isColorCode = false; + break; + } + } + + if (isColorCode) + { + i += 6; // bỏ qua 6 ký tự hex + continue; + } + } + result.Append(str[i]); + } + return result.ToString(); + } + + /// + /// Format thời gian cho task (tương đương CDlgTask::FormatTime trong C++). + /// + private static string FormatTime(int nSec, string format, int totalTime) + { + int hours = nSec / 3600; + int minutes = (nSec % 3600) / 60; + int seconds = nSec % 60; + + string timeStr; + if (hours > 0) + timeStr = $"{hours:D2}:{minutes:D2}:{seconds:D2}"; + else + timeStr = $"{minutes:D2}:{seconds:D2}"; + + if (!string.IsNullOrEmpty(format)) + { + try { return string.Format(format, timeStr); } + catch { return timeStr; } + } + return timeStr; + } + + /// + /// Lấy string từ bảng String Table (StringFromTable). + /// Tương đương pUIMan->GetStringFromTable(id) trong C++. + /// + private static string GetStringFromTable(int id) + { + // Sử dụng hệ thống StringTable có sẵn trong project + var gameUI = CECUIManager.Instance?.GetInGameUIMan(); + string result = gameUI?.GetStringFromTable(id); + return result ?? $"[STR_{id}]"; + } + + /// + /// Lấy tên NPC theo ID từ ElementDataMan. + /// + private static string GetNPCName(int npcId) + { + // TODO: Kết nối với ElementDataMan của bạn khi đã port xong + // var pDataMan = EC_Game.GetElementDataMan(); + // return pDataMan?.GetNPCName(npcId); + return $"NPC_{npcId}"; + } + + /// + /// Lấy tên Monster theo ID từ ElementDataMan. + /// + private static string GetMonsterName(int monsterId) + { + // TODO: Kết nối với ElementDataMan của bạn khi đã port xong + // var pDataMan = EC_Game.GetElementDataMan(); + // return pDataMan?.GetMonsterName(monsterId); + return $"Monster_{monsterId}"; + } + + /// + /// Lấy tên Item theo ID từ ElementDataMan. + /// + private static string GetItemName(int itemId) + { + // TODO: Kết nối với ElementDataMan của bạn khi đã port xong + // var pDataMan = EC_Game.GetElementDataMan(); + // return pDataMan?.GetItemName(itemId); + return $"Item_{itemId}"; + } } } \ No newline at end of file