From 705f7db84dd85555a6aef1e136cf251725cef293 Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Sat, 25 Dec 2021 20:27:52 +0100 Subject: yuzu: Add ui files for multiplayer rooms --- src/common/CMakeLists.txt | 1 + src/common/announce_multiplayer_room.h | 138 +++++ src/core/CMakeLists.txt | 14 +- src/core/announce_multiplayer_session.cpp | 165 ++++++ src/core/announce_multiplayer_session.h | 96 ++++ src/core/core.cpp | 17 +- src/core/hle/service/nifm/nifm.cpp | 4 +- src/core/hle/service/sockets/bsd.cpp | 4 +- src/core/hle/service/sockets/bsd.h | 2 +- src/core/hle/service/sockets/sockets_translate.cpp | 2 +- src/core/hle/service/sockets/sockets_translate.h | 2 +- src/core/internal_network/network.cpp | 637 +++++++++++++++++++++ src/core/internal_network/network.h | 117 ++++ src/core/internal_network/network_interface.cpp | 209 +++++++ src/core/internal_network/network_interface.h | 28 + src/core/internal_network/sockets.h | 95 +++ src/core/network/network.cpp | 637 --------------------- src/core/network/network.h | 117 ---- src/core/network/network_interface.cpp | 209 ------- src/core/network/network_interface.h | 28 - src/core/network/sockets.h | 94 --- src/network/room.cpp | 8 +- src/network/room.h | 4 +- src/network/room_member.cpp | 25 +- src/network/verify_user.h | 2 +- src/tests/CMakeLists.txt | 2 +- src/tests/core/internal_network/network.cpp | 27 + src/tests/core/network/network.cpp | 27 - src/web_service/CMakeLists.txt | 4 +- src/web_service/announce_room_json.cpp | 157 +++++ src/web_service/announce_room_json.h | 46 ++ src/yuzu/CMakeLists.txt | 28 +- src/yuzu/configuration/config.cpp | 79 +++ src/yuzu/configuration/config.h | 2 + src/yuzu/configuration/configure_dialog.cpp | 8 +- src/yuzu/configuration/configure_dialog.h | 3 +- src/yuzu/configuration/configure_network.cpp | 2 +- src/yuzu/configuration/configure_web.cpp | 5 + src/yuzu/configuration/configure_web.h | 1 + src/yuzu/configuration/configure_web.ui | 10 + src/yuzu/game_list.cpp | 6 + src/yuzu/game_list.h | 8 + src/yuzu/main.cpp | 39 +- src/yuzu/main.h | 5 + src/yuzu/main.ui | 52 ++ src/yuzu/multiplayer/chat_room.cpp | 490 ++++++++++++++++ src/yuzu/multiplayer/chat_room.h | 74 +++ src/yuzu/multiplayer/chat_room.ui | 59 ++ src/yuzu/multiplayer/client_room.cpp | 115 ++++ src/yuzu/multiplayer/client_room.h | 39 ++ src/yuzu/multiplayer/client_room.ui | 80 +++ src/yuzu/multiplayer/direct_connect.cpp | 129 +++++ src/yuzu/multiplayer/direct_connect.h | 43 ++ src/yuzu/multiplayer/direct_connect.ui | 168 ++++++ src/yuzu/multiplayer/host_room.cpp | 237 ++++++++ src/yuzu/multiplayer/host_room.h | 74 +++ src/yuzu/multiplayer/host_room.ui | 207 +++++++ src/yuzu/multiplayer/lobby.cpp | 360 ++++++++++++ src/yuzu/multiplayer/lobby.h | 127 ++++ src/yuzu/multiplayer/lobby.ui | 123 ++++ src/yuzu/multiplayer/lobby_p.h | 239 ++++++++ src/yuzu/multiplayer/message.cpp | 79 +++ src/yuzu/multiplayer/message.h | 65 +++ src/yuzu/multiplayer/moderation_dialog.cpp | 113 ++++ src/yuzu/multiplayer/moderation_dialog.h | 42 ++ src/yuzu/multiplayer/moderation_dialog.ui | 84 +++ src/yuzu/multiplayer/state.cpp | 299 ++++++++++ src/yuzu/multiplayer/state.h | 92 +++ src/yuzu/multiplayer/validation.h | 49 ++ src/yuzu/uisettings.h | 13 + src/yuzu/util/clickable_label.cpp | 12 + src/yuzu/util/clickable_label.h | 22 + src/yuzu_cmd/yuzu.cpp | 158 +++++ 73 files changed, 5604 insertions(+), 1154 deletions(-) create mode 100644 src/common/announce_multiplayer_room.h create mode 100644 src/core/announce_multiplayer_session.cpp create mode 100644 src/core/announce_multiplayer_session.h create mode 100644 src/core/internal_network/network.cpp create mode 100644 src/core/internal_network/network.h create mode 100644 src/core/internal_network/network_interface.cpp create mode 100644 src/core/internal_network/network_interface.h create mode 100644 src/core/internal_network/sockets.h delete mode 100644 src/core/network/network.cpp delete mode 100644 src/core/network/network.h delete mode 100644 src/core/network/network_interface.cpp delete mode 100644 src/core/network/network_interface.h delete mode 100644 src/core/network/sockets.h create mode 100644 src/tests/core/internal_network/network.cpp delete mode 100644 src/tests/core/network/network.cpp create mode 100644 src/web_service/announce_room_json.cpp create mode 100644 src/web_service/announce_room_json.h create mode 100644 src/yuzu/multiplayer/chat_room.cpp create mode 100644 src/yuzu/multiplayer/chat_room.h create mode 100644 src/yuzu/multiplayer/chat_room.ui create mode 100644 src/yuzu/multiplayer/client_room.cpp create mode 100644 src/yuzu/multiplayer/client_room.h create mode 100644 src/yuzu/multiplayer/client_room.ui create mode 100644 src/yuzu/multiplayer/direct_connect.cpp create mode 100644 src/yuzu/multiplayer/direct_connect.h create mode 100644 src/yuzu/multiplayer/direct_connect.ui create mode 100644 src/yuzu/multiplayer/host_room.cpp create mode 100644 src/yuzu/multiplayer/host_room.h create mode 100644 src/yuzu/multiplayer/host_room.ui create mode 100644 src/yuzu/multiplayer/lobby.cpp create mode 100644 src/yuzu/multiplayer/lobby.h create mode 100644 src/yuzu/multiplayer/lobby.ui create mode 100644 src/yuzu/multiplayer/lobby_p.h create mode 100644 src/yuzu/multiplayer/message.cpp create mode 100644 src/yuzu/multiplayer/message.h create mode 100644 src/yuzu/multiplayer/moderation_dialog.cpp create mode 100644 src/yuzu/multiplayer/moderation_dialog.h create mode 100644 src/yuzu/multiplayer/moderation_dialog.ui create mode 100644 src/yuzu/multiplayer/state.cpp create mode 100644 src/yuzu/multiplayer/state.h create mode 100644 src/yuzu/multiplayer/validation.h create mode 100644 src/yuzu/util/clickable_label.cpp create mode 100644 src/yuzu/util/clickable_label.h (limited to 'src') diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d574e4b79..05fdfea82 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -41,6 +41,7 @@ add_custom_command(OUTPUT scm_rev.cpp add_library(common STATIC algorithm.h alignment.h + announce_multiplayer_room.h assert.cpp assert.h atomic_helpers.h diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h new file mode 100644 index 000000000..5ca5893ef --- /dev/null +++ b/src/common/announce_multiplayer_room.h @@ -0,0 +1,138 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "web_service/web_result.h" + +namespace AnnounceMultiplayerRoom { + +using MacAddress = std::array; + +struct Room { + struct Member { + std::string username; + std::string nickname; + std::string avatar_url; + MacAddress mac_address; + std::string game_name; + u64 game_id; + }; + std::string id; + std::string verify_UID; ///< UID used for verification + std::string name; + std::string description; + std::string owner; + std::string ip; + u16 port; + u32 max_player; + u32 net_version; + bool has_password; + std::string preferred_game; + u64 preferred_game_id; + + std::vector members; +}; +using RoomList = std::vector; + +/** + * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should + * implement this interface. + */ +class Backend { +public: + virtual ~Backend() = default; + + /** + * Sets the Information that gets used for the announce + * @param uid The Id of the room + * @param name The name of the room + * @param description The room description + * @param port The port of the room + * @param net_version The version of the libNetwork that gets used + * @param has_password True if the room is passowrd protected + * @param preferred_game The preferred game of the room + * @param preferred_game_id The title id of the preferred game + */ + virtual void SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) = 0; + /** + * Adds a player information to the data that gets announced + * @param nickname The nickname of the player + * @param mac_address The MAC Address of the player + * @param game_id The title id of the game the player plays + * @param game_name The name of the game the player plays + */ + virtual void AddPlayer(const std::string& username, const std::string& nickname, + const std::string& avatar_url, const MacAddress& mac_address, + const u64 game_id, const std::string& game_name) = 0; + + /** + * Updates the data in the announce service. Re-register the room when required. + * @result The result of the update attempt + */ + virtual WebService::WebResult Update() = 0; + + /** + * Registers the data in the announce service + * @result The result of the register attempt. When the result code is Success, A global Guid of + * the room which may be used for verification will be in the result's returned_data. + */ + virtual WebService::WebResult Register() = 0; + + /** + * Empties the stored players + */ + virtual void ClearPlayers() = 0; + + /** + * Get the room information from the announce service + * @result A list of all rooms the announce service has + */ + virtual RoomList GetRoomList() = 0; + + /** + * Sends a delete message to the announce service + */ + virtual void Delete() = 0; +}; + +/** + * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a + * functional backend implementation is not available. + */ +class NullBackend : public Backend { +public: + ~NullBackend() = default; + void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const bool /*has_password*/, const std::string& /*preferred_game*/, + const u64 /*preferred_game_id*/) override {} + void AddPlayer(const std::string& /*username*/, const std::string& /*nickname*/, + const std::string& /*avatar_url*/, const MacAddress& /*mac_address*/, + const u64 /*game_id*/, const std::string& /*game_name*/) override {} + WebService::WebResult Update() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing"}; + } + WebService::WebResult Register() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing"}; + } + void ClearPlayers() override {} + RoomList GetRoomList() override { + return RoomList{}; + } + + void Delete() override {} +}; + +} // namespace AnnounceMultiplayerRoom diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 32cc2f392..48f5c1ee0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(core STATIC + announce_multiplayer_session.cpp + announce_multiplayer_session.h arm/arm_interface.h arm/arm_interface.cpp arm/cpu_interrupt_handler.cpp @@ -741,11 +743,11 @@ add_library(core STATIC memory/dmnt_cheat_vm.h memory.cpp memory.h - network/network.cpp - network/network.h - network/network_interface.cpp - network/network_interface.h - network/sockets.h + internal_network/network.cpp + internal_network/network.h + internal_network/network_interface.cpp + internal_network/network_interface.h + internal_network/sockets.h perf_stats.cpp perf_stats.h reporter.cpp @@ -780,7 +782,7 @@ endif() create_target_directory_groups(core) -target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) +target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core) target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus) if (MINGW) target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp new file mode 100644 index 000000000..a680ad202 --- /dev/null +++ b/src/core/announce_multiplayer_session.cpp @@ -0,0 +1,165 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "announce_multiplayer_session.h" +#include "common/announce_multiplayer_room.h" +#include "common/assert.h" +#include "common/settings.h" +#include "network/network.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/announce_room_json.h" +#endif + +namespace Core { + +// Time between room is announced to web_service +static constexpr std::chrono::seconds announce_time_interval(15); + +AnnounceMultiplayerSession::AnnounceMultiplayerSession() { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#else + backend = std::make_unique(); +#endif +} + +WebService::WebResult AnnounceMultiplayerSession::Register() { + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Network is not initialized"}; + } + if (room->GetState() != Network::Room::State::Open) { + return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open"}; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + LOG_INFO(WebService, "Room has been registered"); + room->SetVerifyUID(result.returned_data); + registered = true; + return WebService::WebResult{WebService::WebResult::Code::Success}; +} + +void AnnounceMultiplayerSession::Start() { + if (announce_multiplayer_thread) { + Stop(); + } + shutdown_event.Reset(); + announce_multiplayer_thread = + std::make_unique(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); +} + +void AnnounceMultiplayerSession::Stop() { + if (announce_multiplayer_thread) { + shutdown_event.Set(); + announce_multiplayer_thread->join(); + announce_multiplayer_thread.reset(); + backend->Delete(); + registered = false; + } +} + +AnnounceMultiplayerSession::CallbackHandle AnnounceMultiplayerSession::BindErrorCallback( + std::function function) { + std::lock_guard lock(callback_mutex); + auto handle = std::make_shared>(function); + error_callbacks.insert(handle); + return handle; +} + +void AnnounceMultiplayerSession::UnbindErrorCallback(CallbackHandle handle) { + std::lock_guard lock(callback_mutex); + error_callbacks.erase(handle); +} + +AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { + Stop(); +} + +void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr room) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation( + room_information.name, room_information.description, room_information.port, + room_information.member_slots, Network::network_version, room->HasPassword(), + room_information.preferred_game, room_information.preferred_game_id); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member.username, member.nickname, member.avatar_url, member.mac_address, + member.game_info.id, member.game_info.name); + } +} + +void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + // Invokes all current bound error callbacks. + const auto ErrorCallback = [this](WebService::WebResult result) { + std::lock_guard lock(callback_mutex); + for (auto callback : error_callbacks) { + (*callback)(result); + } + }; + + if (!registered) { + WebService::WebResult result = Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + return; + } + } + + auto update_time = std::chrono::steady_clock::now(); + std::future future; + while (!shutdown_event.WaitUntil(update_time)) { + update_time += announce_time_interval; + std::shared_ptr room = Network::GetRoom().lock(); + if (!room) { + break; + } + if (room->GetState() != Network::Room::State::Open) { + break; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Update(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + } + if (result.result_string == "404") { + registered = false; + // Needs to register the room again + WebService::WebResult register_result = Register(); + if (register_result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(register_result); + } + } + } +} + +AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() { + return backend->GetRoomList(); +} + +bool AnnounceMultiplayerSession::IsRunning() const { + return announce_multiplayer_thread != nullptr; +} + +void AnnounceMultiplayerSession::UpdateCredentials() { + ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running"); + +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#endif +} + +} // namespace Core diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h new file mode 100644 index 000000000..2aaf55017 --- /dev/null +++ b/src/core/announce_multiplayer_session.h @@ -0,0 +1,96 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "common/thread.h" + +namespace Network { +class Room; +} + +namespace Core { + +/** + * Instruments AnnounceMultiplayerRoom::Backend. + * Creates a thread that regularly updates the room information and submits them + * An async get of room information is also possible + */ +class AnnounceMultiplayerSession { +public: + using CallbackHandle = std::shared_ptr>; + AnnounceMultiplayerSession(); + ~AnnounceMultiplayerSession(); + + /** + * Allows to bind a function that will get called if the announce encounters an error + * @param function The function that gets called + * @return A handle that can be used the unbind the function + */ + CallbackHandle BindErrorCallback(std::function function); + + /** + * Unbind a function from the error callbacks + * @param handle The handle for the function that should get unbind + */ + void UnbindErrorCallback(CallbackHandle handle); + + /** + * Registers a room to web services + * @return The result of the registration attempt. + */ + WebService::WebResult Register(); + + /** + * Starts the announce of a room to web services + */ + void Start(); + + /** + * Stops the announce to web services + */ + void Stop(); + + /** + * Returns a list of all room information the backend got + * @param func A function that gets executed when the async get finished, e.g. a signal + * @return a list of rooms received from the web service + */ + AnnounceMultiplayerRoom::RoomList GetRoomList(); + + /** + * Whether the announce session is still running + */ + bool IsRunning() const; + + /** + * Recreates the backend, updating the credentials. + * This can only be used when the announce session is not running. + */ + void UpdateCredentials(); + +private: + Common::Event shutdown_event; + std::mutex callback_mutex; + std::set error_callbacks; + std::unique_ptr announce_multiplayer_thread; + + /// Backend interface that logs fields + std::unique_ptr backend; + + std::atomic_bool registered = false; ///< Whether the room has been registered + + void UpdateBackendData(std::shared_ptr room); + void AnnounceMultiplayerLoop(); +}; + +} // namespace Core diff --git a/src/core/core.cpp b/src/core/core.cpp index 0ede0d85c..5df32c1e7 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -43,14 +43,15 @@ #include "core/hle/service/service.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/time/time_manager.h" +#include "core/internal_network/network.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/memory/cheat_engine.h" -#include "core/network/network.h" #include "core/perf_stats.h" #include "core/reporter.h" #include "core/telemetry_session.h" #include "core/tools/freezer.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -315,6 +316,15 @@ struct System::Impl { GetAndResetPerfStats(); perf_stats->BeginSystemFrame(); + std::string name = "Unknown Game"; + const Loader::ResultStatus res{app_loader->ReadTitle(name)}; + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info; + game_info.name = name; + game_info.id = program_id; + room_member->SendGameInfo(game_info); + } + status = SystemResultStatus::Success; return status; } @@ -362,6 +372,11 @@ struct System::Impl { memory.Reset(); applet_manager.ClearAll(); + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info{}; + room_member->SendGameInfo(game_info); + } + LOG_DEBUG(Core, "Shutdown OK"); } diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 7055ea93e..2889973e4 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -18,8 +18,8 @@ namespace { } // Anonymous namespace -#include "core/network/network.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" namespace Service::NIFM { diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 3e9dc4a13..c7194731e 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -13,8 +13,8 @@ #include "core/hle/kernel/k_thread.h" #include "core/hle/service/sockets/bsd.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index fed740d87..9ea36428d 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -16,7 +16,7 @@ class System; namespace Network { class Socket; -} +} // namespace Network namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.cpp b/src/core/hle/service/sockets/sockets_translate.cpp index 9c0936d97..2db10ec81 100644 --- a/src/core/hle/service/sockets/sockets_translate.cpp +++ b/src/core/hle/service/sockets/sockets_translate.cpp @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.h b/src/core/hle/service/sockets/sockets_translate.h index 5e9809add..c93291d3e 100644 --- a/src/core/hle/service/sockets/sockets_translate.h +++ b/src/core/hle/service/sockets/sockets_translate.h @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/internal_network/network.cpp b/src/core/internal_network/network.cpp new file mode 100644 index 000000000..36c43cc8f --- /dev/null +++ b/src/core/internal_network/network.cpp @@ -0,0 +1,637 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include "common/error.h" + +#ifdef _WIN32 +#include +#include +#elif YUZU_UNIX +#include +#include +#include +#include +#include +#include +#include +#include +#else +#error "Unimplemented platform" +#endif + +#include "common/assert.h" +#include "common/common_types.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" +#include "core/internal_network/sockets.h" + +namespace Network { + +namespace { + +#ifdef _WIN32 + +using socklen_t = int; + +void Initialize() { + WSADATA wsa_data; + (void)WSAStartup(MAKEWORD(2, 2), &wsa_data); +} + +void Finalize() { + WSACleanup(); +} + +sockaddr TranslateFromSockAddrIn(SockAddrIn input) { + sockaddr_in result; + +#if YUZU_UNIX + result.sin_len = sizeof(result); +#endif + + switch (static_cast(input.family)) { + case Domain::INET: + result.sin_family = AF_INET; + break; + default: + UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.family); + result.sin_family = AF_INET; + break; + } + + result.sin_port = htons(input.portno); + + auto& ip = result.sin_addr.S_un.S_un_b; + ip.s_b1 = input.ip[0]; + ip.s_b2 = input.ip[1]; + ip.s_b3 = input.ip[2]; + ip.s_b4 = input.ip[3]; + + sockaddr addr; + std::memcpy(&addr, &result, sizeof(addr)); + return addr; +} + +LINGER MakeLinger(bool enable, u32 linger_value) { + ASSERT(linger_value <= std::numeric_limits::max()); + + LINGER value; + value.l_onoff = enable ? 1 : 0; + value.l_linger = static_cast(linger_value); + return value; +} + +bool EnableNonBlock(SOCKET fd, bool enable) { + u_long value = enable ? 1 : 0; + return ioctlsocket(fd, FIONBIO, &value) != SOCKET_ERROR; +} + +Errno TranslateNativeError(int e) { + switch (e) { + case WSAEBADF: + return Errno::BADF; + case WSAEINVAL: + return Errno::INVAL; + case WSAEMFILE: + return Errno::MFILE; + case WSAENOTCONN: + return Errno::NOTCONN; + case WSAEWOULDBLOCK: + return Errno::AGAIN; + case WSAECONNREFUSED: + return Errno::CONNREFUSED; + case WSAEHOSTUNREACH: + return Errno::HOSTUNREACH; + case WSAENETDOWN: + return Errno::NETDOWN; + case WSAENETUNREACH: + return Errno::NETUNREACH; + default: + return Errno::OTHER; + } +} + +#elif YUZU_UNIX // ^ _WIN32 v YUZU_UNIX + +using SOCKET = int; +using WSAPOLLFD = pollfd; +using ULONG = u64; + +constexpr SOCKET INVALID_SOCKET = -1; +constexpr SOCKET SOCKET_ERROR = -1; + +constexpr int SD_RECEIVE = SHUT_RD; +constexpr int SD_SEND = SHUT_WR; +constexpr int SD_BOTH = SHUT_RDWR; + +void Initialize() {} + +void Finalize() {} + +sockaddr TranslateFromSockAddrIn(SockAddrIn input) { + sockaddr_in result; + + switch (static_cast(input.family)) { + case Domain::INET: + result.sin_family = AF_INET; + break; + default: + UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.family); + result.sin_family = AF_INET; + break; + } + + result.sin_port = htons(input.portno); + + result.sin_addr.s_addr = input.ip[0] | input.ip[1] << 8 | input.ip[2] << 16 | input.ip[3] << 24; + + sockaddr addr; + std::memcpy(&addr, &result, sizeof(addr)); + return addr; +} + +int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) { + return poll(fds, static_cast(nfds), timeout); +} + +int closesocket(SOCKET fd) { + return close(fd); +} + +linger MakeLinger(bool enable, u32 linger_value) { + linger value; + value.l_onoff = enable ? 1 : 0; + value.l_linger = linger_value; + return value; +} + +bool EnableNonBlock(int fd, bool enable) { + int flags = fcntl(fd, F_GETFL); + if (flags == -1) { + return false; + } + if (enable) { + flags |= O_NONBLOCK; + } else { + flags &= ~O_NONBLOCK; + } + return fcntl(fd, F_SETFL, flags) == 0; +} + +Errno TranslateNativeError(int e) { + switch (e) { + case EBADF: + return Errno::BADF; + case EINVAL: + return Errno::INVAL; + case EMFILE: + return Errno::MFILE; + case ENOTCONN: + return Errno::NOTCONN; + case EAGAIN: + return Errno::AGAIN; + case ECONNREFUSED: + return Errno::CONNREFUSED; + case EHOSTUNREACH: + return Errno::HOSTUNREACH; + case ENETDOWN: + return Errno::NETDOWN; + case ENETUNREACH: + return Errno::NETUNREACH; + default: + return Errno::OTHER; + } +} + +#endif + +Errno GetAndLogLastError() { +#ifdef _WIN32 + int e = WSAGetLastError(); +#else + int e = errno; +#endif + const Errno err = TranslateNativeError(e); + if (err == Errno::AGAIN) { + return err; + } + LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e)); + return err; +} + +int TranslateDomain(Domain domain) { + switch (domain) { + case Domain::INET: + return AF_INET; + default: + UNIMPLEMENTED_MSG("Unimplemented domain={}", domain); + return 0; + } +} + +int TranslateType(Type type) { + switch (type) { + case Type::STREAM: + return SOCK_STREAM; + case Type::DGRAM: + return SOCK_DGRAM; + default: + UNIMPLEMENTED_MSG("Unimplemented type={}", type); + return 0; + } +} + +int TranslateProtocol(Protocol protocol) { + switch (protocol) { + case Protocol::TCP: + return IPPROTO_TCP; + case Protocol::UDP: + return IPPROTO_UDP; + default: + UNIMPLEMENTED_MSG("Unimplemented protocol={}", protocol); + return 0; + } +} + +SockAddrIn TranslateToSockAddrIn(sockaddr input_) { + sockaddr_in input; + std::memcpy(&input, &input_, sizeof(input)); + + SockAddrIn result; + + switch (input.sin_family) { + case AF_INET: + result.family = Domain::INET; + break; + default: + UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.sin_family); + result.family = Domain::INET; + break; + } + + result.portno = ntohs(input.sin_port); + + result.ip = TranslateIPv4(input.sin_addr); + + return result; +} + +short TranslatePollEvents(PollEvents events) { + short result = 0; + + if (True(events & PollEvents::In)) { + events &= ~PollEvents::In; + result |= POLLIN; + } + if (True(events & PollEvents::Pri)) { + events &= ~PollEvents::Pri; +#ifdef _WIN32 + LOG_WARNING(Service, "Winsock doesn't support POLLPRI"); +#else + result |= POLLPRI; +#endif + } + if (True(events & PollEvents::Out)) { + events &= ~PollEvents::Out; + result |= POLLOUT; + } + + UNIMPLEMENTED_IF_MSG((u16)events != 0, "Unhandled guest events=0x{:x}", (u16)events); + + return result; +} + +PollEvents TranslatePollRevents(short revents) { + PollEvents result{}; + const auto translate = [&result, &revents](short host, PollEvents guest) { + if ((revents & host) != 0) { + revents &= static_cast(~host); + result |= guest; + } + }; + + translate(POLLIN, PollEvents::In); + translate(POLLPRI, PollEvents::Pri); + translate(POLLOUT, PollEvents::Out); + translate(POLLERR, PollEvents::Err); + translate(POLLHUP, PollEvents::Hup); + + UNIMPLEMENTED_IF_MSG(revents != 0, "Unhandled host revents=0x{:x}", revents); + + return result; +} + +template +Errno SetSockOpt(SOCKET fd, int option, T value) { + const int result = + setsockopt(fd, SOL_SOCKET, option, reinterpret_cast(&value), sizeof(value)); + if (result != SOCKET_ERROR) { + return Errno::SUCCESS; + } + return GetAndLogLastError(); +} + +} // Anonymous namespace + +NetworkInstance::NetworkInstance() { + Initialize(); +} + +NetworkInstance::~NetworkInstance() { + Finalize(); +} + +std::optional GetHostIPv4Address() { + const std::string& selected_network_interface = Settings::values.network_interface.GetValue(); + const auto network_interfaces = Network::GetAvailableNetworkInterfaces(); + if (network_interfaces.size() == 0) { + LOG_ERROR(Network, "GetAvailableNetworkInterfaces returned no interfaces"); + return {}; + } + + const auto res = + std::ranges::find_if(network_interfaces, [&selected_network_interface](const auto& iface) { + return iface.name == selected_network_interface; + }); + + if (res != network_interfaces.end()) { + char ip_addr[16] = {}; + ASSERT(inet_ntop(AF_INET, &res->ip_address, ip_addr, sizeof(ip_addr)) != nullptr); + return TranslateIPv4(res->ip_address); + } else { + LOG_ERROR(Network, "Couldn't find selected interface \"{}\"", selected_network_interface); + return {}; + } +} + +std::pair Poll(std::vector& pollfds, s32 timeout) { + const size_t num = pollfds.size(); + + std::vector host_pollfds(pollfds.size()); + std::transform(pollfds.begin(), pollfds.end(), host_pollfds.begin(), [](PollFD fd) { + WSAPOLLFD result; + result.fd = fd.socket->fd; + result.events = TranslatePollEvents(fd.events); + result.revents = 0; + return result; + }); + + const int result = WSAPoll(host_pollfds.data(), static_cast(num), timeout); + if (result == 0) { + ASSERT(std::all_of(host_pollfds.begin(), host_pollfds.end(), + [](WSAPOLLFD fd) { return fd.revents == 0; })); + return {0, Errno::SUCCESS}; + } + + for (size_t i = 0; i < num; ++i) { + pollfds[i].revents = TranslatePollRevents(host_pollfds[i].revents); + } + + if (result > 0) { + return {result, Errno::SUCCESS}; + } + + ASSERT(result == SOCKET_ERROR); + + return {-1, GetAndLogLastError()}; +} + +Socket::~Socket() { + if (fd == INVALID_SOCKET) { + return; + } + (void)closesocket(fd); + fd = INVALID_SOCKET; +} + +Socket::Socket(Socket&& rhs) noexcept : fd{std::exchange(rhs.fd, INVALID_SOCKET)} {} + +Errno Socket::Initialize(Domain domain, Type type, Protocol protocol) { + fd = socket(TranslateDomain(domain), TranslateType(type), TranslateProtocol(protocol)); + if (fd != INVALID_SOCKET) { + return Errno::SUCCESS; + } + + return GetAndLogLastError(); +} + +std::pair Socket::Accept() { + sockaddr addr; + socklen_t addrlen = sizeof(addr); + const SOCKET new_socket = accept(fd, &addr, &addrlen); + + if (new_socket == INVALID_SOCKET) { + return {AcceptResult{}, GetAndLogLastError()}; + } + + AcceptResult result; + result.socket = std::make_unique(); + result.socket->fd = new_socket; + + ASSERT(addrlen == sizeof(sockaddr_in)); + result.sockaddr_in = TranslateToSockAddrIn(addr); + + return {std::move(result), Errno::SUCCESS}; +} + +Errno Socket::Connect(SockAddrIn addr_in) { + const sockaddr host_addr_in = TranslateFromSockAddrIn(addr_in); + if (connect(fd, &host_addr_in, sizeof(host_addr_in)) != SOCKET_ERROR) { + return Errno::SUCCESS; + } + + return GetAndLogLastError(); +} + +std::pair Socket::GetPeerName() { + sockaddr addr; + socklen_t addrlen = sizeof(addr); + if (getpeername(fd, &addr, &addrlen) == SOCKET_ERROR) { + return {SockAddrIn{}, GetAndLogLastError()}; + } + + ASSERT(addrlen == sizeof(sockaddr_in)); + return {TranslateToSockAddrIn(addr), Errno::SUCCESS}; +} + +std::pair Socket::GetSockName() { + sockaddr addr; + socklen_t addrlen = sizeof(addr); + if (getsockname(fd, &addr, &addrlen) == SOCKET_ERROR) { + return {SockAddrIn{}, GetAndLogLastError()}; + } + + ASSERT(addrlen == sizeof(sockaddr_in)); + return {TranslateToSockAddrIn(addr), Errno::SUCCESS}; +} + +Errno Socket::Bind(SockAddrIn addr) { + const sockaddr addr_in = TranslateFromSockAddrIn(addr); + if (bind(fd, &addr_in, sizeof(addr_in)) != SOCKET_ERROR) { + return Errno::SUCCESS; + } + + return GetAndLogLastError(); +} + +Errno Socket::Listen(s32 backlog) { + if (listen(fd, backlog) != SOCKET_ERROR) { + return Errno::SUCCESS; + } + + return GetAndLogLastError(); +} + +Errno Socket::Shutdown(ShutdownHow how) { + int host_how = 0; + switch (how) { + case ShutdownHow::RD: + host_how = SD_RECEIVE; + break; + case ShutdownHow::WR: + host_how = SD_SEND; + break; + case ShutdownHow::RDWR: + host_how = SD_BOTH; + break; + default: + UNIMPLEMENTED_MSG("Unimplemented flag how={}", how); + return Errno::SUCCESS; + } + if (shutdown(fd, host_how) != SOCKET_ERROR) { + return Errno::SUCCESS; + } + + return GetAndLogLastError(); +} + +std::pair Socket::Recv(int flags, std::vector& message) { + ASSERT(flags == 0); + ASSERT(message.size() < static_cast(std::numeric_limits::max())); + + const auto result = + recv(fd, reinterpret_cast(message.data()), static_cast(message.size()), 0); + if (result != SOCKET_ERROR) { + return {static_cast(result), Errno::SUCCESS}; + } + + return {-1, GetAndLogLastError()}; +} + +std::pair Socket::RecvFrom(int flags, std::vector& message, SockAddrIn* addr) { + ASSERT(flags == 0); + ASSERT(message.size() < static_cast(std::numeric_limits::max())); + + sockaddr addr_in{}; + socklen_t addrlen = sizeof(addr_in); + socklen_t* const p_addrlen = addr ? &addrlen : nullptr; + sockaddr* const p_addr_in = addr ? &addr_in : nullptr; + + const auto result = recvfrom(fd, reinterpret_cast(message.data()), + static_cast(message.size()), 0, p_addr_in, p_addrlen); + if (result != SOCKET_ERROR) { + if (addr) { + ASSERT(addrlen == sizeof(addr_in)); + *addr = TranslateToSockAddrIn(addr_in); + } + return {static_cast(result), Errno::SUCCESS}; + } + + return {-1, GetAndLogLastError()}; +} + +std::pair Socket::Send(const std::vector& message, int flags) { + ASSERT(message.size() < static_cast(std::numeric_limits::max())); + ASSERT(flags == 0); + + const auto result = send(fd, reinterpret_cast(message.data()), + static_cast(message.size()), 0); + if (result != SOCKET_ERROR) { + return {static_cast(result), Errno::SUCCESS}; + } + + return {-1, GetAndLogLastError()}; +} + +std::pair Socket::SendTo(u32 flags, const std::vector& message, + const SockAddrIn* addr) { + ASSERT(flags == 0); + + const sockaddr* to = nullptr; + const int tolen = addr ? sizeof(sockaddr) : 0; + sockaddr host_addr_in; + + if (addr) { + host_addr_in = TranslateFromSockAddrIn(*addr); + to = &host_addr_in; + } + + const auto result = sendto(fd, reinterpret_cast(message.data()), + static_cast(message.size()), 0, to, tolen); + if (result != SOCKET_ERROR) { + return {static_cast(result), Errno::SUCCESS}; + } + + return {-1, GetAndLogLastError()}; +} + +Errno Socket::Close() { + [[maybe_unused]] const int result = closesocket(fd); + ASSERT(result == 0); + fd = INVALID_SOCKET; + + return Errno::SUCCESS; +} + +Errno Socket::SetLinger(bool enable, u32 linger) { + return SetSockOpt(fd, SO_LINGER, MakeLinger(enable, linger)); +} + +Errno Socket::SetReuseAddr(bool enable) { + return SetSockOpt(fd, SO_REUSEADDR, enable ? 1 : 0); +} + +Errno Socket::SetKeepAlive(bool enable) { + return SetSockOpt(fd, SO_KEEPALIVE, enable ? 1 : 0); +} + +Errno Socket::SetBroadcast(bool enable) { + return SetSockOpt(fd, SO_BROADCAST, enable ? 1 : 0); +} + +Errno Socket::SetSndBuf(u32 value) { + return SetSockOpt(fd, SO_SNDBUF, value); +} + +Errno Socket::SetRcvBuf(u32 value) { + return SetSockOpt(fd, SO_RCVBUF, value); +} + +Errno Socket::SetSndTimeo(u32 value) { + return SetSockOpt(fd, SO_SNDTIMEO, value); +} + +Errno Socket::SetRcvTimeo(u32 value) { + return SetSockOpt(fd, SO_RCVTIMEO, value); +} + +Errno Socket::SetNonBlock(bool enable) { + if (EnableNonBlock(fd, enable)) { + return Errno::SUCCESS; + } + return GetAndLogLastError(); +} + +bool Socket::IsOpened() const { + return fd != INVALID_SOCKET; +} + +} // namespace Network diff --git a/src/core/internal_network/network.h b/src/core/internal_network/network.h new file mode 100644 index 000000000..10e5ef10d --- /dev/null +++ b/src/core/internal_network/network.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "common/common_funcs.h" +#include "common/common_types.h" + +#ifdef _WIN32 +#include +#elif YUZU_UNIX +#include +#endif + +namespace Network { + +class Socket; + +/// Error code for network functions +enum class Errno { + SUCCESS, + BADF, + INVAL, + MFILE, + NOTCONN, + AGAIN, + CONNREFUSED, + HOSTUNREACH, + NETDOWN, + NETUNREACH, + OTHER, +}; + +/// Address families +enum class Domain { + INET, ///< Address family for IPv4 +}; + +/// Socket types +enum class Type { + STREAM, + DGRAM, + RAW, + SEQPACKET, +}; + +/// Protocol values for sockets +enum class Protocol { + ICMP, + TCP, + UDP, +}; + +/// Shutdown mode +enum class ShutdownHow { + RD, + WR, + RDWR, +}; + +/// Array of IPv4 address +using IPv4Address = std::array; + +/// Cross-platform sockaddr structure +struct SockAddrIn { + Domain family; + IPv4Address ip; + u16 portno; +}; + +/// Cross-platform poll fd structure + +enum class PollEvents : u16 { + // Using Pascal case because IN is a macro on Windows. + In = 1 << 0, + Pri = 1 << 1, + Out = 1 << 2, + Err = 1 << 3, + Hup = 1 << 4, + Nval = 1 << 5, +}; + +DECLARE_ENUM_FLAG_OPERATORS(PollEvents); + +struct PollFD { + Socket* socket; + PollEvents events; + PollEvents revents; +}; + +class NetworkInstance { +public: + explicit NetworkInstance(); + ~NetworkInstance(); +}; + +#ifdef _WIN32 +constexpr IPv4Address TranslateIPv4(in_addr addr) { + auto& bytes = addr.S_un.S_un_b; + return IPv4Address{bytes.s_b1, bytes.s_b2, bytes.s_b3, bytes.s_b4}; +} +#elif YUZU_UNIX +constexpr IPv4Address TranslateIPv4(in_addr addr) { + const u32 bytes = addr.s_addr; + return IPv4Address{static_cast(bytes), static_cast(bytes >> 8), + static_cast(bytes >> 16), static_cast(bytes >> 24)}; +} +#endif + +/// @brief Returns host's IPv4 address +/// @return human ordered IPv4 address (e.g. 192.168.0.1) as an array +std::optional GetHostIPv4Address(); + +} // namespace Network diff --git a/src/core/internal_network/network_interface.cpp b/src/core/internal_network/network_interface.cpp new file mode 100644 index 000000000..0f0a66160 --- /dev/null +++ b/src/core/internal_network/network_interface.cpp @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include + +#include "common/bit_cast.h" +#include "common/common_types.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "core/internal_network/network_interface.h" + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace Network { + +#ifdef _WIN32 + +std::vector GetAvailableNetworkInterfaces() { + std::vector adapter_addresses; + DWORD ret = ERROR_BUFFER_OVERFLOW; + DWORD buf_size = 0; + + // retry up to 5 times + for (int i = 0; i < 5 && ret == ERROR_BUFFER_OVERFLOW; i++) { + ret = GetAdaptersAddresses( + AF_INET, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS, + nullptr, adapter_addresses.data(), &buf_size); + + if (ret != ERROR_BUFFER_OVERFLOW) { + break; + } + + adapter_addresses.resize((buf_size / sizeof(IP_ADAPTER_ADDRESSES)) + 1); + } + + if (ret != NO_ERROR) { + LOG_ERROR(Network, "Failed to get network interfaces with GetAdaptersAddresses"); + return {}; + } + + std::vector result; + + for (auto current_address = adapter_addresses.data(); current_address != nullptr; + current_address = current_address->Next) { + if (current_address->FirstUnicastAddress == nullptr || + current_address->FirstUnicastAddress->Address.lpSockaddr == nullptr) { + continue; + } + + if (current_address->OperStatus != IfOperStatusUp) { + continue; + } + + const auto ip_addr = Common::BitCast( + *current_address->FirstUnicastAddress->Address.lpSockaddr) + .sin_addr; + + ULONG mask = 0; + if (ConvertLengthToIpv4Mask(current_address->FirstUnicastAddress->OnLinkPrefixLength, + &mask) != NO_ERROR) { + LOG_ERROR(Network, "Failed to convert IPv4 prefix length to subnet mask"); + continue; + } + + struct in_addr gateway = {.S_un{.S_addr{0}}}; + if (current_address->FirstGatewayAddress != nullptr && + current_address->FirstGatewayAddress->Address.lpSockaddr != nullptr) { + gateway = Common::BitCast( + *current_address->FirstGatewayAddress->Address.lpSockaddr) + .sin_addr; + } + + result.emplace_back(NetworkInterface{ + .name{Common::UTF16ToUTF8(std::wstring{current_address->FriendlyName})}, + .ip_address{ip_addr}, + .subnet_mask = in_addr{.S_un{.S_addr{mask}}}, + .gateway = gateway}); + } + + return result; +} + +#else + +std::vector GetAvailableNetworkInterfaces() { + struct ifaddrs* ifaddr = nullptr; + + if (getifaddrs(&ifaddr) != 0) { + LOG_ERROR(Network, "Failed to get network interfaces with getifaddrs: {}", + std::strerror(errno)); + return {}; + } + + std::vector result; + + for (auto ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || ifa->ifa_netmask == nullptr) { + continue; + } + + if (ifa->ifa_addr->sa_family != AF_INET) { + continue; + } + + if ((ifa->ifa_flags & IFF_UP) == 0 || (ifa->ifa_flags & IFF_LOOPBACK) != 0) { + continue; + } + + u32 gateway{}; + + std::ifstream file{"/proc/net/route"}; + if (!file.is_open()) { + LOG_ERROR(Network, "Failed to open \"/proc/net/route\""); + + result.emplace_back(NetworkInterface{ + .name{ifa->ifa_name}, + .ip_address{Common::BitCast(*ifa->ifa_addr).sin_addr}, + .subnet_mask{Common::BitCast(*ifa->ifa_netmask).sin_addr}, + .gateway{in_addr{.s_addr = gateway}}}); + continue; + } + + // ignore header + file.ignore(std::numeric_limits::max(), '\n'); + + bool gateway_found = false; + + for (std::string line; std::getline(file, line);) { + std::istringstream iss{line}; + + std::string iface_name; + iss >> iface_name; + if (iface_name != ifa->ifa_name) { + continue; + } + + iss >> std::hex; + + u32 dest{}; + iss >> dest; + if (dest != 0) { + // not the default route + continue; + } + + iss >> gateway; + + u16 flags{}; + iss >> flags; + + // flag RTF_GATEWAY (defined in ) + if ((flags & 0x2) == 0) { + continue; + } + + gateway_found = true; + break; + } + + if (!gateway_found) { + gateway = 0; + } + + result.emplace_back(NetworkInterface{ + .name{ifa->ifa_name}, + .ip_address{Common::BitCast(*ifa->ifa_addr).sin_addr}, + .subnet_mask{Common::BitCast(*ifa->ifa_netmask).sin_addr}, + .gateway{in_addr{.s_addr = gateway}}}); + } + + freeifaddrs(ifaddr); + + return result; +} + +#endif + +std::optional GetSelectedNetworkInterface() { + const auto& selected_network_interface = Settings::values.network_interface.GetValue(); + const auto network_interfaces = Network::GetAvailableNetworkInterfaces(); + if (network_interfaces.size() == 0) { + LOG_ERROR(Network, "GetAvailableNetworkInterfaces returned no interfaces"); + return std::nullopt; + } + + const auto res = + std::ranges::find_if(network_interfaces, [&selected_network_interface](const auto& iface) { + return iface.name == selected_network_interface; + }); + + if (res == network_interfaces.end()) { + LOG_ERROR(Network, "Couldn't find selected interface \"{}\"", selected_network_interface); + return std::nullopt; + } + + return *res; +} + +} // namespace Network diff --git a/src/core/internal_network/network_interface.h b/src/core/internal_network/network_interface.h new file mode 100644 index 000000000..9b98b6b42 --- /dev/null +++ b/src/core/internal_network/network_interface.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace Network { + +struct NetworkInterface { + std::string name; + struct in_addr ip_address; + struct in_addr subnet_mask; + struct in_addr gateway; +}; + +std::vector GetAvailableNetworkInterfaces(); +std::optional GetSelectedNetworkInterface(); + +} // namespace Network diff --git a/src/core/internal_network/sockets.h b/src/core/internal_network/sockets.h new file mode 100644 index 000000000..77e27e928 --- /dev/null +++ b/src/core/internal_network/sockets.h @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#if defined(_WIN32) +#elif !YUZU_UNIX +#error "Platform not implemented" +#endif + +#include "common/common_types.h" +#include "core/internal_network/network.h" + +// TODO: C++20 Replace std::vector usages with std::span + +namespace Network { + +class Socket { +public: + struct AcceptResult { + std::unique_ptr socket; + SockAddrIn sockaddr_in; + }; + + explicit Socket() = default; + ~Socket(); + + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + + Socket(Socket&& rhs) noexcept; + + // Avoid closing sockets implicitly + Socket& operator=(Socket&&) noexcept = delete; + + Errno Initialize(Domain domain, Type type, Protocol protocol); + + Errno Close(); + + std::pair Accept(); + + Errno Connect(SockAddrIn addr_in); + + std::pair GetPeerName(); + + std::pair GetSockName(); + + Errno Bind(SockAddrIn addr); + + Errno Listen(s32 backlog); + + Errno Shutdown(ShutdownHow how); + + std::pair Recv(int flags, std::vector& message); + + std::pair RecvFrom(int flags, std::vector& message, SockAddrIn* addr); + + std::pair Send(const std::vector& message, int flags); + + std::pair SendTo(u32 flags, const std::vector& message, const SockAddrIn* addr); + + Errno SetLinger(bool enable, u32 linger); + + Errno SetReuseAddr(bool enable); + + Errno SetKeepAlive(bool enable); + + Errno SetBroadcast(bool enable); + + Errno SetSndBuf(u32 value); + + Errno SetRcvBuf(u32 value); + + Errno SetSndTimeo(u32 value); + + Errno SetRcvTimeo(u32 value); + + Errno SetNonBlock(bool enable); + + bool IsOpened() const; + +#if defined(_WIN32) + SOCKET fd = INVALID_SOCKET; +#elif YUZU_UNIX + int fd = -1; +#endif +}; + +std::pair Poll(std::vector& poll_fds, s32 timeout); + +} // namespace Network diff --git a/src/core/network/network.cpp b/src/core/network/network.cpp deleted file mode 100644 index fdafbea92..000000000 --- a/src/core/network/network.cpp +++ /dev/null @@ -1,637 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include -#include - -#include "common/error.h" - -#ifdef _WIN32 -#include -#include -#elif YUZU_UNIX -#include -#include -#include -#include -#include -#include -#include -#include -#else -#error "Unimplemented platform" -#endif - -#include "common/assert.h" -#include "common/common_types.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/network/network.h" -#include "core/network/network_interface.h" -#include "core/network/sockets.h" - -namespace Network { - -namespace { - -#ifdef _WIN32 - -using socklen_t = int; - -void Initialize() { - WSADATA wsa_data; - (void)WSAStartup(MAKEWORD(2, 2), &wsa_data); -} - -void Finalize() { - WSACleanup(); -} - -sockaddr TranslateFromSockAddrIn(SockAddrIn input) { - sockaddr_in result; - -#if YUZU_UNIX - result.sin_len = sizeof(result); -#endif - - switch (static_cast(input.family)) { - case Domain::INET: - result.sin_family = AF_INET; - break; - default: - UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.family); - result.sin_family = AF_INET; - break; - } - - result.sin_port = htons(input.portno); - - auto& ip = result.sin_addr.S_un.S_un_b; - ip.s_b1 = input.ip[0]; - ip.s_b2 = input.ip[1]; - ip.s_b3 = input.ip[2]; - ip.s_b4 = input.ip[3]; - - sockaddr addr; - std::memcpy(&addr, &result, sizeof(addr)); - return addr; -} - -LINGER MakeLinger(bool enable, u32 linger_value) { - ASSERT(linger_value <= std::numeric_limits::max()); - - LINGER value; - value.l_onoff = enable ? 1 : 0; - value.l_linger = static_cast(linger_value); - return value; -} - -bool EnableNonBlock(SOCKET fd, bool enable) { - u_long value = enable ? 1 : 0; - return ioctlsocket(fd, FIONBIO, &value) != SOCKET_ERROR; -} - -Errno TranslateNativeError(int e) { - switch (e) { - case WSAEBADF: - return Errno::BADF; - case WSAEINVAL: - return Errno::INVAL; - case WSAEMFILE: - return Errno::MFILE; - case WSAENOTCONN: - return Errno::NOTCONN; - case WSAEWOULDBLOCK: - return Errno::AGAIN; - case WSAECONNREFUSED: - return Errno::CONNREFUSED; - case WSAEHOSTUNREACH: - return Errno::HOSTUNREACH; - case WSAENETDOWN: - return Errno::NETDOWN; - case WSAENETUNREACH: - return Errno::NETUNREACH; - default: - return Errno::OTHER; - } -} - -#elif YUZU_UNIX // ^ _WIN32 v YUZU_UNIX - -using SOCKET = int; -using WSAPOLLFD = pollfd; -using ULONG = u64; - -constexpr SOCKET INVALID_SOCKET = -1; -constexpr SOCKET SOCKET_ERROR = -1; - -constexpr int SD_RECEIVE = SHUT_RD; -constexpr int SD_SEND = SHUT_WR; -constexpr int SD_BOTH = SHUT_RDWR; - -void Initialize() {} - -void Finalize() {} - -sockaddr TranslateFromSockAddrIn(SockAddrIn input) { - sockaddr_in result; - - switch (static_cast(input.family)) { - case Domain::INET: - result.sin_family = AF_INET; - break; - default: - UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.family); - result.sin_family = AF_INET; - break; - } - - result.sin_port = htons(input.portno); - - result.sin_addr.s_addr = input.ip[0] | input.ip[1] << 8 | input.ip[2] << 16 | input.ip[3] << 24; - - sockaddr addr; - std::memcpy(&addr, &result, sizeof(addr)); - return addr; -} - -int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) { - return poll(fds, static_cast(nfds), timeout); -} - -int closesocket(SOCKET fd) { - return close(fd); -} - -linger MakeLinger(bool enable, u32 linger_value) { - linger value; - value.l_onoff = enable ? 1 : 0; - value.l_linger = linger_value; - return value; -} - -bool EnableNonBlock(int fd, bool enable) { - int flags = fcntl(fd, F_GETFL); - if (flags == -1) { - return false; - } - if (enable) { - flags |= O_NONBLOCK; - } else { - flags &= ~O_NONBLOCK; - } - return fcntl(fd, F_SETFL, flags) == 0; -} - -Errno TranslateNativeError(int e) { - switch (e) { - case EBADF: - return Errno::BADF; - case EINVAL: - return Errno::INVAL; - case EMFILE: - return Errno::MFILE; - case ENOTCONN: - return Errno::NOTCONN; - case EAGAIN: - return Errno::AGAIN; - case ECONNREFUSED: - return Errno::CONNREFUSED; - case EHOSTUNREACH: - return Errno::HOSTUNREACH; - case ENETDOWN: - return Errno::NETDOWN; - case ENETUNREACH: - return Errno::NETUNREACH; - default: - return Errno::OTHER; - } -} - -#endif - -Errno GetAndLogLastError() { -#ifdef _WIN32 - int e = WSAGetLastError(); -#else - int e = errno; -#endif - const Errno err = TranslateNativeError(e); - if (err == Errno::AGAIN) { - return err; - } - LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e)); - return err; -} - -int TranslateDomain(Domain domain) { - switch (domain) { - case Domain::INET: - return AF_INET; - default: - UNIMPLEMENTED_MSG("Unimplemented domain={}", domain); - return 0; - } -} - -int TranslateType(Type type) { - switch (type) { - case Type::STREAM: - return SOCK_STREAM; - case Type::DGRAM: - return SOCK_DGRAM; - default: - UNIMPLEMENTED_MSG("Unimplemented type={}", type); - return 0; - } -} - -int TranslateProtocol(Protocol protocol) { - switch (protocol) { - case Protocol::TCP: - return IPPROTO_TCP; - case Protocol::UDP: - return IPPROTO_UDP; - default: - UNIMPLEMENTED_MSG("Unimplemented protocol={}", protocol); - return 0; - } -} - -SockAddrIn TranslateToSockAddrIn(sockaddr input_) { - sockaddr_in input; - std::memcpy(&input, &input_, sizeof(input)); - - SockAddrIn result; - - switch (input.sin_family) { - case AF_INET: - result.family = Domain::INET; - break; - default: - UNIMPLEMENTED_MSG("Unhandled sockaddr family={}", input.sin_family); - result.family = Domain::INET; - break; - } - - result.portno = ntohs(input.sin_port); - - result.ip = TranslateIPv4(input.sin_addr); - - return result; -} - -short TranslatePollEvents(PollEvents events) { - short result = 0; - - if (True(events & PollEvents::In)) { - events &= ~PollEvents::In; - result |= POLLIN; - } - if (True(events & PollEvents::Pri)) { - events &= ~PollEvents::Pri; -#ifdef _WIN32 - LOG_WARNING(Service, "Winsock doesn't support POLLPRI"); -#else - result |= POLLPRI; -#endif - } - if (True(events & PollEvents::Out)) { - events &= ~PollEvents::Out; - result |= POLLOUT; - } - - UNIMPLEMENTED_IF_MSG((u16)events != 0, "Unhandled guest events=0x{:x}", (u16)events); - - return result; -} - -PollEvents TranslatePollRevents(short revents) { - PollEvents result{}; - const auto translate = [&result, &revents](short host, PollEvents guest) { - if ((revents & host) != 0) { - revents &= static_cast(~host); - result |= guest; - } - }; - - translate(POLLIN, PollEvents::In); - translate(POLLPRI, PollEvents::Pri); - translate(POLLOUT, PollEvents::Out); - translate(POLLERR, PollEvents::Err); - translate(POLLHUP, PollEvents::Hup); - - UNIMPLEMENTED_IF_MSG(revents != 0, "Unhandled host revents=0x{:x}", revents); - - return result; -} - -template -Errno SetSockOpt(SOCKET fd, int option, T value) { - const int result = - setsockopt(fd, SOL_SOCKET, option, reinterpret_cast(&value), sizeof(value)); - if (result != SOCKET_ERROR) { - return Errno::SUCCESS; - } - return GetAndLogLastError(); -} - -} // Anonymous namespace - -NetworkInstance::NetworkInstance() { - Initialize(); -} - -NetworkInstance::~NetworkInstance() { - Finalize(); -} - -std::optional GetHostIPv4Address() { - const std::string& selected_network_interface = Settings::values.network_interface.GetValue(); - const auto network_interfaces = Network::GetAvailableNetworkInterfaces(); - if (network_interfaces.size() == 0) { - LOG_ERROR(Network, "GetAvailableNetworkInterfaces returned no interfaces"); - return {}; - } - - const auto res = - std::ranges::find_if(network_interfaces, [&selected_network_interface](const auto& iface) { - return iface.name == selected_network_interface; - }); - - if (res != network_interfaces.end()) { - char ip_addr[16] = {}; - ASSERT(inet_ntop(AF_INET, &res->ip_address, ip_addr, sizeof(ip_addr)) != nullptr); - return TranslateIPv4(res->ip_address); - } else { - LOG_ERROR(Network, "Couldn't find selected interface \"{}\"", selected_network_interface); - return {}; - } -} - -std::pair Poll(std::vector& pollfds, s32 timeout) { - const size_t num = pollfds.size(); - - std::vector host_pollfds(pollfds.size()); - std::transform(pollfds.begin(), pollfds.end(), host_pollfds.begin(), [](PollFD fd) { - WSAPOLLFD result; - result.fd = fd.socket->fd; - result.events = TranslatePollEvents(fd.events); - result.revents = 0; - return result; - }); - - const int result = WSAPoll(host_pollfds.data(), static_cast(num), timeout); - if (result == 0) { - ASSERT(std::all_of(host_pollfds.begin(), host_pollfds.end(), - [](WSAPOLLFD fd) { return fd.revents == 0; })); - return {0, Errno::SUCCESS}; - } - - for (size_t i = 0; i < num; ++i) { - pollfds[i].revents = TranslatePollRevents(host_pollfds[i].revents); - } - - if (result > 0) { - return {result, Errno::SUCCESS}; - } - - ASSERT(result == SOCKET_ERROR); - - return {-1, GetAndLogLastError()}; -} - -Socket::~Socket() { - if (fd == INVALID_SOCKET) { - return; - } - (void)closesocket(fd); - fd = INVALID_SOCKET; -} - -Socket::Socket(Socket&& rhs) noexcept : fd{std::exchange(rhs.fd, INVALID_SOCKET)} {} - -Errno Socket::Initialize(Domain domain, Type type, Protocol protocol) { - fd = socket(TranslateDomain(domain), TranslateType(type), TranslateProtocol(protocol)); - if (fd != INVALID_SOCKET) { - return Errno::SUCCESS; - } - - return GetAndLogLastError(); -} - -std::pair Socket::Accept() { - sockaddr addr; - socklen_t addrlen = sizeof(addr); - const SOCKET new_socket = accept(fd, &addr, &addrlen); - - if (new_socket == INVALID_SOCKET) { - return {AcceptResult{}, GetAndLogLastError()}; - } - - AcceptResult result; - result.socket = std::make_unique(); - result.socket->fd = new_socket; - - ASSERT(addrlen == sizeof(sockaddr_in)); - result.sockaddr_in = TranslateToSockAddrIn(addr); - - return {std::move(result), Errno::SUCCESS}; -} - -Errno Socket::Connect(SockAddrIn addr_in) { - const sockaddr host_addr_in = TranslateFromSockAddrIn(addr_in); - if (connect(fd, &host_addr_in, sizeof(host_addr_in)) != SOCKET_ERROR) { - return Errno::SUCCESS; - } - - return GetAndLogLastError(); -} - -std::pair Socket::GetPeerName() { - sockaddr addr; - socklen_t addrlen = sizeof(addr); - if (getpeername(fd, &addr, &addrlen) == SOCKET_ERROR) { - return {SockAddrIn{}, GetAndLogLastError()}; - } - - ASSERT(addrlen == sizeof(sockaddr_in)); - return {TranslateToSockAddrIn(addr), Errno::SUCCESS}; -} - -std::pair Socket::GetSockName() { - sockaddr addr; - socklen_t addrlen = sizeof(addr); - if (getsockname(fd, &addr, &addrlen) == SOCKET_ERROR) { - return {SockAddrIn{}, GetAndLogLastError()}; - } - - ASSERT(addrlen == sizeof(sockaddr_in)); - return {TranslateToSockAddrIn(addr), Errno::SUCCESS}; -} - -Errno Socket::Bind(SockAddrIn addr) { - const sockaddr addr_in = TranslateFromSockAddrIn(addr); - if (bind(fd, &addr_in, sizeof(addr_in)) != SOCKET_ERROR) { - return Errno::SUCCESS; - } - - return GetAndLogLastError(); -} - -Errno Socket::Listen(s32 backlog) { - if (listen(fd, backlog) != SOCKET_ERROR) { - return Errno::SUCCESS; - } - - return GetAndLogLastError(); -} - -Errno Socket::Shutdown(ShutdownHow how) { - int host_how = 0; - switch (how) { - case ShutdownHow::RD: - host_how = SD_RECEIVE; - break; - case ShutdownHow::WR: - host_how = SD_SEND; - break; - case ShutdownHow::RDWR: - host_how = SD_BOTH; - break; - default: - UNIMPLEMENTED_MSG("Unimplemented flag how={}", how); - return Errno::SUCCESS; - } - if (shutdown(fd, host_how) != SOCKET_ERROR) { - return Errno::SUCCESS; - } - - return GetAndLogLastError(); -} - -std::pair Socket::Recv(int flags, std::vector& message) { - ASSERT(flags == 0); - ASSERT(message.size() < static_cast(std::numeric_limits::max())); - - const auto result = - recv(fd, reinterpret_cast(message.data()), static_cast(message.size()), 0); - if (result != SOCKET_ERROR) { - return {static_cast(result), Errno::SUCCESS}; - } - - return {-1, GetAndLogLastError()}; -} - -std::pair Socket::RecvFrom(int flags, std::vector& message, SockAddrIn* addr) { - ASSERT(flags == 0); - ASSERT(message.size() < static_cast(std::numeric_limits::max())); - - sockaddr addr_in{}; - socklen_t addrlen = sizeof(addr_in); - socklen_t* const p_addrlen = addr ? &addrlen : nullptr; - sockaddr* const p_addr_in = addr ? &addr_in : nullptr; - - const auto result = recvfrom(fd, reinterpret_cast(message.data()), - static_cast(message.size()), 0, p_addr_in, p_addrlen); - if (result != SOCKET_ERROR) { - if (addr) { - ASSERT(addrlen == sizeof(addr_in)); - *addr = TranslateToSockAddrIn(addr_in); - } - return {static_cast(result), Errno::SUCCESS}; - } - - return {-1, GetAndLogLastError()}; -} - -std::pair Socket::Send(const std::vector& message, int flags) { - ASSERT(message.size() < static_cast(std::numeric_limits::max())); - ASSERT(flags == 0); - - const auto result = send(fd, reinterpret_cast(message.data()), - static_cast(message.size()), 0); - if (result != SOCKET_ERROR) { - return {static_cast(result), Errno::SUCCESS}; - } - - return {-1, GetAndLogLastError()}; -} - -std::pair Socket::SendTo(u32 flags, const std::vector& message, - const SockAddrIn* addr) { - ASSERT(flags == 0); - - const sockaddr* to = nullptr; - const int tolen = addr ? sizeof(sockaddr) : 0; - sockaddr host_addr_in; - - if (addr) { - host_addr_in = TranslateFromSockAddrIn(*addr); - to = &host_addr_in; - } - - const auto result = sendto(fd, reinterpret_cast(message.data()), - static_cast(message.size()), 0, to, tolen); - if (result != SOCKET_ERROR) { - return {static_cast(result), Errno::SUCCESS}; - } - - return {-1, GetAndLogLastError()}; -} - -Errno Socket::Close() { - [[maybe_unused]] const int result = closesocket(fd); - ASSERT(result == 0); - fd = INVALID_SOCKET; - - return Errno::SUCCESS; -} - -Errno Socket::SetLinger(bool enable, u32 linger) { - return SetSockOpt(fd, SO_LINGER, MakeLinger(enable, linger)); -} - -Errno Socket::SetReuseAddr(bool enable) { - return SetSockOpt(fd, SO_REUSEADDR, enable ? 1 : 0); -} - -Errno Socket::SetKeepAlive(bool enable) { - return SetSockOpt(fd, SO_KEEPALIVE, enable ? 1 : 0); -} - -Errno Socket::SetBroadcast(bool enable) { - return SetSockOpt(fd, SO_BROADCAST, enable ? 1 : 0); -} - -Errno Socket::SetSndBuf(u32 value) { - return SetSockOpt(fd, SO_SNDBUF, value); -} - -Errno Socket::SetRcvBuf(u32 value) { - return SetSockOpt(fd, SO_RCVBUF, value); -} - -Errno Socket::SetSndTimeo(u32 value) { - return SetSockOpt(fd, SO_SNDTIMEO, value); -} - -Errno Socket::SetRcvTimeo(u32 value) { - return SetSockOpt(fd, SO_RCVTIMEO, value); -} - -Errno Socket::SetNonBlock(bool enable) { - if (EnableNonBlock(fd, enable)) { - return Errno::SUCCESS; - } - return GetAndLogLastError(); -} - -bool Socket::IsOpened() const { - return fd != INVALID_SOCKET; -} - -} // namespace Network diff --git a/src/core/network/network.h b/src/core/network/network.h deleted file mode 100644 index 10e5ef10d..000000000 --- a/src/core/network/network.h +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#include "common/common_funcs.h" -#include "common/common_types.h" - -#ifdef _WIN32 -#include -#elif YUZU_UNIX -#include -#endif - -namespace Network { - -class Socket; - -/// Error code for network functions -enum class Errno { - SUCCESS, - BADF, - INVAL, - MFILE, - NOTCONN, - AGAIN, - CONNREFUSED, - HOSTUNREACH, - NETDOWN, - NETUNREACH, - OTHER, -}; - -/// Address families -enum class Domain { - INET, ///< Address family for IPv4 -}; - -/// Socket types -enum class Type { - STREAM, - DGRAM, - RAW, - SEQPACKET, -}; - -/// Protocol values for sockets -enum class Protocol { - ICMP, - TCP, - UDP, -}; - -/// Shutdown mode -enum class ShutdownHow { - RD, - WR, - RDWR, -}; - -/// Array of IPv4 address -using IPv4Address = std::array; - -/// Cross-platform sockaddr structure -struct SockAddrIn { - Domain family; - IPv4Address ip; - u16 portno; -}; - -/// Cross-platform poll fd structure - -enum class PollEvents : u16 { - // Using Pascal case because IN is a macro on Windows. - In = 1 << 0, - Pri = 1 << 1, - Out = 1 << 2, - Err = 1 << 3, - Hup = 1 << 4, - Nval = 1 << 5, -}; - -DECLARE_ENUM_FLAG_OPERATORS(PollEvents); - -struct PollFD { - Socket* socket; - PollEvents events; - PollEvents revents; -}; - -class NetworkInstance { -public: - explicit NetworkInstance(); - ~NetworkInstance(); -}; - -#ifdef _WIN32 -constexpr IPv4Address TranslateIPv4(in_addr addr) { - auto& bytes = addr.S_un.S_un_b; - return IPv4Address{bytes.s_b1, bytes.s_b2, bytes.s_b3, bytes.s_b4}; -} -#elif YUZU_UNIX -constexpr IPv4Address TranslateIPv4(in_addr addr) { - const u32 bytes = addr.s_addr; - return IPv4Address{static_cast(bytes), static_cast(bytes >> 8), - static_cast(bytes >> 16), static_cast(bytes >> 24)}; -} -#endif - -/// @brief Returns host's IPv4 address -/// @return human ordered IPv4 address (e.g. 192.168.0.1) as an array -std::optional GetHostIPv4Address(); - -} // namespace Network diff --git a/src/core/network/network_interface.cpp b/src/core/network/network_interface.cpp deleted file mode 100644 index 15ecc6abf..000000000 --- a/src/core/network/network_interface.cpp +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include - -#include "common/bit_cast.h" -#include "common/common_types.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "common/string_util.h" -#include "core/network/network_interface.h" - -#ifdef _WIN32 -#include -#else -#include -#include -#include -#endif - -namespace Network { - -#ifdef _WIN32 - -std::vector GetAvailableNetworkInterfaces() { - std::vector adapter_addresses; - DWORD ret = ERROR_BUFFER_OVERFLOW; - DWORD buf_size = 0; - - // retry up to 5 times - for (int i = 0; i < 5 && ret == ERROR_BUFFER_OVERFLOW; i++) { - ret = GetAdaptersAddresses( - AF_INET, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS, - nullptr, adapter_addresses.data(), &buf_size); - - if (ret != ERROR_BUFFER_OVERFLOW) { - break; - } - - adapter_addresses.resize((buf_size / sizeof(IP_ADAPTER_ADDRESSES)) + 1); - } - - if (ret != NO_ERROR) { - LOG_ERROR(Network, "Failed to get network interfaces with GetAdaptersAddresses"); - return {}; - } - - std::vector result; - - for (auto current_address = adapter_addresses.data(); current_address != nullptr; - current_address = current_address->Next) { - if (current_address->FirstUnicastAddress == nullptr || - current_address->FirstUnicastAddress->Address.lpSockaddr == nullptr) { - continue; - } - - if (current_address->OperStatus != IfOperStatusUp) { - continue; - } - - const auto ip_addr = Common::BitCast( - *current_address->FirstUnicastAddress->Address.lpSockaddr) - .sin_addr; - - ULONG mask = 0; - if (ConvertLengthToIpv4Mask(current_address->FirstUnicastAddress->OnLinkPrefixLength, - &mask) != NO_ERROR) { - LOG_ERROR(Network, "Failed to convert IPv4 prefix length to subnet mask"); - continue; - } - - struct in_addr gateway = {.S_un{.S_addr{0}}}; - if (current_address->FirstGatewayAddress != nullptr && - current_address->FirstGatewayAddress->Address.lpSockaddr != nullptr) { - gateway = Common::BitCast( - *current_address->FirstGatewayAddress->Address.lpSockaddr) - .sin_addr; - } - - result.emplace_back(NetworkInterface{ - .name{Common::UTF16ToUTF8(std::wstring{current_address->FriendlyName})}, - .ip_address{ip_addr}, - .subnet_mask = in_addr{.S_un{.S_addr{mask}}}, - .gateway = gateway}); - } - - return result; -} - -#else - -std::vector GetAvailableNetworkInterfaces() { - struct ifaddrs* ifaddr = nullptr; - - if (getifaddrs(&ifaddr) != 0) { - LOG_ERROR(Network, "Failed to get network interfaces with getifaddrs: {}", - std::strerror(errno)); - return {}; - } - - std::vector result; - - for (auto ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { - if (ifa->ifa_addr == nullptr || ifa->ifa_netmask == nullptr) { - continue; - } - - if (ifa->ifa_addr->sa_family != AF_INET) { - continue; - } - - if ((ifa->ifa_flags & IFF_UP) == 0 || (ifa->ifa_flags & IFF_LOOPBACK) != 0) { - continue; - } - - u32 gateway{}; - - std::ifstream file{"/proc/net/route"}; - if (!file.is_open()) { - LOG_ERROR(Network, "Failed to open \"/proc/net/route\""); - - result.emplace_back(NetworkInterface{ - .name{ifa->ifa_name}, - .ip_address{Common::BitCast(*ifa->ifa_addr).sin_addr}, - .subnet_mask{Common::BitCast(*ifa->ifa_netmask).sin_addr}, - .gateway{in_addr{.s_addr = gateway}}}); - continue; - } - - // ignore header - file.ignore(std::numeric_limits::max(), '\n'); - - bool gateway_found = false; - - for (std::string line; std::getline(file, line);) { - std::istringstream iss{line}; - - std::string iface_name; - iss >> iface_name; - if (iface_name != ifa->ifa_name) { - continue; - } - - iss >> std::hex; - - u32 dest{}; - iss >> dest; - if (dest != 0) { - // not the default route - continue; - } - - iss >> gateway; - - u16 flags{}; - iss >> flags; - - // flag RTF_GATEWAY (defined in ) - if ((flags & 0x2) == 0) { - continue; - } - - gateway_found = true; - break; - } - - if (!gateway_found) { - gateway = 0; - } - - result.emplace_back(NetworkInterface{ - .name{ifa->ifa_name}, - .ip_address{Common::BitCast(*ifa->ifa_addr).sin_addr}, - .subnet_mask{Common::BitCast(*ifa->ifa_netmask).sin_addr}, - .gateway{in_addr{.s_addr = gateway}}}); - } - - freeifaddrs(ifaddr); - - return result; -} - -#endif - -std::optional GetSelectedNetworkInterface() { - const auto& selected_network_interface = Settings::values.network_interface.GetValue(); - const auto network_interfaces = Network::GetAvailableNetworkInterfaces(); - if (network_interfaces.size() == 0) { - LOG_ERROR(Network, "GetAvailableNetworkInterfaces returned no interfaces"); - return std::nullopt; - } - - const auto res = - std::ranges::find_if(network_interfaces, [&selected_network_interface](const auto& iface) { - return iface.name == selected_network_interface; - }); - - if (res == network_interfaces.end()) { - LOG_ERROR(Network, "Couldn't find selected interface \"{}\"", selected_network_interface); - return std::nullopt; - } - - return *res; -} - -} // namespace Network diff --git a/src/core/network/network_interface.h b/src/core/network/network_interface.h deleted file mode 100644 index 9b98b6b42..000000000 --- a/src/core/network/network_interface.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -#ifdef _WIN32 -#include -#else -#include -#endif - -namespace Network { - -struct NetworkInterface { - std::string name; - struct in_addr ip_address; - struct in_addr subnet_mask; - struct in_addr gateway; -}; - -std::vector GetAvailableNetworkInterfaces(); -std::optional GetSelectedNetworkInterface(); - -} // namespace Network diff --git a/src/core/network/sockets.h b/src/core/network/sockets.h deleted file mode 100644 index f889159f5..000000000 --- a/src/core/network/sockets.h +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -#if defined(_WIN32) -#elif !YUZU_UNIX -#error "Platform not implemented" -#endif - -#include "common/common_types.h" -#include "core/network/network.h" - -// TODO: C++20 Replace std::vector usages with std::span - -namespace Network { - -class Socket { -public: - struct AcceptResult { - std::unique_ptr socket; - SockAddrIn sockaddr_in; - }; - - explicit Socket() = default; - ~Socket(); - - Socket(const Socket&) = delete; - Socket& operator=(const Socket&) = delete; - - Socket(Socket&& rhs) noexcept; - - // Avoid closing sockets implicitly - Socket& operator=(Socket&&) noexcept = delete; - - Errno Initialize(Domain domain, Type type, Protocol protocol); - - Errno Close(); - - std::pair Accept(); - - Errno Connect(SockAddrIn addr_in); - - std::pair GetPeerName(); - - std::pair GetSockName(); - - Errno Bind(SockAddrIn addr); - - Errno Listen(s32 backlog); - - Errno Shutdown(ShutdownHow how); - - std::pair Recv(int flags, std::vector& message); - - std::pair RecvFrom(int flags, std::vector& message, SockAddrIn* addr); - - std::pair Send(const std::vector& message, int flags); - - std::pair SendTo(u32 flags, const std::vector& message, const SockAddrIn* addr); - - Errno SetLinger(bool enable, u32 linger); - - Errno SetReuseAddr(bool enable); - - Errno SetKeepAlive(bool enable); - - Errno SetBroadcast(bool enable); - - Errno SetSndBuf(u32 value); - - Errno SetRcvBuf(u32 value); - - Errno SetSndTimeo(u32 value); - - Errno SetRcvTimeo(u32 value); - - Errno SetNonBlock(bool enable); - - bool IsOpened() const; - -#if defined(_WIN32) - SOCKET fd = INVALID_SOCKET; -#elif YUZU_UNIX - int fd = -1; -#endif -}; - -std::pair Poll(std::vector& poll_fds, s32 timeout); - -} // namespace Network diff --git a/src/network/room.cpp b/src/network/room.cpp index cd0c0ebc4..528867146 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -251,7 +251,7 @@ public: void Room::RoomImpl::ServerLoop() { while (state != State::Closed) { ENetEvent event; - if (enet_host_service(server, &event, 50) > 0) { + if (enet_host_service(server, &event, 16) > 0) { switch (event.type) { case ENET_EVENT_TYPE_RECEIVE: switch (event.packet->data[0]) { @@ -599,7 +599,7 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { if (sending_member == members.end()) { return false; } - if (room_information.enable_citra_mods && + if (room_information.enable_yuzu_mods && sending_member->user_data.moderator) { // Community moderator return true; @@ -1014,7 +1014,7 @@ bool Room::Create(const std::string& name, const std::string& description, const u32 max_connections, const std::string& host_username, const std::string& preferred_game, u64 preferred_game_id, std::unique_ptr verify_backend, - const Room::BanList& ban_list, bool enable_citra_mods) { + const Room::BanList& ban_list, bool enable_yuzu_mods) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -1037,7 +1037,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->room_information.host_username = host_username; - room_impl->room_information.enable_citra_mods = enable_citra_mods; + room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods; room_impl->password = password; room_impl->verify_backend = std::move(verify_backend); room_impl->username_ban_list = ban_list.first; diff --git a/src/network/room.h b/src/network/room.h index a67984837..5d4371c16 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -32,7 +32,7 @@ struct RoomInformation { std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game std::string host_username; ///< Forum username of the host - bool enable_citra_mods; ///< Allow Citra Moderators to moderate on this room + bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room }; struct GameInfo { @@ -148,7 +148,7 @@ public: const std::string& host_username = "", const std::string& preferred_game = "", u64 preferred_game_id = 0, std::unique_ptr verify_backend = nullptr, - const BanList& ban_list = {}, bool enable_citra_mods = false); + const BanList& ban_list = {}, bool enable_yuzu_mods = false); /** * Sets the verification GUID of the room. diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index e43004027..d6ace9b39 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -86,7 +86,7 @@ public: * @params password The password for the room * the server to assign one for us. */ - void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, + 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 = ""); @@ -159,7 +159,7 @@ void RoomMember::RoomMemberImpl::MemberLoop() { while (IsConnected()) { std::lock_guard lock(network_mutex); ENetEvent event; - if (enet_host_service(client, &event, 100) > 0) { + if (enet_host_service(client, &event, 16) > 0) { switch (event.type) { case ENET_EVENT_TYPE_RECEIVE: switch (event.packet->data[0]) { @@ -251,16 +251,17 @@ void RoomMember::RoomMemberImpl::MemberLoop() { break; } } + std::list packets; { - std::lock_guard lock(send_list_mutex); - for (const auto& packet : send_list) { - ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), - ENET_PACKET_FLAG_RELIABLE); - enet_peer_send(server, 0, enetPacket); - } - enet_host_flush(client); - send_list.clear(); + std::lock_guard send_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(); }; @@ -274,14 +275,14 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { send_list.push_back(std::move(packet)); } -void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, +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(IdJoinRequest); - packet << nickname; + packet << nickname_; packet << console_id_hash; packet << preferred_mac; packet << network_version; diff --git a/src/network/verify_user.h b/src/network/verify_user.h index 01b9877c8..5c3852d4a 100644 --- a/src/network/verify_user.h +++ b/src/network/verify_user.h @@ -13,7 +13,7 @@ struct UserData { std::string username; std::string display_name; std::string avatar_url; - bool moderator = false; ///< Whether the user is a Citra Moderator. + bool moderator = false; ///< Whether the user is a yuzu Moderator. }; /** diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a69ccb264..fbbcf673a 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -7,7 +7,7 @@ add_executable(tests common/ring_buffer.cpp common/unique_function.cpp core/core_timing.cpp - core/network/network.cpp + core/internal_network/network.cpp tests.cpp video_core/buffer_base.cpp input_common/calibration_configuration_job.cpp diff --git a/src/tests/core/internal_network/network.cpp b/src/tests/core/internal_network/network.cpp new file mode 100644 index 000000000..164b0ff24 --- /dev/null +++ b/src/tests/core/internal_network/network.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" + +TEST_CASE("Network::Errors", "[core]") { + Network::NetworkInstance network_instance; // initialize network + + Network::Socket socks[2]; + for (Network::Socket& sock : socks) { + REQUIRE(sock.Initialize(Network::Domain::INET, Network::Type::STREAM, + Network::Protocol::TCP) == Network::Errno::SUCCESS); + } + + Network::SockAddrIn addr{ + Network::Domain::INET, + {127, 0, 0, 1}, + 1, // hopefully nobody running this test has something listening on port 1 + }; + REQUIRE(socks[0].Connect(addr) == Network::Errno::CONNREFUSED); + + std::vector message{1, 2, 3, 4}; + REQUIRE(socks[1].Recv(0, message).second == Network::Errno::NOTCONN); +} diff --git a/src/tests/core/network/network.cpp b/src/tests/core/network/network.cpp deleted file mode 100644 index 1bbb8372f..000000000 --- a/src/tests/core/network/network.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include - -#include "core/network/network.h" -#include "core/network/sockets.h" - -TEST_CASE("Network::Errors", "[core]") { - Network::NetworkInstance network_instance; // initialize network - - Network::Socket socks[2]; - for (Network::Socket& sock : socks) { - REQUIRE(sock.Initialize(Network::Domain::INET, Network::Type::STREAM, - Network::Protocol::TCP) == Network::Errno::SUCCESS); - } - - Network::SockAddrIn addr{ - Network::Domain::INET, - {127, 0, 0, 1}, - 1, // hopefully nobody running this test has something listening on port 1 - }; - REQUIRE(socks[0].Connect(addr) == Network::Errno::CONNREFUSED); - - std::vector message{1, 2, 3, 4}; - REQUIRE(socks[1].Recv(0, message).second == Network::Errno::NOTCONN); -} diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index ae85a72ea..aff81f26d 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(web_service STATIC + announce_room_json.cpp + announce_room_json.h telemetry_json.cpp telemetry_json.h verify_login.cpp @@ -9,4 +11,4 @@ add_library(web_service STATIC ) create_target_directory_groups(web_service) -target_link_libraries(web_service PRIVATE common nlohmann_json::nlohmann_json httplib) +target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp new file mode 100644 index 000000000..31804d2b1 --- /dev/null +++ b/src/web_service/announce_room_json.cpp @@ -0,0 +1,157 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/detached_tasks.h" +#include "common/logging/log.h" +#include "web_service/announce_room_json.h" +#include "web_service/web_backend.h" + +namespace AnnounceMultiplayerRoom { + +void to_json(nlohmann::json& json, const Room::Member& member) { + if (!member.username.empty()) { + json["username"] = member.username; + } + json["nickname"] = member.nickname; + if (!member.avatar_url.empty()) { + json["avatarUrl"] = member.avatar_url; + } + json["gameName"] = member.game_name; + json["gameId"] = member.game_id; +} + +void from_json(const nlohmann::json& json, Room::Member& member) { + member.nickname = json.at("nickname").get(); + member.game_name = json.at("gameName").get(); + member.game_id = json.at("gameId").get(); + try { + member.username = json.at("username").get(); + member.avatar_url = json.at("avatarUrl").get(); + } catch (const nlohmann::detail::out_of_range&) { + member.username = member.avatar_url = ""; + LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname); + } +} + +void to_json(nlohmann::json& json, const Room& room) { + json["port"] = room.port; + json["name"] = room.name; + if (!room.description.empty()) { + json["description"] = room.description; + } + json["preferredGameName"] = room.preferred_game; + json["preferredGameId"] = room.preferred_game_id; + json["maxPlayers"] = room.max_player; + json["netVersion"] = room.net_version; + json["hasPassword"] = room.has_password; + if (room.members.size() > 0) { + nlohmann::json member_json = room.members; + json["players"] = member_json; + } +} + +void from_json(const nlohmann::json& json, Room& room) { + room.verify_UID = json.at("externalGuid").get(); + room.ip = json.at("address").get(); + room.name = json.at("name").get(); + try { + room.description = json.at("description").get(); + } catch (const nlohmann::detail::out_of_range&) { + room.description = ""; + LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.name); + } + room.owner = json.at("owner").get(); + room.port = json.at("port").get(); + room.preferred_game = json.at("preferredGameName").get(); + room.preferred_game_id = json.at("preferredGameId").get(); + room.max_player = json.at("maxPlayers").get(); + room.net_version = json.at("netVersion").get(); + room.has_password = json.at("hasPassword").get(); + try { + room.members = json.at("players").get>(); + } catch (const nlohmann::detail::out_of_range& e) { + LOG_DEBUG(Network, "Out of range {}", e.what()); + } +} + +} // namespace AnnounceMultiplayerRoom + +namespace WebService { + +void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const std::string& preferred_game, + const u64 preferred_game_id) { + room.name = name; + room.description = description; + room.port = port; + room.max_player = max_player; + room.net_version = net_version; + room.has_password = has_password; + room.preferred_game = preferred_game; + room.preferred_game_id = preferred_game_id; +} +void RoomJson::AddPlayer(const std::string& username_, const std::string& nickname_, + const std::string& avatar_url, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) { + AnnounceMultiplayerRoom::Room::Member member; + member.username = username_; + member.nickname = nickname_; + member.avatar_url = avatar_url; + member.mac_address = mac_address; + member.game_id = game_id; + member.game_name = game_name; + room.members.push_back(member); +} + +WebService::WebResult RoomJson::Update() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be updated"); + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Room is not registered"}; + } + nlohmann::json json{{"players", room.members}}; + return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); +} + +WebService::WebResult RoomJson::Register() { + nlohmann::json json = room; + auto result = client.PostJson("/lobby", json.dump(), false); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + auto reply_json = nlohmann::json::parse(result.returned_data); + room = reply_json.get(); + room_id = reply_json.at("id").get(); + return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_UID}; +} + +void RoomJson::ClearPlayers() { + room.members.clear(); +} + +AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { + auto reply = client.GetJson("/lobby", true).returned_data; + if (reply.empty()) { + return {}; + } + return nlohmann::json::parse(reply).at("rooms").get(); +} + +void RoomJson::Delete() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be deleted"); + return; + } + Common::DetachedTasks::AddTask( + [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() { + // create a new client here because the this->client might be destroyed. + Client{host, username, token}.DeleteJson(fmt::format("/lobby/{}", room_id), "", false); + }); +} + +} // namespace WebService diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h new file mode 100644 index 000000000..ac02af5b1 --- /dev/null +++ b/src/web_service/announce_room_json.h @@ -0,0 +1,46 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/announce_multiplayer_room.h" +#include "web_service/web_backend.h" + +namespace WebService { + +/** + * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from + * JSON, and submits/gets it to/from the yuzu web service + */ +class RoomJson : public AnnounceMultiplayerRoom::Backend { +public: + RoomJson(const std::string& host, const std::string& username, const std::string& token) + : client(host, username, token), host(host), username(username), token(token) {} + ~RoomJson() = default; + void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, + const std::string& preferred_game, + const u64 preferred_game_id) override; + void AddPlayer(const std::string& username_, const std::string& nickname_, + const std::string& avatar_url, + const AnnounceMultiplayerRoom::MacAddress& mac_address, const u64 game_id, + const std::string& game_name) override; + WebResult Update() override; + WebResult Register() override; + void ClearPlayers() override; + AnnounceMultiplayerRoom::RoomList GetRoomList() override; + void Delete() override; + +private: + AnnounceMultiplayerRoom::Room room; + Client client; + std::string host; + std::string username; + std::string token; + std::string room_id; +}; + +} // namespace WebService diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 57e0e7025..f3cad8f31 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -156,10 +156,36 @@ add_executable(yuzu main.cpp main.h main.ui + multiplayer/chat_room.cpp + multiplayer/chat_room.h + multiplayer/chat_room.ui + multiplayer/client_room.h + multiplayer/client_room.cpp + multiplayer/client_room.ui + multiplayer/direct_connect.cpp + multiplayer/direct_connect.h + multiplayer/direct_connect.ui + multiplayer/host_room.cpp + multiplayer/host_room.h + multiplayer/host_room.ui + multiplayer/lobby.cpp + multiplayer/lobby.h + multiplayer/lobby.ui + multiplayer/lobby_p.h + multiplayer/message.cpp + multiplayer/message.h + multiplayer/moderation_dialog.cpp + multiplayer/moderation_dialog.h + multiplayer/moderation_dialog.ui + multiplayer/state.cpp + multiplayer/state.h + multiplayer/validation.h startup_checks.cpp startup_checks.h uisettings.cpp uisettings.h + util/clickable_label.cpp + util/clickable_label.h util/controller_navigation.cpp util/controller_navigation.h util/limitable_input_dialog.cpp @@ -256,7 +282,7 @@ endif() create_target_directory_groups(yuzu) -target_link_libraries(yuzu PRIVATE common core input_common video_core) +target_link_libraries(yuzu PRIVATE common core input_common network video_core) target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index c841843f0..7c11e2c03 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -11,6 +11,7 @@ #include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/hid/controllers/npad.h" #include "input_common/main.h" +#include "network/network.h" #include "yuzu/configuration/config.h" namespace FS = Common::FS; @@ -584,6 +585,48 @@ void Config::ReadMiscellaneousValues() { qt_config->endGroup(); } +void Config::ReadMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + UISettings::values.nickname = ReadSetting(QStringLiteral("nickname"), QString{}).toString(); + UISettings::values.ip = ReadSetting(QStringLiteral("ip"), QString{}).toString(); + UISettings::values.port = + ReadSetting(QStringLiteral("port"), Network::DefaultRoomPort).toString(); + UISettings::values.room_nickname = + ReadSetting(QStringLiteral("room_nickname"), QString{}).toString(); + UISettings::values.room_name = ReadSetting(QStringLiteral("room_name"), QString{}).toString(); + UISettings::values.room_port = + ReadSetting(QStringLiteral("room_port"), QStringLiteral("24872")).toString(); + bool ok; + UISettings::values.host_type = ReadSetting(QStringLiteral("host_type"), 0).toUInt(&ok); + if (!ok) { + UISettings::values.host_type = 0; + } + UISettings::values.max_player = ReadSetting(QStringLiteral("max_player"), 8).toUInt(); + UISettings::values.game_id = ReadSetting(QStringLiteral("game_id"), 0).toULongLong(); + UISettings::values.room_description = + ReadSetting(QStringLiteral("room_description"), QString{}).toString(); + // Read ban list back + int size = qt_config->beginReadArray(QStringLiteral("username_ban_list")); + UISettings::values.ban_list.first.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.first[i] = + ReadSetting(QStringLiteral("username")).toString().toStdString(); + } + qt_config->endArray(); + size = qt_config->beginReadArray(QStringLiteral("ip_ban_list")); + UISettings::values.ban_list.second.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.ban_list.second[i] = + ReadSetting(QStringLiteral("ip")).toString().toStdString(); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::ReadPathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -794,6 +837,7 @@ void Config::ReadUIValues() { ReadPathValues(); ReadScreenshotValues(); ReadShortcutValues(); + ReadMultiplayerValues(); ReadBasicSetting(UISettings::values.single_window_mode); ReadBasicSetting(UISettings::values.fullscreen); @@ -1161,6 +1205,40 @@ void Config::SaveMiscellaneousValues() { qt_config->endGroup(); } +void Config::SaveMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + WriteSetting(QStringLiteral("nickname"), UISettings::values.nickname, QString{}); + WriteSetting(QStringLiteral("ip"), UISettings::values.ip, QString{}); + WriteSetting(QStringLiteral("port"), UISettings::values.port, Network::DefaultRoomPort); + WriteSetting(QStringLiteral("room_nickname"), UISettings::values.room_nickname, QString{}); + WriteSetting(QStringLiteral("room_name"), UISettings::values.room_name, QString{}); + WriteSetting(QStringLiteral("room_port"), UISettings::values.room_port, + QStringLiteral("24872")); + WriteSetting(QStringLiteral("host_type"), UISettings::values.host_type, 0); + WriteSetting(QStringLiteral("max_player"), UISettings::values.max_player, 8); + WriteSetting(QStringLiteral("game_id"), UISettings::values.game_id, 0); + WriteSetting(QStringLiteral("room_description"), UISettings::values.room_description, + QString{}); + // Write ban list + qt_config->beginWriteArray(QStringLiteral("username_ban_list")); + for (std::size_t i = 0; i < UISettings::values.ban_list.first.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("username"), + QString::fromStdString(UISettings::values.ban_list.first[i])); + } + qt_config->endArray(); + qt_config->beginWriteArray(QStringLiteral("ip_ban_list")); + for (std::size_t i = 0; i < UISettings::values.ban_list.second.size(); ++i) { + qt_config->setArrayIndex(static_cast(i)); + WriteSetting(QStringLiteral("ip"), + QString::fromStdString(UISettings::values.ban_list.second[i])); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::SavePathValues() { qt_config->beginGroup(QStringLiteral("Paths")); @@ -1347,6 +1425,7 @@ void Config::SaveUIValues() { SavePathValues(); SaveScreenshotValues(); SaveShortcutValues(); + SaveMultiplayerValues(); WriteBasicSetting(UISettings::values.single_window_mode); WriteBasicSetting(UISettings::values.fullscreen); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index a71eabe8e..937b2d95b 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -89,6 +89,7 @@ private: void ReadUIGamelistValues(); void ReadUILayoutValues(); void ReadWebServiceValues(); + void ReadMultiplayerValues(); void SaveValues(); void SavePlayerValue(std::size_t player_index); @@ -118,6 +119,7 @@ private: void SaveUIGamelistValues(); void SaveUILayoutValues(); void SaveWebServiceValues(); + void SaveMultiplayerValues(); /** * Reads a setting from the qt_config. diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index e99657bd6..92ef4467b 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -29,9 +29,10 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, InputCommon::InputSubsystem* input_subsystem, - Core::System& system_) - : QDialog(parent), ui{std::make_unique()}, registry{registry_}, - system{system_}, audio_tab{std::make_unique(system_, this)}, + Core::System& system_, bool enable_web_config) + : QDialog(parent), ui{std::make_unique()}, + registry(registry_), system{system_}, audio_tab{std::make_unique(system_, + this)}, cpu_tab{std::make_unique(system_, this)}, debug_tab_tab{std::make_unique(system_, this)}, filesystem_tab{std::make_unique(this)}, @@ -64,6 +65,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui->tabWidget->addTab(ui_tab.get(), tr("Game List")); ui->tabWidget->addTab(web_tab.get(), tr("Web")); + web_tab->SetWebServiceConfigEnabled(enable_web_config); hotkeys_tab->Populate(registry); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h index 12cf25daf..cec1610ad 100644 --- a/src/yuzu/configuration/configure_dialog.h +++ b/src/yuzu/configuration/configure_dialog.h @@ -41,7 +41,8 @@ class ConfigureDialog : public QDialog { public: explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, - InputCommon::InputSubsystem* input_subsystem, Core::System& system_); + InputCommon::InputSubsystem* input_subsystem, Core::System& system_, + bool enable_web_config = true); ~ConfigureDialog() override; void ApplyConfiguration(); diff --git a/src/yuzu/configuration/configure_network.cpp b/src/yuzu/configuration/configure_network.cpp index 8ed08fa6a..ba1986eb1 100644 --- a/src/yuzu/configuration/configure_network.cpp +++ b/src/yuzu/configuration/configure_network.cpp @@ -4,7 +4,7 @@ #include #include "common/settings.h" #include "core/core.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network_interface.h" #include "ui_configure_network.h" #include "yuzu/configuration/configure_network.h" diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp index d779251b4..ff4bf44f4 100644 --- a/src/yuzu/configuration/configure_web.cpp +++ b/src/yuzu/configuration/configure_web.cpp @@ -169,3 +169,8 @@ void ConfigureWeb::OnLoginVerified() { "correctly, and that your internet connection is working.")); } } + +void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) { + ui->label_disable_info->setVisible(!enabled); + ui->groupBoxWebConfig->setEnabled(enabled); +} diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h index 9054711ea..041b51149 100644 --- a/src/yuzu/configuration/configure_web.h +++ b/src/yuzu/configuration/configure_web.h @@ -20,6 +20,7 @@ public: ~ConfigureWeb() override; void ApplyConfiguration(); + void SetWebServiceConfigEnabled(bool enabled); private: void changeEvent(QEvent* event) override; diff --git a/src/yuzu/configuration/configure_web.ui b/src/yuzu/configuration/configure_web.ui index 35b4274b0..3ac3864be 100644 --- a/src/yuzu/configuration/configure_web.ui +++ b/src/yuzu/configuration/configure_web.ui @@ -112,6 +112,16 @@ + + + + Web Service configuration can only be changed when a public room isn't being hosted. + + + true + + + diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 05d309827..5bcf582bf 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -499,6 +499,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { } item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); + + emit PopulatingCompleted(); } void GameList::PopupContextMenu(const QPoint& menu_location) { @@ -752,6 +754,10 @@ void GameList::LoadCompatibilityList() { } } +QStandardItemModel* GameList::GetModel() const { + return item_model; +} + void GameList::PopulateAsync(QVector& game_dirs) { tree_view->setEnabled(false); diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index bc36d015a..9605985cc 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -16,9 +16,14 @@ #include #include "common/common_types.h" +#include "core/core.h" #include "uisettings.h" #include "yuzu/compatibility_list.h" +namespace Core { +class System; +} + class ControllerNavigation; class GameListWorker; class GameListSearchField; @@ -84,6 +89,8 @@ public: void SaveInterfaceLayout(); void LoadInterfaceLayout(); + QStandardItemModel* GetModel() const; + /// Disables events from the emulated controller void UnloadController(); @@ -108,6 +115,7 @@ signals: void OpenDirectory(const QString& directory); void AddDirectory(); void ShowList(bool show); + void PopulatingCompleted(); private slots: void OnItemExpanded(const QModelIndex& item); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 2814548eb..5d8132673 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -32,6 +32,7 @@ #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" +#include "yuzu/multiplayer/state.h" #include "yuzu/util/controller_navigation.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows @@ -132,6 +133,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/main.h" #include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" using namespace Common::Literals; @@ -271,6 +273,8 @@ GMainWindow::GMainWindow(bool has_broken_vulkan) SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + Network::Init(); + RegisterMetaTypes(); InitializeWidgets(); @@ -459,6 +463,7 @@ GMainWindow::~GMainWindow() { if (render_window->parent() == nullptr) { delete render_window; } + Network::Shutdown(); } void GMainWindow::RegisterMetaTypes() { @@ -822,6 +827,10 @@ void GMainWindow::InitializeWidgets() { } }); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, + ui->action_Show_Room); + multiplayer_state->setVisible(false); + // Create status bar message_label = new QLabel(); // Configured separately for left alignment @@ -854,6 +863,9 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(label); } + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + tas_label = new QLabel(); tas_label->setObjectName(QStringLiteral("TASlabel")); tas_label->setFocusPolicy(Qt::NoFocus); @@ -1163,6 +1175,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); @@ -1180,6 +1194,9 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); } void GMainWindow::ConnectMenuEvents() { @@ -1223,6 +1240,18 @@ void GMainWindow::ConnectMenuEvents() { ui->action_Reset_Window_Size_900, ui->action_Reset_Window_Size_1080}); + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + // Tools connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this, ReinitializeKeyBehavior::Warning)); @@ -2783,7 +2812,8 @@ void GMainWindow::OnConfigure() { const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); Settings::SetConfiguringGlobal(true); - ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system); + ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system, + !multiplayer_state->IsHostingPublicRoom()); connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, &GMainWindow::OnLanguageChanged); @@ -2840,6 +2870,11 @@ void GMainWindow::OnConfigure() { if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); } + + if (!multiplayer_state->IsHostingPublicRoom()) { + multiplayer_state->UpdateCredentials(); + } + emit UpdateThemedIcons(); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); @@ -3660,6 +3695,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) { } render_window->close(); + multiplayer_state->Close(); QWidget::closeEvent(event); } @@ -3856,6 +3892,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { UISettings::values.language = locale; LoadTranslation(); ui->retranslateUi(this); + multiplayer_state->retranslateUi(); UpdateWindowTitle(); } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 27204f5a2..8d5c1398f 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -11,6 +11,7 @@ #include #include +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "yuzu/compatibility_list.h" #include "yuzu/hotkeys.h" @@ -22,6 +23,7 @@ #endif class Config; +class ClickableLabel; class EmuThread; class GameList; class GImageInfo; @@ -31,6 +33,7 @@ class MicroProfileDialog; class ProfilerWidget; class ControllerDialog; class QLabel; +class MultiplayerState; class QPushButton; class QProgressDialog; class WaitTreeWidget; @@ -200,6 +203,8 @@ private: void ConnectMenuEvents(); void UpdateMenuState(); + MultiplayerState* multiplayer_state = nullptr; + void PreventOSSleep(); void AllowOSSleep(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6ab95b9a5..60a8deab1 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -120,6 +120,20 @@ + + + true + + + Multiplayer + + + + + + + + &Tools @@ -154,6 +168,7 @@ + @@ -245,6 +260,43 @@ Show Status Bar + + + true + + + Browse Public Game Lobby + + + + + true + + + Create Room + + + + + false + + + Leave Room + + + + + Direct Connect to Room + + + + + false + + + Show Current Room + + true diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp new file mode 100644 index 000000000..6dd4bc36d --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -0,0 +1,490 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_chat_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/message.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +class ChatMessage { +public: + explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + nickname = QString::fromStdString(chat.nickname); + username = QString::fromStdString(chat.username); + message = QString::fromStdString(chat.message); + + // Check for user pings + QString cur_nickname, cur_username; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = QString::fromStdString(room->GetNickname()); + cur_username = QString::fromStdString(room->GetUsername()); + } + + // Handle pings at the beginning and end of message + QString fixed_message = QStringLiteral(" %1 ").arg(message); + if (fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_nickname)) || + (!cur_username.isEmpty() && + fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_username)))) { + + contains_ping = true; + } else { + contains_ping = false; + } + } + + bool ContainsPing() const { + return contains_ping; + } + + /// Format the message using the players color + QString GetPlayerChatMessage(u16 player) const { + auto color = player_color[player % 16]; + QString name; + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + + QString style, text_color; + if (ContainsPing()) { + // Add a background color to these messages + style = QStringLiteral("background-color: %1").arg(QString::fromStdString(ping_color)); + // Add a font color + text_color = QStringLiteral("color='#000000'"); + } + + return QStringLiteral("[%1] <%3> %6") + .arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color, + message.toHtmlEscaped()); + } + +private: + static constexpr std::array player_color = { + {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", + "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + static constexpr char ping_color[] = "#FFFF00"; + + QString timestamp; + QString nickname; + QString username; + QString message; + bool contains_ping; +}; + +class StatusMessage { +public: + explicit StatusMessage(const QString& msg, QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + message = msg; + } + + QString GetSystemChatMessage() const { + return QStringLiteral("[%1] * %3") + .arg(timestamp, QString::fromStdString(system_color), message); + } + +private: + static constexpr const char system_color[] = "#FF8C00"; + QString timestamp; + QString message; +}; + +class PlayerListItem : public QStandardItem { +public: + static const int NicknameRole = Qt::UserRole + 1; + static const int UsernameRole = Qt::UserRole + 2; + static const int AvatarUrlRole = Qt::UserRole + 3; + static const int GameNameRole = Qt::UserRole + 4; + + PlayerListItem() = default; + explicit PlayerListItem(const std::string& nickname, const std::string& username, + const std::string& avatar_url, const std::string& game_name) { + setEditable(false); + setData(QString::fromStdString(nickname), NicknameRole); + setData(QString::fromStdString(username), UsernameRole); + setData(QString::fromStdString(avatar_url), AvatarUrlRole); + if (game_name.empty()) { + setData(QObject::tr("Not playing a game"), GameNameRole); + } else { + setData(QString::fromStdString(game_name), GameNameRole); + } + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return QStandardItem::data(role); + } + QString name; + const QString nickname = data(NicknameRole).toString(); + const QString username = data(UsernameRole).toString(); + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + return QStringLiteral("%1\n %2").arg(name, data(GameNameRole).toString()); + } +}; + +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { + ui->setupUi(this); + + // set the item_model for player_view + + player_list = new QStandardItemModel(ui->player_view); + ui->player_view->setModel(player_list); + ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); + // set a header to make it look better though there is only one column + player_list->insertColumns(0, 1); + player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); + + ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); + + // register the network structs to use in slots and signals + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + // setup the callbacks for network updates + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnChatMessageRecieved( + [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); + member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); + } else { + // TODO (jroweboy) network was not initialized? + } + + // Connect all the widgets to the appropriate events + connect(ui->player_view, &QTreeView::customContextMenuRequested, this, + &ChatRoom::PopupContextMenu); + connect(ui->chat_message, &QLineEdit::returnPressed, this, &ChatRoom::OnSendChat); + connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged); + connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat); +} + +ChatRoom::~ChatRoom() = default; + +void ChatRoom::SetModPerms(bool is_mod) { + has_mod_perms = is_mod; +} + +void ChatRoom::RetranslateUi() { + ui->retranslateUi(this); +} + +void ChatRoom::Clear() { + ui->chat_history->clear(); + block_list.clear(); +} + +void ChatRoom::AppendStatusMessage(const QString& msg) { + ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage()); +} + +void ChatRoom::AppendChatMessage(const QString& msg) { + ui->chat_history->append(msg); +} + +void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { + if (auto room = Network::GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&nickname](const Network::RoomMember::MemberInformation& member) { + return member.nickname == nickname; + }); + if (it == members.end()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + return; + } + room->SendModerationRequest(type, nickname); + } +} + +bool ChatRoom::ValidateMessage(const std::string& msg) { + return !msg.empty(); +} + +void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) { + // TODO(B3N30): change title + if (auto room_member = Network::GetRoomMember().lock()) { + SetPlayerList(room_member->GetMemberInformation()); + } +} + +void ChatRoom::Disable() { + ui->send_message->setDisabled(true); + ui->chat_message->setDisabled(true); +} + +void ChatRoom::Enable() { + ui->send_message->setEnabled(true); + ui->chat_message->setEnabled(true); +} + +void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { + if (!ValidateMessage(chat.message)) { + return; + } + if (auto room = Network::GetRoomMember().lock()) { + // get the id of the player + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); + return; + } + if (block_list.count(chat.nickname)) { + LOG_INFO(Network, "Chat message received from blocked player {}. Ignoring it.", + chat.nickname); + return; + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + if (m.ContainsPing()) { + emit UserPinged(); + } + AppendChatMessage(m.GetPlayerChatMessage(player)); + } +} + +void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) { + QString name; + if (status_message.username.empty() || status_message.username == status_message.nickname) { + name = QString::fromStdString(status_message.nickname); + } else { + name = QStringLiteral("%1 (%2)").arg(QString::fromStdString(status_message.nickname), + QString::fromStdString(status_message.username)); + } + QString message; + switch (status_message.type) { + case Network::IdMemberJoin: + message = tr("%1 has joined").arg(name); + break; + case Network::IdMemberLeave: + message = tr("%1 has left").arg(name); + break; + case Network::IdMemberKicked: + message = tr("%1 has been kicked").arg(name); + break; + case Network::IdMemberBanned: + message = tr("%1 has been banned").arg(name); + break; + case Network::IdAddressUnbanned: + message = tr("%1 has been unbanned").arg(name); + break; + } + if (!message.isEmpty()) + AppendStatusMessage(message); +} + +void ChatRoom::OnSendChat() { + if (auto room = Network::GetRoomMember().lock()) { + if (room->GetState() != Network::RoomMember::State::Joined && + room->GetState() != Network::RoomMember::State::Moderator) { + + return; + } + auto message = ui->chat_message->text().toStdString(); + if (!ValidateMessage(message)) { + return; + } + auto nick = room->GetNickname(); + auto username = room->GetUsername(); + Network::ChatEntry chat{nick, username, message}; + + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Cannot find self in the player list when sending a message."); + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + room->SendChatMessage(message); + AppendChatMessage(m.GetPlayerChatMessage(player)); + ui->chat_message->clear(); + } +} + +void ChatRoom::UpdateIconDisplay() { + for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) { + QStandardItem* item = player_list->invisibleRootItem()->child(row); + const std::string avatar_url = + item->data(PlayerListItem::AvatarUrlRole).toString().toStdString(); + if (icon_cache.count(avatar_url)) { + item->setData(icon_cache.at(avatar_url), Qt::DecorationRole); + } else { + item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48), + Qt::DecorationRole); + } + } +} + +void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) { + // TODO(B3N30): Remember which row is selected + player_list->removeRows(0, player_list->rowCount()); + for (const auto& member : member_list) { + if (member.nickname.empty()) + continue; + QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, + member.avatar_url, member.game_info.name); + +#ifdef ENABLE_WEB_SERVICE + if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) { + // Start a request to get the member's avatar + const QUrl url(QString::fromStdString(member.avatar_url)); + QFuture future = QtConcurrent::run([url] { + WebService::Client client( + QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", ""); + auto result = client.GetImage(url.path().toStdString(), true); + if (result.returned_data.empty()) { + LOG_ERROR(WebService, "Failed to get avatar"); + } + return result.returned_data; + }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, + [this, future_watcher, avatar_url = member.avatar_url] { + const std::string result = future_watcher->result(); + if (result.empty()) + return; + QPixmap pixmap; + if (!pixmap.loadFromData(reinterpret_cast(result.data()), + result.size())) + return; + icon_cache[avatar_url] = + pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Update all the displayed icons with the new icon_cache + UpdateIconDisplay(); + }); + future_watcher->setFuture(future); + } +#endif + + player_list->invisibleRootItem()->appendRow(name_item); + } + UpdateIconDisplay(); + // TODO(B3N30): Restore row selection +} + +void ChatRoom::OnChatTextChanged() { + if (ui->chat_message->text().length() > static_cast(Network::MaxMessageSize)) + ui->chat_message->setText( + ui->chat_message->text().left(static_cast(Network::MaxMessageSize))); +} + +void ChatRoom::PopupContextMenu(const QPoint& menu_location) { + QModelIndex item = ui->player_view->indexAt(menu_location); + if (!item.isValid()) + return; + + std::string nickname = + player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); + + QMenu context_menu; + + QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString(); + if (!username.isEmpty()) { + QAction* view_profile_action = context_menu.addAction(tr("View Profile")); + connect(view_profile_action, &QAction::triggered, [username] { + QDesktopServices::openUrl( + QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username))); + }); + } + + std::string cur_nickname; + if (auto room = Network::GetRoomMember().lock()) { + cur_nickname = room->GetNickname(); + } + + if (nickname != cur_nickname) { // You can't block yourself + QAction* block_action = context_menu.addAction(tr("Block Player")); + + block_action->setCheckable(true); + block_action->setChecked(block_list.count(nickname) > 0); + + connect(block_action, &QAction::triggered, [this, nickname] { + if (block_list.count(nickname)) { + block_list.erase(nickname); + } else { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Block Player"), + tr("When you block a player, you will no longer receive chat messages from " + "them.

Are you sure you would like to block %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + block_list.emplace(nickname); + } + }); + } + + if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself + context_menu.addSeparator(); + + QAction* kick_action = context_menu.addAction(tr("Kick")); + QAction* ban_action = context_menu.addAction(tr("Ban")); + + connect(kick_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = + QMessageBox::question(this, tr("Kick Player"), + tr("Are you sure you would like to kick %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModKick, nickname); + }); + connect(ban_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Ban Player"), + tr("Are you sure you would like to kick and ban %1?\n\nThis would " + "ban both their forum username and their IP address.") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModBan, nickname); + }); + } + + context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); +} diff --git a/src/yuzu/multiplayer/chat_room.h b/src/yuzu/multiplayer/chat_room.h new file mode 100644 index 000000000..a810377f7 --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.h @@ -0,0 +1,74 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "network/network.h" + +namespace Ui { +class ChatRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +class ChatRoom : public QWidget { + Q_OBJECT + +public: + explicit ChatRoom(QWidget* parent); + void RetranslateUi(); + void SetPlayerList(const Network::RoomMember::MemberList& member_list); + void Clear(); + void AppendStatusMessage(const QString& msg); + ~ChatRoom(); + + void SetModPerms(bool is_mod); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation& info); + void OnChatReceive(const Network::ChatEntry&); + void OnStatusMessageReceive(const Network::StatusMessageEntry&); + void OnSendChat(); + void OnChatTextChanged(); + void PopupContextMenu(const QPoint& menu_location); + void Disable(); + void Enable(); + +signals: + void ChatReceived(const Network::ChatEntry&); + void StatusMessageReceived(const Network::StatusMessageEntry&); + void UserPinged(); + +private: + static constexpr u32 max_chat_lines = 1000; + void AppendChatMessage(const QString&); + bool ValidateMessage(const std::string&); + void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); + + bool has_mod_perms = false; + QStandardItemModel* player_list; + std::unique_ptr ui; + std::unordered_set block_list; + std::unordered_map icon_cache; +}; + +Q_DECLARE_METATYPE(Network::ChatEntry); +Q_DECLARE_METATYPE(Network::StatusMessageEntry); +Q_DECLARE_METATYPE(Network::RoomInformation); +Q_DECLARE_METATYPE(Network::RoomMember::State); +Q_DECLARE_METATYPE(Network::RoomMember::Error); diff --git a/src/yuzu/multiplayer/chat_room.ui b/src/yuzu/multiplayer/chat_room.ui new file mode 100644 index 000000000..f2b31b5da --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.ui @@ -0,0 +1,59 @@ + + + ChatRoom + + + + 0 + 0 + 807 + 432 + + + + Room Window + + + + + + + + + + + false + + + true + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Send Chat Message + + + + + + + Send Message + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp new file mode 100644 index 000000000..7b2e16e06 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.cpp @@ -0,0 +1,115 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_client_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/moderation_dialog.h" +#include "yuzu/multiplayer/state.h" + +ClientRoomWindow::ClientRoomWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()) { + ui->setupUi(this); + + // setup the callbacks for network updates + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnRoomInformationChanged( + [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); }); + member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit StateChanged(state); }); + + connect(this, &ClientRoomWindow::RoomInformationChanged, this, + &ClientRoomWindow::OnRoomUpdate); + connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); + // Update the state + OnStateChange(member->GetState()); + } else { + // TODO (jroweboy) network was not initialized? + } + + connect(ui->disconnect, &QPushButton::clicked, this, &ClientRoomWindow::Disconnect); + ui->disconnect->setDefault(false); + ui->disconnect->setAutoDefault(false); + connect(ui->moderation, &QPushButton::clicked, [this] { + ModerationDialog dialog(this); + dialog.exec(); + }); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); + connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); + UpdateView(); +} + +ClientRoomWindow::~ClientRoomWindow() = default; + +void ClientRoomWindow::SetModPerms(bool is_mod) { + ui->chat->SetModPerms(is_mod); + ui->moderation->setVisible(is_mod); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); +} + +void ClientRoomWindow::RetranslateUi() { + ui->retranslateUi(this); + ui->chat->RetranslateUi(); +} + +void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { + UpdateView(); +} + +void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + ui->chat->Clear(); + ui->chat->AppendStatusMessage(tr("Connected")); + SetModPerms(state == Network::RoomMember::State::Moderator); + } + UpdateView(); +} + +void ClientRoomWindow::Disconnect() { + auto parent = static_cast(parentWidget()); + if (parent->OnCloseRoom()) { + ui->chat->AppendStatusMessage(tr("Disconnected")); + close(); + } +} + +void ClientRoomWindow::UpdateView() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + ui->chat->Enable(); + ui->disconnect->setEnabled(true); + auto memberlist = member->GetMemberInformation(); + ui->chat->SetPlayerList(memberlist); + const auto information = member->GetRoomInformation(); + setWindowTitle(QString(tr("%1 (%2/%3 members) - connected")) + .arg(QString::fromStdString(information.name)) + .arg(memberlist.size()) + .arg(information.member_slots)); + ui->description->setText(QString::fromStdString(information.description)); + return; + } + } + // TODO(B3N30): can't get RoomMember*, show error and close window + close(); +} + +void ClientRoomWindow::UpdateIconDisplay() { + ui->chat->UpdateIconDisplay(); +} diff --git a/src/yuzu/multiplayer/client_room.h b/src/yuzu/multiplayer/client_room.h new file mode 100644 index 000000000..607b4073d --- /dev/null +++ b/src/yuzu/multiplayer/client_room.h @@ -0,0 +1,39 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "yuzu/multiplayer/chat_room.h" + +namespace Ui { +class ClientRoom; +} + +class ClientRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit ClientRoomWindow(QWidget* parent); + ~ClientRoomWindow(); + + void RetranslateUi(); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation&); + void OnStateChange(const Network::RoomMember::State&); + +signals: + void RoomInformationChanged(const Network::RoomInformation&); + void StateChanged(const Network::RoomMember::State&); + void ShowNotification(); + +private: + void Disconnect(); + void UpdateView(); + void SetModPerms(bool is_mod); + + QStandardItemModel* player_list; + std::unique_ptr ui; +}; diff --git a/src/yuzu/multiplayer/client_room.ui b/src/yuzu/multiplayer/client_room.ui new file mode 100644 index 000000000..97e88b502 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.ui @@ -0,0 +1,80 @@ + + + ClientRoom + + + + 0 + 0 + 807 + 432 + + + + Room Window + + + + + + + + 0 + + + + + Room Description + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Moderation... + + + false + + + + + + + Leave Room + + + + + + + + + + + + + + + ChatRoom + QWidget +
multiplayer/chat_room.h
+ 1 +
+
+ + +
diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp new file mode 100644 index 000000000..27741d657 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -0,0 +1,129 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "common/settings.h" +#include "network/network.h" +#include "ui_direct_connect.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" + +enum class ConnectionType : u8 { TraversalServer, IP }; + +DirectConnectWindow::DirectConnectWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()) { + + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->ip->setValidator(validation.GetIP()); + ui->ip->setText(UISettings::values.ip); + ui->port->setValidator(validation.GetPort()); + ui->port->setText(UISettings::values.port); + + // TODO(jroweboy): Show or hide the connection options based on the current value of the combo + // box. Add this back in when the traversal server support is added. + connect(ui->connect, &QPushButton::clicked, this, &DirectConnectWindow::Connect); +} + +DirectConnectWindow::~DirectConnectWindow() = default; + +void DirectConnectWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +void DirectConnectWindow::Connect() { + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (const auto member = Network::GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + switch (static_cast(ui->connection_type->currentIndex())) { + case ConnectionType::TraversalServer: + break; + case ConnectionType::IP: + if (!ui->ip->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::IP_ADDRESS_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + break; + } + + // Store settings + UISettings::values.nickname = ui->nickname->text(); + UISettings::values.ip = ui->ip->text(); + UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty()) + ? ui->port->text() + : UISettings::values.port; + + // attempt to connect in a different thread + QFuture f = QtConcurrent::run([&] { + if (auto room_member = Network::GetRoomMember().lock()) { + auto port = UISettings::values.port.toUInt(); + room_member->Join(ui->nickname->text().toStdString(), "", + ui->ip->text().toStdString().c_str(), port, 0, + Network::NoPreferredMac, ui->password->text().toStdString().c_str()); + } + }); + watcher->setFuture(f); + // and disable widgets and display a connecting while we wait + BeginConnecting(); +} + +void DirectConnectWindow::BeginConnecting() { + ui->connect->setEnabled(false); + ui->connect->setText(tr("Connecting")); +} + +void DirectConnectWindow::EndConnecting() { + ui->connect->setEnabled(true); + ui->connect->setText(tr("Connect")); +} + +void DirectConnectWindow::OnConnection() { + EndConnecting(); + + if (auto room_member = Network::GetRoomMember().lock()) { + if (room_member->GetState() == Network::RoomMember::State::Joined || + room_member->GetState() == Network::RoomMember::State::Moderator) { + + close(); + } + } +} diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h new file mode 100644 index 000000000..e38961ed0 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.h @@ -0,0 +1,43 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class DirectConnect; +} + +class DirectConnectWindow : public QDialog { + Q_OBJECT + +public: + explicit DirectConnectWindow(QWidget* parent = nullptr); + ~DirectConnectWindow(); + + void RetranslateUi(); + +signals: + /** + * Signalled by this widget when it is closing itself and destroying any state such as + * connections that it might have. + */ + void Closed(); + +private slots: + void OnConnection(); + +private: + void Connect(); + void BeginConnecting(); + void EndConnecting(); + + QFutureWatcher* watcher; + std::unique_ptr ui; + Validation validation; +}; diff --git a/src/yuzu/multiplayer/direct_connect.ui b/src/yuzu/multiplayer/direct_connect.ui new file mode 100644 index 000000000..681b6bf69 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.ui @@ -0,0 +1,168 @@ + + + DirectConnect + + + + 0 + 0 + 455 + 161 + + + + Direct Connect + + + + + + + + + + 0 + + + 0 + + + + + + IP Address + + + + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + + + IP + + + + + + + <html><head/><body><p>IPv4 address of the host</p></body></html> + + + 16 + + + + + + + Port + + + + + + + <html><head/><body><p>Port number the host is listening on</p></body></html> + + + 5 + + + 24872 + + + + + + + + + + + + + + Nickname + + + + + + + 20 + + + + + + + Password + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Connect + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp new file mode 100644 index 000000000..1b73e2bec --- /dev/null +++ b/src/yuzu/multiplayer/host_room.cpp @@ -0,0 +1,237 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/announce_multiplayer_session.h" +#include "ui_host_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif + +HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()), announce_multiplayer_session(session) { + ui->setupUi(this); + + // set up validation for all of the fields + ui->room_name->setValidator(validation.GetRoomName()); + ui->username->setValidator(validation.GetNickname()); + ui->port->setValidator(validation.GetPort()); + ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort)); + + // Create a proxy to the game list to display the list of preferred games + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new ComboBoxProxyModel; + proxy->setSourceModel(game_list); + proxy->sort(0, Qt::AscendingOrder); + ui->game_list->setModel(proxy); + + // Connect all the widgets to the appropriate events + connect(ui->host, &QPushButton::clicked, this, &HostRoomWindow::Host); + + // Restore the settings: + ui->username->setText(UISettings::values.room_nickname); + if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->room_name->setText(UISettings::values.room_name); + ui->port->setText(UISettings::values.room_port); + ui->max_player->setValue(UISettings::values.max_player); + int index = UISettings::values.host_type; + if (index < ui->host_type->count()) { + ui->host_type->setCurrentIndex(index); + } + index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole); + if (index != -1) { + ui->game_list->setCurrentIndex(index); + } + ui->room_description->setText(UISettings::values.room_description); +} + +HostRoomWindow::~HostRoomWindow() = default; + +void HostRoomWindow::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } +} + +void HostRoomWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +std::unique_ptr HostRoomWindow::CreateVerifyBackend( + bool use_validation) const { + std::unique_ptr verify_backend; + if (use_validation) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = std::make_unique(Settings::values.web_api_url); +#else + verify_backend = std::make_unique(); +#endif + } else { + verify_backend = std::make_unique(); + } + return verify_backend; +} + +void HostRoomWindow::Host() { + if (!ui->username->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (!ui->room_name->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOMNAME_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + if (ui->game_list->currentIndex() == -1) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED); + return; + } + if (auto member = Network::GetRoomMember().lock()) { + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + auto parent = static_cast(parentWidget()); + if (!parent->OnCloseRoom()) { + close(); + return; + } + } + ui->host->setDisabled(true); + + auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString(); + auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; + auto password = ui->password->text().toStdString(); + const bool is_public = ui->host_type->currentIndex() == 0; + Network::Room::BanList ban_list{}; + if (ui->load_ban_list->isChecked()) { + ban_list = UISettings::values.ban_list; + } + if (auto room = Network::GetRoom().lock()) { + bool created = room->Create( + ui->room_name->text().toStdString(), + ui->room_description->toPlainText().toStdString(), "", port, password, + ui->max_player->value(), Settings::values.yuzu_username.GetValue(), + game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); + if (!created) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::COULD_NOT_CREATE_ROOM); + LOG_ERROR(Network, "Could not create room!"); + ui->host->setEnabled(true); + return; + } + } + // Start the announce session if they chose Public + if (is_public) { + if (auto session = announce_multiplayer_session.lock()) { + // Register the room first to ensure verify_UID is present when we connect + WebService::WebResult result = session->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + QMessageBox::warning( + this, tr("Error"), + tr("Failed to announce the room to the public lobby. In order to host a " + "room publicly, you must have a valid yuzu account configured in " + "Emulation -> Configure -> Web. If you do not want to publish a room in " + "the public lobby, then select Unlisted instead.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); + ui->host->setEnabled(true); + if (auto room = Network::GetRoom().lock()) { + room->Destroy(); + } + return; + } + session->Start(); + } else { + LOG_ERROR(Network, "Starting announce session failed"); + } + } + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (is_public) { + WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, + Settings::values.yuzu_token); + if (auto room = Network::GetRoom().lock()) { + token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; + } + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + // TODO: Check what to do with this + member->Join(ui->username->text().toStdString(), "", "127.0.0.1", port, 0, + Network::NoPreferredMac, password, token); + + // Store settings + UISettings::values.room_nickname = ui->username->text(); + UISettings::values.room_name = ui->room_name->text(); + UISettings::values.game_id = + ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + UISettings::values.max_player = ui->max_player->value(); + + UISettings::values.host_type = ui->host_type->currentIndex(); + UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) + ? ui->port->text() + : QString::number(Network::DefaultRoomPort); + UISettings::values.room_description = ui->room_description->toPlainText(); + ui->host->setEnabled(true); + close(); + } +} + +QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { + if (role != Qt::DisplayRole) { + auto val = QSortFilterProxyModel::data(idx, role); + // If its the icon, shrink it to 16x16 + if (role == Qt::DecorationRole) + val = val.value().scaled(16, 16, Qt::KeepAspectRatio); + return val; + } + std::string filename; + Common::SplitPath( + QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(), + nullptr, &filename, nullptr); + QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString(); + return title.isEmpty() ? QString::fromStdString(filename) : title; +} + +bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + auto leftData = left.data(GameListItemPath::TitleRole).toString(); + auto rightData = right.data(GameListItemPath::TitleRole).toString(); + return leftData.compare(rightData) < 0; +} diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h new file mode 100644 index 000000000..d84f93ffd --- /dev/null +++ b/src/yuzu/multiplayer/host_room.h @@ -0,0 +1,74 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "network/network.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class HostRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +namespace Network::VerifyUser { +class Backend; +}; + +class HostRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session); + ~HostRoomWindow(); + + /** + * Updates the dialog with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +private: + void Host(); + std::unique_ptr CreateVerifyBackend(bool use_validation) const; + + std::unique_ptr ui; + std::weak_ptr announce_multiplayer_session; + QStandardItemModel* game_list; + ComboBoxProxyModel* proxy; + Validation validation; +}; + +/** + * Proxy Model for the game list combo box so we can reuse the game list model while still + * displaying the fields slightly differently + */ +class ComboBoxProxyModel : public QSortFilterProxyModel { + Q_OBJECT + +public: + int columnCount(const QModelIndex& idx) const override { + return 1; + } + + QVariant data(const QModelIndex& idx, int role) const override; + + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; +}; diff --git a/src/yuzu/multiplayer/host_room.ui b/src/yuzu/multiplayer/host_room.ui new file mode 100644 index 000000000..d54cf49c6 --- /dev/null +++ b/src/yuzu/multiplayer/host_room.ui @@ -0,0 +1,207 @@ + + + HostRoom + + + + 0 + 0 + 607 + 211 + + + + Create Room + + + + + + + 0 + + + 0 + + + 0 + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Room Name + + + + + + + 50 + + + + + + + Preferred Game + + + + + + + + + + Max Players + + + + + + + 2 + + + 16 + + + 8 + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Username + + + + + + + QLineEdit::PasswordEchoOnEdit + + + (Leave blank for open game) + + + + + + + Qt::ImhDigitsOnly + + + 5 + + + + + + + Password + + + + + + + Port + + + + + + + + + + + + + + Room Description + + + + + + + + + + + + + + Load Previous Ban List + + + true + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Public + + + + + Unlisted + + + + + + + + Host Room + + + + + + + + + + diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp new file mode 100644 index 000000000..fcaa7b517 --- /dev/null +++ b/src/yuzu/multiplayer/lobby.cpp @@ -0,0 +1,360 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/logging/log.h" +#include "common/settings.h" +#include "network/network.h" +#include "ui_lobby.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/lobby_p.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +Lobby::Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique()), announce_multiplayer_session(session) { + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + + model = new QStandardItemModel(ui->room_list); + + // Create a proxy to the game list to get the list of games owned + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new LobbyFilterProxyModel(this, game_list); + proxy->setSourceModel(model); + proxy->setDynamicSortFilter(true); + proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy->setSortLocaleAware(true); + ui->room_list->setModel(proxy); + ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive); + ui->room_list->header()->stretchLastSection(); + ui->room_list->setAlternatingRowColors(true); + ui->room_list->setSelectionMode(QHeaderView::SingleSelection); + ui->room_list->setSelectionBehavior(QHeaderView::SelectRows); + ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setSortingEnabled(true); + ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers); + ui->room_list->setExpandsOnDoubleClick(false); + ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + + // UI Buttons + connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby); + connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned); + connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull); + connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch); + connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom); + connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom); + + // Actions + connect(&room_list_watcher, &QFutureWatcher::finished, this, + &Lobby::OnRefreshLobby); + + // manually start a refresh when the window is opening + // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as + // part of the constructor, but offload the refresh until after the window shown. perhaps emit a + // refreshroomlist signal from places that open the lobby + RefreshLobby(); +} + +Lobby::~Lobby() = default; + +void Lobby::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } + if (proxy) + proxy->UpdateGameList(game_list); +} + +void Lobby::RetranslateUi() { + ui->retranslateUi(this); +} + +QString Lobby::PasswordPrompt() { + bool ok; + const QString text = + QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"), + QLineEdit::Password, QString(), &ok); + return ok ? text : QString(); +} + +void Lobby::OnExpandRoom(const QModelIndex& index) { + QModelIndex member_index = proxy->index(index.row(), Column::MEMBER); + auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList(); +} + +void Lobby::OnJoinRoom(const QModelIndex& source) { + if (const auto member = Network::GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + QModelIndex index = source; + // If the user double clicks on a child row (aka the player list) then use the parent instead + if (source.parent() != QModelIndex()) { + index = source.parent(); + } + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + + // Get a password to pass if the room is password protected + QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME); + bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool(); + const std::string password = has_password ? PasswordPrompt().toStdString() : ""; + if (has_password && password.empty()) { + return; + } + + QModelIndex connection_index = proxy->index(index.row(), Column::HOST); + const std::string nickname = ui->nickname->text().toStdString(); + const std::string ip = + proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); + int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); + const std::string verify_UID = + proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); + + // attempt to connect in a different thread + QFuture f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (!Settings::values.yuzu_username.empty() && !Settings::values.yuzu_token.empty()) { + WebService::Client client(Settings::values.web_api_url, Settings::values.yuzu_username, + Settings::values.yuzu_token); + token = client.GetExternalJWT(verify_UID).returned_data; + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + if (auto room_member = Network::GetRoomMember().lock()) { + room_member->Join(nickname, "", ip.c_str(), port, 0, Network::NoPreferredMac, password, + token); + } + }); + watcher->setFuture(f); + + // TODO(jroweboy): disable widgets and display a connecting while we wait + + // Save settings + UISettings::values.nickname = ui->nickname->text(); + UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString(); + UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString(); +} + +void Lobby::ResetModel() { + model->clear(); + model->insertColumns(0, Column::TOTAL); + model->setHeaderData(Column::EXPAND, Qt::Horizontal, QString(), Qt::DisplayRole); + model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole); + model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole); + model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole); + model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole); +} + +void Lobby::RefreshLobby() { + if (auto session = announce_multiplayer_session.lock()) { + ResetModel(); + ui->refresh_list->setEnabled(false); + ui->refresh_list->setText(tr("Refreshing")); + room_list_watcher.setFuture( + QtConcurrent::run([session]() { return session->GetRoomList(); })); + } else { + // TODO(jroweboy): Display an error box about announce couldn't be started + } +} + +void Lobby::OnRefreshLobby() { + AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result(); + for (auto room : new_room_list) { + // find the icon for the game if this person owns that game. + QPixmap smdh_icon; + for (int r = 0; r < game_list->rowCount(); ++r) { + auto index = game_list->index(r, 0); + auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong(); + if (game_id != 0 && room.preferred_game_id == game_id) { + smdh_icon = game_list->data(index, Qt::DecorationRole).value(); + } + } + + QList members; + for (auto member : room.members) { + QVariant var; + var.setValue(LobbyMember{QString::fromStdString(member.username), + QString::fromStdString(member.nickname), member.game_id, + QString::fromStdString(member.game_name)}); + members.append(var); + } + + auto first_item = new LobbyItem(); + auto row = QList({ + first_item, + new LobbyItemName(room.has_password, QString::fromStdString(room.name)), + new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game), + smdh_icon), + new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip), + room.port, QString::fromStdString(room.verify_UID)), + new LobbyItemMemberList(members, room.max_player), + }); + model->appendRow(row); + // To make the rows expandable, add the member data as a child of the first column of the + // rows with people in them and have qt set them to colspan after the model is finished + // resetting + if (!room.description.empty()) { + first_item->appendRow( + new LobbyItemDescription(QString::fromStdString(room.description))); + } + if (!room.members.empty()) { + first_item->appendRow(new LobbyItemExpandedMemberList(members)); + } + } + + // Reenable the refresh button and resize the columns + ui->refresh_list->setEnabled(true); + ui->refresh_list->setText(tr("Refresh List")); + ui->room_list->header()->stretchLastSection(); + for (int i = 0; i < Column::TOTAL - 1; ++i) { + ui->room_list->resizeColumnToContents(i); + } + + // Set the member list child items to span all columns + for (int i = 0; i < proxy->rowCount(); i++) { + auto parent = model->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true); + } + } +} + +LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list) + : QSortFilterProxyModel(parent), game_list(list) {} + +void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) { + game_list = list; +} + +bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { + // Prioritize filters by fastest to compute + + // pass over any child rows (aka row that shows the players in the room) + if (sourceParent != QModelIndex()) { + return true; + } + + // filter by filled rooms + if (filter_full) { + QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent); + int player_count = + sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size(); + int max_players = + sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt(); + if (player_count >= max_players) { + return false; + } + } + + // filter by search parameters + if (!filter_search.isEmpty()) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent); + QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent); + bool preferred_game_match = sourceModel() + ->data(game_name, LobbyItemGame::GameNameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool room_name_match = sourceModel() + ->data(room_name, LobbyItemName::NameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool username_match = sourceModel() + ->data(host_name, LobbyItemHost::HostUsernameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + if (!preferred_game_match && !room_name_match && !username_match) { + return false; + } + } + + // filter by game owned + if (filter_owned) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QList owned_games; + for (int r = 0; r < game_list->rowCount(); ++r) { + owned_games.append(QModelIndex(game_list->index(r, 0))); + } + auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong(); + if (current_id == 0) { + // TODO(jroweboy): homebrew often doesn't have a game id and this hides them + return false; + } + bool owned = false; + for (const auto& game : owned_games) { + auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong(); + if (current_id == game_id) { + owned = true; + } + } + if (!owned) { + return false; + } + } + + return true; +} + +void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) { + sourceModel()->sort(column, order); +} + +void LobbyFilterProxyModel::SetFilterOwned(bool filter) { + filter_owned = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterFull(bool filter) { + filter_full = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { + filter_search = filter; + invalidate(); +} diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h new file mode 100644 index 000000000..aea4a0e4e --- /dev/null +++ b/src/yuzu/multiplayer/lobby.h @@ -0,0 +1,127 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "core/announce_multiplayer_session.h" +#include "network/network.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class Lobby; +} + +class LobbyModel; +class LobbyFilterProxyModel; + +/** + * Listing of all public games pulled from services. The lobby should be simple enough for users to + * find the game they want to play, and join it. + */ +class Lobby : public QDialog { + Q_OBJECT + +public: + explicit Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session); + ~Lobby() override; + + /** + * Updates the lobby with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +public slots: + /** + * Begin the process to pull the latest room list from web services. After the listing is + * returned from web services, `LobbyRefreshed` will be signalled + */ + void RefreshLobby(); + +private slots: + /** + * Pulls the list of rooms from network and fills out the lobby model with the results + */ + void OnRefreshLobby(); + + /** + * Handler for single clicking on a room in the list. Expands the treeitem to show player + * information for the people in the room + * + * index - The row of the proxy model that the user wants to join. + */ + void OnExpandRoom(const QModelIndex&); + + /** + * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts + * to connect. Will also prompt for a password in case one is required. + * + * index - The row of the proxy model that the user wants to join. + */ + void OnJoinRoom(const QModelIndex&); + +signals: + void StateChanged(const Network::RoomMember::State&); + +private: + /** + * Removes all entries in the Lobby before refreshing. + */ + void ResetModel(); + + /** + * Prompts for a password. Returns an empty QString if the user either did not provide a + * password or if the user closed the window. + */ + QString PasswordPrompt(); + + std::unique_ptr ui; + + QStandardItemModel* model{}; + QStandardItemModel* game_list{}; + LobbyFilterProxyModel* proxy{}; + + QFutureWatcher room_list_watcher; + std::weak_ptr announce_multiplayer_session; + QFutureWatcher* watcher; + Validation validation; +}; + +/** + * Proxy Model for filtering the lobby + */ +class LobbyFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT; + +public: + explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list); + + /** + * Updates the filter with a new game list model. + * This model should be the processed one created by the Lobby. + */ + void UpdateGameList(QStandardItemModel* list); + + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + void sort(int column, Qt::SortOrder order) override; + +public slots: + void SetFilterOwned(bool); + void SetFilterFull(bool); + void SetFilterSearch(const QString&); + +private: + QStandardItemModel* game_list; + bool filter_owned = false; + bool filter_full = false; + QString filter_search; +}; diff --git a/src/yuzu/multiplayer/lobby.ui b/src/yuzu/multiplayer/lobby.ui new file mode 100644 index 000000000..4c9901c9a --- /dev/null +++ b/src/yuzu/multiplayer/lobby.ui @@ -0,0 +1,123 @@ + + + Lobby + + + + 0 + 0 + 903 + 487 + + + + Public Room Browser + + + + + + 3 + + + + + 6 + + + + + + + Nickname + + + + + + + Nickname + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filters + + + + + + + Search + + + true + + + + + + + Games I Own + + + + + + + Hide Full Rooms + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh Lobby + + + + + + + + + + + + + + + + + + + + diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h new file mode 100644 index 000000000..749ca627f --- /dev/null +++ b/src/yuzu/multiplayer/lobby_p.h @@ -0,0 +1,239 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" + +namespace Column { +enum List { + EXPAND, + ROOM_NAME, + GAME_NAME, + HOST, + MEMBER, + TOTAL, +}; +} + +class LobbyItem : public QStandardItem { +public: + LobbyItem() = default; + explicit LobbyItem(const QString& string) : QStandardItem(string) {} + virtual ~LobbyItem() override = default; +}; + +class LobbyItemName : public LobbyItem { +public: + static const int NameRole = Qt::UserRole + 1; + static const int PasswordRole = Qt::UserRole + 2; + + LobbyItemName() = default; + explicit LobbyItemName(bool has_password, QString name) : LobbyItem() { + setData(name, NameRole); + setData(has_password, PasswordRole); + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon::fromTheme(QStringLiteral("lock")).pixmap(16) : QIcon(); + } + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(NameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0; + } +}; + +class LobbyItemDescription : public LobbyItem { +public: + static const int DescriptionRole = Qt::UserRole + 1; + + LobbyItemDescription() = default; + explicit LobbyItemDescription(QString description) { + setData(description, DescriptionRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto description = data(DescriptionRole).toString(); + description.prepend(QStringLiteral("Description: ")); + return description; + } + + bool operator<(const QStandardItem& other) const override { + return data(DescriptionRole) + .toString() + .localeAwareCompare(other.data(DescriptionRole).toString()) < 0; + } +}; + +class LobbyItemGame : public LobbyItem { +public: + static const int TitleIDRole = Qt::UserRole + 1; + static const int GameNameRole = Qt::UserRole + 2; + static const int GameIconRole = Qt::UserRole + 3; + + LobbyItemGame() = default; + explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) { + setData(static_cast(title_id), TitleIDRole); + setData(game_name, GameNameRole); + if (!smdh_icon.isNull()) { + setData(smdh_icon, GameIconRole); + } + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + auto val = data(GameIconRole); + if (val.isValid()) { + val = val.value().scaled(16, 16, Qt::KeepAspectRatio); + } + return val; + } else if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(GameNameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(GameNameRole) + .toString() + .localeAwareCompare(other.data(GameNameRole).toString()) < 0; + } +}; + +class LobbyItemHost : public LobbyItem { +public: + static const int HostUsernameRole = Qt::UserRole + 1; + static const int HostIPRole = Qt::UserRole + 2; + static const int HostPortRole = Qt::UserRole + 3; + static const int HostVerifyUIDRole = Qt::UserRole + 4; + + LobbyItemHost() = default; + explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_UID) { + setData(username, HostUsernameRole); + setData(ip, HostIPRole); + setData(port, HostPortRole); + setData(verify_UID, HostVerifyUIDRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(HostUsernameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(HostUsernameRole) + .toString() + .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0; + } +}; + +class LobbyMember { +public: + LobbyMember() = default; + LobbyMember(const LobbyMember& other) = default; + explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) + : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), + game_name(std::move(game_name)) {} + ~LobbyMember() = default; + + QString GetName() const { + if (username.isEmpty() || username == nickname) { + return nickname; + } else { + return QStringLiteral("%1 (%2)").arg(nickname, username); + } + } + u64 GetTitleId() const { + return title_id; + } + QString GetGameName() const { + return game_name; + } + +private: + QString username; + QString nickname; + u64 title_id; + QString game_name; +}; + +Q_DECLARE_METATYPE(LobbyMember); + +class LobbyItemMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + static const int MaxPlayerRole = Qt::UserRole + 2; + + LobbyItemMemberList() = default; + explicit LobbyItemMemberList(QList members, u32 max_players) { + setData(members, MemberListRole); + setData(max_players, MaxPlayerRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + return QStringLiteral("%1 / %2").arg(QString::number(members.size()), + data(MaxPlayerRole).toString()); + } + + bool operator<(const QStandardItem& other) const override { + // sort by rooms that have the most players + int left_members = data(MemberListRole).toList().size(); + int right_members = other.data(MemberListRole).toList().size(); + return left_members < right_members; + } +}; + +/** + * Member information for when a lobby is expanded in the UI + */ +class LobbyItemExpandedMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + + LobbyItemExpandedMemberList() = default; + explicit LobbyItemExpandedMemberList(QList members) { + setData(members, MemberListRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + QString out; + bool first = true; + for (const auto& member : members) { + if (!first) + out.append(QStringLiteral("\n")); + const auto& m = member.value(); + if (m.GetGameName().isEmpty()) { + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName()); + } else { + out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); + } + first = false; + } + return out; + } +}; diff --git a/src/yuzu/multiplayer/message.cpp b/src/yuzu/multiplayer/message.cpp new file mode 100644 index 000000000..458f1e7d1 --- /dev/null +++ b/src/yuzu/multiplayer/message.cpp @@ -0,0 +1,79 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "yuzu/multiplayer/message.h" + +namespace NetworkMessage { +const ConnectionError ErrorManager::USERNAME_NOT_VALID( + QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::ROOMNAME_NOT_VALID( + QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::USERNAME_NOT_VALID_SERVER( + QT_TR_NOOP("Username is already in use or not valid. Please choose another.")); +const ConnectionError ErrorManager::IP_ADDRESS_NOT_VALID( + QT_TR_NOOP("IP is not a valid IPv4 address.")); +const ConnectionError ErrorManager::PORT_NOT_VALID( + QT_TR_NOOP("Port must be a number between 0 to 65535.")); +const ConnectionError ErrorManager::GAME_NOT_SELECTED(QT_TR_NOOP( + "You must choose a Preferred Game to host a room. If you do not have any games in your game " + "list yet, add a game folder by clicking on the plus icon in the game list.")); +const ConnectionError ErrorManager::NO_INTERNET( + QT_TR_NOOP("Unable to find an internet connection. Check your internet settings.")); +const ConnectionError ErrorManager::UNABLE_TO_CONNECT( + QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If " + "you still cannot connect, contact the room host and verify that the host is " + "properly configured with the external port forwarded.")); +const ConnectionError ErrorManager::ROOM_IS_FULL( + QT_TR_NOOP("Unable to connect to the room because it is already full.")); +const ConnectionError ErrorManager::COULD_NOT_CREATE_ROOM( + QT_TR_NOOP("Creating a room failed. Please retry. Restarting yuzu might be necessary.")); +const ConnectionError ErrorManager::HOST_BANNED( + QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you " + "or try a different room.")); +const ConnectionError ErrorManager::WRONG_VERSION( + QT_TR_NOOP("Version mismatch! Please update to the latest version of yuzu. If the problem " + "persists, contact the room host and ask them to update the server.")); +const ConnectionError ErrorManager::WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); +const ConnectionError ErrorManager::GENERIC_ERROR(QT_TR_NOOP( + "An unknown error occurred. If this error continues to occur, please open an issue")); +const ConnectionError ErrorManager::LOST_CONNECTION( + QT_TR_NOOP("Connection to room lost. Try to reconnect.")); +const ConnectionError ErrorManager::HOST_KICKED( + QT_TR_NOOP("You have been kicked by the room host.")); +const ConnectionError ErrorManager::MAC_COLLISION( + QT_TR_NOOP("MAC address is already in use. Please choose another.")); +const ConnectionError ErrorManager::CONSOLE_ID_COLLISION(QT_TR_NOOP( + "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " + "> Configure > System to regenerate your Console ID.")); +const ConnectionError ErrorManager::PERMISSION_DENIED( + QT_TR_NOOP("You do not have enough permission to perform this action.")); +const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP( + "The user you are trying to kick/ban could not be found.\nThey may have left the room.")); + +static bool WarnMessage(const std::string& title, const std::string& text) { + return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), + QObject::tr(text.c_str()), + QMessageBox::Ok | QMessageBox::Cancel); +} + +void ErrorManager::ShowError(const ConnectionError& e) { + QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str())); +} + +bool WarnCloseRoom() { + return WarnMessage( + QT_TR_NOOP("Leave Room"), + QT_TR_NOOP("You are about to close the room. Any network connections will be closed.")); +} + +bool WarnDisconnect() { + return WarnMessage( + QT_TR_NOOP("Disconnect"), + QT_TR_NOOP("You are about to leave the room. Any network connections will be closed.")); +} + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/message.h b/src/yuzu/multiplayer/message.h new file mode 100644 index 000000000..49a31997d --- /dev/null +++ b/src/yuzu/multiplayer/message.h @@ -0,0 +1,65 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +namespace NetworkMessage { + +class ConnectionError { + +public: + explicit ConnectionError(std::string str) : err(std::move(str)) {} + const std::string& GetString() const { + return err; + } + +private: + std::string err; +}; + +class ErrorManager : QObject { + Q_OBJECT +public: + /// When the nickname is considered invalid by the client + static const ConnectionError USERNAME_NOT_VALID; + static const ConnectionError ROOMNAME_NOT_VALID; + /// When the nickname is considered invalid by the room server + static const ConnectionError USERNAME_NOT_VALID_SERVER; + static const ConnectionError IP_ADDRESS_NOT_VALID; + static const ConnectionError PORT_NOT_VALID; + static const ConnectionError GAME_NOT_SELECTED; + static const ConnectionError NO_INTERNET; + static const ConnectionError UNABLE_TO_CONNECT; + static const ConnectionError ROOM_IS_FULL; + static const ConnectionError COULD_NOT_CREATE_ROOM; + static const ConnectionError HOST_BANNED; + static const ConnectionError WRONG_VERSION; + static const ConnectionError WRONG_PASSWORD; + static const ConnectionError GENERIC_ERROR; + static const ConnectionError LOST_CONNECTION; + static const ConnectionError HOST_KICKED; + static const ConnectionError MAC_COLLISION; + static const ConnectionError CONSOLE_ID_COLLISION; + static const ConnectionError PERMISSION_DENIED; + static const ConnectionError NO_SUCH_USER; + /** + * Shows a standard QMessageBox with a error message + */ + static void ShowError(const ConnectionError& e); +}; +/** + * Show a standard QMessageBox with a warning message about leaving the room + * return true if the user wants to close the network connection + */ +bool WarnCloseRoom(); + +/** + * Show a standard QMessageBox with a warning message about disconnecting from the room + * return true if the user wants to disconnect + */ +bool WarnDisconnect(); + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/moderation_dialog.cpp b/src/yuzu/multiplayer/moderation_dialog.cpp new file mode 100644 index 000000000..e97f30ee5 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.cpp @@ -0,0 +1,113 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "network/network.h" +#include "network/room_member.h" +#include "ui_moderation_dialog.h" +#include "yuzu/multiplayer/moderation_dialog.h" + +namespace Column { +enum { + SUBJECT, + TYPE, + COUNT, +}; +} + +ModerationDialog::ModerationDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + + qRegisterMetaType(); + + if (auto member = Network::GetRoomMember().lock()) { + callback_handle_status_message = member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ModerationDialog::StatusMessageReceived, this, + &ModerationDialog::OnStatusMessageReceived); + callback_handle_ban_list = member->BindOnBanListReceived( + [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); }); + connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList); + } + + // Initialize the UI + model = new QStandardItemModel(ui->ban_list_view); + model->insertColumns(0, Column::COUNT); + model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject")); + model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type")); + + ui->ban_list_view->setModel(model); + + // Load the ban list in background + LoadBanList(); + + connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); }); + connect(ui->unban, &QPushButton::clicked, this, [this] { + auto index = ui->ban_list_view->currentIndex(); + SendUnbanRequest(model->item(index.row(), 0)->text()); + }); + connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); }); +} + +ModerationDialog::~ModerationDialog() { + if (callback_handle_status_message) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_status_message); + } + } + + if (callback_handle_ban_list) { + if (auto room = Network::GetRoomMember().lock()) { + room->Unbind(callback_handle_ban_list); + } + } +} + +void ModerationDialog::LoadBanList() { + if (auto room = Network::GetRoomMember().lock()) { + ui->refresh->setEnabled(false); + ui->refresh->setText(tr("Refreshing")); + ui->unban->setEnabled(false); + room->RequestBanList(); + } +} + +void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { + model->removeRows(0, model->rowCount()); + for (const auto& username : ban_list.first) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username)); + QStandardItem* type_item = new QStandardItem(tr("Forum Username")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (const auto& ip : ban_list.second) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip)); + QStandardItem* type_item = new QStandardItem(tr("IP Address")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (int i = 0; i < Column::COUNT - 1; ++i) { + ui->ban_list_view->resizeColumnToContents(i); + } + ui->refresh->setEnabled(true); + ui->refresh->setText(tr("Refresh")); + ui->unban->setEnabled(false); +} + +void ModerationDialog::SendUnbanRequest(const QString& subject) { + if (auto room = Network::GetRoomMember().lock()) { + room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); + } +} + +void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) { + if (status_message.type != Network::IdMemberBanned && + status_message.type != Network::IdAddressUnbanned) + return; + + // Update the ban list for ban/unban + LoadBanList(); +} diff --git a/src/yuzu/multiplayer/moderation_dialog.h b/src/yuzu/multiplayer/moderation_dialog.h new file mode 100644 index 000000000..d10083d5b --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.h @@ -0,0 +1,42 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "network/room.h" +#include "network/room_member.h" + +namespace Ui { +class ModerationDialog; +} + +class QStandardItemModel; + +class ModerationDialog : public QDialog { + Q_OBJECT + +public: + explicit ModerationDialog(QWidget* parent = nullptr); + ~ModerationDialog(); + +signals: + void StatusMessageReceived(const Network::StatusMessageEntry&); + void BanListReceived(const Network::Room::BanList&); + +private: + void LoadBanList(); + void PopulateBanList(const Network::Room::BanList& ban_list); + void SendUnbanRequest(const QString& subject); + void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message); + + std::unique_ptr ui; + QStandardItemModel* model; + Network::RoomMember::CallbackHandle callback_handle_status_message; + Network::RoomMember::CallbackHandle callback_handle_ban_list; +}; + +Q_DECLARE_METATYPE(Network::Room::BanList); diff --git a/src/yuzu/multiplayer/moderation_dialog.ui b/src/yuzu/multiplayer/moderation_dialog.ui new file mode 100644 index 000000000..808d99414 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.ui @@ -0,0 +1,84 @@ + + + ModerationDialog + + + Moderation + + + + 0 + 0 + 500 + 300 + + + + + + + Ban List + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refreshing + + + false + + + + + + + Unban + + + false + + + + + + + + + + + + + + + QDialogButtonBox::Ok + + + + + + + + buttonBox + accepted() + ModerationDialog + accept() + + + + diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp new file mode 100644 index 000000000..b2aaed982 --- /dev/null +++ b/src/yuzu/multiplayer/state.cpp @@ -0,0 +1,299 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/announce_multiplayer_room.h" +#include "common/logging/log.h" +#include "yuzu/game_list.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" + +MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model, + QAction* leave_room, QAction* show_room) + : QWidget(parent), game_list_model(game_list_model), leave_room(leave_room), + show_room(show_room) { + if (auto member = Network::GetRoomMember().lock()) { + // register the network structs to use in slots and signals + state_callback_handle = member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &MultiplayerState::NetworkStateChanged, this, + &MultiplayerState::OnNetworkStateChanged); + error_callback_handle = member->BindOnError( + [this](const Network::RoomMember::Error& error) { emit NetworkError(error); }); + connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError); + } + + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + announce_multiplayer_session = std::make_shared(); + announce_multiplayer_session->BindErrorCallback( + [this](const WebService::WebResult& result) { emit AnnounceFailed(result); }); + connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed); + + status_text = new ClickableLabel(this); + status_icon = new ClickableLabel(this); + status_text->setToolTip(tr("Current connection status")); + status_text->setText(tr("Not Connected. Click here to find a room!")); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + + connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + + connect(static_cast(QApplication::instance()), &QApplication::focusChanged, this, + [this](QWidget* /*old*/, QWidget* now) { + if (client_room && client_room->isAncestorOf(now)) { + HideNotification(); + } + }); +} + +MultiplayerState::~MultiplayerState() { + if (state_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(state_callback_handle); + } + } + + if (error_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(error_callback_handle); + } + } +} + +void MultiplayerState::Close() { + if (host_room) + host_room->close(); + if (direct_connect) + direct_connect->close(); + if (client_room) + client_room->close(); + if (lobby) + lobby->close(); +} + +void MultiplayerState::retranslateUi() { + status_text->setToolTip(tr("Current connection status")); + + if (current_state == Network::RoomMember::State::Uninitialized) { + status_text->setText(tr("Not Connected. Click here to find a room!")); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_text->setText(tr("Connected")); + } else { + status_text->setText(tr("Not Connected")); + } + + if (lobby) + lobby->RetranslateUi(); + if (host_room) + host_room->RetranslateUi(); + if (client_room) + client_room->RetranslateUi(); + if (direct_connect) + direct_connect->RetranslateUi(); +} + +void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { + LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + OnOpenNetworkRoom(); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); + leave_room->setEnabled(true); + show_room->setEnabled(true); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + status_text->setText(tr("Not Connected")); + leave_room->setEnabled(false); + show_room->setEnabled(false); + } + + current_state = state; +} + +void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) { + LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error)); + switch (error) { + case Network::RoomMember::Error::LostConnection: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::LOST_CONNECTION); + break; + case Network::RoomMember::Error::HostKicked: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_KICKED); + break; + case Network::RoomMember::Error::CouldNotConnect: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::NameCollision: + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::USERNAME_NOT_VALID_SERVER); + break; + case Network::RoomMember::Error::MacCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::MAC_COLLISION); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::CONSOLE_ID_COLLISION); + break; + case Network::RoomMember::Error::RoomIsFull: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOM_IS_FULL); + break; + case Network::RoomMember::Error::WrongPassword: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_PASSWORD); + break; + case Network::RoomMember::Error::WrongVersion: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_VERSION); + break; + case Network::RoomMember::Error::HostBanned: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_BANNED); + break; + case Network::RoomMember::Error::UnknownError: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::PermissionDenied: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PERMISSION_DENIED); + break; + case Network::RoomMember::Error::NoSuchUser: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + break; + } +} + +void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) { + announce_multiplayer_session->Stop(); + QMessageBox::warning(this, tr("Error"), + tr("Failed to update the room information. Please check your Internet " + "connection and try hosting the room again.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); +} + +void MultiplayerState::UpdateThemedIcons() { + if (show_notification) { + status_icon->setPixmap( + QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + } + if (client_room) + client_room->UpdateIconDisplay(); +} + +static void BringWidgetToFront(QWidget* widget) { + widget->show(); + widget->activateWindow(); + widget->raise(); +} + +void MultiplayerState::OnViewLobby() { + if (lobby == nullptr) { + lobby = new Lobby(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(lobby); +} + +void MultiplayerState::OnCreateRoom() { + if (host_room == nullptr) { + host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(host_room); +} + +bool MultiplayerState::OnCloseRoom() { + if (!NetworkMessage::WarnCloseRoom()) + return false; + if (auto room = Network::GetRoom().lock()) { + // if you are in a room, leave it + if (auto member = Network::GetRoomMember().lock()) { + member->Leave(); + LOG_DEBUG(Frontend, "Left the room (as a client)"); + } + + // if you are hosting a room, also stop hosting + if (room->GetState() != Network::Room::State::Open) { + return true; + } + // Save ban list + UISettings::values.ban_list = std::move(room->GetBanList()); + + room->Destroy(); + announce_multiplayer_session->Stop(); + LOG_DEBUG(Frontend, "Closed the room (as a server)"); + } + return true; +} + +void MultiplayerState::ShowNotification() { + if (client_room && client_room->isAncestorOf(QApplication::focusWidget())) + return; // Do not show notification if the chat window currently has focus + show_notification = true; + QApplication::alert(nullptr); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + status_text->setText(tr("New Messages Received")); +} + +void MultiplayerState::HideNotification() { + show_notification = false; + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); +} + +void MultiplayerState::OnOpenNetworkRoom() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (client_room == nullptr) { + client_room = new ClientRoomWindow(this); + connect(client_room, &ClientRoomWindow::ShowNotification, this, + &MultiplayerState::ShowNotification); + } + BringWidgetToFront(client_room); + return; + } + } + // If the user is not a member of a room, show the lobby instead. + // This is currently only used on the clickable label in the status bar + OnViewLobby(); +} + +void MultiplayerState::OnDirectConnectToRoom() { + if (direct_connect == nullptr) { + direct_connect = new DirectConnectWindow(this); + } + BringWidgetToFront(direct_connect); +} + +bool MultiplayerState::IsHostingPublicRoom() const { + return announce_multiplayer_session->IsRunning(); +} + +void MultiplayerState::UpdateCredentials() { + announce_multiplayer_session->UpdateCredentials(); +} + +void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) { + game_list_model = game_list; + if (lobby) { + lobby->UpdateGameList(game_list); + } + if (host_room) { + host_room->UpdateGameList(game_list); + } +} diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h new file mode 100644 index 000000000..414454acb --- /dev/null +++ b/src/yuzu/multiplayer/state.h @@ -0,0 +1,92 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/announce_multiplayer_session.h" +#include "network/network.h" + +class QStandardItemModel; +class Lobby; +class HostRoomWindow; +class ClientRoomWindow; +class DirectConnectWindow; +class ClickableLabel; + +class MultiplayerState : public QWidget { + Q_OBJECT; + +public: + explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room, + QAction* show_room); + ~MultiplayerState(); + + /** + * Close all open multiplayer related dialogs + */ + void Close(); + + ClickableLabel* GetStatusText() const { + return status_text; + } + + ClickableLabel* GetStatusIcon() const { + return status_icon; + } + + void retranslateUi(); + + /** + * Whether a public room is being hosted or not. + * When this is true, Web Services configuration should be disabled. + */ + bool IsHostingPublicRoom() const; + + void UpdateCredentials(); + + /** + * Updates the multiplayer dialogs with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* game_list); + +public slots: + void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnNetworkError(const Network::RoomMember::Error& error); + void OnViewLobby(); + void OnCreateRoom(); + bool OnCloseRoom(); + void OnOpenNetworkRoom(); + void OnDirectConnectToRoom(); + void OnAnnounceFailed(const WebService::WebResult&); + void UpdateThemedIcons(); + void ShowNotification(); + void HideNotification(); + +signals: + void NetworkStateChanged(const Network::RoomMember::State&); + void NetworkError(const Network::RoomMember::Error&); + void AnnounceFailed(const WebService::WebResult&); + +private: + Lobby* lobby = nullptr; + HostRoomWindow* host_room = nullptr; + ClientRoomWindow* client_room = nullptr; + DirectConnectWindow* direct_connect = nullptr; + ClickableLabel* status_icon = nullptr; + ClickableLabel* status_text = nullptr; + QStandardItemModel* game_list_model = nullptr; + QAction* leave_room; + QAction* show_room; + std::shared_ptr announce_multiplayer_session; + Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; + bool has_mod_perms = false; + Network::RoomMember::CallbackHandle state_callback_handle; + Network::RoomMember::CallbackHandle error_callback_handle; + + bool show_notification = false; +}; + +Q_DECLARE_METATYPE(WebService::WebResult); diff --git a/src/yuzu/multiplayer/validation.h b/src/yuzu/multiplayer/validation.h new file mode 100644 index 000000000..1c215a190 --- /dev/null +++ b/src/yuzu/multiplayer/validation.h @@ -0,0 +1,49 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +class Validation { +public: + Validation() + : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {} + + ~Validation() = default; + + const QValidator* GetRoomName() const { + return &room_name; + } + const QValidator* GetNickname() const { + return &nickname; + } + const QValidator* GetIP() const { + return &ip; + } + const QValidator* GetPort() const { + return &port; + } + +private: + /// room name can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp room_name_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator room_name; + + /// nickname can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp nickname_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator nickname; + + /// ipv4 address only + // TODO remove this when we support hostnames in direct connect + QRegExp ip_regex = QRegExp(QStringLiteral( + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|" + "2[0-4][0-9]|25[0-5])")); + QRegExpValidator ip; + + /// port must be between 0 and 65535 + QIntValidator port; +}; diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 2f6948243..aca1a28e1 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -102,6 +102,19 @@ struct Values { Settings::Setting callout_flags{0, "calloutFlags"}; + // multiplayer settings + QString nickname; + QString ip; + QString port; + QString room_nickname; + QString room_name; + quint32 max_player; + QString room_port; + uint host_type; + qulonglong game_id; + QString room_description; + std::pair, std::vector> ban_list; + // logging Settings::Setting show_console{false, "showConsole"}; diff --git a/src/yuzu/util/clickable_label.cpp b/src/yuzu/util/clickable_label.cpp new file mode 100644 index 000000000..5bde838ca --- /dev/null +++ b/src/yuzu/util/clickable_label.cpp @@ -0,0 +1,12 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "yuzu/util/clickable_label.h" + +ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f) + : QLabel(parent) {} + +void ClickableLabel::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + emit clicked(); +} diff --git a/src/yuzu/util/clickable_label.h b/src/yuzu/util/clickable_label.h new file mode 100644 index 000000000..3c65a74be --- /dev/null +++ b/src/yuzu/util/clickable_label.h @@ -0,0 +1,22 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index cb301e78b..0194940be 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,7 @@ #include "core/loader/loader.h" #include "core/telemetry_session.h" #include "input_common/main.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "yuzu_cmd/config.h" #include "yuzu_cmd/emu_window/emu_window_sdl2.h" @@ -60,6 +62,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; static void PrintHelp(const char* argv0) { std::cout << "Usage: " << argv0 << " [options] \n" + "-m, --multiplayer=nick:password@address:port" + " Nickname, password, address and port for multiplayer\n" "-f, --fullscreen Start in fullscreen mode\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n" @@ -71,6 +75,107 @@ static void PrintVersion() { std::cout << "yuzu " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; } +static void OnStateChanged(const Network::RoomMember::State& state) { + switch (state) { + case Network::RoomMember::State::Idle: + LOG_DEBUG(Network, "Network is idle"); + break; + case Network::RoomMember::State::Joining: + LOG_DEBUG(Network, "Connection sequence to room started"); + break; + case Network::RoomMember::State::Joined: + LOG_DEBUG(Network, "Successfully joined to the room"); + break; + case Network::RoomMember::State::Moderator: + LOG_DEBUG(Network, "Successfully joined the room as a moderator"); + break; + default: + break; + } +} + +static void OnNetworkError(const Network::RoomMember::Error& error) { + switch (error) { + case Network::RoomMember::Error::LostConnection: + LOG_DEBUG(Network, "Lost connection to the room"); + break; + case Network::RoomMember::Error::CouldNotConnect: + LOG_ERROR(Network, "Error: Could not connect"); + exit(1); + break; + case Network::RoomMember::Error::NameCollision: + LOG_ERROR( + Network, + "You tried to use the same nickname as another user that is connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::MacCollision: + LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " + "connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); + exit(1); + break; + case Network::RoomMember::Error::WrongPassword: + LOG_ERROR(Network, "Room replied with: Wrong password"); + exit(1); + break; + case Network::RoomMember::Error::WrongVersion: + LOG_ERROR(Network, + "You are using a different version than the room you are trying to connect to"); + exit(1); + break; + case Network::RoomMember::Error::RoomIsFull: + LOG_ERROR(Network, "The room is full"); + exit(1); + break; + case Network::RoomMember::Error::HostKicked: + LOG_ERROR(Network, "You have been kicked by the host"); + break; + case Network::RoomMember::Error::HostBanned: + LOG_ERROR(Network, "You have been banned by the host"); + break; + case Network::RoomMember::Error::UnknownError: + LOG_ERROR(Network, "UnknownError"); + break; + case Network::RoomMember::Error::PermissionDenied: + LOG_ERROR(Network, "PermissionDenied"); + break; + case Network::RoomMember::Error::NoSuchUser: + LOG_ERROR(Network, "NoSuchUser"); + break; + } +} + +static void OnMessageReceived(const Network::ChatEntry& msg) { + std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; +} + +static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { + std::string message; + switch (msg.type) { + case Network::IdMemberJoin: + message = fmt::format("{} has joined", msg.nickname); + break; + case Network::IdMemberLeave: + message = fmt::format("{} has left", msg.nickname); + break; + case Network::IdMemberKicked: + message = fmt::format("{} has been kicked", msg.nickname); + break; + case Network::IdMemberBanned: + message = fmt::format("{} has been banned", msg.nickname); + break; + case Network::IdAddressUnbanned: + message = fmt::format("{} has been unbanned", msg.nickname); + break; + } + if (!message.empty()) + std::cout << std::endl << "* " << message << std::endl << std::endl; +} + /// Application entry point int main(int argc, char** argv) { Common::Log::Initialize(); @@ -92,10 +197,16 @@ int main(int argc, char** argv) { std::optional config_path; std::string program_args; + bool use_multiplayer = false; bool fullscreen = false; + std::string nickname{}; + std::string password{}; + std::string address{}; + u16 port = Network::DefaultRoomPort; static struct option long_options[] = { // clang-format off + {"multiplayer", required_argument, 0, 'm'}, {"fullscreen", no_argument, 0, 'f'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, @@ -109,6 +220,38 @@ int main(int argc, char** argv) { int arg = getopt_long(argc, argv, "g:fhvp::c:", long_options, &option_index); if (arg != -1) { switch (static_cast(arg)) { + case 'm': { + use_multiplayer = true; + const std::string str_arg(optarg); + // regex to check if the format is nickname:password@ip:port + // with optional :password + const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); + if (!std::regex_match(str_arg, re)) { + std::cout << "Wrong format for option --multiplayer\n"; + PrintHelp(argv[0]); + return 0; + } + + std::smatch match; + std::regex_search(str_arg, match, re); + ASSERT(match.size() == 5); + nickname = match[1]; + password = match[2]; + address = match[3]; + if (!match[4].str().empty()) + port = std::stoi(match[4]); + std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); + if (!std::regex_match(nickname, nickname_re)) { + std::cout + << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; + return 0; + } + if (address.empty()) { + std::cout << "Address to room must not be empty.\n"; + return 0; + } + break; + } case 'f': fullscreen = true; LOG_INFO(Frontend, "Starting in fullscreen mode..."); @@ -215,6 +358,21 @@ int main(int argc, char** argv) { system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); + if (use_multiplayer) { + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnChatMessageRecieved(OnMessageReceived); + member->BindOnStatusMessageReceived(OnStatusMessageReceived); + member->BindOnStateChanged(OnStateChanged); + member->BindOnError(OnNetworkError); + LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, + nickname); + member->Join(nickname, "", address.c_str(), port, 0, Network::NoPreferredMac, password); + } else { + LOG_ERROR(Network, "Could not access RoomMember"); + return 0; + } + } + // Core is loaded, start the GPU (makes the GPU contexts current to this thread) system.GPU().Start(); system.GetCpuManager().OnGpuReady(); -- cgit v1.2.3