From 2adc3ede5957c1351653e7a3f7ba5714bdff6b09 Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Wed, 18 Mar 2026 11:39:26 +0700 Subject: [PATCH 01/15] Add logic update friend UI, friend status --- .../Scripts/Managers/EC_Friend.cs | 96 +++++++++- .../Scripts/Network/CSNetwork/GameSession.cs | 73 ++++++++ .../Scripts/Players/EC_HostMsg.cs | 4 +- .../Scripts/UI/Dialogs/DlgFriendList.cs | 165 +++++++++++++++++- Assets/Scripts/CECHostPlayer.Chat.cs | 7 +- 5 files changed, 326 insertions(+), 19 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Friend.cs b/Assets/PerfectWorld/Scripts/Managers/EC_Friend.cs index f48abf62e3..7229264ad0 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_Friend.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_Friend.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; +using System.Text; using BrewMonster.Network; +using CSNetwork; +using CSNetwork.Protocols.RPCData; namespace BrewMonster { @@ -22,16 +25,18 @@ namespace BrewMonster public bool IsGameOnline() => IsGameOnline(Status); public bool IsGTOnline() => IsGTOnline(Status); + // C++ gnetdef.h: GAME_ONLINE=0x01, GT_INVISIBLE=0x80 public static bool IsGameOnline(byte s) { - // TODO: implement đúng theo logic C++ return (s & 0x01) != 0; } + // C++: except GAME_ONLINE and GT_INVISIBLE, any other status means GT online public static bool IsGTOnline(byte s) { - // TODO: implement đúng theo logic C++ - return (s & 0x02) != 0; + const byte GAME_ONLINE = 0x01; + const byte GT_INVISIBLE = 0x80; + return (s & unchecked((byte)~GAME_ONLINE) & unchecked((byte)~GT_INVISIBLE)) != 0; } } @@ -86,6 +91,68 @@ namespace BrewMonster public bool CheckInit() => m_Groups.Count > 0; + /// Reset from server getfriends_re. C++ format: status list is pairs (friendIndex, statusByte). + public void ResetFromServer(List groups, List friends, List status) + { + m_Groups.Clear(); + m_FriendTable.Clear(); + m_OfflineMsgs.Clear(); + m_FriendEx.Clear(); + m_SendInfo.Clear(); + + if (groups != null) + { + for (int i = 0; i < groups.Count; i++) + { + var g = groups[i]; + int gid = g?.gid ?? 0; + string gname = DecodeOctets(g?.name); + AddGroup(gid, string.IsNullOrEmpty(gname) ? $"Group{gid}" : gname); + } + } + + int friendCount = friends?.Count ?? 0; + for (int i = 0; i < friendCount; i++) + { + var f = friends[i]; + if (f == null) continue; + + int gid = f.gid; + if (GetGroupByID(gid) == null) + AddGroup(gid, $"Group{gid}"); + + string name = DecodeOctets(f.name); + AddFriend(f.rid, f.cls, gid, 0, name); + } + + // Apply status pairs: [friendIndex0, status0, friendIndex1, status1, ...] + if (status != null && (status.Count % 2) == 0 && friends != null) + { + for (int i = 0; i < status.Count; i += 2) + { + int friendIndex = status[i]; + byte st = status[i + 1]; + if (friendIndex < 0 || friendIndex >= friends.Count) continue; + var fi = friends[friendIndex]; + if (fi == null) continue; + ChangeFriendStatus(fi.rid, st); + } + } + } + + private static string DecodeOctets(Octets o) + { + if (o == null || o.Size <= 0) return string.Empty; + try + { + return Encoding.Unicode.GetString(o.ToArray(), 0, o.Size); + } + catch + { + return string.Empty; + } + } + public Friend AddFriend(int id, int profession, int groupId, byte status, string name) { var friend = new Friend @@ -130,8 +197,21 @@ namespace BrewMonster public void ChangeFriendStatus(int idFriend, byte status) { - if (m_FriendTable.TryGetValue(idFriend, out var friend)) - friend.Status = status; + if (!m_FriendTable.TryGetValue(idFriend, out var friend)) + return; + + friend.Status = status; + m_FriendTable[idFriend] = friend; + + var group = GetGroupByID(friend.GroupId); + if (group == null) return; + + for (int i = 0; i < group.Friends.Count; i++) + { + if (group.Friends[i].Id != idFriend) continue; + group.Friends[i] = friend; + break; + } } public void ChangeFriendGroup(int idFriend, int newGroupId) @@ -216,7 +296,7 @@ namespace BrewMonster public int GetGroupNum() => m_Groups.Count; - public GROUP? GetGroupByIndex(int index) + public GROUP GetGroupByIndex(int index) { if (index < 0 || index >= m_Groups.Count) return null; @@ -224,7 +304,7 @@ namespace BrewMonster return m_Groups[index]; } - public GROUP? GetGroupByID(int id) + public GROUP GetGroupByID(int id) { return m_Groups.Find(g => g.GroupId == id); } @@ -235,7 +315,7 @@ namespace BrewMonster public int GetOfflineMsgNum() => m_OfflineMsgs.Count; - public MESSAGE? GetOfflineMsg(int index) + public MESSAGE GetOfflineMsg(int index) { if (index < 0 || index >= m_OfflineMsgs.Count) return null; diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 0e451977a6..556064d102 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -66,6 +66,15 @@ namespace CSNetwork /// Raised when server sends PROTOCOL_ADDFRIEND_RE(203). Args: retcode (0=success), message. public event Action AddFriendResultReceived; + /// Raised when server sends PROTOCOL_GETFRIENDS_RE(207). + public event Action FriendListReceived; + + /// Raised when server sends PROTOCOL_FRIENDSTATUS(214). Args: roleid, status. + public event Action FriendStatusChanged; + + /// Raised when server sends PROTOCOL_FRIENDEXTLIST. + public event Action FriendExtListReceived; + /// Raised when the underlying network disconnects. public event Action Disconnected; @@ -612,6 +621,18 @@ namespace CSNetwork OnAddFriendRe((addfriend_re)protocol); break; + case ProtocolType.PROTOCOL_GETFRIENDS_RE: + OnGetFriendsRe((getfriends_re)protocol); + break; + + case ProtocolType.PROTOCOL_FRIENDEXTLIST: + OnFriendExtList((friendextlist)protocol); + break; + + case ProtocolType.PROTOCOL_FRIENDSTATUS: + OnFriendStatus((friendstatus)protocol); + break; + default: _logger.Log(LogType.Warning, $"Received unhandled protocol type: {protocol.GetPType()}"); break; @@ -635,6 +656,47 @@ namespace CSNetwork PostToUnityContext(() => FriendRequestReceived?.Invoke(p.Xid, p.Srcroleid, askerName)); } + private void OnGetFriendsRe(getfriends_re p) + { + PostToUnityContext(() => + { + try + { + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + host?.GetFriendMan()?.ResetFromServer(p.Groups, p.Friends, p.Status); + } + catch (Exception ex) + { + _logger.Log(LogType.Error, $"OnGetFriendsRe failed: {ex.Message}"); + _logger.LogException(ex); + } + FriendListReceived?.Invoke(p); + }); + } + + private void OnFriendExtList(friendextlist p) + { + PostToUnityContext(() => FriendExtListReceived?.Invoke(p)); + } + + private void OnFriendStatus(friendstatus p) + { + PostToUnityContext(() => + { + try + { + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + host?.GetFriendMan()?.ChangeFriendStatus(p.Roleid, p.Status); + } + catch (Exception ex) + { + _logger.Log(LogType.Error, $"OnFriendStatus failed: {ex.Message}"); + _logger.LogException(ex); + } + FriendStatusChanged?.Invoke(p.Roleid, p.Status); + }); + } + private void OnAddFriendRe(addfriend_re p) { string friendName = ""; @@ -2468,6 +2530,17 @@ namespace CSNetwork SendProtocol(p); } + /// Send PROTOCOL_DELFRIEND(212). Delete a friend by role id. + public void Friend_Del(int friendId) + { + var p = new delfriend(); + p.Roleid = m_iCharID; + p.Friendid = friendId; + p.Localsid = (int)_localsid; + SendProtocol(p); + Friend_GetList(); + } + public void c2s_SendCmdTeamKickMember(int idMember) { var g = new gamedatasend(); diff --git a/Assets/PerfectWorld/Scripts/Players/EC_HostMsg.cs b/Assets/PerfectWorld/Scripts/Players/EC_HostMsg.cs index d844edb57e..0910bfa859 100644 --- a/Assets/PerfectWorld/Scripts/Players/EC_HostMsg.cs +++ b/Assets/PerfectWorld/Scripts/Players/EC_HostMsg.cs @@ -13,6 +13,8 @@ namespace BrewMonster { public partial class CECHostPlayer { + private readonly CECFriendMan _friendMan = new CECFriendMan(); + public CECFriendMan GetFriendMan() => _friendMan; // 服务器控制的额外操作限制 public enum PLAYER_LIMIT @@ -369,7 +371,7 @@ namespace BrewMonster CECNPC pNPC = EC_Game.GetGameRun().GetWorld().GetNPCMan().GetNPC(m_pPetCorral.GetActivePetNPCID()); if (pNPC && pNPC.GetMasterID() == GetCharacterID()) { - //pNPC.BubbleText(CECNPC::BUBBLE_HPWARN, 0); + //pNPC.BubbleText(CECNPC::BUBBLE_HPWARN, 0); } } } diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgFriendList.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgFriendList.cs index 02923f0597..ba44ae801e 100644 --- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgFriendList.cs +++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgFriendList.cs @@ -1,6 +1,8 @@ using BrewMonster.Network; using BrewMonster.Scripts.Pet; using BrewMonster.UI; +using CSNetwork; +using CSNetwork.Protocols; using CSNetwork.GPDataType; using ModelRenderer.Scripts.GameData; using System; @@ -14,6 +16,13 @@ namespace BrewMonster { public class CDlgFriendList : AUIDialog { + private enum FriendTab + { + Friend = 0, + Blacklist = 1, + Archrival = 2 + } + [Header("Buttons")] [SerializeField] private Button m_helpBtn; [SerializeField] private Button m_friendBtn; @@ -41,6 +50,14 @@ namespace BrewMonster [SerializeField] private GameObject m_contextMenu; [SerializeField] private GameObject m_inputName; + private CSNetwork.GameSession _session; + private FriendTab _currentTab = FriendTab.Friend; + private int _selectedFriendId; + private Image _selectedRowImage; + private Color _selectedRowOriginalColor; + private TMP_InputField _addNameInput; + private static readonly Color SelectedRowColor = new Color32(255, 255, 255, 60); + public void OnInitDialog() { if (!IsShow()) @@ -48,6 +65,18 @@ namespace BrewMonster Show(true); } + _session = UnityGameSession.Instance?.GameSession; + if (_session != null) + { + _session.FriendListReceived -= OnFriendListReceived; + _session.FriendListReceived += OnFriendListReceived; + _session.FriendStatusChanged -= OnFriendStatusChanged; + _session.FriendStatusChanged += OnFriendStatusChanged; + _session.Friend_GetList(); + } + + RefreshListFromHost(); + if (m_helpBtn != null) { m_helpBtn.onClick.RemoveAllListeners(); @@ -62,17 +91,18 @@ namespace BrewMonster m_friendBtn.onClick.RemoveAllListeners(); m_friendBtn.onClick.AddListener(() => { - // TODO: Show friend list + _currentTab = FriendTab.Friend; + RefreshListFromHost(); }); } - if (m_blacklistBtn != null) { m_blacklistBtn.onClick.RemoveAllListeners(); m_blacklistBtn.onClick.AddListener(() => { - // TODO: Show blacklist + _currentTab = FriendTab.Blacklist; + RefreshListFromHost(); }); } @@ -81,7 +111,8 @@ namespace BrewMonster m_archrivalBtn.onClick.RemoveAllListeners(); m_archrivalBtn.onClick.AddListener(() => { - // TODO: Show archrival list + _currentTab = FriendTab.Archrival; + RefreshListFromHost(); }); } @@ -90,7 +121,9 @@ namespace BrewMonster m_addBtn.onClick.RemoveAllListeners(); m_addBtn.onClick.AddListener(() => { - m_inputName.SetActive(true); + EnsureAddNameInput(); + if (m_inputName != null) + m_inputName.SetActive(true); }); } @@ -99,7 +132,9 @@ namespace BrewMonster m_deleteBtn.onClick.RemoveAllListeners(); m_deleteBtn.onClick.AddListener(() => { - // TODO: Delete selected friend + if (_session == null) return; + if (_selectedFriendId <= 0) return; + _session.Friend_Del(_selectedFriendId); }); } @@ -117,7 +152,15 @@ namespace BrewMonster m_confirmBtn.onClick.RemoveAllListeners(); m_confirmBtn.onClick.AddListener(() => { - // TODO: Confirm adding friend with name from input field + if (_session == null) return; + EnsureAddNameInput(); + var n = _addNameInput != null ? _addNameInput.text : string.Empty; + n = (n ?? string.Empty).Trim(); + if (n.Length <= 0) return; + + _session.Friend_Add(0, n); + if (m_inputName != null) + m_inputName.SetActive(false); }); } @@ -184,5 +227,113 @@ namespace BrewMonster }); } } + + public override void OnDisable() + { + base.OnDisable(); + if (_session != null) + { + _session.FriendListReceived -= OnFriendListReceived; + _session.FriendStatusChanged -= OnFriendStatusChanged; + } + } + + private void OnFriendListReceived(getfriends_re _) + { + RefreshListFromHost(); + } + + private void OnFriendStatusChanged(int _, byte __) + { + RefreshListFromHost(); + } + + private void RefreshListFromHost() + { + if (m_friendContainer == null || m_friendDetailPrefabs == null) + return; + + ClearSelection(); + + for (int i = m_friendContainer.childCount - 1; i >= 0; i--) + { + var child = m_friendContainer.GetChild(i); + if (child != null) + Destroy(child.gameObject); + } + + if (_currentTab != FriendTab.Friend) + return; + + var host = GetHostPlayer(); + var man = host?.GetFriendMan(); + if (man == null || !man.CheckInit()) + return; + + int groupNum = man.GetGroupNum(); + for (int gi = 0; gi < groupNum; gi++) + { + var g = man.GetGroupByIndex(gi); + if (g == null) continue; + + foreach (var f in g.Friends) + { + var go = Instantiate(m_friendDetailPrefabs, m_friendContainer); + WireRowSelection(go, f.Id); + var nameTxt = go.transform.Find("text_name")?.GetComponent(); + var stateTxt = go.transform.Find("text_state")?.GetComponent(); + + if (nameTxt != null) nameTxt.text = string.IsNullOrEmpty(f.Name) ? f.Id.ToString() : f.Name; + if (stateTxt != null) + { + bool online = f.IsGameOnline(); + stateTxt.text = online ? "Online" : "Offline"; + } + } + } + } + + private void WireRowSelection(GameObject rowGo, int friendId) + { + if (rowGo == null) return; + + var btn = rowGo.GetComponent