From 2015dbf73e06cb8dadd06bbfe8491575bfd00877 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Thu, 12 Mar 2026 20:58:29 +0700
Subject: [PATCH 1/4] Add PROTOCOL_ADDFRIEND and enable add friend request
---
.../Prefab/UI/PlayerOptionPopup.prefab | 2 +-
.../Scripts/Network/CSNetwork/GameSession.cs | 76 ++++++++++++++++++-
.../Scripts/Network/CSNetwork/OctetsStream.cs | 17 +++++
.../CSNetwork/Protocols/addfriend_re.cs | 43 +++++++++++
.../CSNetwork/Protocols/addfriend_re.cs.meta | 2 +
.../CSNetwork/Protocols/addfriendrqst.cs | 42 ++++++++++
.../CSNetwork/Protocols/addfriendrqst.cs.meta | 2 +
.../Protocols/rpcdata/GFriendInfo.cs | 32 ++++++++
.../Protocols/rpcdata/GFriendInfo.cs.meta | 2 +
.../Scripts/Network/UnityGameSession.cs | 35 ++++++++-
.../Scripts/UI/Dialogs/DlgPlayerOptions.cs | 4 +-
.../Scripts/UI/Login/LoginScreenUI.cs | 1 +
12 files changed, 253 insertions(+), 5 deletions(-)
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs.meta
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs.meta
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs.meta
diff --git a/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab b/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab
index d432d82ba0..c71763bd99 100644
--- a/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab
+++ b/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab
@@ -509,7 +509,7 @@ MonoBehaviour:
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
- m_Interactable: 0
+ m_Interactable: 1
m_TargetGraphic: {fileID: 3492245093881047436}
m_OnClick:
m_PersistentCalls:
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
index 319935be24..c02d9adfa7 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
@@ -51,6 +51,12 @@ namespace CSNetwork
/// Raised when server sends PROTOCOL_PLAYERLOGOUT(69).
public event Action PlayerLogoutReceived;
+ /// Raised when server sends RPC_ADDFRIENDRQST(204) — another player wants to add you as friend. Args: srcroleid, askerName.
+ public event Action FriendRequestReceived;
+
+ /// Raised when server sends PROTOCOL_ADDFRIEND_RE(203). Args: retcode (0=success), message.
+ public event Action AddFriendResultReceived;
+
/// Raised when the underlying network disconnects.
public event Action Disconnected;
@@ -587,6 +593,14 @@ namespace CSNetwork
}
break;
+ case ProtocolType.RPC_ADDFRIENDRQST:
+ OnAddFriendRqst((addfriendrqst)protocol);
+ break;
+
+ case ProtocolType.PROTOCOL_ADDFRIEND_RE:
+ OnAddFriendRe((addfriend_re)protocol);
+ break;
+
default:
_logger.Log(LogType.Warning, $"Received unhandled protocol type: {protocol.GetPType()}");
break;
@@ -601,6 +615,43 @@ namespace CSNetwork
// PostToUnityContext(() => PlayerLogoutReceived?.Invoke(protocol));
}
+ private void OnAddFriendRqst(addfriendrqst p)
+ {
+ string askerName = "";
+ if (p.Srcname != null && p.Srcname.Size > 0)
+ askerName = System.Text.Encoding.Unicode.GetString(p.Srcname.ToArray(), 0, p.Srcname.Size);
+ PostToUnityContext(() => FriendRequestReceived?.Invoke(p.Srcroleid, askerName));
+ }
+
+ private void OnAddFriendRe(addfriend_re p)
+ {
+ string friendName = "";
+ if (p.info?.name != null && p.info.name.Size > 0)
+ friendName = System.Text.Encoding.Unicode.GetString(p.info.name.ToArray(), 0, p.info.name.Size);
+ string msg = p.retcode == 0
+ ? $"Add friend success: {friendName}"
+ : $"Add friend failed: {GetAddFriendRetcodeMessage(p.retcode)}";
+ PostToUnityContext(() => AddFriendResultReceived?.Invoke(p.retcode, msg));
+ }
+
+ /// FS_ERR server retcodes for addfriend_re.
+ private static string GetAddFriendRetcodeMessage(byte retcode)
+ {
+ switch (retcode)
+ {
+ case 0: return "Success";
+ case 1: return "Player is offline";
+ case 2: return "Request was refused";
+ case 3: return "Timeout";
+ case 4: return "No remaining space";
+ case 5: return "Not found";
+ case 6: return "Invalid parameter";
+ case 7: return "Duplicate entry";
+ case 8: return "Friend list has not been retrieved yet";
+ default: return $"retcode={retcode}";
+ }
+ }
+
// void CECGameSession::OnPrtcPlayerLogout(GNET::Protocol* pProtocol)
// {
// using namespace GNET;
@@ -2039,6 +2090,29 @@ namespace CSNetwork
SendProtocol(g);
}
+ /// Send PROTOCOL_ADDFRIEND(202). Port of CECGameSession::friend_Add(idPlayer, szName).
+ public void Friend_Add(int idPlayer, string name)
+ {
+ var p = new addfriend();
+ p.Srcroleid = m_iCharID;
+ p.Dstroleid = idPlayer;
+ if (!string.IsNullOrEmpty(name))
+ p.Dstname = new Octets(System.Text.Encoding.Unicode.GetBytes(name));
+ else
+ p.Dstname = new Octets();
+ p.Srclsid = (int)_localsid;
+ SendProtocol(p);
+ }
+
+ /// Send PROTOCOL_GETFRIENDS(206). Port of CECGameSession::friend_GetList().
+ public void Friend_GetList()
+ {
+ var p = new getfriends();
+ p.Roleid = m_iCharID;
+ p.Localsid = (int)_localsid;
+ SendProtocol(p);
+ }
+
public void c2s_SendCmdTeamKickMember(int idMember)
{
var g = new gamedatasend();
@@ -2047,7 +2121,7 @@ namespace CSNetwork
}
public void c2s_SendCmdTeamLeaveParty()
- {
+ {
var g = new gamedatasend();
g.Data = C2SCommandFactory.CreateTeamLeavePartyCommand();
SendProtocol(g);
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/OctetsStream.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/OctetsStream.cs
index 5e9d7affe4..24402c1b0b 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/OctetsStream.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/OctetsStream.cs
@@ -109,6 +109,23 @@ namespace CSNetwork
_position += 4;
}
+ /// Write int32 in little-endian (for GNET protocol compatibility with C++ client/server).
+ public void WriteInt32LE(int value)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ _octets.Insert(_position, bytes);
+ _position += 4;
+ }
+
+ /// Read int32 in little-endian (for GNET protocol compatibility with C++ client/server).
+ public int ReadInt32LE()
+ {
+ if (_position + 4 > _octets.Length)
+ throw new IndexOutOfRangeException("Attempt to read beyond the end of the stream.");
+ var bytes = ReadBytes(4);
+ return BitConverter.ToInt32(bytes, 0);
+ }
+
public void Write(float value)
{
var bytes = BitConverter.GetBytes(value);
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs
new file mode 100644
index 0000000000..778976ce52
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs
@@ -0,0 +1,43 @@
+using CSNetwork.Protocols.RPCData;
+
+namespace CSNetwork.Protocols
+{
+ /// PROTOCOL_ADDFRIEND_RE(203). Port of GNET::AddFriend_Re (inl/addfriend_re).
+ public class addfriend_re : Protocol
+ {
+ public byte retcode { get; set; }
+ public GFriendInfo info { get; set; }
+ public uint srclsid { get; set; }
+
+ public addfriend_re() : base(ProtocolType.PROTOCOL_ADDFRIEND_RE)
+ {
+ info = new GFriendInfo();
+ }
+
+ public override Protocol Clone() => new addfriend_re
+ {
+ retcode = retcode,
+ info = info != null ? new GFriendInfo { rid = info.rid, cls = info.cls, gid = info.gid, name = new Octets(info.name?.ToArray() ?? System.Array.Empty()) } : new GFriendInfo(),
+ srclsid = srclsid
+ };
+
+ public override void Marshal(OctetsStream os)
+ {
+ os.Write(retcode);
+ if (info != null) info.Marshal(os); else new GFriendInfo().Marshal(os);
+ os.Write(srclsid);
+ }
+
+ public override void Unmarshal(OctetsStream os)
+ {
+ retcode = os.ReadByte();
+ info = new GFriendInfo();
+ info.Unmarshal(os);
+ srclsid = os.ReadUInt32();
+ }
+
+ public override int PriorPolicy() => 1;
+
+ public override bool SizePolicy(int size) => size <= 128;
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs.meta
new file mode 100644
index 0000000000..8c52d27971
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriend_re.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 158e35cf46fa51a4cba4fa7485fdb534
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
new file mode 100644
index 0000000000..09c5142f92
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace CSNetwork.Protocols
+{
+ /// RPC_ADDFRIENDRQST(204). Port of GNET AddFriendRqstArg; server notifies target of friend request.
+ public class addfriendrqst : Protocol
+ {
+ public int Srcroleid { get; set; }
+ public Octets Srcname { get; set; }
+ public uint Dstlsid { get; set; }
+
+ public addfriendrqst() : base(ProtocolType.RPC_ADDFRIENDRQST)
+ {
+ Srcname = new Octets();
+ }
+
+ public override Protocol Clone() => new addfriendrqst
+ {
+ Srcroleid = Srcroleid,
+ Srcname = new Octets(Srcname.ToArray()),
+ Dstlsid = Dstlsid
+ };
+
+ public override void Marshal(OctetsStream os)
+ {
+ os.Write(Srcroleid);
+ os.Write(Srcname);
+ os.Write(Dstlsid);
+ }
+
+ public override void Unmarshal(OctetsStream os)
+ {
+ Srcroleid = os.ReadInt32();
+ Srcname = os.ReadOctets();
+ Dstlsid = os.ReadUInt32();
+ }
+
+ public override int PriorPolicy() => 1;
+
+ public override bool SizePolicy(int size) => size <= 256;
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs.meta
new file mode 100644
index 0000000000..d935487cb5
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bf82fc1afd675674588f017b6f9e0621
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs
new file mode 100644
index 0000000000..c592b4b9f8
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs
@@ -0,0 +1,32 @@
+namespace CSNetwork.Protocols.RPCData
+{
+ /// Port of GNET::GFriendInfo (rpcdata/gfriendinfo).
+ public class GFriendInfo : IMarshallable
+ {
+ public int rid;
+ public byte cls;
+ public byte gid;
+ public Octets name;
+
+ public GFriendInfo()
+ {
+ name = new Octets();
+ }
+
+ public void Marshal(OctetsStream os)
+ {
+ os.Write(rid);
+ os.Write(cls);
+ os.Write(gid);
+ os.Write(name ?? new Octets());
+ }
+
+ public void Unmarshal(OctetsStream os)
+ {
+ rid = os.ReadInt32();
+ cls = os.ReadByte();
+ gid = os.ReadByte();
+ name = os.ReadOctets();
+ }
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs.meta
new file mode 100644
index 0000000000..0a4b947df7
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a0bf31ab9d8853a479215aa7e1fa5e94
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
index 01a1120f31..4c3501d56f 100644
--- a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
@@ -173,7 +173,9 @@ namespace BrewMonster.Network
// Subscribe to unexpected disconnects
_gameSession.Disconnected += OnUnexpectedDisconnect;
-
+ _gameSession.FriendRequestReceived += OnFriendRequestReceived;
+ _gameSession.AddFriendResultReceived += OnAddFriendResultReceived;
+
_isInitialized = true;
DontDestroyOnLoad(gameObject);
@@ -184,6 +186,8 @@ namespace BrewMonster.Network
// Tell LoginScene what to show next.
LogoutFlowState.NextLoginEntry = entryTarget;
_gameSession.Disconnected -= OnUnexpectedDisconnect;
+ _gameSession.FriendRequestReceived -= OnFriendRequestReceived;
+ _gameSession.AddFriendResultReceived -= OnAddFriendResultReceived;
EC_ManMessageMono.Instance.CECNPCMan.Release();
if (clearSavedCreds)
@@ -619,6 +623,16 @@ namespace BrewMonster.Network
{
Instance._gameSession.c2s_SendCmdDuelReply(accept, idInviter);
}
+ /// Send PROTOCOL_ADDFRIEND(202). Port of CECGameSession::friend_Add.
+ public static void Friend_Add(int idPlayer, string name)
+ {
+ Instance._gameSession.Friend_Add(idPlayer, name ?? "");
+ }
+ /// Send PROTOCOL_GETFRIENDS(206). Port of CECGameSession::friend_GetList().
+ public static void Friend_GetList()
+ {
+ Instance._gameSession.Friend_GetList();
+ }
public static void c2s_CmdTeamKickMember(int idMember)
{
Instance._gameSession.c2s_SendCmdTeamKickMember(idMember);
@@ -694,6 +708,25 @@ namespace BrewMonster.Network
///
/// Handles unexpected server disconnections. Shows a message box and returns to login.
///
+ private void OnFriendRequestReceived(int srcroleid, string askerName)
+ {
+ string name = string.IsNullOrEmpty(askerName) ? ("Player " + srcroleid) : askerName;
+ CECUIManager.Instance?.ShowMessageBox(
+ title: "Friend Request",
+ message: $"{name} wants to add you as a friend.",
+ messageBoxType: MessageBoxType.BothYesNoButton,
+ onClickedYes: () => { /* TODO: accept and call friend_AddResponse */ },
+ onClickedNo: () => { /* TODO: refuse */ });
+ }
+
+ private void OnAddFriendResultReceived(byte retcode, string message)
+ {
+ CECUIManager.Instance?.ShowMessageBox(
+ title: retcode == 0 ? "Friend added" : "Add friend failed",
+ message: message,
+ messageBoxType: MessageBoxType.YesButton);
+ }
+
private void OnUnexpectedDisconnect()
{
// If this was an intentional disconnect (logout), skip UI
diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
index 0d341b9901..9c6c348d21 100644
--- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs
@@ -78,8 +78,8 @@ namespace BrewMonster.UI
void OnAddFriend(int characterId)
{
- Debug.Log("OnAddFriend: " + characterId);
- // TODO: c2s add friend when available
+ string name = EC_ManMessageMono.Instance?.GetECManPlayer?.GetElsePlayer(characterId)?.GetName() ?? "";
+ UnityGameSession.Friend_Add(characterId, name);
}
void OnDuel(int characterId)
diff --git a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
index 0fbd136d8b..85014f61e4 100644
--- a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
@@ -370,6 +370,7 @@ namespace BrewMonster.UI
await Task.Delay(1000);
UnityGameSession.RequestCheckSecurityPassWd("");
await Task.Delay(1000);
+ UnityGameSession.Friend_GetList(); // C++ friend_GetList(); required before Add Friend
}
//private void OnInventoryReceived(List inventoryData)
From 8655b963536ac8a59153fa6302c31e1b0da94442 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Mon, 16 Mar 2026 17:39:25 +0700
Subject: [PATCH 2/4] Handle getfriend_re so the game didnt died and add friend
request handle
---
.../Scripts/Network/CSNetwork/GameSession.cs | 29 ++++++++++--
.../CSNetwork/Protocols/addfriendrqst.cs | 7 ++-
.../CSNetwork/Protocols/addfriendrqstres.cs | 44 ++++++++++++++++++
.../Protocols/addfriendrqstres.cs.meta | 2 +
.../CSNetwork/Protocols/getfriends_re.cs | 45 +++++++++++++++++++
.../CSNetwork/Protocols/getfriends_re.cs.meta | 2 +
.../Scripts/UI/Login/LoginScreenUI.cs | 3 +-
7 files changed, 127 insertions(+), 5 deletions(-)
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs.meta
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs.meta
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
index c02d9adfa7..5e8bad1b18 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
@@ -51,8 +51,11 @@ namespace CSNetwork
/// Raised when server sends PROTOCOL_PLAYERLOGOUT(69).
public event Action PlayerLogoutReceived;
- /// Raised when server sends RPC_ADDFRIENDRQST(204) — another player wants to add you as friend. Args: srcroleid, askerName.
- public event Action FriendRequestReceived;
+ ///
+ /// Raised when server sends RPC_ADDFRIENDRQST(204) — another player wants to add you as friend.
+ /// Args: xid (RPC transaction id), srcroleid, askerName.
+ ///
+ public event Action FriendRequestReceived;
/// Raised when server sends PROTOCOL_ADDFRIEND_RE(203). Args: retcode (0=success), message.
public event Action AddFriendResultReceived;
@@ -620,7 +623,8 @@ namespace CSNetwork
string askerName = "";
if (p.Srcname != null && p.Srcname.Size > 0)
askerName = System.Text.Encoding.Unicode.GetString(p.Srcname.ToArray(), 0, p.Srcname.Size);
- PostToUnityContext(() => FriendRequestReceived?.Invoke(p.Srcroleid, askerName));
+ // Forward xid + srcroleid + name to Unity layer so it can answer via Friend_AddResponse(xid, agree)
+ PostToUnityContext(() => FriendRequestReceived?.Invoke(p.Xid, p.Srcroleid, askerName));
}
private void OnAddFriendRe(addfriend_re p)
@@ -631,6 +635,11 @@ namespace CSNetwork
string msg = p.retcode == 0
? $"Add friend success: {friendName}"
: $"Add friend failed: {GetAddFriendRetcodeMessage(p.retcode)}";
+ if (p.retcode == 0)
+ {
+ Friend_GetList();
+ }
+
PostToUnityContext(() => AddFriendResultReceived?.Invoke(p.retcode, msg));
}
@@ -2104,6 +2113,20 @@ namespace CSNetwork
SendProtocol(p);
}
+ ///
+ /// Send RPC_ADDFRIENDRQST(204) response. Port of CECGameSession::friend_AddResponse(dwHandle, bAgree).
+ /// Answer to received friend request: xid from request, agree = true (0) or false (69).
+ /// Matches GNET Rpc XID: low bit = is_request; response must clear that bit.
+ ///
+ public void Friend_AddResponse(uint xid, bool agree)
+ {
+ const byte ERR_TRADE_AGREE = 0;
+ const byte ERR_TRADE_REFUSE = 69;
+ uint responseXid = xid & 0x7FFFFFFFu; // clear request bit, keep count
+ var p = new addfriendrqstres(responseXid, agree ? ERR_TRADE_AGREE : ERR_TRADE_REFUSE);
+ SendProtocol(p);
+ }
+
/// Send PROTOCOL_GETFRIENDS(206). Port of CECGameSession::friend_GetList().
public void Friend_GetList()
{
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
index 09c5142f92..a52735a7ae 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs
@@ -2,9 +2,11 @@ using System;
namespace CSNetwork.Protocols
{
- /// RPC_ADDFRIENDRQST(204). Port of GNET AddFriendRqstArg; server notifies target of friend request.
+ /// RPC_ADDFRIENDRQST(204). Port of GNET AddFriendRqst (argument + xid). Server notifies target of friend request. Body: xid (4) + AddFriendRqstArg.
public class addfriendrqst : Protocol
{
+ /// RPC transaction id; must be sent back in addfriendrqstres so server can match the response.
+ public uint Xid { get; set; }
public int Srcroleid { get; set; }
public Octets Srcname { get; set; }
public uint Dstlsid { get; set; }
@@ -16,6 +18,7 @@ namespace CSNetwork.Protocols
public override Protocol Clone() => new addfriendrqst
{
+ Xid = Xid,
Srcroleid = Srcroleid,
Srcname = new Octets(Srcname.ToArray()),
Dstlsid = Dstlsid
@@ -23,6 +26,7 @@ namespace CSNetwork.Protocols
public override void Marshal(OctetsStream os)
{
+ os.Write(Xid);
os.Write(Srcroleid);
os.Write(Srcname);
os.Write(Dstlsid);
@@ -30,6 +34,7 @@ namespace CSNetwork.Protocols
public override void Unmarshal(OctetsStream os)
{
+ Xid = os.ReadUInt32();
Srcroleid = os.ReadInt32();
Srcname = os.ReadOctets();
Dstlsid = os.ReadUInt32();
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs
new file mode 100644
index 0000000000..77836886de
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace CSNetwork.Protocols
+{
+ ///
+ /// RPC_ADDFRIENDRQST(204) response. Port of GNET Rpc marshal (xid + AddFriendRqstRes).
+ /// Client sends this to answer a friend request: retcode 0 = agree, 69 = refuse (ERR_TRADE_AGREE / ERR_TRADE_REFUSE).
+ ///
+ public class addfriendrqstres : Protocol
+ {
+ /// RPC transaction id from the received addfriendrqst; server uses it to match the response.
+ public uint Xid { get; set; }
+ /// 0 = ERR_TRADE_AGREE (accept), 69 = ERR_TRADE_REFUSE (decline).
+ public byte retcode { get; set; }
+
+ public addfriendrqstres() : base(ProtocolType.RPC_ADDFRIENDRQST)
+ {
+ }
+
+ public addfriendrqstres(uint xid, byte retcode) : base(ProtocolType.RPC_ADDFRIENDRQST)
+ {
+ Xid = xid;
+ this.retcode = retcode;
+ }
+
+ public override Protocol Clone() => new addfriendrqstres(Xid, retcode);
+
+ public override void Marshal(OctetsStream os)
+ {
+ os.Write(Xid);
+ os.Write(retcode);
+ }
+
+ public override void Unmarshal(OctetsStream os)
+ {
+ Xid = os.ReadUInt32();
+ retcode = os.ReadByte();
+ }
+
+ public override int PriorPolicy() => 1;
+
+ public override bool SizePolicy(int size) => size <= 64;
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs.meta
new file mode 100644
index 0000000000..6391223f99
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqstres.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 80bb3b37b64736040a16d67848d9f953
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
new file mode 100644
index 0000000000..b49c5ea69d
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace CSNetwork.Protocols
+{
+ /// PROTOCOL_GETFRIENDS_RE(207). Server response to getfriends(206). Payload 11 bytes.
+ public class getfriends_re : Protocol
+ {
+ public byte Retcode { get; set; }
+ public int Roleid { get; set; }
+ public int Localsid { get; set; }
+ public short Extra { get; set; }
+
+ public getfriends_re() : base(ProtocolType.PROTOCOL_GETFRIENDS_RE)
+ {
+ }
+
+ public override Protocol Clone() => new getfriends_re
+ {
+ Retcode = Retcode,
+ Roleid = Roleid,
+ Localsid = Localsid,
+ Extra = Extra
+ };
+
+ public override void Marshal(OctetsStream os)
+ {
+ os.Write(Retcode);
+ os.Write(Roleid);
+ os.Write(Localsid);
+ os.Write(Extra);
+ }
+
+ public override void Unmarshal(OctetsStream os)
+ {
+ Retcode = os.ReadByte();
+ Roleid = os.ReadInt32();
+ Localsid = os.ReadInt32();
+ Extra = os.ReadInt16();
+ }
+
+ public override int PriorPolicy() => 1;
+
+ public override bool SizePolicy(int size) => size <= 4096;
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs.meta
new file mode 100644
index 0000000000..2deea0dd28
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bd80f916689adb4488d055af7d1f0465
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
index 85014f61e4..34668e2fa4 100644
--- a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
+++ b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs
@@ -369,8 +369,9 @@ namespace BrewMonster.UI
UnityGameSession.RequestAllInventoriesAsync(() => { /*BMLogger.Log("Sent Inventory Detail Requests (all packs)");*/ }, 0, 1, 2);
await Task.Delay(1000);
UnityGameSession.RequestCheckSecurityPassWd("");
+ // C++ friend_GetList(); required before Add Friend
await Task.Delay(1000);
- UnityGameSession.Friend_GetList(); // C++ friend_GetList(); required before Add Friend
+ UnityGameSession.Friend_GetList();
}
//private void OnInventoryReceived(List inventoryData)
From 5dfab238fab15e58bd9bc3799efe8c066bfac923 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Mon, 16 Mar 2026 17:39:30 +0700
Subject: [PATCH 3/4] Update UnityGameSession.cs
---
.../Scripts/Network/UnityGameSession.cs | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
index 4c3501d56f..5d024ef10c 100644
--- a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
+++ b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
@@ -163,6 +163,9 @@ namespace BrewMonster.Network
{
BaseSecurity.Initizalize();
ProtocolFactory.RegisterAllProtocols();
+ // Type 204 is used for both addfriendrqst (server→client request) and addfriendrqstres (client→server response).
+ // Client only receives addfriendrqst; ensure decode uses it so we don't get InvalidCastException.
+ Protocol.Register((uint)ProtocolType.RPC_ADDFRIENDRQST);
_gameSession = new GameSession();
#if UNITY_EDITOR
var path = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets"));
@@ -633,6 +636,10 @@ namespace BrewMonster.Network
{
Instance._gameSession.Friend_GetList();
}
+ public static void Friend_AddResponse(uint xid, bool agree)
+ {
+ Instance._gameSession.Friend_AddResponse(xid, agree);
+ }
public static void c2s_CmdTeamKickMember(int idMember)
{
Instance._gameSession.c2s_SendCmdTeamKickMember(idMember);
@@ -708,15 +715,15 @@ namespace BrewMonster.Network
///
/// Handles unexpected server disconnections. Shows a message box and returns to login.
///
- private void OnFriendRequestReceived(int srcroleid, string askerName)
+ private void OnFriendRequestReceived(uint xid, int srcroleid, string askerName)
{
string name = string.IsNullOrEmpty(askerName) ? ("Player " + srcroleid) : askerName;
CECUIManager.Instance?.ShowMessageBox(
title: "Friend Request",
message: $"{name} wants to add you as a friend.",
messageBoxType: MessageBoxType.BothYesNoButton,
- onClickedYes: () => { /* TODO: accept and call friend_AddResponse */ },
- onClickedNo: () => { /* TODO: refuse */ });
+ onClickedYes: () => Friend_AddResponse(xid, agree: true),
+ onClickedNo: () => Friend_AddResponse(xid, agree: false));
}
private void OnAddFriendResultReceived(byte retcode, string message)
From ad505e37b991d267d98de04bcfb9862c8089c7b2 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Tue, 17 Mar 2026 13:59:12 +0700
Subject: [PATCH 4/4] Fixing metadata handle after getfriend re cause game dead
---
.../CSNetwork/Protocols/friendextlist.cs | 11 ++++
.../CSNetwork/Protocols/getfriends_re.cs | 38 ++++++++++----
.../Protocols/rpcdata/GFriendExtInfo.cs | 51 +++++++++++++++++++
.../Protocols/rpcdata/GFriendExtInfo.cs.meta | 2 +
.../CSNetwork/Protocols/rpcdata/GGroupInfo.cs | 27 ++++++++++
.../Protocols/rpcdata/GGroupInfo.cs.meta | 2 +
.../Protocols/rpcdata/GSendAUMailRecord.cs | 22 ++++++++
.../rpcdata/GSendAUMailRecord.cs.meta | 2 +
8 files changed, 144 insertions(+), 11 deletions(-)
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs.meta
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs.meta
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs
create mode 100644 Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs.meta
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/friendextlist.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/friendextlist.cs
index 30c038650e..1f05c3502c 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/friendextlist.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/friendextlist.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
+using CSNetwork.Protocols.RPCData;
namespace CSNetwork.Protocols
{
public class friendextlist : Protocol
{
public int Roleid { get; set; }
+ public List ExtraInfo { get; set; } = new List();
+ public List SendInfo { get; set; } = new List();
public int Localsid { get; set; }
public friendextlist() : base(ProtocolType.PROTOCOL_FRIENDEXTLIST)
@@ -16,18 +19,26 @@ namespace CSNetwork.Protocols
public override Protocol Clone() => new friendextlist
{
Roleid = Roleid,
+ ExtraInfo = new List(ExtraInfo ?? new List()),
+ SendInfo = new List(SendInfo ?? new List()),
Localsid = Localsid
};
public override void Marshal(OctetsStream os)
{
os.Write(Roleid);
+ os.WriteList(ExtraInfo ?? new List());
+ os.WriteList(SendInfo ?? new List());
os.Write(Localsid);
}
public override void Unmarshal(OctetsStream os)
{
Roleid = os.ReadInt32();
+ ExtraInfo = new List();
+ os.ReadList(ExtraInfo);
+ SendInfo = new List();
+ os.ReadList(SendInfo);
Localsid = os.ReadInt32();
}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
index b49c5ea69d..56ecc4febc 100644
--- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs
@@ -1,14 +1,17 @@
using System;
+using System.Collections.Generic;
+using CSNetwork.Protocols.RPCData;
namespace CSNetwork.Protocols
{
- /// PROTOCOL_GETFRIENDS_RE(207). Server response to getfriends(206). Payload 11 bytes.
+ /// PROTOCOL_GETFRIENDS_RE(207). Server response to getfriends(206).
public class getfriends_re : Protocol
{
- public byte Retcode { get; set; }
public int Roleid { get; set; }
+ public List Groups { get; set; } = new List();
+ public List Friends { get; set; } = new List();
+ public List Status { get; set; } = new List();
public int Localsid { get; set; }
- public short Extra { get; set; }
public getfriends_re() : base(ProtocolType.PROTOCOL_GETFRIENDS_RE)
{
@@ -16,30 +19,43 @@ namespace CSNetwork.Protocols
public override Protocol Clone() => new getfriends_re
{
- Retcode = Retcode,
Roleid = Roleid,
- Localsid = Localsid,
- Extra = Extra
+ Groups = new List(Groups ?? new List()),
+ Friends = new List(Friends ?? new List()),
+ Status = new List(Status ?? new List()),
+ Localsid = Localsid
};
public override void Marshal(OctetsStream os)
{
- os.Write(Retcode);
os.Write(Roleid);
+ os.WriteList(Groups ?? new List());
+ os.WriteList(Friends ?? new List());
+ os.WriteCompactUInt((uint)(Status?.Count ?? 0));
+ if (Status != null)
+ {
+ for (int i = 0; i < Status.Count; i++)
+ os.Write(Status[i]);
+ }
os.Write(Localsid);
- os.Write(Extra);
}
public override void Unmarshal(OctetsStream os)
{
- Retcode = os.ReadByte();
Roleid = os.ReadInt32();
+ Groups = new List();
+ os.ReadList(Groups);
+ Friends = new List();
+ os.ReadList(Friends);
+ uint sc = os.ReadCompactUInt();
+ Status = new List((int)sc);
+ for (int i = 0; i < sc; i++)
+ Status.Add(os.ReadByte());
Localsid = os.ReadInt32();
- Extra = os.ReadInt16();
}
public override int PriorPolicy() => 1;
- public override bool SizePolicy(int size) => size <= 4096;
+ public override bool SizePolicy(int size) => size <= 8192;
}
}
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs
new file mode 100644
index 0000000000..203e96eff6
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs
@@ -0,0 +1,51 @@
+namespace CSNetwork.Protocols.RPCData
+{
+ /// Port of GNET::GFriendExtInfo (rpcdata/gfriendextinfo).
+ public class GFriendExtInfo : IMarshallable
+ {
+ public int uid;
+ public int rid;
+ public int level;
+ public int last_logintime;
+ public int update_time;
+ public byte reincarnation_times;
+ public Octets remarks;
+ public short reserved1;
+ public int reserved2;
+ public int reserved3;
+
+ public GFriendExtInfo()
+ {
+ remarks = new Octets();
+ }
+
+ public void Marshal(OctetsStream os)
+ {
+ os.Write(uid);
+ os.Write(rid);
+ os.Write(level);
+ os.Write(last_logintime);
+ os.Write(update_time);
+ os.Write(reincarnation_times);
+ os.Write(remarks ?? new Octets());
+ os.Write(reserved1);
+ os.Write(reserved2);
+ os.Write(reserved3);
+ }
+
+ public void Unmarshal(OctetsStream os)
+ {
+ uid = os.ReadInt32();
+ rid = os.ReadInt32();
+ level = os.ReadInt32();
+ last_logintime = os.ReadInt32();
+ update_time = os.ReadInt32();
+ reincarnation_times = os.ReadByte();
+ remarks = os.ReadOctets();
+ reserved1 = os.ReadInt16();
+ reserved2 = os.ReadInt32();
+ reserved3 = os.ReadInt32();
+ }
+ }
+}
+
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs.meta
new file mode 100644
index 0000000000..3ce1c12b2c
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GFriendExtInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cbd227cebee40b0438f44b5a52f47459
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs
new file mode 100644
index 0000000000..e04ffdfdd5
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs
@@ -0,0 +1,27 @@
+namespace CSNetwork.Protocols.RPCData
+{
+ /// Port of GNET::GGroupInfo (rpcdata/ggroupinfo).
+ public class GGroupInfo : IMarshallable
+ {
+ public byte gid;
+ public Octets name;
+
+ public GGroupInfo()
+ {
+ name = new Octets();
+ }
+
+ public void Marshal(OctetsStream os)
+ {
+ os.Write(gid);
+ os.Write(name ?? new Octets());
+ }
+
+ public void Unmarshal(OctetsStream os)
+ {
+ gid = os.ReadByte();
+ name = os.ReadOctets();
+ }
+ }
+}
+
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs.meta
new file mode 100644
index 0000000000..03cb0beee3
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GGroupInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 14af8f8307d31b94585bf4526fcdc1b1
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs
new file mode 100644
index 0000000000..ebdf64d2c0
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs
@@ -0,0 +1,22 @@
+namespace CSNetwork.Protocols.RPCData
+{
+ /// Port of GNET::GSendAUMailRecord (rpcdata/gsendaumailrecord).
+ public class GSendAUMailRecord : IMarshallable
+ {
+ public int rid;
+ public int sendmail_time;
+
+ public void Marshal(OctetsStream os)
+ {
+ os.Write(rid);
+ os.Write(sendmail_time);
+ }
+
+ public void Unmarshal(OctetsStream os)
+ {
+ rid = os.ReadInt32();
+ sendmail_time = os.ReadInt32();
+ }
+ }
+}
+
diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs.meta b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs.meta
new file mode 100644
index 0000000000..72df71bfd2
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/rpcdata/GSendAUMailRecord.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c573cfeb052d9f840891d71dfbaecd6a
\ No newline at end of file