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