| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <QComboBox> | ||
| #include <QDialogButtonBox> | ||
| #include <QGridLayout> | ||
| #include <QGroupBox> | ||
| #include <QHeaderView> | ||
| #include <QInputDialog> | ||
| #include <QLabel> | ||
| #include <QLineEdit> | ||
| #include <QPushButton> | ||
| #include <QRadioButton> | ||
| #include <QSpacerItem> | ||
| #include <QTableWidget> | ||
| #include <QTableWidgetItem> | ||
| #include <QVBoxLayout> | ||
|
|
||
| #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<std::string, std::string> 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<int>(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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Copyright 2019 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <vector> | ||
|
|
||
| #include <QDialog> | ||
|
|
||
| #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<NetPlaySession> m_sessions; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| // Copyright 2019 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #include "UICommon/NetPlayIndex.h" | ||
|
|
||
| #include <numeric> | ||
| #include <string> | ||
|
|
||
| #include <picojson/picojson.h> | ||
|
|
||
| #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<picojson::value> ParseResponse(std::vector<u8> response) | ||
| { | ||
| std::string response_string(reinterpret_cast<char*>(response.data()), response.size()); | ||
|
|
||
| picojson::value json; | ||
|
|
||
| auto error = picojson::parse(json, response_string); | ||
|
|
||
| if (!error.empty()) | ||
| return {}; | ||
|
|
||
| return json; | ||
| } | ||
|
|
||
| std::optional<std::vector<NetPlaySession>> | ||
| NetPlayIndex::List(const std::map<std::string, std::string>& 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<NetPlaySession> sessions; | ||
|
|
||
| for (const auto& entry : entries.get<picojson::array>()) | ||
| { | ||
| 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<std::string>() || !region.is<std::string>() || !method.is<std::string>() || | ||
| !server_id.is<std::string>() || !game_id.is<std::string>() || !has_password.is<bool>() || | ||
| !player_count.is<double>() || !port.is<double>() || !in_game.is<bool>()) | ||
| { | ||
| 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<bool>(); | ||
| session.player_count = static_cast<int>(player_count.get<double>()); | ||
| session.port = static_cast<int>(port.get<double>()); | ||
| session.in_game = in_game.get<bool>(); | ||
|
|
||
| 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<std::pair<std::string, std::string>> 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<std::string> 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright 2019 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <map> | ||
| #include <optional> | ||
| #include <string> | ||
| #include <thread> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #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<std::string> DecryptID(const std::string& password) const; | ||
| }; | ||
|
|
||
| class NetPlayIndex | ||
| { | ||
| public: | ||
| explicit NetPlayIndex(); | ||
| ~NetPlayIndex(); | ||
|
|
||
| std::optional<std::vector<NetPlaySession>> | ||
| List(const std::map<std::string, std::string>& filters = {}); | ||
|
|
||
| static std::vector<std::pair<std::string, std::string>> 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; | ||
| }; |