| | |
| | |
| | |
| |
|
| | #include <algorithm> |
| | #include <atomic> |
| | #include <list> |
| | #include <mutex> |
| | #include <set> |
| | #include <thread> |
| | #include "common/assert.h" |
| | #include "enet/enet.h" |
| | #include "network/packet.h" |
| | #include "network/room_member.h" |
| |
|
| | namespace Network { |
| |
|
| | constexpr u32 ConnectionTimeoutMs = 5000; |
| |
|
| | class RoomMember::RoomMemberImpl { |
| | public: |
| | ENetHost* client = nullptr; |
| | ENetPeer* server = nullptr; |
| |
|
| | |
| | MemberList member_information; |
| | |
| | RoomInformation room_information; |
| |
|
| | |
| | GameInfo current_game_info; |
| |
|
| | std::atomic<State> state{State::Idle}; |
| | void SetState(const State new_state); |
| | void SetError(const Error new_error); |
| | bool IsConnected() const; |
| |
|
| | std::string nickname; |
| |
|
| | std::string username; |
| | mutable std::mutex username_mutex; |
| |
|
| | MacAddress mac_address; |
| |
|
| | std::mutex network_mutex; |
| | |
| | std::unique_ptr<std::thread> loop_thread; |
| | std::mutex send_list_mutex; |
| | std::list<Packet> send_list; |
| |
|
| | template <typename T> |
| | using CallbackSet = std::set<CallbackHandle<T>>; |
| | std::mutex callback_mutex; |
| |
|
| | class Callbacks { |
| | public: |
| | template <typename T> |
| | CallbackSet<T>& Get(); |
| |
|
| | private: |
| | CallbackSet<WifiPacket> callback_set_wifi_packet; |
| | CallbackSet<ChatEntry> callback_set_chat_messages; |
| | CallbackSet<StatusMessageEntry> callback_set_status_messages; |
| | CallbackSet<RoomInformation> callback_set_room_information; |
| | CallbackSet<State> callback_set_state; |
| | CallbackSet<Error> callback_set_error; |
| | CallbackSet<Room::BanList> callback_set_ban_list; |
| | }; |
| | Callbacks callbacks; |
| |
|
| | void MemberLoop(); |
| |
|
| | void StartLoop(); |
| |
|
| | |
| | |
| | |
| | |
| | void Send(Packet&& packet); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, |
| | const MacAddress& preferred_mac = NoPreferredMac, |
| | const std::string& password = "", const std::string& token = ""); |
| |
|
| | |
| | |
| | |
| | |
| | void HandleJoinPacket(const ENetEvent* event); |
| | |
| | |
| | |
| | |
| | void HandleRoomInformationPacket(const ENetEvent* event); |
| |
|
| | |
| | |
| | |
| | |
| | void HandleWifiPackets(const ENetEvent* event); |
| |
|
| | |
| | |
| | |
| | |
| | void HandleChatPacket(const ENetEvent* event); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | void HandleStatusMessagePacket(const ENetEvent* event); |
| |
|
| | |
| | |
| | |
| | |
| | void HandleModBanListResponsePacket(const ENetEvent* event); |
| |
|
| | |
| | |
| | |
| | void Disconnect(); |
| |
|
| | template <typename T> |
| | void Invoke(const T& data); |
| |
|
| | template <typename T> |
| | CallbackHandle<T> Bind(std::function<void(const T&)> callback); |
| | }; |
| |
|
| | |
| | void RoomMember::RoomMemberImpl::SetState(const State new_state) { |
| | if (state != new_state) { |
| | state = new_state; |
| | Invoke<State>(state); |
| | } |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::SetError(const Error new_error) { |
| | Invoke<Error>(new_error); |
| | } |
| |
|
| | bool RoomMember::RoomMemberImpl::IsConnected() const { |
| | return state == State::Joining || state == State::Joined || state == State::Moderator; |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::MemberLoop() { |
| | |
| | while (IsConnected()) { |
| | std::lock_guard network_lock(network_mutex); |
| | ENetEvent event; |
| | if (enet_host_service(client, &event, 16) > 0) { |
| | switch (event.type) { |
| | case ENET_EVENT_TYPE_RECEIVE: |
| | switch (event.packet->data[0]) { |
| | case IdWifiPacket: |
| | HandleWifiPackets(&event); |
| | break; |
| | case IdChatMessage: |
| | HandleChatPacket(&event); |
| | break; |
| | case IdStatusMessage: |
| | HandleStatusMessagePacket(&event); |
| | break; |
| | case IdRoomInformation: |
| | HandleRoomInformationPacket(&event); |
| | break; |
| | case IdJoinSuccess: |
| | case IdJoinSuccessAsMod: |
| | |
| | |
| | ASSERT_MSG(member_information.size() > 0, |
| | "We have not yet received member information."); |
| | HandleJoinPacket(&event); |
| | if (event.packet->data[0] == IdJoinSuccessAsMod) { |
| | SetState(State::Moderator); |
| | } else { |
| | SetState(State::Joined); |
| | } |
| | break; |
| | case IdModBanListResponse: |
| | HandleModBanListResponsePacket(&event); |
| | break; |
| | case IdRoomIsFull: |
| | SetState(State::Idle); |
| | SetError(Error::RoomIsFull); |
| | break; |
| | case IdNameCollision: |
| | SetState(State::Idle); |
| | SetError(Error::NameCollision); |
| | break; |
| | case IdMacCollision: |
| | SetState(State::Idle); |
| | SetError(Error::MacCollision); |
| | break; |
| | case IdConsoleIdCollision: |
| | SetState(State::Idle); |
| | SetError(Error::ConsoleIdCollision); |
| | break; |
| | case IdVersionMismatch: |
| | SetState(State::Idle); |
| | SetError(Error::WrongVersion); |
| | break; |
| | case IdWrongPassword: |
| | SetState(State::Idle); |
| | SetError(Error::WrongPassword); |
| | break; |
| | case IdCloseRoom: |
| | SetState(State::Idle); |
| | SetError(Error::LostConnection); |
| | break; |
| | case IdHostKicked: |
| | SetState(State::Idle); |
| | SetError(Error::HostKicked); |
| | break; |
| | case IdHostBanned: |
| | SetState(State::Idle); |
| | SetError(Error::HostBanned); |
| | break; |
| | case IdModPermissionDenied: |
| | SetError(Error::PermissionDenied); |
| | break; |
| | case IdModNoSuchUser: |
| | SetError(Error::NoSuchUser); |
| | break; |
| | } |
| | enet_packet_destroy(event.packet); |
| | break; |
| | case ENET_EVENT_TYPE_DISCONNECT: |
| | if (state == State::Joined || state == State::Moderator) { |
| | SetState(State::Idle); |
| | SetError(Error::LostConnection); |
| | } |
| | break; |
| | case ENET_EVENT_TYPE_NONE: |
| | break; |
| | case ENET_EVENT_TYPE_CONNECT: |
| | |
| | |
| | ASSERT_MSG(false, "Received unexpected connect event while already connected"); |
| | break; |
| | } |
| | } |
| |
|
| | std::list<Packet> packets; |
| | { |
| | std::lock_guard send_list_lock(send_list_mutex); |
| | packets.swap(send_list); |
| | } |
| | for (const auto& packet : packets) { |
| | ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), |
| | ENET_PACKET_FLAG_RELIABLE); |
| | enet_peer_send(server, 0, enetPacket); |
| | } |
| | enet_host_flush(client); |
| | } |
| | Disconnect(); |
| | }; |
| |
|
| | void RoomMember::RoomMemberImpl::StartLoop() { |
| | loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::Send(Packet&& packet) { |
| | std::lock_guard lock(send_list_mutex); |
| | send_list.push_back(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, |
| | const std::string& console_id_hash, |
| | const MacAddress& preferred_mac, |
| | const std::string& password, |
| | const std::string& token) { |
| | Packet packet; |
| | packet << static_cast<u8>(IdJoinRequest); |
| | packet << nickname; |
| | packet << console_id_hash; |
| | packet << preferred_mac; |
| | packet << network_version; |
| | packet << password; |
| | packet << token; |
| | Send(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) { |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | RoomInformation info{}; |
| | packet >> info.name; |
| | packet >> info.description; |
| | packet >> info.member_slots; |
| | packet >> info.port; |
| | packet >> info.preferred_game; |
| | packet >> info.host_username; |
| | room_information.name = info.name; |
| | room_information.description = info.description; |
| | room_information.member_slots = info.member_slots; |
| | room_information.port = info.port; |
| | room_information.preferred_game = info.preferred_game; |
| | room_information.host_username = info.host_username; |
| |
|
| | u32 num_members; |
| | packet >> num_members; |
| | member_information.resize(num_members); |
| |
|
| | for (auto& member : member_information) { |
| | packet >> member.nickname; |
| | packet >> member.mac_address; |
| | packet >> member.game_info.name; |
| | packet >> member.game_info.id; |
| | packet >> member.username; |
| | packet >> member.display_name; |
| | packet >> member.avatar_url; |
| |
|
| | { |
| | std::lock_guard lock(username_mutex); |
| | if (member.nickname == nickname) { |
| | username = member.username; |
| | } |
| | } |
| | } |
| | Invoke(room_information); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | |
| | packet >> mac_address; |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { |
| | WifiPacket wifi_packet{}; |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | |
| | u8 frame_type; |
| | packet >> frame_type; |
| | WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type); |
| |
|
| | wifi_packet.type = type; |
| | packet >> wifi_packet.channel; |
| | packet >> wifi_packet.transmitter_address; |
| | packet >> wifi_packet.destination_address; |
| | packet >> wifi_packet.data; |
| |
|
| | Invoke<WifiPacket>(wifi_packet); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | ChatEntry chat_entry{}; |
| | packet >> chat_entry.nickname; |
| | packet >> chat_entry.username; |
| | packet >> chat_entry.message; |
| | chat_entry.message.resize(std::min(chat_entry.message.find('\0'), chat_entry.message.size())); |
| | Invoke<ChatEntry>(chat_entry); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) { |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | StatusMessageEntry status_message_entry{}; |
| | u8 type{}; |
| | packet >> type; |
| | status_message_entry.type = static_cast<StatusMessageTypes>(type); |
| | packet >> status_message_entry.nickname; |
| | packet >> status_message_entry.username; |
| | Invoke<StatusMessageEntry>(status_message_entry); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) { |
| | Packet packet; |
| | packet.Append(event->packet->data, event->packet->dataLength); |
| |
|
| | |
| | packet.IgnoreBytes(sizeof(u8)); |
| |
|
| | Room::BanList ban_list = {}; |
| | packet >> ban_list.first; |
| | packet >> ban_list.second; |
| | Invoke<Room::BanList>(ban_list); |
| | } |
| |
|
| | void RoomMember::RoomMemberImpl::Disconnect() { |
| | member_information.clear(); |
| | room_information.member_slots = 0; |
| | room_information.name.clear(); |
| |
|
| | if (!server) |
| | return; |
| | enet_peer_disconnect(server, 0); |
| |
|
| | ENetEvent event; |
| | while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) { |
| | switch (event.type) { |
| | case ENET_EVENT_TYPE_RECEIVE: |
| | enet_packet_destroy(event.packet); |
| | break; |
| | case ENET_EVENT_TYPE_DISCONNECT: |
| | server = nullptr; |
| | return; |
| | case ENET_EVENT_TYPE_NONE: |
| | case ENET_EVENT_TYPE_CONNECT: |
| | break; |
| | } |
| | } |
| | |
| | enet_peer_reset(server); |
| | server = nullptr; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_wifi_packet; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>& |
| | RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_state; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>& |
| | RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_error; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& |
| | RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_room_information; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_chat_messages; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>& |
| | RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_status_messages; |
| | } |
| |
|
| | template <> |
| | RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>& |
| | RoomMember::RoomMemberImpl::Callbacks::Get() { |
| | return callback_set_ban_list; |
| | } |
| |
|
| | template <typename T> |
| | void RoomMember::RoomMemberImpl::Invoke(const T& data) { |
| | std::lock_guard lock(callback_mutex); |
| | CallbackSet<T> callback_set = callbacks.Get<T>(); |
| | for (auto const& callback : callback_set) |
| | (*callback)(data); |
| | } |
| |
|
| | template <typename T> |
| | RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind( |
| | std::function<void(const T&)> callback) { |
| | std::lock_guard lock(callback_mutex); |
| | CallbackHandle<T> handle; |
| | handle = std::make_shared<std::function<void(const T&)>>(callback); |
| | callbacks.Get<T>().insert(handle); |
| | return handle; |
| | } |
| |
|
| | |
| | RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {} |
| |
|
| | RoomMember::~RoomMember() { |
| | ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected"); |
| | if (room_member_impl->loop_thread) { |
| | Leave(); |
| | } |
| | } |
| |
|
| | RoomMember::State RoomMember::GetState() const { |
| | return room_member_impl->state; |
| | } |
| |
|
| | const RoomMember::MemberList& RoomMember::GetMemberInformation() const { |
| | return room_member_impl->member_information; |
| | } |
| |
|
| | const std::string& RoomMember::GetNickname() const { |
| | return room_member_impl->nickname; |
| | } |
| |
|
| | const std::string& RoomMember::GetUsername() const { |
| | std::lock_guard lock(room_member_impl->username_mutex); |
| | return room_member_impl->username; |
| | } |
| |
|
| | const MacAddress& RoomMember::GetMacAddress() const { |
| | ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); |
| | return room_member_impl->mac_address; |
| | } |
| |
|
| | RoomInformation RoomMember::GetRoomInformation() const { |
| | return room_member_impl->room_information; |
| | } |
| |
|
| | void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, |
| | const char* server_addr, u16 server_port, u16 client_port, |
| | const MacAddress& preferred_mac, const std::string& password, |
| | const std::string& token) { |
| | |
| | if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { |
| | Leave(); |
| | } |
| | |
| | else if (room_member_impl->loop_thread) { |
| | room_member_impl->loop_thread.reset(); |
| | } |
| |
|
| | if (!room_member_impl->client) { |
| | room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0); |
| | ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); |
| | } |
| |
|
| | room_member_impl->SetState(State::Joining); |
| |
|
| | ENetAddress address{}; |
| | enet_address_set_host(&address, server_addr); |
| | address.port = server_port; |
| | room_member_impl->server = |
| | enet_host_connect(room_member_impl->client, &address, NumChannels, 0); |
| |
|
| | if (!room_member_impl->server) { |
| | room_member_impl->SetState(State::Idle); |
| | room_member_impl->SetError(Error::UnknownError); |
| | return; |
| | } |
| |
|
| | ENetEvent event{}; |
| | int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs); |
| | if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { |
| | room_member_impl->nickname = nick; |
| | room_member_impl->StartLoop(); |
| | room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); |
| | SendGameInfo(room_member_impl->current_game_info); |
| | } else { |
| | enet_peer_disconnect(room_member_impl->server, 0); |
| | room_member_impl->SetState(State::Idle); |
| | room_member_impl->SetError(Error::CouldNotConnect); |
| | } |
| | } |
| |
|
| | bool RoomMember::IsConnected() const { |
| | return room_member_impl->IsConnected(); |
| | } |
| |
|
| | void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) { |
| | Packet packet; |
| | packet << static_cast<u8>(IdWifiPacket); |
| | packet << static_cast<u8>(wifi_packet.type); |
| | packet << wifi_packet.channel; |
| | packet << wifi_packet.transmitter_address; |
| | packet << wifi_packet.destination_address; |
| | packet << wifi_packet.data; |
| | room_member_impl->Send(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::SendChatMessage(const std::string& message) { |
| | Packet packet; |
| | packet << static_cast<u8>(IdChatMessage); |
| | packet << message; |
| | room_member_impl->Send(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::SendGameInfo(const GameInfo& game_info) { |
| | room_member_impl->current_game_info = game_info; |
| | if (!IsConnected()) |
| | return; |
| |
|
| | Packet packet; |
| | packet << static_cast<u8>(IdSetGameInfo); |
| | packet << game_info.name; |
| | packet << game_info.id; |
| | room_member_impl->Send(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) { |
| | ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban, |
| | "type is not a moderation request"); |
| | if (!IsConnected()) |
| | return; |
| |
|
| | Packet packet; |
| | packet << static_cast<u8>(type); |
| | packet << nickname; |
| | room_member_impl->Send(std::move(packet)); |
| | } |
| |
|
| | void RoomMember::RequestBanList() { |
| | if (!IsConnected()) |
| | return; |
| |
|
| | Packet packet; |
| | packet << static_cast<u8>(IdModGetBanList); |
| | room_member_impl->Send(std::move(packet)); |
| | } |
| |
|
| | RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged( |
| | std::function<void(const RoomMember::State&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError( |
| | std::function<void(const RoomMember::Error&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived( |
| | std::function<void(const WifiPacket&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged( |
| | std::function<void(const RoomInformation&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved( |
| | std::function<void(const ChatEntry&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived( |
| | std::function<void(const StatusMessageEntry&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived( |
| | std::function<void(const Room::BanList&)> callback) { |
| | return room_member_impl->Bind(callback); |
| | } |
| |
|
| | template <typename T> |
| | void RoomMember::Unbind(CallbackHandle<T> handle) { |
| | std::lock_guard lock(room_member_impl->callback_mutex); |
| | room_member_impl->callbacks.Get<T>().erase(handle); |
| | } |
| |
|
| | void RoomMember::Leave() { |
| | room_member_impl->SetState(State::Idle); |
| | room_member_impl->loop_thread->join(); |
| | room_member_impl->loop_thread.reset(); |
| |
|
| | enet_host_destroy(room_member_impl->client); |
| | room_member_impl->client = nullptr; |
| | } |
| |
|
| | template void RoomMember::Unbind(CallbackHandle<WifiPacket>); |
| | template void RoomMember::Unbind(CallbackHandle<RoomMember::State>); |
| | template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>); |
| | template void RoomMember::Unbind(CallbackHandle<RoomInformation>); |
| | template void RoomMember::Unbind(CallbackHandle<ChatEntry>); |
| | template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>); |
| | template void RoomMember::Unbind(CallbackHandle<Room::BanList>); |
| |
|
| | } |
| |
|