// 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 "citra_qt/game_list_p.h" #include "citra_qt/main.h" #include "citra_qt/multiplayer/host_room.h" #include "citra_qt/multiplayer/message.h" #include "citra_qt/multiplayer/state.h" #include "citra_qt/multiplayer/validation.h" #include "citra_qt/ui_settings.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" #include "core/hle/service/cfg/cfg.h" #include "core/settings.h" #include "ui_host_room.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.citra_username.empty()) { // Use Citra Web Service user name as nickname by default ui->username->setText(QString::fromStdString(Settings::values.citra_username)); } 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::ShowError(NetworkMessage::USERNAME_NOT_VALID); return; } if (!ui->room_name->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::ROOMNAME_NOT_VALID); return; } if (!ui->port->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID); return; } if (ui->game_list->currentIndex() == -1) { NetworkMessage::ShowError(NetworkMessage::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.citra_username, game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); if (!created) { NetworkMessage::ShowError(NetworkMessage::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 Common::WebResult result = session->Register(); if (result.result_code != Common::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 Citra 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.citra_username, Settings::values.citra_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 member->Join(ui->username->text().toStdString(), Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), "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(); Settings::Apply(); 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; }