// Copyright 2017 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/NetPlay/NetPlaySetupDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayProto.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/QtUtils/NonDefaultQPushButton.h" #include "DolphinQt/QtUtils/UTF8CodePointCountValidator.h" #include "DolphinQt/Settings.h" #include "Common/Version.h" #include "DolphinQt/NetPlay/NetPlayBrowser.h" #include "UICommon/GameFile.h" #include "UICommon/NetPlayIndex.h" NetPlaySetupDialog::NetPlaySetupDialog(const GameListModel& game_list_model, QWidget* parent) : QDialog(parent), m_game_list_model(game_list_model) { setWindowTitle(tr("NetPlay Setup")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); CreateMainLayout(); bool use_index = Config::Get(Config::NETPLAY_USE_INDEX); std::string index_name = Config::Get(Config::NETPLAY_INDEX_NAME); 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); int host_port = Config::Get(Config::NETPLAY_HOST_PORT); #ifdef USE_UPNP bool use_upnp = Config::Get(Config::NETPLAY_USE_UPNP); m_host_upnp->setChecked(use_upnp); #endif m_nickname_edit->setText(QString::fromStdString(nickname)); m_connection_type->setCurrentIndex(traversal_choice == "direct" ? 0 : 1); m_connect_port_box->setValue(connect_port); m_host_port_box->setValue(host_port); m_host_server_name->setEnabled(use_index); m_host_server_name->setText(QString::fromStdString(index_name)); // Browser Stuff const auto& settings = Settings::Instance().GetQSettings(); const QByteArray geometry = settings.value(QStringLiteral("netplaybrowser/geometry")).toByteArray(); if (!geometry.isEmpty()) restoreGeometry(geometry); const QString region = settings.value(QStringLiteral("netplaybrowser/region")).toString(); const bool valid_region = m_region_combo->findText(region) != -1; if (valid_region) m_region_combo->setCurrentText(region); m_edit_name->setText(settings.value(QStringLiteral("netplaybrowser/name")).toString()); const QString visibility = settings.value(QStringLiteral("netplaybrowser/visibility")).toString(); if (visibility == QStringLiteral("public")) m_radio_public->setChecked(true); else if (visibility == QStringLiteral("private")) m_radio_private->setChecked(true); m_check_hide_ingame->setChecked(true); OnConnectionTypeChanged(m_connection_type->currentIndex()); ConnectWidgets(); m_refresh_run.Set(true); m_refresh_thread = std::thread([this] { RefreshLoopBrowser(); }); UpdateListBrowser(); RefreshBrowser(); } void NetPlaySetupDialog::CreateMainLayout() { m_main_layout = new QGridLayout; m_button_box = new QDialogButtonBox(QDialogButtonBox::Cancel); m_nickname_edit = new QLineEdit; m_connection_type = new QComboBox; m_connection_type->setCurrentIndex(1); m_reset_traversal_button = new NonDefaultQPushButton(tr("Reset Traversal Settings")); m_tab_widget = new QTabWidget; m_nickname_edit->setValidator( new UTF8CodePointCountValidator(NetPlay::MAX_NAME_LENGTH, m_nickname_edit)); // Connection widget auto* connection_widget = new QWidget; auto* connection_layout = new QGridLayout; // NetPlay Browser auto* browser_widget = new QWidget; auto* layout = new QVBoxLayout; m_table_widget = new QTableWidget; m_table_widget->setTabKeyNavigation(false); m_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); m_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); m_table_widget->setWordWrap(false); 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_region_combo->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); m_status_label = new QLabel; m_b_button_box = new QDialogButtonBox; m_button_refresh = new QPushButton(tr("Refresh")); m_edit_name = new QLineEdit; m_check_hide_ingame = new QCheckBox(tr("Hide In-Game Sessions")); 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); layout->addWidget(m_table_widget); layout->addWidget(m_status_label); layout->addWidget(m_b_button_box); m_b_button_box->addButton(m_button_refresh, QDialogButtonBox::ResetRole); browser_widget->setLayout(layout); m_ip_label = new QLabel; m_ip_edit = new QLineEdit; m_connect_port_label = new QLabel(tr("Port:")); m_connect_port_box = new QSpinBox; m_connect_button = new NonDefaultQPushButton(tr("Connect")); m_connect_port_box->setMaximum(65535); connection_layout->addWidget(m_ip_label, 0, 0); connection_layout->addWidget(m_ip_edit, 0, 1); connection_layout->addWidget(m_connect_port_label, 0, 2); connection_layout->addWidget(m_connect_port_box, 0, 3); connection_layout->addWidget( new QLabel( tr("ALERT:\n\n" "All players must use the same Dolphin version, unless MPN Dolphin is used to host\n" "If enabled, SD cards must be identical between players.\n" "If DSP LLE is used, DSP ROMs must be identical between players.\n" "If a game is hanging on boot, it may not support Dual Core Netplay." " Disable Dual Core.\n" "If connecting directly, the host must have the chosen UDP port open/forwarded!\n" "\n" "Wii Remote support in netplay is experimental and may not work correctly.\n" "Use at your own risk.\n")), 1, 0, -1, -1); connection_layout->addWidget(m_connect_button, 3, 3, Qt::AlignRight); connection_widget->setLayout(connection_layout); // Host widget auto* host_widget = new QWidget; auto* host_layout = new QGridLayout; m_host_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_name = new QLineEdit; #ifdef USE_UPNP m_host_upnp = new QCheckBox(tr("Forward port (UPnP)")); #endif m_host_games = new QListWidget; m_host_button = new NonDefaultQPushButton(tr("Host")); m_host_port_box->setMaximum(65535); m_host_server_name->setToolTip(tr("Name of your session shown in the server browser")); m_host_server_name->setPlaceholderText(tr("Name")); host_layout->addWidget(m_host_port_box, 0, 0, Qt::AlignLeft); #ifdef USE_UPNP host_layout->addWidget(m_host_upnp, 0, 5, Qt::AlignRight); #endif host_layout->addWidget(m_host_games, 2, 0, 1, -1); host_layout->addWidget(m_host_button, 4, 5, Qt::AlignRight); host_widget->setLayout(host_layout); m_connection_type->addItem(tr("Direct Connection")); m_connection_type->addItem(tr("Traversal Server")); m_main_layout->addWidget(new QLabel(tr("Connection Type:")), 0, 0); m_main_layout->addWidget(m_connection_type, 0, 1); m_main_layout->addWidget(m_reset_traversal_button, 0, 2); m_main_layout->addWidget(new QLabel(tr("Nickname:")), 1, 0); m_main_layout->addWidget(m_nickname_edit, 1, 1); m_main_layout->addWidget(m_tab_widget, 2, 0, 1, -1); m_main_layout->addWidget(m_button_box, 3, 0, 1, -1); // Tabs m_tab_widget->addTab(connection_widget, tr("Connect")); m_tab_widget->addTab(host_widget, tr("Host")); m_tab_widget->addTab(browser_widget, tr("Browser")); setLayout(m_main_layout); } NetPlaySetupDialog::~NetPlaySetupDialog() { m_refresh_run.Set(false); m_refresh_event.Set(); if (m_refresh_thread.joinable()) m_refresh_thread.join(); SaveSettings(); } void NetPlaySetupDialog::ConnectWidgets() { connect(m_connection_type, &QComboBox::currentIndexChanged, this, &NetPlaySetupDialog::OnConnectionTypeChanged); connect(m_nickname_edit, &QLineEdit::textChanged, this, &NetPlaySetupDialog::SaveSettings); // Connect widget connect(m_ip_edit, &QLineEdit::textChanged, this, &NetPlaySetupDialog::SaveSettings); connect(m_connect_port_box, &QSpinBox::valueChanged, this, &NetPlaySetupDialog::SaveSettings); // Host widget connect(m_host_port_box, &QSpinBox::valueChanged, this, &NetPlaySetupDialog::SaveSettings); connect(m_host_games, &QListWidget::currentRowChanged, [this](int index) { Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"), m_host_games->item(index)->text()); }); // refresh browser on tab changed connect(m_tab_widget, &QTabWidget::currentChanged, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_host_games, &QListWidget::itemDoubleClicked, this, &NetPlaySetupDialog::accept); connect(m_host_force_port_check, &QCheckBox::toggled, [this](bool value) { m_host_force_port_box->setEnabled(value); }); connect(m_host_chunked_upload_limit_check, &QCheckBox::toggled, this, [this](bool value) { m_host_chunked_upload_limit_box->setEnabled(value); SaveSettings(); }); connect(m_host_chunked_upload_limit_box, &QSpinBox::valueChanged, this, &NetPlaySetupDialog::SaveSettings); connect(m_host_server_browser, &QCheckBox::toggled, this, &NetPlaySetupDialog::SaveSettings); connect(m_host_server_name, &QLineEdit::textChanged, this, &NetPlaySetupDialog::SaveSettings); #ifdef USE_UPNP connect(m_host_upnp, &QCheckBox::stateChanged, this, &NetPlaySetupDialog::SaveSettings); #endif connect(m_connect_button, &QPushButton::clicked, this, &QDialog::accept); connect(m_host_button, &QPushButton::clicked, this, &QDialog::accept); connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_reset_traversal_button, &QPushButton::clicked, this, &NetPlaySetupDialog::ResetTraversalHost); // Browser Stuff connect(m_region_combo, qOverload(&QComboBox::currentIndexChanged), this, &NetPlaySetupDialog::RefreshBrowser); connect(m_button_refresh, &QPushButton::clicked, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_radio_all, &QRadioButton::toggled, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_radio_private, &QRadioButton::toggled, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_check_hide_ingame, &QRadioButton::toggled, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_edit_name, &QLineEdit::textChanged, this, &NetPlaySetupDialog::RefreshBrowser); connect(m_table_widget, &QTableWidget::itemDoubleClicked, this, &NetPlaySetupDialog::acceptBrowser); connect(this, &NetPlaySetupDialog::UpdateStatusRequestedBrowser, this, &NetPlaySetupDialog::OnUpdateStatusRequestedBrowser, Qt::QueuedConnection); connect(this, &NetPlaySetupDialog::UpdateListRequestedBrowser, this, &NetPlaySetupDialog::OnUpdateListRequestedBrowser, Qt::QueuedConnection); } void NetPlaySetupDialog::SaveSettings() { Config::ConfigChangeCallbackGuard config_guard; Config::SetBaseOrCurrent(Config::NETPLAY_NICKNAME, m_nickname_edit->text().toStdString()); Config::SetBaseOrCurrent(m_connection_type->currentIndex() == 0 ? Config::NETPLAY_ADDRESS : Config::NETPLAY_HOST_CODE, m_ip_edit->text().toStdString()); Config::SetBaseOrCurrent(Config::NETPLAY_CONNECT_PORT, static_cast(m_connect_port_box->value())); Config::SetBaseOrCurrent(Config::NETPLAY_HOST_PORT, static_cast(m_host_port_box->value())); #ifdef USE_UPNP Config::SetBaseOrCurrent(Config::NETPLAY_USE_UPNP, m_host_upnp->isChecked()); #endif Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_REGION, "NA"); Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_NAME, m_nickname_edit->text().toStdString()); Config::SetBaseOrCurrent(Config::NETPLAY_INDEX_PASSWORD, ""); // Browser Stuff auto& settings = Settings::Instance().GetQSettings(); settings.setValue(QStringLiteral("netplaybrowser/geometry"), saveGeometry()); settings.setValue(QStringLiteral("netplaybrowser/region"), m_region_combo->currentText()); settings.setValue(QStringLiteral("netplaybrowser/name"), m_edit_name->text()); QString visibility(QStringLiteral("all")); if (m_radio_public->isChecked()) visibility = QStringLiteral("public"); else if (m_radio_private->isChecked()) visibility = QStringLiteral("private"); settings.setValue(QStringLiteral("netplaybrowser/visibility"), visibility); settings.setValue(QStringLiteral("netplaybrowser/hide_incompatible"), true); settings.setValue(QStringLiteral("netplaybrowser/hide_ingame"), m_check_hide_ingame->isChecked()); } void NetPlaySetupDialog::OnConnectionTypeChanged(int index) { m_connect_port_box->setHidden(index != 0); m_connect_port_label->setHidden(index != 0); m_host_port_box->setHidden(index != 0); #ifdef USE_UPNP m_host_upnp->setHidden(index != 0); #endif m_reset_traversal_button->setHidden(index == 0); std::string address = index == 0 ? Config::Get(Config::NETPLAY_ADDRESS) : Config::Get(Config::NETPLAY_HOST_CODE); m_ip_label->setText(index == 0 ? tr("IP Address:") : tr("Host Code:")); m_ip_edit->setText(QString::fromStdString(address)); Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, std::string(index == 0 ? "direct" : "traversal")); } void NetPlaySetupDialog::show() { // Here i'm setting the lobby name if it's empty to make // NetPlay sessions start more easily for first time players if (m_host_server_name->text().isEmpty()) { std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); m_host_server_name->setText(QString::fromStdString(nickname)); } m_connection_type->setCurrentIndex(1); m_tab_widget->setCurrentIndex(2); // start on browser PopulateGameList(); QDialog::show(); } void NetPlaySetupDialog::accept() { SaveSettings(); if (m_tab_widget->currentIndex() == 0) { emit Join(); } else { auto items = m_host_games->selectedItems(); if (items.empty()) { ModalMessageBox::critical(this, tr("Error"), tr("You must select a game to host!")); return; } emit Host(*items[0]->data(Qt::UserRole).value>()); } } void NetPlaySetupDialog::PopulateGameList() { QSignalBlocker blocker(m_host_games); m_host_games->clear(); for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++) { std::shared_ptr game = m_game_list_model.GetGameFile(i); auto* item = new QListWidgetItem(QString::fromStdString(m_game_list_model.GetNetPlayName(*game))); item->setData(Qt::UserRole, QVariant::fromValue(std::move(game))); m_host_games->addItem(item); } m_host_games->sortItems(); const QString selected_game = Settings::GetQSettings().value(QStringLiteral("netplay/hostgame"), QString{}).toString(); const auto find_list = m_host_games->findItems(selected_game, Qt::MatchFlag::MatchExactly); if (find_list.count() > 0) m_host_games->setCurrentItem(find_list[0]); } void NetPlaySetupDialog::ResetTraversalHost() { Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_SERVER, Config::NETPLAY_TRAVERSAL_SERVER.GetDefaultValue()); Config::SetBaseOrCurrent(Config::NETPLAY_TRAVERSAL_PORT, Config::NETPLAY_TRAVERSAL_PORT.GetDefaultValue()); ModalMessageBox::information( this, tr("Reset Traversal Server"), tr("Reset Traversal Server to %1:%2") .arg(QString::fromStdString(Config::NETPLAY_TRAVERSAL_SERVER.GetDefaultValue()), QString::number(Config::NETPLAY_TRAVERSAL_PORT.GetDefaultValue()))); } void NetPlaySetupDialog::RefreshBrowser() { std::map filters; if (!m_edit_name->text().isEmpty()) filters["name"] = m_edit_name->text().toStdString(); if (true) filters["version"] = Common::GetScmDescStr(); 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(); if (m_check_hide_ingame->isChecked()) filters["in_game"] = "0"; std::unique_lock lock(m_refresh_filters_mutex); m_refresh_filters = std::move(filters); m_refresh_event.Set(); SaveSettings(); } void NetPlaySetupDialog::RefreshLoopBrowser() { while (m_refresh_run.IsSet()) { m_refresh_event.Wait(); std::unique_lock lock(m_refresh_filters_mutex); if (m_refresh_filters) { auto filters = std::move(*m_refresh_filters); m_refresh_filters.reset(); lock.unlock(); emit UpdateStatusRequestedBrowser(tr("Refreshing...")); NetPlayIndex client; auto entries = client.List(filters); if (entries) { emit UpdateListRequestedBrowser(std::move(*entries)); } else { emit UpdateStatusRequestedBrowser(tr("Error obtaining session list: %1") .arg(QString::fromStdString(client.GetLastError()))); } } } } void NetPlaySetupDialog::UpdateListBrowser() { const int session_count = static_cast(m_sessions.size()); m_table_widget->clear(); m_table_widget->setColumnCount(4); m_table_widget->setHorizontalHeaderLabels({ tr("Name"), tr("Game"), tr("Players"), tr("In-Game"), }); auto* hor_header = m_table_widget->horizontalHeader(); hor_header->setSectionResizeMode(0, QHeaderView::Stretch); hor_header->setSectionResizeMode(0, QHeaderView::Stretch); hor_header->setSectionResizeMode(1, QHeaderView::Stretch); hor_header->setSectionResizeMode(2, QHeaderView::ResizeToContents); hor_header->setSectionResizeMode(3, QHeaderView::ResizeToContents); hor_header->setHighlightSections(false); m_table_widget->setRowCount(session_count); for (int i = 0; i < session_count; i++) { const auto& entry = m_sessions[i]; auto* name = new QTableWidgetItem(QString::fromStdString(entry.name)); 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)); const bool enabled = Common::GetScmDescStr() == entry.version; for (const auto& item : {name, game_id, player_count, in_game}) item->setFlags(enabled ? Qt::ItemIsEnabled | Qt::ItemIsSelectable : Qt::NoItemFlags); m_table_widget->setItem(i, 0, name); m_table_widget->setItem(i, 1, game_id); m_table_widget->setItem(i, 2, player_count); m_table_widget->setItem(i, 3, in_game); } m_status_label->setText( (session_count == 1 ? tr("%1 session found") : tr("%1 sessions found")).arg(session_count)); } void NetPlaySetupDialog::OnSelectionChangedBrowser() { return; } void NetPlaySetupDialog::OnUpdateStatusRequestedBrowser(const QString& status) { m_status_label->setText(status); } void NetPlaySetupDialog::OnUpdateListRequestedBrowser(std::vector sessions) { m_sessions = std::move(sessions); UpdateListBrowser(); } void NetPlaySetupDialog::acceptBrowser() { if (m_table_widget->selectedItems().isEmpty()) return; 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 JoinBrowser(); }