summaryrefslogtreecommitdiff
path: root/src/network/room_member.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/network/room_member.cpp')
-rw-r--r--src/network/room_member.cpp707
1 files changed, 707 insertions, 0 deletions
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
new file mode 100644
index 000000000..9f08bf611
--- /dev/null
+++ b/src/network/room_member.cpp
@@ -0,0 +1,707 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <atomic>
5#include <list>
6#include <mutex>
7#include <set>
8#include <thread>
9#include "common/assert.h"
10#include "common/socket_types.h"
11#include "enet/enet.h"
12#include "network/packet.h"
13#include "network/room_member.h"
14
15namespace Network {
16
17constexpr u32 ConnectionTimeoutMs = 5000;
18
19class RoomMember::RoomMemberImpl {
20public:
21 ENetHost* client = nullptr; ///< ENet network interface.
22 ENetPeer* server = nullptr; ///< The server peer the client is connected to
23
24 /// Information about the clients connected to the same room as us.
25 MemberList member_information;
26 /// Information about the room we're connected to.
27 RoomInformation room_information;
28
29 /// The current game name, id and version
30 GameInfo current_game_info;
31
32 std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
33 void SetState(const State new_state);
34 void SetError(const Error new_error);
35 bool IsConnected() const;
36
37 std::string nickname; ///< The nickname of this member.
38
39 std::string username; ///< The username of this member.
40 mutable std::mutex username_mutex; ///< Mutex for locking username.
41
42 IPv4Address fake_ip; ///< The fake ip of this member.
43
44 std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
45 /// Thread that receives and dispatches network packets
46 std::unique_ptr<std::thread> loop_thread;
47 std::mutex send_list_mutex; ///< Mutex that controls access to the `send_list` variable.
48 std::list<Packet> send_list; ///< A list that stores all packets to send the async
49
50 template <typename T>
51 using CallbackSet = std::set<CallbackHandle<T>>;
52 std::mutex callback_mutex; ///< The mutex used for handling callbacks
53
54 class Callbacks {
55 public:
56 template <typename T>
57 CallbackSet<T>& Get();
58
59 private:
60 CallbackSet<ProxyPacket> callback_set_proxy_packet;
61 CallbackSet<ChatEntry> callback_set_chat_messages;
62 CallbackSet<StatusMessageEntry> callback_set_status_messages;
63 CallbackSet<RoomInformation> callback_set_room_information;
64 CallbackSet<State> callback_set_state;
65 CallbackSet<Error> callback_set_error;
66 CallbackSet<Room::BanList> callback_set_ban_list;
67 };
68 Callbacks callbacks; ///< All CallbackSets to all events
69
70 void MemberLoop();
71
72 void StartLoop();
73
74 /**
75 * Sends data to the room. It will be send on channel 0 with flag RELIABLE
76 * @param packet The data to send
77 */
78 void Send(Packet&& packet);
79
80 /**
81 * Sends a request to the server, asking for permission to join a room with the specified
82 * nickname and preferred fake ip.
83 * @params nickname The desired nickname.
84 * @params preferred_fake_ip The preferred IP address to use in the room, the NoPreferredIP
85 * tells
86 * @params password The password for the room
87 * the server to assign one for us.
88 */
89 void SendJoinRequest(const std::string& nickname_,
90 const IPv4Address& preferred_fake_ip = NoPreferredIP,
91 const std::string& password = "", const std::string& token = "");
92
93 /**
94 * Extracts a MAC Address from a received ENet packet.
95 * @param event The ENet event that was received.
96 */
97 void HandleJoinPacket(const ENetEvent* event);
98 /**
99 * Extracts RoomInformation and MemberInformation from a received ENet packet.
100 * @param event The ENet event that was received.
101 */
102 void HandleRoomInformationPacket(const ENetEvent* event);
103
104 /**
105 * Extracts a ProxyPacket from a received ENet packet.
106 * @param event The ENet event that was received.
107 */
108 void HandleProxyPackets(const ENetEvent* event);
109
110 /**
111 * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
112 * @param event The ENet event that was received.
113 */
114 void HandleChatPacket(const ENetEvent* event);
115
116 /**
117 * Extracts a system message entry from a received ENet packet and adds it to the system message
118 * queue.
119 * @param event The ENet event that was received.
120 */
121 void HandleStatusMessagePacket(const ENetEvent* event);
122
123 /**
124 * Extracts a ban list request response from a received ENet packet.
125 * @param event The ENet event that was received.
126 */
127 void HandleModBanListResponsePacket(const ENetEvent* event);
128
129 /**
130 * Disconnects the RoomMember from the Room
131 */
132 void Disconnect();
133
134 template <typename T>
135 void Invoke(const T& data);
136
137 template <typename T>
138 CallbackHandle<T> Bind(std::function<void(const T&)> callback);
139};
140
141// RoomMemberImpl
142void RoomMember::RoomMemberImpl::SetState(const State new_state) {
143 if (state != new_state) {
144 state = new_state;
145 Invoke<State>(state);
146 }
147}
148
149void RoomMember::RoomMemberImpl::SetError(const Error new_error) {
150 Invoke<Error>(new_error);
151}
152
153bool RoomMember::RoomMemberImpl::IsConnected() const {
154 return state == State::Joining || state == State::Joined || state == State::Moderator;
155}
156
157void RoomMember::RoomMemberImpl::MemberLoop() {
158 // Receive packets while the connection is open
159 while (IsConnected()) {
160 std::lock_guard lock(network_mutex);
161 ENetEvent event;
162 if (enet_host_service(client, &event, 100) > 0) {
163 switch (event.type) {
164 case ENET_EVENT_TYPE_RECEIVE:
165 switch (event.packet->data[0]) {
166 case IdProxyPacket:
167 HandleProxyPackets(&event);
168 break;
169 case IdChatMessage:
170 HandleChatPacket(&event);
171 break;
172 case IdStatusMessage:
173 HandleStatusMessagePacket(&event);
174 break;
175 case IdRoomInformation:
176 HandleRoomInformationPacket(&event);
177 break;
178 case IdJoinSuccess:
179 case IdJoinSuccessAsMod:
180 // The join request was successful, we are now in the room.
181 // If we joined successfully, there must be at least one client in the room: us.
182 ASSERT_MSG(member_information.size() > 0,
183 "We have not yet received member information.");
184 HandleJoinPacket(&event); // Get the MAC Address for the client
185 if (event.packet->data[0] == IdJoinSuccessAsMod) {
186 SetState(State::Moderator);
187 } else {
188 SetState(State::Joined);
189 }
190 break;
191 case IdModBanListResponse:
192 HandleModBanListResponsePacket(&event);
193 break;
194 case IdRoomIsFull:
195 SetState(State::Idle);
196 SetError(Error::RoomIsFull);
197 break;
198 case IdNameCollision:
199 SetState(State::Idle);
200 SetError(Error::NameCollision);
201 break;
202 case IdIpCollision:
203 SetState(State::Idle);
204 SetError(Error::IpCollision);
205 break;
206 case IdVersionMismatch:
207 SetState(State::Idle);
208 SetError(Error::WrongVersion);
209 break;
210 case IdWrongPassword:
211 SetState(State::Idle);
212 SetError(Error::WrongPassword);
213 break;
214 case IdCloseRoom:
215 SetState(State::Idle);
216 SetError(Error::LostConnection);
217 break;
218 case IdHostKicked:
219 SetState(State::Idle);
220 SetError(Error::HostKicked);
221 break;
222 case IdHostBanned:
223 SetState(State::Idle);
224 SetError(Error::HostBanned);
225 break;
226 case IdModPermissionDenied:
227 SetError(Error::PermissionDenied);
228 break;
229 case IdModNoSuchUser:
230 SetError(Error::NoSuchUser);
231 break;
232 }
233 enet_packet_destroy(event.packet);
234 break;
235 case ENET_EVENT_TYPE_DISCONNECT:
236 if (state == State::Joined || state == State::Moderator) {
237 SetState(State::Idle);
238 SetError(Error::LostConnection);
239 }
240 break;
241 case ENET_EVENT_TYPE_NONE:
242 break;
243 case ENET_EVENT_TYPE_CONNECT:
244 // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
245 // already connected
246 ASSERT_MSG(false, "Received unexpected connect event while already connected");
247 break;
248 }
249 }
250 std::list<Packet> packets;
251 {
252 std::lock_guard send_lock(send_list_mutex);
253 packets.swap(send_list);
254 }
255 for (const auto& packet : packets) {
256 ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(),
257 ENET_PACKET_FLAG_RELIABLE);
258 enet_peer_send(server, 0, enetPacket);
259 }
260 enet_host_flush(client);
261 }
262 Disconnect();
263};
264
265void RoomMember::RoomMemberImpl::StartLoop() {
266 loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this);
267}
268
269void RoomMember::RoomMemberImpl::Send(Packet&& packet) {
270 std::lock_guard lock(send_list_mutex);
271 send_list.push_back(std::move(packet));
272}
273
274void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_,
275 const IPv4Address& preferred_fake_ip,
276 const std::string& password,
277 const std::string& token) {
278 Packet packet;
279 packet.Write(static_cast<u8>(IdJoinRequest));
280 packet.Write(nickname_);
281 packet.Write(preferred_fake_ip);
282 packet.Write(network_version);
283 packet.Write(password);
284 packet.Write(token);
285 Send(std::move(packet));
286}
287
288void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) {
289 Packet packet;
290 packet.Append(event->packet->data, event->packet->dataLength);
291
292 // Ignore the first byte, which is the message id.
293 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
294
295 RoomInformation info{};
296 packet.Read(info.name);
297 packet.Read(info.description);
298 packet.Read(info.member_slots);
299 packet.Read(info.port);
300 packet.Read(info.preferred_game.name);
301 packet.Read(info.host_username);
302 room_information.name = info.name;
303 room_information.description = info.description;
304 room_information.member_slots = info.member_slots;
305 room_information.port = info.port;
306 room_information.preferred_game = info.preferred_game;
307 room_information.host_username = info.host_username;
308
309 u32 num_members;
310 packet.Read(num_members);
311 member_information.resize(num_members);
312
313 for (auto& member : member_information) {
314 packet.Read(member.nickname);
315 packet.Read(member.fake_ip);
316 packet.Read(member.game_info.name);
317 packet.Read(member.game_info.id);
318 packet.Read(member.username);
319 packet.Read(member.display_name);
320 packet.Read(member.avatar_url);
321
322 {
323 std::lock_guard lock(username_mutex);
324 if (member.nickname == nickname) {
325 username = member.username;
326 }
327 }
328 }
329 Invoke(room_information);
330}
331
332void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
333 Packet packet;
334 packet.Append(event->packet->data, event->packet->dataLength);
335
336 // Ignore the first byte, which is the message id.
337 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
338
339 // Parse the MAC Address from the packet
340 packet.Read(fake_ip);
341}
342
343void RoomMember::RoomMemberImpl::HandleProxyPackets(const ENetEvent* event) {
344 ProxyPacket proxy_packet{};
345 Packet packet;
346 packet.Append(event->packet->data, event->packet->dataLength);
347
348 // Ignore the first byte, which is the message id.
349 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
350
351 // Parse the ProxyPacket from the packet
352 u8 local_family;
353 packet.Read(local_family);
354 proxy_packet.local_endpoint.family = static_cast<Domain>(local_family);
355 packet.Read(proxy_packet.local_endpoint.ip);
356 packet.Read(proxy_packet.local_endpoint.portno);
357
358 u8 remote_family;
359 packet.Read(remote_family);
360 proxy_packet.remote_endpoint.family = static_cast<Domain>(remote_family);
361 packet.Read(proxy_packet.remote_endpoint.ip);
362 packet.Read(proxy_packet.remote_endpoint.portno);
363
364 u8 protocol_type;
365 packet.Read(protocol_type);
366 proxy_packet.protocol = static_cast<Protocol>(protocol_type);
367
368 packet.Read(proxy_packet.broadcast);
369 packet.Read(proxy_packet.data);
370
371 Invoke<ProxyPacket>(proxy_packet);
372}
373
374void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
375 Packet packet;
376 packet.Append(event->packet->data, event->packet->dataLength);
377
378 // Ignore the first byte, which is the message id.
379 packet.IgnoreBytes(sizeof(u8));
380
381 ChatEntry chat_entry{};
382 packet.Read(chat_entry.nickname);
383 packet.Read(chat_entry.username);
384 packet.Read(chat_entry.message);
385 Invoke<ChatEntry>(chat_entry);
386}
387
388void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
389 Packet packet;
390 packet.Append(event->packet->data, event->packet->dataLength);
391
392 // Ignore the first byte, which is the message id.
393 packet.IgnoreBytes(sizeof(u8));
394
395 StatusMessageEntry status_message_entry{};
396 u8 type{};
397 packet.Read(type);
398 status_message_entry.type = static_cast<StatusMessageTypes>(type);
399 packet.Read(status_message_entry.nickname);
400 packet.Read(status_message_entry.username);
401 Invoke<StatusMessageEntry>(status_message_entry);
402}
403
404void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) {
405 Packet packet;
406 packet.Append(event->packet->data, event->packet->dataLength);
407
408 // Ignore the first byte, which is the message id.
409 packet.IgnoreBytes(sizeof(u8));
410
411 Room::BanList ban_list = {};
412 packet.Read(ban_list.first);
413 packet.Read(ban_list.second);
414 Invoke<Room::BanList>(ban_list);
415}
416
417void RoomMember::RoomMemberImpl::Disconnect() {
418 member_information.clear();
419 room_information.member_slots = 0;
420 room_information.name.clear();
421
422 if (!server) {
423 return;
424 }
425 enet_peer_disconnect(server, 0);
426
427 ENetEvent event;
428 while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) {
429 switch (event.type) {
430 case ENET_EVENT_TYPE_RECEIVE:
431 enet_packet_destroy(event.packet); // Ignore all incoming data
432 break;
433 case ENET_EVENT_TYPE_DISCONNECT:
434 server = nullptr;
435 return;
436 case ENET_EVENT_TYPE_NONE:
437 case ENET_EVENT_TYPE_CONNECT:
438 break;
439 }
440 }
441 // didn't disconnect gracefully force disconnect
442 enet_peer_reset(server);
443 server = nullptr;
444}
445
446template <>
447RoomMember::RoomMemberImpl::CallbackSet<ProxyPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
448 return callback_set_proxy_packet;
449}
450
451template <>
452RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
453RoomMember::RoomMemberImpl::Callbacks::Get() {
454 return callback_set_state;
455}
456
457template <>
458RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>&
459RoomMember::RoomMemberImpl::Callbacks::Get() {
460 return callback_set_error;
461}
462
463template <>
464RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>&
465RoomMember::RoomMemberImpl::Callbacks::Get() {
466 return callback_set_room_information;
467}
468
469template <>
470RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() {
471 return callback_set_chat_messages;
472}
473
474template <>
475RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>&
476RoomMember::RoomMemberImpl::Callbacks::Get() {
477 return callback_set_status_messages;
478}
479
480template <>
481RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>&
482RoomMember::RoomMemberImpl::Callbacks::Get() {
483 return callback_set_ban_list;
484}
485
486template <typename T>
487void RoomMember::RoomMemberImpl::Invoke(const T& data) {
488 std::lock_guard lock(callback_mutex);
489 CallbackSet<T> callback_set = callbacks.Get<T>();
490 for (auto const& callback : callback_set) {
491 (*callback)(data);
492 }
493}
494
495template <typename T>
496RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind(
497 std::function<void(const T&)> callback) {
498 std::lock_guard lock(callback_mutex);
499 CallbackHandle<T> handle;
500 handle = std::make_shared<std::function<void(const T&)>>(callback);
501 callbacks.Get<T>().insert(handle);
502 return handle;
503}
504
505// RoomMember
506RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {}
507
508RoomMember::~RoomMember() {
509 ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected");
510 if (room_member_impl->loop_thread) {
511 Leave();
512 }
513}
514
515RoomMember::State RoomMember::GetState() const {
516 return room_member_impl->state;
517}
518
519const RoomMember::MemberList& RoomMember::GetMemberInformation() const {
520 return room_member_impl->member_information;
521}
522
523const std::string& RoomMember::GetNickname() const {
524 return room_member_impl->nickname;
525}
526
527const std::string& RoomMember::GetUsername() const {
528 std::lock_guard lock(room_member_impl->username_mutex);
529 return room_member_impl->username;
530}
531
532const IPv4Address& RoomMember::GetFakeIpAddress() const {
533 ASSERT_MSG(IsConnected(), "Tried to get fake ip address while not connected");
534 return room_member_impl->fake_ip;
535}
536
537RoomInformation RoomMember::GetRoomInformation() const {
538 return room_member_impl->room_information;
539}
540
541void RoomMember::Join(const std::string& nick, const char* server_addr, u16 server_port,
542 u16 client_port, const IPv4Address& preferred_fake_ip,
543 const std::string& password, const std::string& token) {
544 // If the member is connected, kill the connection first
545 if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) {
546 Leave();
547 }
548 // If the thread isn't running but the ptr still exists, reset it
549 else if (room_member_impl->loop_thread) {
550 room_member_impl->loop_thread.reset();
551 }
552
553 if (!room_member_impl->client) {
554 room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
555 ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
556 }
557
558 room_member_impl->SetState(State::Joining);
559
560 ENetAddress address{};
561 enet_address_set_host(&address, server_addr);
562 address.port = server_port;
563 room_member_impl->server =
564 enet_host_connect(room_member_impl->client, &address, NumChannels, 0);
565
566 if (!room_member_impl->server) {
567 room_member_impl->SetState(State::Idle);
568 room_member_impl->SetError(Error::UnknownError);
569 return;
570 }
571
572 ENetEvent event{};
573 int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
574 if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
575 room_member_impl->nickname = nick;
576 room_member_impl->StartLoop();
577 room_member_impl->SendJoinRequest(nick, preferred_fake_ip, password, token);
578 SendGameInfo(room_member_impl->current_game_info);
579 } else {
580 enet_peer_disconnect(room_member_impl->server, 0);
581 room_member_impl->SetState(State::Idle);
582 room_member_impl->SetError(Error::CouldNotConnect);
583 }
584}
585
586bool RoomMember::IsConnected() const {
587 return room_member_impl->IsConnected();
588}
589
590void RoomMember::SendProxyPacket(const ProxyPacket& proxy_packet) {
591 Packet packet;
592 packet.Write(static_cast<u8>(IdProxyPacket));
593
594 packet.Write(static_cast<u8>(proxy_packet.local_endpoint.family));
595 packet.Write(proxy_packet.local_endpoint.ip);
596 packet.Write(proxy_packet.local_endpoint.portno);
597
598 packet.Write(static_cast<u8>(proxy_packet.remote_endpoint.family));
599 packet.Write(proxy_packet.remote_endpoint.ip);
600 packet.Write(proxy_packet.remote_endpoint.portno);
601
602 packet.Write(static_cast<u8>(proxy_packet.protocol));
603 packet.Write(proxy_packet.broadcast);
604 packet.Write(proxy_packet.data);
605
606 room_member_impl->Send(std::move(packet));
607}
608
609void RoomMember::SendChatMessage(const std::string& message) {
610 Packet packet;
611 packet.Write(static_cast<u8>(IdChatMessage));
612 packet.Write(message);
613 room_member_impl->Send(std::move(packet));
614}
615
616void RoomMember::SendGameInfo(const GameInfo& game_info) {
617 room_member_impl->current_game_info = game_info;
618 if (!IsConnected())
619 return;
620
621 Packet packet;
622 packet.Write(static_cast<u8>(IdSetGameInfo));
623 packet.Write(game_info.name);
624 packet.Write(game_info.id);
625 room_member_impl->Send(std::move(packet));
626}
627
628void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) {
629 ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban,
630 "type is not a moderation request");
631 if (!IsConnected())
632 return;
633
634 Packet packet;
635 packet.Write(static_cast<u8>(type));
636 packet.Write(nickname);
637 room_member_impl->Send(std::move(packet));
638}
639
640void RoomMember::RequestBanList() {
641 if (!IsConnected())
642 return;
643
644 Packet packet;
645 packet.Write(static_cast<u8>(IdModGetBanList));
646 room_member_impl->Send(std::move(packet));
647}
648
649RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged(
650 std::function<void(const RoomMember::State&)> callback) {
651 return room_member_impl->Bind(callback);
652}
653
654RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError(
655 std::function<void(const RoomMember::Error&)> callback) {
656 return room_member_impl->Bind(callback);
657}
658
659RoomMember::CallbackHandle<ProxyPacket> RoomMember::BindOnProxyPacketReceived(
660 std::function<void(const ProxyPacket&)> callback) {
661 return room_member_impl->Bind(callback);
662}
663
664RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
665 std::function<void(const RoomInformation&)> callback) {
666 return room_member_impl->Bind(callback);
667}
668
669RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved(
670 std::function<void(const ChatEntry&)> callback) {
671 return room_member_impl->Bind(callback);
672}
673
674RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived(
675 std::function<void(const StatusMessageEntry&)> callback) {
676 return room_member_impl->Bind(callback);
677}
678
679RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived(
680 std::function<void(const Room::BanList&)> callback) {
681 return room_member_impl->Bind(callback);
682}
683
684template <typename T>
685void RoomMember::Unbind(CallbackHandle<T> handle) {
686 std::lock_guard lock(room_member_impl->callback_mutex);
687 room_member_impl->callbacks.Get<T>().erase(handle);
688}
689
690void RoomMember::Leave() {
691 room_member_impl->SetState(State::Idle);
692 room_member_impl->loop_thread->join();
693 room_member_impl->loop_thread.reset();
694
695 enet_host_destroy(room_member_impl->client);
696 room_member_impl->client = nullptr;
697}
698
699template void RoomMember::Unbind(CallbackHandle<ProxyPacket>);
700template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
701template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
702template void RoomMember::Unbind(CallbackHandle<RoomInformation>);
703template void RoomMember::Unbind(CallbackHandle<ChatEntry>);
704template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>);
705template void RoomMember::Unbind(CallbackHandle<Room::BanList>);
706
707} // namespace Network