diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index cee9d7434c..ab3fd11c34 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -36,6 +36,7 @@ public: static int CurlProgressCallback(Impl* impl, double dlnow, double dltotal, double ulnow, double ultotal); + std::string EscapeComponent(const std::string& string); private: static std::mutex s_curl_was_inited_mutex; @@ -74,6 +75,11 @@ void HttpRequest::FollowRedirects(long max) m_impl->FollowRedirects(max); } +std::string HttpRequest::EscapeComponent(const std::string& string) +{ + return m_impl->EscapeComponent(string); +} + HttpRequest::Response HttpRequest::Get(const std::string& url, const Headers& headers) { return m_impl->Fetch(url, Impl::Method::GET, headers, nullptr, 0); @@ -159,6 +165,11 @@ void HttpRequest::Impl::FollowRedirects(long max) curl_easy_setopt(m_curl.get(), CURLOPT_MAXREDIRS, max); } +std::string HttpRequest::Impl::EscapeComponent(const std::string& string) +{ + return curl_easy_escape(m_curl.get(), string.c_str(), static_cast(string.size())); +} + static size_t CurlWriteCallback(char* data, size_t size, size_t nmemb, void* userdata) { auto* buffer = static_cast*>(userdata); diff --git a/Source/Core/Common/HttpRequest.h b/Source/Core/Common/HttpRequest.h index 17e31ff354..54f13ff73b 100644 --- a/Source/Core/Common/HttpRequest.h +++ b/Source/Core/Common/HttpRequest.h @@ -34,6 +34,7 @@ public: void SetCookies(const std::string& cookies); void UseIPv4(); void FollowRedirects(long max = 1); + std::string EscapeComponent(const std::string& string); Response Get(const std::string& url, const Headers& headers = {}); Response Post(const std::string& url, const std::vector& payload, const Headers& headers = {}); diff --git a/Source/Core/Core/Config/NetplaySettings.cpp b/Source/Core/Core/Config/NetplaySettings.cpp index 1f01452bcb..a5310a4f0b 100644 --- a/Source/Core/Core/Config/NetplaySettings.cpp +++ b/Source/Core/Core/Config/NetplaySettings.cpp @@ -19,6 +19,15 @@ const ConfigInfo NETPLAY_TRAVERSAL_SERVER{{System::Main, "NetPlay", const ConfigInfo NETPLAY_TRAVERSAL_PORT{{System::Main, "NetPlay", "TraversalPort"}, 6262}; const ConfigInfo NETPLAY_TRAVERSAL_CHOICE{{System::Main, "NetPlay", "TraversalChoice"}, "direct"}; +const ConfigInfo NETPLAY_INDEX_URL{{System::Main, "NetPlay", "IndexServer"}, + "https://lobby.dolphin-emu.org"}; + +const ConfigInfo NETPLAY_USE_INDEX{{System::Main, "NetPlay", "UseIndex"}, false}; +const ConfigInfo NETPLAY_INDEX_NAME{{System::Main, "NetPlay", "IndexName"}, ""}; +const ConfigInfo NETPLAY_INDEX_REGION{{System::Main, "NetPlay", "IndexRegion"}, ""}; +const ConfigInfo NETPLAY_INDEX_PASSWORD{{System::Main, "NetPlay", "IndexPassword"}, + ""}; + const ConfigInfo NETPLAY_HOST_CODE{{System::Main, "NetPlay", "HostCode"}, "00000000"}; const ConfigInfo NETPLAY_HOST_PORT{{System::Main, "NetPlay", "HostPort"}, DEFAULT_LISTEN_PORT}; diff --git a/Source/Core/Core/Config/NetplaySettings.h b/Source/Core/Core/Config/NetplaySettings.h index adbac875e0..b96ee218bb 100644 --- a/Source/Core/Core/Config/NetplaySettings.h +++ b/Source/Core/Core/Config/NetplaySettings.h @@ -19,6 +19,7 @@ extern const ConfigInfo NETPLAY_TRAVERSAL_SERVER; extern const ConfigInfo NETPLAY_TRAVERSAL_PORT; extern const ConfigInfo NETPLAY_TRAVERSAL_CHOICE; extern const ConfigInfo NETPLAY_HOST_CODE; +extern const ConfigInfo NETPLAY_INDEX_URL; extern const ConfigInfo NETPLAY_HOST_PORT; extern const ConfigInfo NETPLAY_ADDRESS; @@ -30,6 +31,11 @@ extern const ConfigInfo NETPLAY_USE_UPNP; extern const ConfigInfo NETPLAY_ENABLE_QOS; +extern const ConfigInfo NETPLAY_USE_INDEX; +extern const ConfigInfo NETPLAY_INDEX_REGION; +extern const ConfigInfo NETPLAY_INDEX_NAME; +extern const ConfigInfo NETPLAY_INDEX_PASSWORD; + extern const ConfigInfo NETPLAY_ENABLE_CHUNKED_UPLOAD_LIMIT; extern const ConfigInfo NETPLAY_CHUNKED_UPLOAD_LIMIT; diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index 017fc75588..abaf5446ba 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -24,6 +24,7 @@ #include "Common/ENetUtil.h" #include "Common/File.h" #include "Common/FileUtil.h" +#include "Common/HttpRequest.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" #include "Common/SFMLHelper.h" @@ -132,6 +133,8 @@ NetPlayServer::NetPlayServer(const u16 port, const bool forward_port, m_server = enet_host_create(&serverAddr, 10, CHANNEL_COUNT, 0, 0); if (m_server != nullptr) m_server->intercept = ENetUtil::InterceptCallback; + + SetupIndex(); } if (m_server != nullptr) { @@ -162,6 +165,45 @@ static void ClearPeerPlayerId(ENetPeer* peer) } } +void NetPlayServer::SetupIndex() +{ + if (!Config::Get(Config::NETPLAY_USE_INDEX)) + return; + + NetPlaySession session; + + session.name = Config::Get(Config::NETPLAY_INDEX_NAME); + session.region = Config::Get(Config::NETPLAY_INDEX_REGION); + session.has_password = !Config::Get(Config::NETPLAY_INDEX_PASSWORD).empty(); + session.method = m_traversal_client ? "traversal" : "direct"; + session.game_id = m_selected_game.empty() ? "UNKNOWN" : m_selected_game; + session.player_count = static_cast(m_players.size()); + session.in_game = m_is_running; + session.port = GetPort(); + + if (m_traversal_client) + { + session.server_id = std::string(g_TraversalClient->GetHostID().data(), 8); + } + else + { + Common::HttpRequest request; + // ENet does not support IPv6, so IPv4 has to be used + request.UseIPv4(); + Common::HttpRequest::Response response = + request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}}); + + if (!response.has_value()) + return; + + session.server_id = std::string(response->begin(), response->end()); + } + + session.EncryptID(Config::Get(Config::NETPLAY_INDEX_PASSWORD)); + + m_index.Add(session); +} + // called from ---NETPLAY--- thread void NetPlayServer::ThreadFunc() { @@ -178,6 +220,11 @@ void NetPlayServer::ThreadFunc() m_ping_timer.Start(); SendToClients(spac); + + m_index.SetPlayerCount(static_cast(m_players.size())); + m_index.SetGame(m_selected_game); + m_index.SetInGame(m_is_running); + m_update_pings = false; } @@ -283,7 +330,7 @@ void NetPlayServer::ThreadFunc() ClearPeerPlayerId(player_entry.second.socket); enet_peer_disconnect(player_entry.second.socket, 0); } -} +} // namespace NetPlay // called from ---NETPLAY--- thread unsigned int NetPlayServer::OnConnect(ENetPeer* socket, sf::Packet& rpac) @@ -1067,11 +1114,14 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player) void NetPlayServer::OnTraversalStateChanged() { + const TraversalClient::State state = m_traversal_client->GetState(); + + if (g_TraversalClient->GetHostID()[0] != '\0') + SetupIndex(); + if (!m_dialog) return; - const TraversalClient::State state = m_traversal_client->GetState(); - if (state == TraversalClient::Failure) m_dialog->OnTraversalError(m_traversal_client->GetFailureReason()); diff --git a/Source/Core/Core/NetPlayServer.h b/Source/Core/Core/NetPlayServer.h index 79e63e1556..baec187324 100644 --- a/Source/Core/Core/NetPlayServer.h +++ b/Source/Core/Core/NetPlayServer.h @@ -21,6 +21,7 @@ #include "Common/TraversalClient.h" #include "Core/NetPlayProto.h" #include "InputCommon/GCPadStatus.h" +#include "UICommon/NetPlayIndex.h" namespace NetPlay { @@ -136,6 +137,7 @@ private: std::vector> GetInterfaceListInternal() const; void ChunkedDataThreadFunc(); void ChunkedDataSend(sf::Packet&& packet, PlayerId pid, const TargetMode target_mode); + void SetupIndex(); bool PlayerHasControllerMapped(PlayerId pid) const; @@ -187,5 +189,6 @@ private: ENetHost* m_server = nullptr; TraversalClient* m_traversal_client = nullptr; NetPlayUI* m_dialog = nullptr; + NetPlayIndex m_index; }; } // namespace NetPlay diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 61b1ad6142..551a7e04d8 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -98,6 +98,7 @@ add_executable(dolphin-emu NetPlay/ChunkedProgressDialog.cpp NetPlay/GameListDialog.cpp NetPlay/MD5Dialog.cpp + NetPlay/NetPlayBrowser.cpp NetPlay/NetPlayDialog.cpp NetPlay/NetPlaySetupDialog.cpp NetPlay/PadMappingDialog.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 1b28442782..5886ba9137 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -147,6 +147,7 @@ + @@ -248,6 +249,7 @@ + @@ -367,6 +369,7 @@ + diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 28bfba749e..a5aeebc65a 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -80,6 +80,7 @@ #include "DolphinQt/HotkeyScheduler.h" #include "DolphinQt/MainWindow.h" #include "DolphinQt/MenuBar.h" +#include "DolphinQt/NetPlay/NetPlayBrowser.h" #include "DolphinQt/NetPlay/NetPlayDialog.h" #include "DolphinQt/NetPlay/NetPlaySetupDialog.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" @@ -453,6 +454,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate); connect(m_menu_bar, &MenuBar::BootWiiSystemMenu, this, &MainWindow::BootWiiSystemMenu); connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog); + connect(m_menu_bar, &MenuBar::BrowseNetPlay, this, &MainWindow::ShowNetPlayBrowser); connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); @@ -1131,6 +1133,13 @@ void MainWindow::ShowNetPlaySetupDialog() m_netplay_setup_dialog->activateWindow(); } +void MainWindow::ShowNetPlayBrowser() +{ + auto* browser = new NetPlayBrowser(this); + connect(browser, &NetPlayBrowser::Join, this, &MainWindow::NetPlayJoin); + browser->exec(); +} + void MainWindow::ShowFIFOPlayer() { if (!m_fifo_window) diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index bf9027960b..4443b7f651 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -148,6 +148,7 @@ private: void ShowAboutDialog(); void ShowHotkeyDialog(); void ShowNetPlaySetupDialog(); + void ShowNetPlayBrowser(); void ShowFIFOPlayer(); void ShowMemcardManager(); void ShowResourcePackManager(); diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 2ff5dfcc5c..a486905a63 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -241,6 +241,7 @@ void MenuBar::AddToolsMenu() gc_ipl->addAction(tr("PAL"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::PAL); }); tools_menu->addAction(tr("Start &NetPlay..."), this, &MenuBar::StartNetPlay); + tools_menu->addAction(tr("Browse &NetPlay Sessions...."), this, &MenuBar::BrowseNetPlay); tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index dbb962e2ac..a6febece35 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -61,6 +61,7 @@ signals: void FrameAdvance(); void Screenshot(); void StartNetPlay(); + void BrowseNetPlay(); void StateLoad(); void StateSave(); void StateLoadSlot(); diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.cpp b/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.cpp new file mode 100644 index 0000000000..8097a0a48a --- /dev/null +++ b/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.cpp @@ -0,0 +1,243 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DolphinQt/NetPlay/NetPlayBrowser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/Version.h" + +#include "Core/Config/NetplaySettings.h" +#include "Core/ConfigManager.h" + +#include "DolphinQt/QtUtils/ModalMessageBox.h" + +NetPlayBrowser::NetPlayBrowser(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("NetPlay Session Browser")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + CreateWidgets(); + ConnectWidgets(); + + resize(750, 500); + + m_table_widget->verticalHeader()->setHidden(true); + m_table_widget->setAlternatingRowColors(true); + + Refresh(); +} + +void NetPlayBrowser::CreateWidgets() +{ + auto* layout = new QVBoxLayout; + + m_table_widget = new QTableWidget; + + m_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); + + m_region_combo = new QComboBox; + + m_region_combo->addItem(tr("Any Region")); + + for (const auto& region : NetPlayIndex::GetRegions()) + { + m_region_combo->addItem( + tr("%1 (%2)").arg(tr(region.second.c_str())).arg(QString::fromStdString(region.first)), + QString::fromStdString(region.first)); + } + + m_status_label = new QLabel; + m_button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + m_button_refresh = new QPushButton(tr("Refresh")); + m_edit_name = new QLineEdit; + m_edit_game_id = new QLineEdit; + + m_radio_all = new QRadioButton(tr("Private and Public")); + m_radio_private = new QRadioButton(tr("Private")); + m_radio_public = new QRadioButton(tr("Public")); + + m_radio_all->setChecked(true); + + auto* filter_box = new QGroupBox(tr("Filters")); + auto* filter_layout = new QGridLayout; + filter_box->setLayout(filter_layout); + + filter_layout->addWidget(new QLabel(tr("Region:")), 0, 0); + filter_layout->addWidget(m_region_combo, 0, 1); + filter_layout->addWidget(new QLabel(tr("Name:")), 1, 0); + filter_layout->addWidget(m_edit_name, 1, 1, 1, -1); + filter_layout->addWidget(new QLabel(tr("Game ID:")), 2, 0); + filter_layout->addWidget(m_edit_game_id, 2, 1, 1, -1); + filter_layout->addWidget(m_radio_all, 3, 1); + filter_layout->addWidget(m_radio_public, 3, 2); + filter_layout->addWidget(m_radio_private, 3, 3); + filter_layout->addItem(new QSpacerItem(4, 1, QSizePolicy::Expanding), 2, 4); + + layout->addWidget(m_table_widget); + layout->addWidget(filter_box); + layout->addWidget(m_status_label); + layout->addWidget(m_button_box); + + m_button_box->addButton(m_button_refresh, QDialogButtonBox::ResetRole); + m_button_box->button(QDialogButtonBox::Ok)->setEnabled(false); + + setLayout(layout); +} + +void NetPlayBrowser::ConnectWidgets() +{ + connect(m_button_box, &QDialogButtonBox::accepted, this, &NetPlayBrowser::accept); + connect(m_button_box, &QDialogButtonBox::rejected, this, &NetPlayBrowser::reject); + connect(m_button_refresh, &QPushButton::pressed, this, &NetPlayBrowser::Refresh); + + connect(m_radio_all, &QRadioButton::toggled, this, &NetPlayBrowser::Refresh); + connect(m_radio_private, &QRadioButton::toggled, this, &NetPlayBrowser::Refresh); + + connect(m_edit_name, &QLineEdit::textChanged, this, &NetPlayBrowser::Refresh); + connect(m_edit_game_id, &QLineEdit::textChanged, this, &NetPlayBrowser::Refresh); + connect(m_table_widget, &QTableWidget::itemSelectionChanged, this, + &NetPlayBrowser::OnSelectionChanged); +} + +void NetPlayBrowser::Refresh() +{ + m_status_label->setText(tr("Refreshing...")); + + m_table_widget->clear(); + m_table_widget->setColumnCount(6); + m_table_widget->setHorizontalHeaderLabels( + {tr("Region"), tr("Name"), tr("Password?"), tr("In-Game?"), tr("Game"), tr("Players")}); + m_table_widget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_table_widget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table_widget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_table_widget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + m_table_widget->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch); + + NetPlayIndex client; + + std::map filters; + + filters["version"] = Common::scm_desc_str; + + if (!m_edit_name->text().isEmpty()) + filters["name"] = m_edit_name->text().toStdString(); + + if (!m_edit_game_id->text().isEmpty()) + filters["game"] = m_edit_game_id->text().toStdString(); + + if (!m_radio_all->isChecked()) + filters["password"] = std::to_string(m_radio_private->isChecked()); + + if (m_region_combo->currentIndex() != 0) + filters["region"] = m_region_combo->currentData().toString().toStdString(); + + auto entries = client.List(filters); + + if (!entries) + { + m_status_label->setText( + tr("Error obtaining session list: %1").arg(QString::fromStdString(client.GetLastError()))); + return; + } + + const int session_count = static_cast(entries.value().size()); + + m_table_widget->setRowCount(session_count); + + for (int i = 0; i < session_count; i++) + { + const auto& entry = entries.value()[i]; + + auto* region = new QTableWidgetItem(QString::fromStdString(entry.region)); + auto* name = new QTableWidgetItem(QString::fromStdString(entry.name)); + auto* password = new QTableWidgetItem(entry.has_password ? tr("Yes") : tr("No")); + auto* in_game = new QTableWidgetItem(entry.in_game ? tr("Yes") : tr("No")); + auto* game_id = new QTableWidgetItem(QString::fromStdString(entry.game_id)); + auto* player_count = new QTableWidgetItem(QStringLiteral("%1").arg(entry.player_count)); + + for (const auto& item : {region, name, password, game_id, player_count}) + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + + m_table_widget->setItem(i, 0, region); + m_table_widget->setItem(i, 1, name); + m_table_widget->setItem(i, 2, password); + m_table_widget->setItem(i, 3, in_game); + m_table_widget->setItem(i, 4, game_id); + m_table_widget->setItem(i, 5, player_count); + } + + m_status_label->setText( + (session_count == 1 ? tr("%1 session found") : tr("%1 sessions found")).arg(session_count)); + + m_sessions = entries.value(); +} + +void NetPlayBrowser::OnSelectionChanged() +{ + m_button_box->button(QDialogButtonBox::Ok) + ->setEnabled(!m_table_widget->selectedItems().isEmpty()); +} + +void NetPlayBrowser::accept() +{ + const int index = m_table_widget->selectedItems()[0]->row(); + + NetPlaySession& session = m_sessions[index]; + + std::string server_id = session.server_id; + + if (m_sessions[index].has_password) + { + auto* dialog = new QInputDialog(this); + + dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint); + dialog->setWindowTitle(tr("Enter password")); + dialog->setLabelText(tr("This session requires a password:")); + dialog->setWindowModality(Qt::WindowModal); + dialog->setTextEchoMode(QLineEdit::Password); + + if (dialog->exec() != QDialog::Accepted) + return; + + const std::string password = dialog->textValue().toStdString(); + + auto decrypted_id = session.DecryptID(password); + + if (!decrypted_id) + { + ModalMessageBox::warning(this, tr("Error"), tr("Invalid password provided.")); + return; + } + + server_id = decrypted_id.value(); + } + + QDialog::accept(); + + Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, session.method); + + Config::SetBaseOrCurrent(Config::NETPLAY_CONNECT_PORT, session.port); + + if (session.method == "traversal") + Config::SetBaseOrCurrent(Config::NETPLAY_HOST_CODE, server_id); + else + Config::SetBaseOrCurrent(Config::NETPLAY_ADDRESS, server_id); + + emit Join(); +} diff --git a/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.h b/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.h new file mode 100644 index 0000000000..f6cdfdfc5a --- /dev/null +++ b/Source/Core/DolphinQt/NetPlay/NetPlayBrowser.h @@ -0,0 +1,52 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +#include + +#include "UICommon/NetPlayIndex.h" + +class QComboBox; +class QDialogButtonBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QRadioButton; +class QTableWidget; + +class NetPlayBrowser : public QDialog +{ + Q_OBJECT +public: + explicit NetPlayBrowser(QWidget* parent = nullptr); + + void accept() override; +signals: + void Join(); + +private: + void CreateWidgets(); + void ConnectWidgets(); + + void Refresh(); + + void OnSelectionChanged(); + + QComboBox* m_region_combo; + QLabel* m_status_label; + QPushButton* m_button_refresh; + QTableWidget* m_table_widget; + QDialogButtonBox* m_button_box; + QLineEdit* m_edit_name; + QLineEdit* m_edit_game_id; + + QRadioButton* m_radio_all; + QRadioButton* m_radio_private; + QRadioButton* m_radio_public; + + std::vector m_sessions; +}; diff --git a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp index b960a99dd5..6cc078a421 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp +++ b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.cpp @@ -22,6 +22,8 @@ #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/Settings.h" +#include "UICommon/NetPlayIndex.h" + NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent) : QDialog(parent), m_game_list_model(Settings::Instance().GetGameListModel()) { @@ -30,6 +32,10 @@ NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent) CreateMainLayout(); + bool use_index = Config::Get(Config::NETPLAY_USE_INDEX); + std::string index_region = Config::Get(Config::NETPLAY_INDEX_REGION); + std::string index_name = Config::Get(Config::NETPLAY_INDEX_NAME); + std::string index_password = Config::Get(Config::NETPLAY_INDEX_PASSWORD); std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); int connect_port = Config::Get(Config::NETPLAY_CONNECT_PORT); @@ -48,10 +54,21 @@ NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent) m_connect_port_box->setValue(connect_port); m_host_port_box->setValue(host_port); - m_host_force_port_check->setChecked(false); m_host_force_port_box->setValue(host_listen_port); m_host_force_port_box->setEnabled(false); + m_host_server_browser->setChecked(use_index); + + m_host_server_region->setEnabled(use_index); + m_host_server_region->setCurrentIndex( + m_host_server_region->findData(QString::fromStdString(index_region))); + + m_host_server_name->setEnabled(use_index); + m_host_server_name->setText(QString::fromStdString(index_name)); + + m_host_server_password->setEnabled(use_index); + m_host_server_password->setText(QString::fromStdString(index_password)); + m_host_chunked_upload_limit_check->setChecked(enable_chunked_upload_limit); m_host_chunked_upload_limit_box->setValue(chunked_upload_limit); m_host_chunked_upload_limit_box->setEnabled(enable_chunked_upload_limit); @@ -112,6 +129,10 @@ void NetPlaySetupDialog::CreateMainLayout() m_host_force_port_box = new QSpinBox; m_host_chunked_upload_limit_check = new QCheckBox(tr("Limit Chunked Upload Speed:")); m_host_chunked_upload_limit_box = new QSpinBox; + m_host_server_browser = new QCheckBox(tr("Show in server browser")); + m_host_server_name = new QLineEdit; + m_host_server_password = new QLineEdit; + m_host_server_region = new QComboBox; #ifdef USE_UPNP m_host_upnp = new QCheckBox(tr("Forward port (UPnP)")); @@ -128,17 +149,33 @@ void NetPlaySetupDialog::CreateMainLayout() m_host_chunked_upload_limit_check->setToolTip(tr( "This will limit the speed of chunked uploading per client, which is used for save sync.")); + m_host_server_name->setToolTip(tr("Name of your session shown in the server browser")); + m_host_server_name->setPlaceholderText(tr("Name")); + m_host_server_password->setToolTip(tr("Password for joining your game (leave empty for none)")); + m_host_server_password->setPlaceholderText(tr("Password")); + + for (const auto& region : NetPlayIndex::GetRegions()) + { + m_host_server_region->addItem( + tr("%1 (%2)").arg(tr(region.second.c_str())).arg(QString::fromStdString(region.first)), + QString::fromStdString(region.first)); + } + host_layout->addWidget(m_host_port_label, 0, 0); host_layout->addWidget(m_host_port_box, 0, 1); #ifdef USE_UPNP host_layout->addWidget(m_host_upnp, 0, 2); #endif - host_layout->addWidget(m_host_games, 1, 0, 1, -1); - host_layout->addWidget(m_host_force_port_check, 2, 0); - host_layout->addWidget(m_host_force_port_box, 2, 1, Qt::AlignLeft); - host_layout->addWidget(m_host_chunked_upload_limit_check, 3, 0); - host_layout->addWidget(m_host_chunked_upload_limit_box, 3, 1, Qt::AlignLeft); - host_layout->addWidget(m_host_button, 2, 2, 2, 1, Qt::AlignRight); + host_layout->addWidget(m_host_server_browser, 1, 0); + host_layout->addWidget(m_host_server_region, 1, 1); + host_layout->addWidget(m_host_server_name, 1, 2); + host_layout->addWidget(m_host_server_password, 1, 3); + host_layout->addWidget(m_host_games, 2, 0, 1, -1); + host_layout->addWidget(m_host_force_port_check, 3, 0); + host_layout->addWidget(m_host_force_port_box, 3, 1, Qt::AlignLeft); + host_layout->addWidget(m_host_chunked_upload_limit_check, 4, 0); + host_layout->addWidget(m_host_chunked_upload_limit_box, 4, 1, Qt::AlignLeft); + host_layout->addWidget(m_host_button, 4, 3, 2, 1, Qt::AlignRight); host_widget->setLayout(host_layout); @@ -199,6 +236,11 @@ void NetPlaySetupDialog::ConnectWidgets() connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_reset_traversal_button, &QPushButton::clicked, this, &NetPlaySetupDialog::ResetTraversalHost); + connect(m_host_server_browser, &QCheckBox::toggled, this, [this](bool value) { + m_host_server_region->setEnabled(value); + m_host_server_name->setEnabled(value); + m_host_server_password->setEnabled(value); + }); } void NetPlaySetupDialog::SaveSettings() @@ -224,6 +266,13 @@ void NetPlaySetupDialog::SaveSettings() m_host_chunked_upload_limit_check->isChecked()); Config::SetBaseOrCurrent(Config::NETPLAY_CHUNKED_UPLOAD_LIMIT, m_host_chunked_upload_limit_box->value()); + + Config::SetBaseOrCurrent(Config::NETPLAY_USE_INDEX, m_host_server_browser->isChecked()); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_REGION, + m_host_server_region->currentData().toString().toStdString()); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_NAME, m_host_server_name->text().toStdString()); + Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_PASSWORD, + m_host_server_password->text().toStdString()); } void NetPlaySetupDialog::OnConnectionTypeChanged(int index) @@ -273,6 +322,12 @@ void NetPlaySetupDialog::accept() return; } + if (m_host_server_browser->isChecked() && m_host_server_name->text().isEmpty()) + { + ModalMessageBox::critical(this, tr("Error"), tr("You must provide a name for your session!")); + return; + } + emit Host(items[0]->text()); } } diff --git a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h index 7f5456848b..ec0d4b3c5d 100644 --- a/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h +++ b/Source/Core/DolphinQt/NetPlay/NetPlaySetupDialog.h @@ -65,6 +65,10 @@ private: QSpinBox* m_host_force_port_box; QCheckBox* m_host_chunked_upload_limit_check; QSpinBox* m_host_chunked_upload_limit_box; + QCheckBox* m_host_server_browser; + QLineEdit* m_host_server_name; + QLineEdit* m_host_server_password; + QComboBox* m_host_server_region; #ifdef USE_UPNP QCheckBox* m_host_upnp; diff --git a/Source/Core/UICommon/CMakeLists.txt b/Source/Core/UICommon/CMakeLists.txt index 0c665a3f20..46f212490b 100644 --- a/Source/Core/UICommon/CMakeLists.txt +++ b/Source/Core/UICommon/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(uicommon DiscordPresence.cpp GameFile.cpp GameFileCache.cpp + NetPlayIndex.cpp ResourcePack/Manager.cpp ResourcePack/Manifest.cpp ResourcePack/ResourcePack.cpp diff --git a/Source/Core/UICommon/NetPlayIndex.cpp b/Source/Core/UICommon/NetPlayIndex.cpp new file mode 100644 index 0000000000..b41920c647 --- /dev/null +++ b/Source/Core/UICommon/NetPlayIndex.cpp @@ -0,0 +1,324 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "UICommon/NetPlayIndex.h" + +#include +#include + +#include + +#include "Common/Common.h" +#include "Common/HttpRequest.h" +#include "Common/Thread.h" +#include "Common/Version.h" + +#include "Core/Config/NetplaySettings.h" + +NetPlayIndex::NetPlayIndex() = default; + +NetPlayIndex::~NetPlayIndex() +{ + if (!m_secret.empty()) + Remove(); +} + +static std::optional ParseResponse(std::vector response) +{ + std::string response_string(reinterpret_cast(response.data()), response.size()); + + picojson::value json; + + auto error = picojson::parse(json, response_string); + + if (!error.empty()) + return {}; + + return json; +} + +std::optional> +NetPlayIndex::List(const std::map& filters) +{ + Common::HttpRequest request; + + std::string list_url = Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/list"; + + if (!filters.empty()) + { + list_url += '?'; + for (const auto& filter : filters) + { + list_url += filter.first + '=' + request.EscapeComponent(filter.second) + '&'; + } + list_url.pop_back(); + } + + auto response = request.Get(list_url, {{"X-Is-Dolphin", "1"}}); + if (!response) + { + m_last_error = "NO_RESPONSE"; + return {}; + } + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + return {}; + } + + const auto& status = json->get("status"); + + if (status.to_str() != "OK") + { + m_last_error = status.to_str(); + return {}; + } + + const auto& entries = json->get("sessions"); + + std::vector sessions; + + for (const auto& entry : entries.get()) + { + NetPlaySession session; + + const auto& name = entry.get("name"); + const auto& region = entry.get("region"); + const auto& method = entry.get("method"); + const auto& game_id = entry.get("game"); + const auto& server_id = entry.get("server_id"); + const auto& has_password = entry.get("password"); + const auto& player_count = entry.get("player_count"); + const auto& port = entry.get("port"); + const auto& in_game = entry.get("in_game"); + + if (!name.is() || !region.is() || !method.is() || + !server_id.is() || !game_id.is() || !has_password.is() || + !player_count.is() || !port.is() || !in_game.is()) + { + continue; + } + + session.name = name.to_str(); + session.region = region.to_str(); + session.game_id = game_id.to_str(); + session.server_id = server_id.to_str(); + session.method = method.to_str(); + session.has_password = has_password.get(); + session.player_count = static_cast(player_count.get()); + session.port = static_cast(port.get()); + session.in_game = in_game.get(); + + sessions.push_back(std::move(session)); + } + + return sessions; +} + +void NetPlayIndex::NotificationLoop() +{ + while (m_running.IsSet()) + { + Common::HttpRequest request; + auto response = request.Get( + Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/active?secret=" + m_secret + + "&player_count=" + std::to_string(m_player_count) + + "&game=" + request.EscapeComponent(m_game) + "&in_game=" + std::to_string(m_in_game), + {{"X-Is-Dolphin", "1"}}); + + if (!response) + continue; + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + m_running.Set(false); + return; + } + + std::string status = json->get("status").to_str(); + + if (status != "OK") + { + m_last_error = std::move(status); + m_running.Set(false); + return; + } + + Common::SleepCurrentThread(1000 * 5); + } +} + +bool NetPlayIndex::Add(NetPlaySession session) +{ + m_running.Set(true); + + Common::HttpRequest request; + auto response = request.Get(Config::Get(Config::NETPLAY_INDEX_URL) + + "/v0/session/add?name=" + request.EscapeComponent(session.name) + + "®ion=" + request.EscapeComponent(session.region) + + "&game=" + request.EscapeComponent(session.game_id) + + "&password=" + std::to_string(session.has_password) + + "&method=" + session.method + "&server_id=" + session.server_id + + "&in_game=" + std::to_string(session.in_game) + + "&port=" + std::to_string(session.port) + + "&player_count=" + std::to_string(session.player_count) + + "&version=" + Common::scm_desc_str, + {{"X-Is-Dolphin", "1"}}); + + if (!response.has_value()) + { + m_last_error = "NO_RESPONSE"; + return false; + } + + auto json = ParseResponse(response.value()); + + if (!json) + { + m_last_error = "BAD_JSON"; + return false; + } + + std::string status = json->get("status").to_str(); + + if (status != "OK") + { + m_last_error = std::move(status); + return false; + } + + m_secret = json->get("secret").to_str(); + m_in_game = session.in_game; + m_player_count = session.player_count; + m_game = session.game_id; + + m_session_thread = std::thread([this] { NotificationLoop(); }); + + m_session_thread.detach(); + + return true; +} + +void NetPlayIndex::SetInGame(bool in_game) +{ + m_in_game = in_game; +} + +void NetPlayIndex::SetPlayerCount(int player_count) +{ + m_player_count = player_count; +} + +void NetPlayIndex::SetGame(const std::string game) +{ + m_game = std::move(game); +} + +void NetPlayIndex::Remove() +{ + if (m_secret.empty()) + return; + + m_running.Set(false); + + if (m_session_thread.joinable()) + m_session_thread.join(); + + // We don't really care whether this fails or not + Common::HttpRequest request; + request.Get(Config::Get(Config::NETPLAY_INDEX_URL) + "/v0/session/remove?secret=" + m_secret, + {{"X-Is-Dolphin", "1"}}); + + m_secret.clear(); +} + +std::vector> NetPlayIndex::GetRegions() +{ + return { + {"EA", _trans("East Asia")}, {"CN", _trans("China")}, {"EU", _trans("Europe")}, + {"NA", _trans("North America")}, {"SA", _trans("South America")}, {"OC", _trans("Oceania")}, + {"AF", _trans("Africa")}, + }; +} + +// This encryption system uses simple XOR operations and a checksum +// It isn't very secure but is preferable to adding another dependency on mbedtls +// The encrypted data is encoded as nibbles with the character 'A' as the base offset + +bool NetPlaySession::EncryptID(const std::string& password) +{ + if (password.empty()) + return false; + + std::string to_encrypt = server_id; + + // Calculate and append checksum to ID + const u8 sum = std::accumulate(to_encrypt.begin(), to_encrypt.end(), u8{0}); + to_encrypt += sum; + + std::string encrypted_id; + + u8 i = 0; + for (const char byte : to_encrypt) + { + char c = byte ^ password[i % password.size()]; + c += i; + encrypted_id += 'A' + ((c & 0xF0) >> 4); + encrypted_id += 'A' + (c & 0x0F); + ++i; + } + + server_id = std::move(encrypted_id); + + return true; +} + +std::optional NetPlaySession::DecryptID(const std::string& password) const +{ + if (password.empty()) + return {}; + + // If the length of an encrypted session id is not divisble by two, it's invalid + if (server_id.empty() || server_id.size() % 2 != 0) + return {}; + + std::string decoded; + + for (size_t i = 0; i < server_id.size(); i += 2) + { + char c = (server_id[i] - 'A') << 4 | (server_id[i + 1] - 'A'); + decoded.push_back(c); + } + + u8 i = 0; + for (auto& c : decoded) + { + c -= i; + c ^= password[i % password.size()]; + ++i; + } + + // Verify checksum + const u8 expected_sum = decoded[decoded.size() - 1]; + + decoded.pop_back(); + + const u8 sum = std::accumulate(decoded.begin(), decoded.end(), u8{0}); + + if (sum != expected_sum) + return {}; + + return decoded; +} + +const std::string& NetPlayIndex::GetLastError() const +{ + return m_last_error; +} diff --git a/Source/Core/UICommon/NetPlayIndex.h b/Source/Core/UICommon/NetPlayIndex.h new file mode 100644 index 0000000000..2bc778efa0 --- /dev/null +++ b/Source/Core/UICommon/NetPlayIndex.h @@ -0,0 +1,66 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/Flag.h" + +struct NetPlaySession +{ + std::string name; + std::string region; + std::string method; + std::string server_id; + std::string game_id; + + int player_count; + int port; + + bool has_password; + bool in_game; + + bool EncryptID(const std::string& password); + std::optional DecryptID(const std::string& password) const; +}; + +class NetPlayIndex +{ +public: + explicit NetPlayIndex(); + ~NetPlayIndex(); + + std::optional> + List(const std::map& filters = {}); + + static std::vector> GetRegions(); + + bool Add(NetPlaySession session); + void Remove(); + + void SetPlayerCount(int player_count); + void SetInGame(bool in_game); + void SetGame(std::string game); + + const std::string& GetLastError() const; + +private: + void NotificationLoop(); + + Common::Flag m_running; + + std::string m_secret; + std::string m_game; + int m_player_count = 0; + bool m_in_game = false; + + std::string m_last_error; + std::thread m_session_thread; +}; diff --git a/Source/Core/UICommon/UICommon.vcxproj b/Source/Core/UICommon/UICommon.vcxproj index b669d9b195..7526d92b0b 100644 --- a/Source/Core/UICommon/UICommon.vcxproj +++ b/Source/Core/UICommon/UICommon.vcxproj @@ -53,6 +53,7 @@ + @@ -69,6 +70,7 @@ + @@ -86,4 +88,4 @@ - \ No newline at end of file +