From 2adc3ede5957c1351653e7a3f7ba5714bdff6b09 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Wed, 18 Mar 2026 11:39:26 +0700
Subject: [PATCH] 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