summaryrefslogtreecommitdiff
path: root/src/network/room_member.cpp
diff options
context:
space:
mode:
authorGravatar FearlessTobi2022-07-06 02:20:39 +0200
committerGravatar FearlessTobi2022-07-25 21:57:14 +0200
commitdcfe0a5febb252e3a4f40c1b25765740a269467f (patch)
tree3f06aa974d4bfba52df45697070ef112b701054b /src/network/room_member.cpp
parentMerge pull request #8564 from lat9nq/dinner-fork (diff)
downloadyuzu-dcfe0a5febb252e3a4f40c1b25765740a269467f.tar.gz
yuzu-dcfe0a5febb252e3a4f40c1b25765740a269467f.tar.xz
yuzu-dcfe0a5febb252e3a4f40c1b25765740a269467f.zip
network: Add initial files and enet dependency
Diffstat (limited to 'src/network/room_member.cpp')
-rw-r--r--src/network/room_member.cpp694
1 files changed, 694 insertions, 0 deletions
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
new file mode 100644
index 000000000..e43004027
--- /dev/null
+++ b/src/network/room_member.cpp
@@ -0,0 +1,694 @@
1// Copyright 2017 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <atomic>
6#include <list>
7#include <mutex>
8#include <set>
9#include <thread>
10#include "common/assert.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 MacAddress mac_address; ///< The mac_address 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<WifiPacket> callback_set_wifi_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 mac.
83 * @params nickname The desired nickname.
84 * @params console_id_hash A hash of the Console ID.
85 * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac 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, const std::string& console_id_hash,
90 const MacAddress& preferred_mac = NoPreferredMac,
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 WifiPacket from a received ENet packet.
106 * @param event The ENet event that was received.
107 */
108 void HandleWifiPackets(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 IdWifiPacket:
167 HandleWifiPackets(&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 IdMacCollision:
203 SetState(State::Idle);
204 SetError(Error::MacCollision);
205 break;
206 case IdConsoleIdCollision:
207 SetState(State::Idle);
208 SetError(Error::ConsoleIdCollision);
209 break;
210 case IdVersionMismatch:
211 SetState(State::Idle);
212 SetError(Error::WrongVersion);
213 break;
214 case IdWrongPassword:
215 SetState(State::Idle);
216 SetError(Error::WrongPassword);
217 break;
218 case IdCloseRoom:
219 SetState(State::Idle);
220 SetError(Error::LostConnection);
221 break;
222 case IdHostKicked:
223 SetState(State::Idle);
224 SetError(Error::HostKicked);
225 break;
226 case IdHostBanned:
227 SetState(State::Idle);
228 SetError(Error::HostBanned);
229 break;
230 case IdModPermissionDenied:
231 SetError(Error::PermissionDenied);
232 break;
233 case IdModNoSuchUser:
234 SetError(Error::NoSuchUser);
235 break;
236 }
237 enet_packet_destroy(event.packet);
238 break;
239 case ENET_EVENT_TYPE_DISCONNECT:
240 if (state == State::Joined || state == State::Moderator) {
241 SetState(State::Idle);
242 SetError(Error::LostConnection);
243 }
244 break;
245 case ENET_EVENT_TYPE_NONE:
246 break;
247 case ENET_EVENT_TYPE_CONNECT:
248 // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
249 // already connected
250 ASSERT_MSG(false, "Received unexpected connect event while already connected");
251 break;
252 }
253 }
254 {
255 std::lock_guard lock(send_list_mutex);
256 for (const auto& packet : send_list) {
257 ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(),
258 ENET_PACKET_FLAG_RELIABLE);
259 enet_peer_send(server, 0, enetPacket);
260 }
261 enet_host_flush(client);
262 send_list.clear();
263 }
264 }
265 Disconnect();
266};
267
268void RoomMember::RoomMemberImpl::StartLoop() {
269 loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this);
270}
271
272void RoomMember::RoomMemberImpl::Send(Packet&& packet) {
273 std::lock_guard lock(send_list_mutex);
274 send_list.push_back(std::move(packet));
275}
276
277void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname,
278 const std::string& console_id_hash,
279 const MacAddress& preferred_mac,
280 const std::string& password,
281 const std::string& token) {
282 Packet packet;
283 packet << static_cast<u8>(IdJoinRequest);
284 packet << nickname;
285 packet << console_id_hash;
286 packet << preferred_mac;
287 packet << network_version;
288 packet << password;
289 packet << token;
290 Send(std::move(packet));
291}
292
293void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) {
294 Packet packet;
295 packet.Append(event->packet->data, event->packet->dataLength);
296
297 // Ignore the first byte, which is the message id.
298 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
299
300 RoomInformation info{};
301 packet >> info.name;
302 packet >> info.description;
303 packet >> info.member_slots;
304 packet >> info.port;
305 packet >> info.preferred_game;
306 packet >> info.host_username;
307 room_information.name = info.name;
308 room_information.description = info.description;
309 room_information.member_slots = info.member_slots;
310 room_information.port = info.port;
311 room_information.preferred_game = info.preferred_game;
312 room_information.host_username = info.host_username;
313
314 u32 num_members;
315 packet >> num_members;
316 member_information.resize(num_members);
317
318 for (auto& member : member_information) {
319 packet >> member.nickname;
320 packet >> member.mac_address;
321 packet >> member.game_info.name;
322 packet >> member.game_info.id;
323 packet >> member.username;
324 packet >> member.display_name;
325 packet >> member.avatar_url;
326
327 {
328 std::lock_guard lock(username_mutex);
329 if (member.nickname == nickname) {
330 username = member.username;
331 }
332 }
333 }
334 Invoke(room_information);
335}
336
337void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
338 Packet packet;
339 packet.Append(event->packet->data, event->packet->dataLength);
340
341 // Ignore the first byte, which is the message id.
342 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
343
344 // Parse the MAC Address from the packet
345 packet >> mac_address;
346}
347
348void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) {
349 WifiPacket wifi_packet{};
350 Packet packet;
351 packet.Append(event->packet->data, event->packet->dataLength);
352
353 // Ignore the first byte, which is the message id.
354 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
355
356 // Parse the WifiPacket from the packet
357 u8 frame_type;
358 packet >> frame_type;
359 WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type);
360
361 wifi_packet.type = type;
362 packet >> wifi_packet.channel;
363 packet >> wifi_packet.transmitter_address;
364 packet >> wifi_packet.destination_address;
365 packet >> wifi_packet.data;
366
367 Invoke<WifiPacket>(wifi_packet);
368}
369
370void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
371 Packet packet;
372 packet.Append(event->packet->data, event->packet->dataLength);
373
374 // Ignore the first byte, which is the message id.
375 packet.IgnoreBytes(sizeof(u8));
376
377 ChatEntry chat_entry{};
378 packet >> chat_entry.nickname;
379 packet >> chat_entry.username;
380 packet >> chat_entry.message;
381 Invoke<ChatEntry>(chat_entry);
382}
383
384void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
385 Packet packet;
386 packet.Append(event->packet->data, event->packet->dataLength);
387
388 // Ignore the first byte, which is the message id.
389 packet.IgnoreBytes(sizeof(u8));
390
391 StatusMessageEntry status_message_entry{};
392 u8 type{};
393 packet >> type;
394 status_message_entry.type = static_cast<StatusMessageTypes>(type);
395 packet >> status_message_entry.nickname;
396 packet >> status_message_entry.username;
397 Invoke<StatusMessageEntry>(status_message_entry);
398}
399
400void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) {
401 Packet packet;
402 packet.Append(event->packet->data, event->packet->dataLength);
403
404 // Ignore the first byte, which is the message id.
405 packet.IgnoreBytes(sizeof(u8));
406
407 Room::BanList ban_list = {};
408 packet >> ban_list.first;
409 packet >> ban_list.second;
410 Invoke<Room::BanList>(ban_list);
411}
412
413void RoomMember::RoomMemberImpl::Disconnect() {
414 member_information.clear();
415 room_information.member_slots = 0;
416 room_information.name.clear();
417
418 if (!server)
419 return;
420 enet_peer_disconnect(server, 0);
421
422 ENetEvent event;
423 while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) {
424 switch (event.type) {
425 case ENET_EVENT_TYPE_RECEIVE:
426 enet_packet_destroy(event.packet); // Ignore all incoming data
427 break;
428 case ENET_EVENT_TYPE_DISCONNECT:
429 server = nullptr;
430 return;
431 case ENET_EVENT_TYPE_NONE:
432 case ENET_EVENT_TYPE_CONNECT:
433 break;
434 }
435 }
436 // didn't disconnect gracefully force disconnect
437 enet_peer_reset(server);
438 server = nullptr;
439}
440
441template <>
442RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
443 return callback_set_wifi_packet;
444}
445
446template <>
447RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
448RoomMember::RoomMemberImpl::Callbacks::Get() {
449 return callback_set_state;
450}
451
452template <>
453RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>&
454RoomMember::RoomMemberImpl::Callbacks::Get() {
455 return callback_set_error;
456}
457
458template <>
459RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>&
460RoomMember::RoomMemberImpl::Callbacks::Get() {
461 return callback_set_room_information;
462}
463
464template <>
465RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() {
466 return callback_set_chat_messages;
467}
468
469template <>
470RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>&
471RoomMember::RoomMemberImpl::Callbacks::Get() {
472 return callback_set_status_messages;
473}
474
475template <>
476RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>&
477RoomMember::RoomMemberImpl::Callbacks::Get() {
478 return callback_set_ban_list;
479}
480
481template <typename T>
482void RoomMember::RoomMemberImpl::Invoke(const T& data) {
483 std::lock_guard lock(callback_mutex);
484 CallbackSet<T> callback_set = callbacks.Get<T>();
485 for (auto const& callback : callback_set)
486 (*callback)(data);
487}
488
489template <typename T>
490RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind(
491 std::function<void(const T&)> callback) {
492 std::lock_guard lock(callback_mutex);
493 CallbackHandle<T> handle;
494 handle = std::make_shared<std::function<void(const T&)>>(callback);
495 callbacks.Get<T>().insert(handle);
496 return handle;
497}
498
499// RoomMember
500RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {}
501
502RoomMember::~RoomMember() {
503 ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected");
504 if (room_member_impl->loop_thread) {
505 Leave();
506 }
507}
508
509RoomMember::State RoomMember::GetState() const {
510 return room_member_impl->state;
511}
512
513const RoomMember::MemberList& RoomMember::GetMemberInformation() const {
514 return room_member_impl->member_information;
515}
516
517const std::string& RoomMember::GetNickname() const {
518 return room_member_impl->nickname;
519}
520
521const std::string& RoomMember::GetUsername() const {
522 std::lock_guard lock(room_member_impl->username_mutex);
523 return room_member_impl->username;
524}
525
526const MacAddress& RoomMember::GetMacAddress() const {
527 ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected");
528 return room_member_impl->mac_address;
529}
530
531RoomInformation RoomMember::GetRoomInformation() const {
532 return room_member_impl->room_information;
533}
534
535void RoomMember::Join(const std::string& nick, const std::string& console_id_hash,
536 const char* server_addr, u16 server_port, u16 client_port,
537 const MacAddress& preferred_mac, const std::string& password,
538 const std::string& token) {
539 // If the member is connected, kill the connection first
540 if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) {
541 Leave();
542 }
543 // If the thread isn't running but the ptr still exists, reset it
544 else if (room_member_impl->loop_thread) {
545 room_member_impl->loop_thread.reset();
546 }
547
548 if (!room_member_impl->client) {
549 room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
550 ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
551 }
552
553 room_member_impl->SetState(State::Joining);
554
555 ENetAddress address{};
556 enet_address_set_host(&address, server_addr);
557 address.port = server_port;
558 room_member_impl->server =
559 enet_host_connect(room_member_impl->client, &address, NumChannels, 0);
560
561 if (!room_member_impl->server) {
562 room_member_impl->SetState(State::Idle);
563 room_member_impl->SetError(Error::UnknownError);
564 return;
565 }
566
567 ENetEvent event{};
568 int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
569 if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
570 room_member_impl->nickname = nick;
571 room_member_impl->StartLoop();
572 room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token);
573 SendGameInfo(room_member_impl->current_game_info);
574 } else {
575 enet_peer_disconnect(room_member_impl->server, 0);
576 room_member_impl->SetState(State::Idle);
577 room_member_impl->SetError(Error::CouldNotConnect);
578 }
579}
580
581bool RoomMember::IsConnected() const {
582 return room_member_impl->IsConnected();
583}
584
585void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) {
586 Packet packet;
587 packet << static_cast<u8>(IdWifiPacket);
588 packet << static_cast<u8>(wifi_packet.type);
589 packet << wifi_packet.channel;
590 packet << wifi_packet.transmitter_address;
591 packet << wifi_packet.destination_address;
592 packet << wifi_packet.data;
593 room_member_impl->Send(std::move(packet));
594}
595
596void RoomMember::SendChatMessage(const std::string& message) {
597 Packet packet;
598 packet << static_cast<u8>(IdChatMessage);
599 packet << message;
600 room_member_impl->Send(std::move(packet));
601}
602
603void RoomMember::SendGameInfo(const GameInfo& game_info) {
604 room_member_impl->current_game_info = game_info;
605 if (!IsConnected())
606 return;
607
608 Packet packet;
609 packet << static_cast<u8>(IdSetGameInfo);
610 packet << game_info.name;
611 packet << game_info.id;
612 room_member_impl->Send(std::move(packet));
613}
614
615void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) {
616 ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban,
617 "type is not a moderation request");
618 if (!IsConnected())
619 return;
620
621 Packet packet;
622 packet << static_cast<u8>(type);
623 packet << nickname;
624 room_member_impl->Send(std::move(packet));
625}
626
627void RoomMember::RequestBanList() {
628 if (!IsConnected())
629 return;
630
631 Packet packet;
632 packet << static_cast<u8>(IdModGetBanList);
633 room_member_impl->Send(std::move(packet));
634}
635
636RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged(
637 std::function<void(const RoomMember::State&)> callback) {
638 return room_member_impl->Bind(callback);
639}
640
641RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError(
642 std::function<void(const RoomMember::Error&)> callback) {
643 return room_member_impl->Bind(callback);
644}
645
646RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived(
647 std::function<void(const WifiPacket&)> callback) {
648 return room_member_impl->Bind(callback);
649}
650
651RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
652 std::function<void(const RoomInformation&)> callback) {
653 return room_member_impl->Bind(callback);
654}
655
656RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved(
657 std::function<void(const ChatEntry&)> callback) {
658 return room_member_impl->Bind(callback);
659}
660
661RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived(
662 std::function<void(const StatusMessageEntry&)> callback) {
663 return room_member_impl->Bind(callback);
664}
665
666RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived(
667 std::function<void(const Room::BanList&)> callback) {
668 return room_member_impl->Bind(callback);
669}
670
671template <typename T>
672void RoomMember::Unbind(CallbackHandle<T> handle) {
673 std::lock_guard lock(room_member_impl->callback_mutex);
674 room_member_impl->callbacks.Get<T>().erase(handle);
675}
676
677void RoomMember::Leave() {
678 room_member_impl->SetState(State::Idle);
679 room_member_impl->loop_thread->join();
680 room_member_impl->loop_thread.reset();
681
682 enet_host_destroy(room_member_impl->client);
683 room_member_impl->client = nullptr;
684}
685
686template void RoomMember::Unbind(CallbackHandle<WifiPacket>);
687template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
688template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
689template void RoomMember::Unbind(CallbackHandle<RoomInformation>);
690template void RoomMember::Unbind(CallbackHandle<ChatEntry>);
691template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>);
692template void RoomMember::Unbind(CallbackHandle<Room::BanList>);
693
694} // namespace Network