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 2a2ebbc801..0e451977a6 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -57,6 +57,15 @@ 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: 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; + /// Raised when the underlying network disconnects. public event Action Disconnected; @@ -595,6 +604,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; @@ -609,6 +626,49 @@ 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); + // 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) + { + 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)}"; + if (p.retcode == 0) + { + Friend_GetList(); + } + + 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; @@ -2371,6 +2431,43 @@ 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 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() + { + var p = new getfriends(); + p.Roleid = m_iCharID; + p.Localsid = (int)_localsid; + SendProtocol(p); + } + public void c2s_SendCmdTeamKickMember(int idMember) { var g = new gamedatasend(); @@ -2379,7 +2476,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..a52735a7ae --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/addfriendrqst.cs @@ -0,0 +1,47 @@ +using System; + +namespace CSNetwork.Protocols +{ + /// 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; } + + public addfriendrqst() : base(ProtocolType.RPC_ADDFRIENDRQST) + { + Srcname = new Octets(); + } + + public override Protocol Clone() => new addfriendrqst + { + Xid = Xid, + Srcroleid = Srcroleid, + Srcname = new Octets(Srcname.ToArray()), + Dstlsid = Dstlsid + }; + + public override void Marshal(OctetsStream os) + { + os.Write(Xid); + os.Write(Srcroleid); + os.Write(Srcname); + os.Write(Dstlsid); + } + + public override void Unmarshal(OctetsStream os) + { + Xid = os.ReadUInt32(); + 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/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/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 new file mode 100644 index 0000000000..56ecc4febc --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/Protocols/getfriends_re.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using CSNetwork.Protocols.RPCData; + +namespace CSNetwork.Protocols +{ + /// PROTOCOL_GETFRIENDS_RE(207). Server response to getfriends(206). + public class getfriends_re : Protocol + { + 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 getfriends_re() : base(ProtocolType.PROTOCOL_GETFRIENDS_RE) + { + } + + public override Protocol Clone() => new getfriends_re + { + Roleid = Roleid, + 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(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); + } + + public override void Unmarshal(OctetsStream os) + { + 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(); + } + + public override int PriorPolicy() => 1; + + public override bool SizePolicy(int size) => size <= 8192; + } +} 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/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/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/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 diff --git a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs index 01e7cd1a7b..611cd7572c 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")); @@ -173,7 +176,9 @@ namespace BrewMonster.Network // Subscribe to unexpected disconnects _gameSession.Disconnected += OnUnexpectedDisconnect; - + _gameSession.FriendRequestReceived += OnFriendRequestReceived; + _gameSession.AddFriendResultReceived += OnAddFriendResultReceived; + _isInitialized = true; DontDestroyOnLoad(gameObject); @@ -184,6 +189,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 +626,20 @@ 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 Friend_AddResponse(uint xid, bool agree) + { + Instance._gameSession.Friend_AddResponse(xid, agree); + } public static void c2s_CmdTeamKickMember(int idMember) { Instance._gameSession.c2s_SendCmdTeamKickMember(idMember); @@ -699,6 +720,25 @@ namespace BrewMonster.Network /// /// Handles unexpected server disconnections. Shows a message box and returns to login. /// + 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: () => Friend_AddResponse(xid, agree: true), + onClickedNo: () => Friend_AddResponse(xid, agree: false)); + } + + 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 9cc7c83ae8..f7d60a9af3 100644 --- a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs +++ b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs @@ -370,7 +370,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(); } //private void OnInventoryReceived(List inventoryData)