From 871196bc100f8bc2474f32b2c1e1e2343037e6ad Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 19 Jan 2018 13:42:21 +0000 Subject: [PATCH 01/16] Citra-qt: Add multiplayer ui --- dist/qt_themes/default/default.qrc | 8 +- .../default/icons/16x16/connected.png | Bin 0 -> 269 bytes .../default/icons/16x16/disconnected.png | Bin 0 -> 306 bytes dist/qt_themes/default/icons/16x16/lock.png | Bin 0 -> 279 bytes src/citra_qt/CMakeLists.txt | 21 ++ src/citra_qt/bootmanager.cpp | 2 - src/citra_qt/configuration/config.cpp | 34 ++ src/citra_qt/game_list.cpp | 4 + src/citra_qt/game_list.h | 2 + src/citra_qt/main.cpp | 168 ++++++++++ src/citra_qt/main.h | 43 ++- src/citra_qt/main.ui | 134 +++++--- src/citra_qt/multiplayer/chat_room.cpp | 208 ++++++++++++ src/citra_qt/multiplayer/chat_room.h | 57 ++++ src/citra_qt/multiplayer/chat_room.ui | 59 ++++ src/citra_qt/multiplayer/client_room.cpp | 121 +++++++ src/citra_qt/multiplayer/client_room.h | 38 +++ src/citra_qt/multiplayer/client_room.ui | 63 ++++ src/citra_qt/multiplayer/direct_connect.cpp | 132 ++++++++ src/citra_qt/multiplayer/direct_connect.h | 39 +++ src/citra_qt/multiplayer/direct_connect.ui | 190 +++++++++++ src/citra_qt/multiplayer/host_room.cpp | 170 ++++++++++ src/citra_qt/multiplayer/host_room.h | 73 ++++ src/citra_qt/multiplayer/host_room.ui | 179 ++++++++++ src/citra_qt/multiplayer/lobby.cpp | 314 ++++++++++++++++++ src/citra_qt/multiplayer/lobby.h | 125 +++++++ src/citra_qt/multiplayer/lobby.ui | 138 ++++++++ src/citra_qt/multiplayer/lobby_p.h | 189 +++++++++++ src/citra_qt/multiplayer/message.cpp | 59 ++++ src/citra_qt/multiplayer/message.h | 53 +++ src/citra_qt/multiplayer/validation.h | 28 ++ src/citra_qt/ui_settings.h | 12 +- src/citra_qt/util/clickable_label.cpp | 13 + src/citra_qt/util/clickable_label.h | 23 ++ 34 files changed, 2653 insertions(+), 46 deletions(-) create mode 100644 dist/qt_themes/default/icons/16x16/connected.png create mode 100644 dist/qt_themes/default/icons/16x16/disconnected.png create mode 100644 dist/qt_themes/default/icons/16x16/lock.png create mode 100644 src/citra_qt/multiplayer/chat_room.cpp create mode 100644 src/citra_qt/multiplayer/chat_room.h create mode 100644 src/citra_qt/multiplayer/chat_room.ui create mode 100644 src/citra_qt/multiplayer/client_room.cpp create mode 100644 src/citra_qt/multiplayer/client_room.h create mode 100644 src/citra_qt/multiplayer/client_room.ui create mode 100644 src/citra_qt/multiplayer/direct_connect.cpp create mode 100644 src/citra_qt/multiplayer/direct_connect.h create mode 100644 src/citra_qt/multiplayer/direct_connect.ui create mode 100644 src/citra_qt/multiplayer/host_room.cpp create mode 100644 src/citra_qt/multiplayer/host_room.h create mode 100644 src/citra_qt/multiplayer/host_room.ui create mode 100644 src/citra_qt/multiplayer/lobby.cpp create mode 100644 src/citra_qt/multiplayer/lobby.h create mode 100644 src/citra_qt/multiplayer/lobby.ui create mode 100644 src/citra_qt/multiplayer/lobby_p.h create mode 100644 src/citra_qt/multiplayer/message.cpp create mode 100644 src/citra_qt/multiplayer/message.h create mode 100644 src/citra_qt/multiplayer/validation.h create mode 100644 src/citra_qt/util/clickable_label.cpp create mode 100644 src/citra_qt/util/clickable_label.h diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index a3e21645a..a524a17e7 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -5,7 +5,13 @@ icons/16x16/checked.png icons/16x16/failed.png - + + icons/16x16/connected.png + + icons/16x16/disconnected.png + + icons/16x16/lock.png + icons/256x256/citra.png diff --git a/dist/qt_themes/default/icons/16x16/connected.png b/dist/qt_themes/default/icons/16x16/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..afa7973948c2fd69a5b838ebc993a4a618e20e31 GIT binary patch literal 269 zcmV+o0rLKdP)wVS)a5(TtL3F*6^H+fmi83={^g~HAo7u1ZbyiW#=5CU=?7R{1!Zbe$F9tyMfYy zZs6*qt^z68Vfj6G&{i5N*B8S8soODKE0Ee2()LDxTpa4zWdu+_YwLCwvHtx4JVP#E TIr_f000000NkvXXu0mjf{AX#) literal 0 HcmV?d00001 diff --git a/dist/qt_themes/default/icons/16x16/disconnected.png b/dist/qt_themes/default/icons/16x16/disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..835b1f0d6b5ceeb11e9a2b7d68c61521aef7bcb8 GIT binary patch literal 306 zcmV-20nPr2P)au7)Yo#YK5^hRAPlWv+5i9m07*qoM6N<$ Ef=+yWl>h($ literal 0 HcmV?d00001 diff --git a/dist/qt_themes/default/icons/16x16/lock.png b/dist/qt_themes/default/icons/16x16/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..496b58078983bc3c4f7dc2808fd02b8deb8b7b67 GIT binary patch literal 279 zcmV+y0qFjTP)value("verify_endpoint_url", "https://services.citra-emu.org/api/profile") .toString() .toStdString(); + Settings::values.announce_multiplayer_room_endpoint_url = + qt_config + ->value("announce_multiplayer_room_endpoint_url", + "https://services.citra-emu.org/api/multiplayer/rooms") + .toString() + .toStdString(); Settings::values.citra_username = qt_config->value("citra_username").toString().toStdString(); Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString(); qt_config->endGroup(); @@ -225,6 +232,18 @@ void Config::ReadValues() { UISettings::values.first_start = qt_config->value("firstStart", true).toBool(); UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt(); + qt_config->beginGroup("Multiplayer"); + UISettings::values.nickname = qt_config->value("nickname", "").toString(); + UISettings::values.ip = qt_config->value("ip", "").toString(); + UISettings::values.port = qt_config->value("port", Network::DefaultRoomPort).toString(); + UISettings::values.room_nickname = qt_config->value("room_nickname", "").toString(); + UISettings::values.room_name = qt_config->value("room_name", "").toString(); + UISettings::values.room_port = qt_config->value("room_port", 24872).toString(); + UISettings::values.host_type = qt_config->value("host_type", 0).toString(); + UISettings::values.max_player = qt_config->value("max_player", 8).toUInt(); + UISettings::values.game_id = qt_config->value("game_id", 0).toULongLong(); + qt_config->endGroup(); + qt_config->endGroup(); } @@ -320,6 +339,9 @@ void Config::SaveValues() { QString::fromStdString(Settings::values.telemetry_endpoint_url)); qt_config->setValue("verify_endpoint_url", QString::fromStdString(Settings::values.verify_endpoint_url)); + qt_config->setValue( + "announce_multiplayer_room_endpoint_url", + QString::fromStdString(Settings::values.announce_multiplayer_room_endpoint_url)); qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username)); qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token)); qt_config->endGroup(); @@ -366,6 +388,18 @@ void Config::SaveValues() { qt_config->setValue("firstStart", UISettings::values.first_start); qt_config->setValue("calloutFlags", UISettings::values.callout_flags); + qt_config->beginGroup("Multiplayer"); + qt_config->setValue("nickname", UISettings::values.nickname); + qt_config->setValue("ip", UISettings::values.ip); + qt_config->setValue("port", UISettings::values.port); + qt_config->setValue("room_nickname", UISettings::values.room_nickname); + qt_config->setValue("room_name", UISettings::values.room_name); + qt_config->setValue("room_port", UISettings::values.room_port); + qt_config->setValue("host_type", UISettings::values.host_type); + qt_config->setValue("max_player", UISettings::values.max_player); + qt_config->setValue("game_id", UISettings::values.game_id); + qt_config->endGroup(); + qt_config->endGroup(); } diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 401cd9d98..1bd387b75 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -374,6 +374,10 @@ void GameList::LoadCompatibilityList() { } } +QStandardItemModel* GameList::GetModel() const { + return item_model; +} + void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { if (!FileUtil::Exists(dir_path.toStdString()) || !FileUtil::IsDirectory(dir_path.toStdString())) { diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index c01dc1d21..376dc8474 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -76,6 +76,8 @@ public: void SaveInterfaceLayout(); void LoadInterfaceLayout(); + QStandardItemModel* GetModel() const; + static const QStringList supported_file_extensions; signals: diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 6709874d5..475093b13 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -31,8 +31,14 @@ #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" #include "citra_qt/main.h" +#include "citra_qt/multiplayer/client_room.h" +#include "citra_qt/multiplayer/direct_connect.h" +#include "citra_qt/multiplayer/host_room.h" +#include "citra_qt/multiplayer/lobby.h" +#include "citra_qt/multiplayer/message.h" #include "citra_qt/ui_settings.h" #include "citra_qt/updater/updater.h" +#include "citra_qt/util/clickable_label.h" #include "common/logging/backend.h" #include "common/logging/filter.h" #include "common/logging/log.h" @@ -129,6 +135,20 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { SetupUIStrings(); + Network::Init(); + + if (auto member = Network::GetRoomMember().lock()) { + // register the network structs to use in slots and signals + qRegisterMetaType(); + state_callback_handle = member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &GMainWindow::NetworkStateChanged, this, &GMainWindow::OnNetworkStateChanged); + } + + qRegisterMetaType(); + + setWindowTitle(QString("Citra %1| %2-%3") + .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); show(); game_list->LoadCompatibilityList(); @@ -153,6 +173,13 @@ GMainWindow::~GMainWindow() { delete render_window; Pica::g_debug_context.reset(); + + if (state_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(state_callback_handle); + } + } + Network::Shutdown(); } void GMainWindow::InitializeWidgets() { @@ -193,12 +220,22 @@ void GMainWindow::InitializeWidgets() { tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); + announce_multiplayer_session = std::make_shared(); + announce_multiplayer_session->BindErrorCallback( + [this](const Common::WebResult& result) { emit AnnounceFailed(result); }); + connect(this, &GMainWindow::AnnounceFailed, this, &GMainWindow::OnAnnounceFailed); + network_status = new ClickableLabel(); + network_status->setToolTip(tr("Current connection status")); + for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); label->setFrameStyle(QFrame::NoFrame); label->setContentsMargins(4, 0, 4, 0); statusBar()->addPermanentWidget(label, 0); } + statusBar()->addPermanentWidget(network_status, 0); + network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + network_status->setText(tr("Not Connected. Join a room for online play!")); statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux @@ -392,6 +429,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + + connect(network_status, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); } void GMainWindow::ConnectMenuEvents() { @@ -418,6 +457,15 @@ void GMainWindow::ConnectMenuEvents() { ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F")); connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar); connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui.action_View_Lobby, &QAction::triggered, this, &GMainWindow::OnViewLobby); + connect(ui.action_Start_Room, &QAction::triggered, this, &GMainWindow::OnCreateRoom); + connect(ui.action_Stop_Room, &QAction::triggered, this, &GMainWindow::OnCloseRoom); + connect(ui.action_Connect_To_Room, &QAction::triggered, this, + &GMainWindow::OnDirectConnectToRoom); + connect(ui.action_Chat, &QAction::triggered, this, &GMainWindow::OnOpenNetworkRoom); + ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key()); ui.action_Screen_Layout_Swap_Screens->setShortcut( GetHotkey("Main Window", "Swap Screens", this)->key()); @@ -880,6 +928,30 @@ void GMainWindow::OnMenuRecentFile() { } } +void GMainWindow::OnNetworkStateChanged(const Network::RoomMember::State& state) { + LOG_INFO(Frontend, "network state change"); + if (state == Network::RoomMember::State::Joined) { + network_status->setPixmap(QIcon::fromTheme("connected").pixmap(16)); + network_status->setText(tr("Connected")); + ui.action_Chat->setEnabled(true); + return; + } + network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + network_status->setText(tr("Not Connected")); + ui.action_Chat->setDisabled(true); + + ChangeRoomState(); +} + +void GMainWindow::OnAnnounceFailed(const Common::WebResult& result) { + announce_multiplayer_session->Stop(); + QMessageBox::warning( + this, tr("Error"), + tr("Announcing the room failed.\nThe room will not get listed publicly.\nError: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); +} + void GMainWindow::OnStartGame() { emu_thread->SetRunning(true); qRegisterMetaType("Core::System::ResultStatus"); @@ -1054,6 +1126,80 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { graphicsSurfaceViewerWidget->show(); } +static void BringWidgetToFront(QWidget* widget) { + widget->show(); + widget->activateWindow(); + widget->raise(); +} + +void GMainWindow::OnViewLobby() { + if (lobby == nullptr) { + lobby = new Lobby(this, game_list->GetModel(), announce_multiplayer_session); + connect(lobby, &Lobby::Closed, [&] { + LOG_INFO(Frontend, "Destroying lobby"); + // lobby->close(); + lobby = nullptr; + }); + } + BringWidgetToFront(lobby); +} + +void GMainWindow::OnCreateRoom() { + if (host_room == nullptr) { + host_room = new HostRoomWindow(this, game_list->GetModel(), announce_multiplayer_session); + connect(host_room, &HostRoomWindow::Closed, [&] { + // host_room->close(); + LOG_INFO(Frontend, "Destroying host room"); + host_room = nullptr; + }); + } + BringWidgetToFront(host_room); +} + +void GMainWindow::OnCloseRoom() { + if (auto room = Network::GetRoom().lock()) { + if (room->GetState() == Network::Room::State::Open) { + if (NetworkMessage::WarnCloseRoom()) { + room->Destroy(); + announce_multiplayer_session->Stop(); + // host_room->close(); + } + } + } +} + +void GMainWindow::OnOpenNetworkRoom() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (client_room == nullptr) { + client_room = new ClientRoomWindow(this); + connect(client_room, &ClientRoomWindow::Closed, [&] { + LOG_INFO(Frontend, "Destroying client room"); + // client_room->close(); + client_room = nullptr; + }); + } + 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 GMainWindow::OnDirectConnectToRoom() { + if (direct_connect == nullptr) { + direct_connect = new DirectConnectWindow(this); + connect(direct_connect, &DirectConnectWindow::Closed, [&] { + LOG_INFO(Frontend, "Destroying direct connect"); + // direct_connect->close(); + direct_connect = nullptr; + }); + } + BringWidgetToFront(direct_connect); +} + void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); @@ -1187,6 +1333,16 @@ void GMainWindow::closeEvent(QCloseEvent* event) { render_window->close(); + // Close Multiplayer windows + if (host_room) + host_room->close(); + if (direct_connect) + direct_connect->close(); + if (client_room) + client_room->close(); + if (lobby) + lobby->close(); + QWidget::closeEvent(event); } @@ -1306,6 +1462,18 @@ void GMainWindow::SyncMenuUISettings() { ui.action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen); } +void GMainWindow::ChangeRoomState() { + if (auto room = Network::GetRoom().lock()) { + if (room->GetState() == Network::Room::State::Open) { + ui.action_Start_Room->setDisabled(true); + ui.action_Stop_Room->setEnabled(true); + return; + } + ui.action_Start_Room->setEnabled(true); + ui.action_Stop_Room->setDisabled(true); + } +} + #ifdef main #undef main #endif diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index c29c0ccfc..315631da2 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -5,15 +5,19 @@ #pragma once #include +#include #include #include #include +#include "common/announce_multiplayer_room.h" #include "core/core.h" #include "core/hle/service/am/am.h" +#include "network/network.h" #include "ui_main.h" class AboutDialog; class Config; +class ClickableLabel; class EmuThread; class GameList; enum class GameListOpenTarget; @@ -33,6 +37,16 @@ class RegistersWidget; class Updater; class WaitTreeWidget; +// Multiplayer forward declarations +class Lobby; +class HostRoomWindow; +class ClientRoomWindow; +class DirectConnectWindow; + +namespace Core { +class AnnounceMultiplayerSession; +} + class GMainWindow : public QMainWindow { Q_OBJECT @@ -50,6 +64,9 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); + void ChangeRoomState(); + + GameList* game_list; GMainWindow(); ~GMainWindow(); @@ -77,6 +94,16 @@ signals: // Signal that tells widgets to update icons to use the current theme void UpdateThemedIcons(); + void NetworkStateChanged(const Network::RoomMember::State&); + void AnnounceFailed(const Common::WebResult&); + +public slots: + void OnViewLobby(); + void OnCreateRoom(); + void OnCloseRoom(); + void OnOpenNetworkRoom(); + void OnDirectConnectToRoom(); + private: void InitializeWidgets(); void InitializeDebugWidgets(); @@ -146,6 +173,8 @@ private slots: /// Called whenever a user selects the "File->Select Game List Root" menu item void OnMenuSelectGameListRoot(); void OnMenuRecentFile(); + void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnAnnounceFailed(const Common::WebResult&); void OnConfigure(); void OnToggleFilterBar(); void OnDisplayTitleBars(bool); @@ -173,7 +202,8 @@ private: Ui::MainWindow ui; GRenderWindow* render_window; - GameList* game_list; + + QFutureWatcher* watcher = nullptr; // Status bar elements QProgressBar* progress_bar = nullptr; @@ -181,9 +211,11 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; + ClickableLabel* network_status = nullptr; QTimer status_bar_update_timer; std::unique_ptr config; + std::shared_ptr announce_multiplayer_session; // Whether emulation is currently running in Citra. bool emulation_running = false; @@ -204,6 +236,14 @@ private: bool explicit_update_check = false; bool defer_update_prompt = false; + // Multiplayer windows + Lobby* lobby = nullptr; + HostRoomWindow* host_room = nullptr; + ClientRoomWindow* client_room = nullptr; + DirectConnectWindow* direct_connect = nullptr; + + Network::RoomMember::CallbackHandle state_callback_handle; + QAction* actions_recent_files[max_recent_files_item]; QTranslator translator; @@ -219,3 +259,4 @@ protected: Q_DECLARE_METATYPE(size_t); Q_DECLARE_METATYPE(Service::AM::InstallStatus); +Q_DECLARE_METATYPE(Common::WebResult); diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index c598c444f..d7ffc6b06 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -45,7 +45,7 @@ 0 0 1081 - 26 + 21 @@ -107,6 +107,20 @@ + + + true + + + Multiplayer + + + + + + + + &Help @@ -122,6 +136,7 @@ + @@ -228,6 +243,43 @@ Create Pica Surface Viewer + + + true + + + Browse Public Game Lobby + + + + + true + + + Create Room + + + + + false + + + Close Room + + + + + Direct Connect to Room + + + + + false + + + Current Room + + true @@ -244,46 +296,46 @@ Opens the maintenance tool to modify your Citra installation - - - true - - - Default - - - - - true - - - Single Screen - - - - - true - - - Large Screen - - - - - true - - - Side by Side - - - - - true - - - Swap Screens - - + + + true + + + Default + + + + + true + + + Single Screen + + + + + true + + + Large Screen + + + + + true + + + Side by Side + + + + + true + + + Swap Screens + + Check for Updates diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp new file mode 100644 index 000000000..46c477eae --- /dev/null +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -0,0 +1,208 @@ +// 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 "citra_qt/game_list_p.h" +#include "citra_qt/multiplayer/chat_room.h" +#include "citra_qt/multiplayer/message.h" +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_chat_room.h" + +class ChatMessage { +public: + explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) { + /// Convert the time to their default locale defined format + static QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + nickname = QString::fromStdString(chat.nickname); + message = QString::fromStdString(chat.message); + } + + /// Format the message using the players color + QString GetPlayerChatMessage(u16 player) const { + auto color = player_color[player % 16]; + return QString("[%1] <%3> %4") + .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped()); + } + +private: + ChatMessage() {} + const QList player_color = { + {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", + "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + QString timestamp; + QString nickname; + QString message; +}; + +class StatusMessage { +public: + explicit StatusMessage(const QString& msg, QTime ts = {}) { + /// Convert the time to their default locale defined format + static QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + message = msg; + } + + QString GetSystemChatMessage() const { + return QString("[%1] %3") + .arg(timestamp, system_color, message); + } + +private: + const QString system_color = "#888888"; + QString timestamp; + QString message; +}; + +ChatRoom::ChatRoom(QWidget* parent) : ui(new Ui::ChatRoom) { + ui->setupUi(this); + + // set the item_model for player_view + enum { + COLUMN_NAME, + COLUMN_GAME, + COLUMN_COUNT, // Number of columns + }; + + player_list = new QStandardItemModel(ui->player_view); + ui->player_view->setModel(player_list); + player_list->insertColumns(0, COLUMN_COUNT); + player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); + player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game")); + + ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); + + // register the network structs to use in slots and signals + 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); }); + connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + } else { + // TODO (jroweboy) network was not initialized? + } + + // Connect all the widgets to the appropriate events + connect(ui->chat_message, &QLineEdit::returnPressed, ui->send_message, &QPushButton::pressed); + connect(ui->chat_message, &QLineEdit::textChanged, this, &::ChatRoom::OnChatTextChanged); + connect(ui->send_message, &QPushButton::pressed, this, &ChatRoom::OnSendChat); +} + +void ChatRoom::Clear() { + ui->chat_history->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); +} + +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; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); + return; + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + AppendChatMessage(m.GetPlayerChatMessage(player)); + } +} + +void ChatRoom::OnSendChat() { + if (auto room = Network::GetRoomMember().lock()) { + if (room->GetState() == Network::RoomMember::State::Joined) { + auto message = ui->chat_message->text().toStdString(); + if (!ValidateMessage(message)) { + return; + } + auto nick = room->GetNickname(); + Network::ChatEntry chat{nick, 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; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player"); + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + room->SendChatMessage(message); + AppendChatMessage(m.GetPlayerChatMessage(player)); + ui->chat_message->clear(); + } + } +} + +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 == "") + continue; + QList l; + std::vector elements = {member.nickname, member.game_info.name}; + for (auto& item : elements) { + QStandardItem* child = new QStandardItem(QString::fromStdString(item)); + child->setEditable(false); + l.append(child); + } + player_list->invisibleRootItem()->appendRow(l); + } + // TODO(B3N30): Restore row selection +} + +void ChatRoom::OnChatTextChanged() { + if (ui->chat_message->text().length() > Network::MaxMessageSize) + ui->chat_message->setText(ui->chat_message->text().left(Network::MaxMessageSize)); +} diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h new file mode 100644 index 000000000..8c07654cc --- /dev/null +++ b/src/citra_qt/multiplayer/chat_room.h @@ -0,0 +1,57 @@ +// 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" + +namespace Ui { +class ChatRoom; +} // namespace Ui + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +class ChatRoom : public QWidget { + Q_OBJECT + +public: + explicit ChatRoom(QWidget* parent); + void SetPlayerList(const Network::RoomMember::MemberList& member_list); + void Clear(); + void AppendStatusMessage(const QString& msg); + +public slots: + void OnRoomUpdate(const Network::RoomInformation& info); + void OnChatReceive(const Network::ChatEntry&); + void OnSendChat(); + void OnChatTextChanged(); + void Disable(); + void Enable(); + +signals: + void ChatReceived(const Network::ChatEntry&); + +private: + const u32 max_chat_lines = 1000; + void AppendChatMessage(const QString&); + bool ValidateMessage(const std::string&); + QStandardItemModel* player_list; + Ui::ChatRoom* ui; +}; + +Q_DECLARE_METATYPE(Network::ChatEntry); +Q_DECLARE_METATYPE(Network::RoomInformation); +Q_DECLARE_METATYPE(Network::RoomMember::State); diff --git a/src/citra_qt/multiplayer/chat_room.ui b/src/citra_qt/multiplayer/chat_room.ui new file mode 100644 index 000000000..8bb1899c0 --- /dev/null +++ b/src/citra_qt/multiplayer/chat_room.ui @@ -0,0 +1,59 @@ + + + ChatRoom + + + + 0 + 0 + 607 + 432 + + + + Room Window + + + + + + + + + + + false + + + true + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Send Chat Message + + + + + + + Send Message + + + + + + + + + + + + diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp new file mode 100644 index 000000000..8baafb418 --- /dev/null +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -0,0 +1,121 @@ +// 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 "citra_qt/game_list_p.h" +#include "citra_qt/multiplayer/client_room.h" +#include "citra_qt/multiplayer/message.h" +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_client_room.h" + +ClientRoomWindow::ClientRoomWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(new Ui::ClientRoom) { + 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); + } else { + // TODO (jroweboy) network was not initialized? + } + + connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); }); + ui->disconnect->setDefault(false); + ui->disconnect->setAutoDefault(false); + UpdateView(); +} + +void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { + UpdateView(); +} + +void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { + switch (state) { + case Network::RoomMember::State::Idle: + LOG_INFO(Network, "State: Idle"); + break; + case Network::RoomMember::State::Joining: + LOG_INFO(Network, "State: Joining"); + break; + case Network::RoomMember::State::Joined: + LOG_INFO(Network, "State: Joined"); + ui->chat->Clear(); + ui->chat->AppendStatusMessage(tr("Connected")); + break; + case Network::RoomMember::State::LostConnection: + NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); + LOG_INFO(Network, "State: LostConnection"); + break; + case Network::RoomMember::State::CouldNotConnect: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + LOG_INFO(Network, "State: CouldNotConnect"); + break; + case Network::RoomMember::State::NameCollision: + NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); + LOG_INFO(Network, "State: NameCollision"); + break; + case Network::RoomMember::State::MacCollision: + NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); + LOG_INFO(Network, "State: MacCollision"); + break; + case Network::RoomMember::State::WrongPassword: + NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); + LOG_INFO(Network, "State: WrongPassword"); + break; + case Network::RoomMember::State::WrongVersion: + NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); + LOG_INFO(Network, "State: WrongVersion"); + break; + default: + break; + } + UpdateView(); +} + +void ClientRoomWindow::Disconnect() { + if (!NetworkMessage::WarnDisconnect()) { + return; + } + if (auto member = Network::GetRoomMember().lock()) { + member->Leave(); + ui->chat->AppendStatusMessage(tr("Disconnected")); + } +} + +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)); + return; + } + } + // TODO(B3N30): can't get RoomMember*, show error and close window + close(); + emit Closed(); + return; +} diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h new file mode 100644 index 000000000..8d282b754 --- /dev/null +++ b/src/citra_qt/multiplayer/client_room.h @@ -0,0 +1,38 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "citra_qt/multiplayer/chat_room.h" + +namespace Ui { +class ClientRoom; +} + +class ClientRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit ClientRoomWindow(QWidget* parent); + +public slots: + void OnRoomUpdate(const Network::RoomInformation&); + void OnStateChange(const Network::RoomMember::State&); + +signals: + /** + * Signalled by this widget when it is closing itself and destroying any state such as + * connections that it might have. + */ + void Closed(); + void RoomInformationChanged(const Network::RoomInformation&); + void StateChanged(const Network::RoomMember::State&); + +private: + void Disconnect(); + void UpdateView(); + + QStandardItemModel* player_list; + Ui::ClientRoom* ui; +}; diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui new file mode 100644 index 000000000..d83c088c2 --- /dev/null +++ b/src/citra_qt/multiplayer/client_room.ui @@ -0,0 +1,63 @@ + + + ClientRoom + + + + 0 + 0 + 607 + 432 + + + + Room Window + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Leave Room + + + + + + + + + + + + + + + ChatRoom + QWidget +
multiplayer/chat_room.h
+ 1 +
+
+ + +
diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp new file mode 100644 index 000000000..9d68d96d1 --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -0,0 +1,132 @@ +// 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 "citra_qt/main.h" +#include "citra_qt/multiplayer/client_room.h" +#include "citra_qt/multiplayer/direct_connect.h" +#include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/validation.h" +#include "citra_qt/ui_settings.h" +#include "core/settings.h" +#include "network/network.h" +#include "ui_direct_connect.h" + +enum class ConnectionType : u8 { TRAVERSAL_SERVER, IP }; + +DirectConnectWindow::DirectConnectWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(new Ui::DirectConnect) { + + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); + + ui->nickname->setValidator(Validation::nickname); + ui->nickname->setText(UISettings::values.nickname); + ui->ip->setValidator(Validation::ip); + ui->ip->setText(UISettings::values.ip); + ui->port->setValidator(Validation::port); + 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::pressed, this, &DirectConnectWindow::Connect); +} + +void DirectConnectWindow::Connect() { + ClearAllError(); + bool isValid = true; + if (!ui->nickname->hasAcceptableInput()) { + isValid = false; + ShowError(NetworkMessage::USERNAME_NOT_VALID); + } + if (const auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + switch (static_cast(ui->connection_type->currentIndex())) { + case ConnectionType::TRAVERSAL_SERVER: + break; + case ConnectionType::IP: + if (!ui->ip->hasAcceptableInput()) { + isValid = false; + NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID); + } + if (!ui->port->hasAcceptableInput()) { + isValid = false; + NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID); + } + break; + } + + if (!isValid) { + return; + } + + // 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; + Settings::Apply(); + + // 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::ClearAllError() {} + +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(); + + bool isConnected = true; + if (auto room_member = Network::GetRoomMember().lock()) { + switch (room_member->GetState()) { + case Network::RoomMember::State::CouldNotConnect: + isConnected = false; + ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::NameCollision: + isConnected = false; + ShowError(NetworkMessage::USERNAME_IN_USE); + break; + case Network::RoomMember::State::Joining: + auto parent = static_cast(parentWidget()); + parent->OnOpenNetworkRoom(); + close(); + } + } +} diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h new file mode 100644 index 000000000..6d9266601 --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.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 +#include +#include + +namespace Ui { +class DirectConnect; +} + +class DirectConnectWindow : public QDialog { + Q_OBJECT + +public: + explicit DirectConnectWindow(QWidget* parent = nullptr); + +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 ClearAllError(); + void BeginConnecting(); + void EndConnecting(); + + QFutureWatcher* watcher; + Ui::DirectConnect* ui; +}; diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/citra_qt/multiplayer/direct_connect.ui new file mode 100644 index 000000000..81aac7431 --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.ui @@ -0,0 +1,190 @@ + + + DirectConnect + + + + 0 + 0 + 455 + 239 + + + + Direct Connect + + + + + + + + + + Instructions + + + + + + <html><head/><body><p>Directly connect to a friend by <span style=" font-weight:600;">Traversal server</span> or by<span style=" font-weight:600;"> IP address</span>. </p><p>To use the <span style=" font-weight:600;">Traversal Server</span>, ask the game host for their &quot;<span style=" font-weight:600;">Host Code</span>&quot; which will be visible on the create room screen after it is created.</p></body></html> + + + Qt::RichText + + + true + + + + + + + + + + 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/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp new file mode 100644 index 000000000..02122f445 --- /dev/null +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -0,0 +1,170 @@ +// 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 "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/validation.h" +#include "citra_qt/ui_settings.h" +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "core/settings.h" +#include "ui_chat_room.h" +#include "ui_host_room.h" + +HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(new Ui::HostRoom), announce_multiplayer_session(session), game_list(list) { + ui->setupUi(this); + + // set up validation for all of the fields + ui->room_name->setValidator(Validation::room_name); + ui->username->setValidator(Validation::nickname); + ui->port->setValidator(Validation::port); + ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort)); + + // Create a proxy to the game list to display the list of preferred games + 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::pressed, this, &HostRoomWindow::Host); + + // Restore the settings: + ui->username->setText(UISettings::values.room_nickname); + 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 = ui->host_type->findData(UISettings::values.host_type); + if (index != -1) { + ui->host_type->setCurrentIndex(index); + } + index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole); + if (index != -1) { + ui->game_list->setCurrentIndex(index); + } +} + +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 (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (!NetworkMessage::WarnDisconnect()) { + close(); + return; + } else { + member->Leave(); + } + } + 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(); + if (auto room = Network::GetRoom().lock()) { + bool created = room->Create(ui->room_name->text().toStdString(), "", port, password, + ui->max_player->value(), game_name.toStdString(), game_id); + if (!created) { + NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); + LOG_ERROR(Network, "Could not create room!"); + ui->host->setEnabled(true); + return; + } + } + member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, + Network::NoPreferredMac, password); + + // 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->currentText(); + UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) + ? ui->port->text() + : QString::number(Network::DefaultRoomPort); + Settings::Apply(); + OnConnection(); + } +} + +void HostRoomWindow::OnConnection() { + ui->host->setEnabled(true); + if (auto room_member = Network::GetRoomMember().lock()) { + switch (room_member->GetState()) { + case Network::RoomMember::State::CouldNotConnect: + ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::NameCollision: + ShowError(NetworkMessage::USERNAME_IN_USE); + break; + case Network::RoomMember::State::Error: + ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::Joining: + if (ui->host_type->currentIndex() == 0) { + if (auto session = announce_multiplayer_session.lock()) { + session->Start(); + } else { + LOG_ERROR(Network, "Starting announce session failed"); + } + } + auto parent = static_cast(parentWidget()); + parent->ChangeRoomState(); + parent->OnOpenNetworkRoom(); + close(); + emit Closed(); + break; + } + } +} + +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 { + // TODO(jroweboy): Sort by game title not filename + auto leftData = left.data(Qt::DisplayRole).toString(); + auto rightData = right.data(Qt::DisplayRole).toString(); + return leftData.compare(rightData) < 0; +} diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h new file mode 100644 index 000000000..289dbb040 --- /dev/null +++ b/src/citra_qt/multiplayer/host_room.h @@ -0,0 +1,73 @@ +// 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 "citra_qt/multiplayer/chat_room.h" +#include "network/network.h" + +namespace Ui { +class HostRoom; +} // namespace Ui + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +class HostRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session); + +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: + /** + * Handler for connection status changes. Launches the chat window if successful or + * displays an error + */ + void OnConnection(); + +private: + void Host(); + + std::weak_ptr announce_multiplayer_session; + QStandardItemModel* game_list; + ComboBoxProxyModel* proxy; + Ui::HostRoom* ui; +}; + +/** + * 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/citra_qt/multiplayer/host_room.ui b/src/citra_qt/multiplayer/host_room.ui new file mode 100644 index 000000000..7edf90628 --- /dev/null +++ b/src/citra_qt/multiplayer/host_room.ui @@ -0,0 +1,179 @@ + + + HostRoom + + + + 0 + 0 + 607 + 165 + + + + Create Room + + + + + + + 0 + + + 0 + + + 0 + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Room Name + + + + + + + 50 + + + + + + + Preferred Game + + + + + + + + + + Max Players + + + + + + + 1 + + + 16 + + + 8 + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Username + + + + + + + QLineEdit::PasswordEchoOnEdit + + + (Leave blank for open game) + + + + + + + Qt::ImhDigitsOnly + + + 5 + + + + + + + Password + + + + + + + Port + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Public + + + + + Unlisted + + + + + + + + Host Room + + + + + + + + + + diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp new file mode 100644 index 000000000..c20c7d59f --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -0,0 +1,314 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/game_list_p.h" +#include "citra_qt/main.h" +#include "citra_qt/multiplayer/client_room.h" +#include "citra_qt/multiplayer/lobby.h" +#include "citra_qt/multiplayer/lobby_p.h" +#include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/validation.h" +#include "citra_qt/ui_settings.h" +#include "common/logging/log.h" +#include "core/settings.h" +#include "network/network.h" +#include "ui_lobby.h" + +Lobby::Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(new Ui::Lobby), announce_multiplayer_session(session), game_list(list) { + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher; + connect(watcher, &QFutureWatcher::finished, this, &Lobby::OnConnection); + + model = new QStandardItemModel(ui->room_list); + proxy = new LobbyFilterProxyModel(this, game_list); + proxy->setSourceModel(model); + proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy->setSortLocaleAware(true); + ui->room_list->setModel(proxy); + ui->room_list->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + 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->setUniformRowHeights(true); + ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->nickname->setValidator(Validation::nickname); + ui->nickname->setText(UISettings::values.nickname); + + // UI Buttons + GMainWindow* p = reinterpret_cast(parent); + connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby); + connect(ui->chat, &QPushButton::pressed, p, &GMainWindow::OnOpenNetworkRoom); + connect(ui->games_owned, &QCheckBox::stateChanged, proxy, + &LobbyFilterProxyModel::SetFilterOwned); + connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull); + connect(ui->search, &QLineEdit::textChanged, proxy, + &LobbyFilterProxyModel::setFilterFixedString); + connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom); + + // Actions + connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby); + // TODO(jroweboy): change this slot to OnConnected? + connect(this, &Lobby::Connected, p, &GMainWindow::OnOpenNetworkRoom); + + // setup the callbacks for network updates + if (auto member = Network::GetRoomMember().lock()) { + member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit StateChanged(state); }); + connect(this, &Lobby::StateChanged, this, &Lobby::OnStateChanged); + } else { + // TODO (jroweboy) network was not initialized? + } + + // 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(); + + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + ui->chat->setEnabled(true); + return; + } + } + ui->chat->setDisabled(true); +} + +Lobby::~Lobby() {} + +const QString Lobby::PasswordPrompt() { + bool ok; + const QString text = + QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"), + QLineEdit::Normal, tr("Password"), &ok); + return ok ? text : QString(); +} + +void Lobby::OnJoinRoom(const QModelIndex& index) { + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); + return; + } + if (const auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + + // Get a password to pass if the room is password protected + QModelIndex password_index = proxy->index(index.row(), Column::PASSWORD); + bool has_password = proxy->data(password_index, LobbyItemPassword::PasswordRole).toBool(); + const std::string password = has_password ? PasswordPrompt().toStdString() : ""; + if (has_password && password.empty()) { + return; + } + + // attempt to connect in a different thread + QFuture f = QtConcurrent::run([&, password] { + if (auto room_member = Network::GetRoomMember().lock()) { + + 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(); + room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredMac, password); + } + }); + watcher->setFuture(f); + // and disable widgets and display a connecting while we wait + QModelIndex connection_index = proxy->index(index.row(), Column::HOST); + + // 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(); + Settings::Apply(); +} + +void Lobby::OnStateChanged(const Network::RoomMember::State& state) { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + ui->chat->setEnabled(true); + return; + } + } + ui->chat->setDisabled(true); +} + +void Lobby::ResetModel() { + model->clear(); + model->insertColumns(0, Column::TOTAL); + model->setHeaderData(Column::PASSWORD, Qt::Horizontal, tr("Password"), 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); + ui->room_list->header()->stretchLastSection(); +} + +void Lobby::RefreshLobby() { + if (auto session = announce_multiplayer_session.lock()) { + ResetModel(); + room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); }); + ui->refresh_list->setEnabled(false); + ui->refresh_list->setText(tr("Refreshing")); + } else { + // TODO(jroweboy): Display an error box about announce couldn't be started + } +} + +void Lobby::OnRefreshLobby() { + AnnounceMultiplayerRoom::RoomList new_room_list = room_list_future.get(); + 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 = QModelIndex(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.name), member.game_id, + QString::fromStdString(member.game_name)}); + members.append(var); + } + + model->appendRow(QList( + {new LobbyItemPassword(room.has_password), + new LobbyItemName(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), + new LobbyItemMemberList(members, room.max_player)})); + } + ui->refresh_list->setEnabled(true); + ui->refresh_list->setText(tr("Refresh List")); +} + +LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list) + : QSortFilterProxyModel(parent), game_list(list) {} + +bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { + // Prioritize filters by fastest to compute + + // 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 + auto search_param = filterRegExp(); + if (!search_param.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(search_param); + bool room_name_match = sourceModel() + ->data(room_name, LobbyItemName::NameRole) + .toString() + .contains(search_param); + bool username_match = sourceModel() + ->data(host_name, LobbyItemHost::HostUsernameRole) + .toString() + .contains(search_param); + 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; + invalidateFilter(); +} + +void LobbyFilterProxyModel::SetFilterFull(bool filter) { + filter_full = filter; + invalidateFilter(); +} + +void Lobby::OnConnection() { + if (auto room_member = Network::GetRoomMember().lock()) { + switch (room_member->GetState()) { + case Network::RoomMember::State::CouldNotConnect: + ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::NameCollision: + ShowError(NetworkMessage::USERNAME_IN_USE); + break; + case Network::RoomMember::State::Error: + ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::Joining: + auto parent = static_cast(parentWidget()); + parent->OnOpenNetworkRoom(); + close(); + break; + } + } +} diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h new file mode 100644 index 000000000..c8ec502f9 --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.h @@ -0,0 +1,125 @@ +// 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 "core/announce_multiplayer_session.h" +#include "network/network.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(); + +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 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&); + + /** + * Handler for connection status changes. Launches the client room window if successful or + * displays an error + */ + void OnConnection(); + + void OnStateChanged(const Network::RoomMember::State&); + +signals: + /** + * Signalled when the latest lobby data is retrieved. + */ + void LobbyRefreshed(); + + /** + * Signalled when the status for room connection changes. + */ + void Connected(); + + /** + * Signalled by this widget when it is closing itself and destroying any state such as + * connections that it might have. + */ + void Closed(); + + 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. + */ + const QString PasswordPrompt(); + + QStandardItemModel* model; + QStandardItemModel* game_list; + LobbyFilterProxyModel* proxy; + + std::future room_list_future; + std::weak_ptr announce_multiplayer_session; + std::unique_ptr ui; + QFutureWatcher* watcher; +}; + +/** + * Proxy Model for filtering the lobby + */ +class LobbyFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT; + +public: + explicit LobbyFilterProxyModel(QWidget* parent, 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); + +private: + QStandardItemModel* game_list; + bool filter_owned = false; + bool filter_full = false; +}; diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui new file mode 100644 index 000000000..e5489b18a --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.ui @@ -0,0 +1,138 @@ + + + Lobby + + + + 0 + 0 + 707 + 487 + + + + Public Room Browser + + + + + + 3 + + + + + 6 + + + + + Nickname + + + false + + + + 6 + + + 1 + + + 6 + + + 4 + + + + + Nickname + + + + + + + + + + Filters + + + false + + + + 6 + + + 1 + + + 6 + + + 4 + + + + + Search + + + true + + + + + + + Games I Own + + + + + + + Hide Full Games + + + + + + + + + + + + Refresh Lobby + + + + + + + Chat + + + + + + + + + + + + + + + + + + + + diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h new file mode 100644 index 000000000..a8a429f12 --- /dev/null +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -0,0 +1,189 @@ +// 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 "common/common_types.h" + +namespace Column { +enum List { + PASSWORD, + ROOM_NAME, + GAME_NAME, + HOST, + MEMBER, + TOTAL, +}; +} + +class LobbyItem : public QStandardItem { +public: + LobbyItem() : QStandardItem() {} + LobbyItem(const QString& string) : QStandardItem(string) {} + virtual ~LobbyItem() override {} +}; + +class LobbyItemPassword : public LobbyItem { +public: + static const int PasswordRole = Qt::UserRole + 1; + LobbyItemPassword() : LobbyItem() {} + LobbyItemPassword(const bool has_password) : LobbyItem() { + setData(has_password, PasswordRole); + } + + QVariant data(int role) const override { + if (role != Qt::DecorationRole) { + return LobbyItem::data(role); + } + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon(":/icons/lock.png") : QIcon(); + } + + bool operator<(const QStandardItem& other) const override { + return data(PasswordRole).toBool() < other.data(PasswordRole).toBool(); + } +}; + +class LobbyItemName : public LobbyItem { +public: + static const int NameRole = Qt::UserRole + 1; + LobbyItemName() : LobbyItem() {} + LobbyItemName(QString name) : LobbyItem() { + setData(name, NameRole); + } + + QVariant data(int role) const override { + 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 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() : LobbyItem() {} + LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) : LobbyItem() { + 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; + + LobbyItemHost() : LobbyItem() {} + LobbyItemHost(QString username, QString ip, u16 port) : LobbyItem() { + setData(username, HostUsernameRole); + setData(ip, HostIPRole); + setData(port, HostPortRole); + } + + 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() {} + LobbyMember(const LobbyMember& other) { + username = other.username; + title_id = other.title_id; + game_name = other.game_name; + } + LobbyMember(const QString username, u64 title_id, const QString game_name) + : username(username), title_id(title_id), game_name(game_name) {} + ~LobbyMember() {} + + QString GetUsername() const { + return username; + } + u64 GetTitleId() const { + return title_id; + } + QString GetGameName() const { + return game_name; + } + +private: + QString username; + 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() : LobbyItem() {} + LobbyItemMemberList(QList members, u32 max_players) : LobbyItem() { + 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 QString("%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; + } +}; diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp new file mode 100644 index 000000000..f74a57025 --- /dev/null +++ b/src/citra_qt/multiplayer/message.cpp @@ -0,0 +1,59 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "citra_qt/multiplayer/message.h" + +namespace NetworkMessage { +const ConnectionError USERNAME_NOT_VALID( + QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ROOMNAME_NOT_VALID( + QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError USERNAME_IN_USE( + QT_TR_NOOP("Username is already in use. Please choose another.")); +const ConnectionError IP_ADDRESS_NOT_VALID(QT_TR_NOOP("IP is not a valid IPv4 address.")); +const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535.")); +const ConnectionError NO_INTERNET( + QT_TR_NOOP("Unable to find an internet connection. Check your internet settings.")); +const ConnectionError UNABLE_TO_CONNECT( + QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct.")); +const ConnectionError COULD_NOT_CREATE_ROOM( + QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); +const ConnectionError 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 WRONG_VERSION( + QT_TR_NOOP("You are using a different version of Citra-Local-Wifi(tm) then the room " + "you are trying to connect to.")); +const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Wrong password.")); +const ConnectionError GENERIC_ERROR(QT_TR_NOOP("An error occured.")); +const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect.")); +const ConnectionError MAC_COLLISION( + QT_TR_NOOP("MAC-Address is already in use. Please choose another.")); + +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 ShowError(const ConnectionError& e) { + QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromStdString(e.GetString())); +} + +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/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h new file mode 100644 index 000000000..3a95c1081 --- /dev/null +++ b/src/citra_qt/multiplayer/message.h @@ -0,0 +1,53 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +namespace NetworkMessage { + +class ConnectionError { + +public: + explicit ConnectionError(const std::string& str) : err(str) {} + const std::string& GetString() const { + return err; + } + +private: + std::string err; +}; + +extern const ConnectionError USERNAME_NOT_VALID; +extern const ConnectionError ROOMNAME_NOT_VALID; +extern const ConnectionError USERNAME_IN_USE; +extern const ConnectionError IP_ADDRESS_NOT_VALID; +extern const ConnectionError PORT_NOT_VALID; +extern const ConnectionError NO_INTERNET; +extern const ConnectionError UNABLE_TO_CONNECT; +extern const ConnectionError COULD_NOT_CREATE_ROOM; +extern const ConnectionError HOST_BANNED; +extern const ConnectionError WRONG_VERSION; +extern const ConnectionError WRONG_PASSWORD; +extern const ConnectionError GENERIC_ERROR; +extern const ConnectionError LOST_CONNECTION; +extern const ConnectionError MAC_COLLISION; + +/** + * Shows a standard QMessageBox with a error message + */ +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/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h new file mode 100644 index 000000000..fa6777beb --- /dev/null +++ b/src/citra_qt/multiplayer/validation.h @@ -0,0 +1,28 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace Validation { +/// room name can be alphanumeric and " " "_" "." and "-" +static const QRegExp room_name_regex("^[a-zA-Z0-9._- ]+$"); +static const QValidator* room_name = new QRegExpValidator(room_name_regex); + +/// nickname can be alphanumeric and " " "_" "." and "-" +static const QRegExp nickname_regex("^[a-zA-Z0-9._- ]+$"); +static const QValidator* nickname = new QRegExpValidator(nickname_regex); + +/// ipv4 address only +// TODO remove this when we support hostnames in direct connect +static const QRegExp ip_regex( + "(([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])"); +static const QValidator* ip = new QRegExpValidator(ip_regex); + +/// port must be between 0 and 65535 +static const QValidator* port = new QIntValidator(0, 65535); +}; // namespace Validation diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index caf6aea6a..895a24c1b 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -56,8 +56,18 @@ struct Values { std::vector shortcuts; uint32_t callout_flags; + + // multiplayer settings + QString nickname; + QString ip; + QString port; + QString room_nickname; + QString room_name; + quint32 max_player; + QString room_port; + QString host_type; + qulonglong game_id; }; extern Values values; - } // namespace UISettings diff --git a/src/citra_qt/util/clickable_label.cpp b/src/citra_qt/util/clickable_label.cpp new file mode 100644 index 000000000..010413271 --- /dev/null +++ b/src/citra_qt/util/clickable_label.cpp @@ -0,0 +1,13 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "citra_qt/util/clickable_label.h" + +ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent) {} + +ClickableLabel::~ClickableLabel() {} + +void ClickableLabel::mouseReleaseEvent(QMouseEvent* event) { + emit clicked(); +} diff --git a/src/citra_qt/util/clickable_label.h b/src/citra_qt/util/clickable_label.h new file mode 100644 index 000000000..780a01bf6 --- /dev/null +++ b/src/citra_qt/util/clickable_label.h @@ -0,0 +1,23 @@ +// 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 ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel(); + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; From ddbbab8fd6203e18054205a14c82b3fb989dd6f4 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 30 Mar 2018 09:51:42 -0600 Subject: [PATCH 02/16] Add network status text to the status bar --- src/citra_qt/main.cpp | 23 +++++++++++++---------- src/citra_qt/main.h | 3 ++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 475093b13..47bc73d90 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -224,8 +224,9 @@ void GMainWindow::InitializeWidgets() { announce_multiplayer_session->BindErrorCallback( [this](const Common::WebResult& result) { emit AnnounceFailed(result); }); connect(this, &GMainWindow::AnnounceFailed, this, &GMainWindow::OnAnnounceFailed); - network_status = new ClickableLabel(); - network_status->setToolTip(tr("Current connection status")); + network_status_text = new ClickableLabel(this); + network_status_icon = new ClickableLabel(this); + network_status_text->setToolTip(tr("Current connection status")); for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); @@ -233,9 +234,10 @@ void GMainWindow::InitializeWidgets() { label->setContentsMargins(4, 0, 4, 0); statusBar()->addPermanentWidget(label, 0); } - statusBar()->addPermanentWidget(network_status, 0); - network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); - network_status->setText(tr("Not Connected. Join a room for online play!")); + statusBar()->addPermanentWidget(network_status_text, 0); + statusBar()->addPermanentWidget(network_status_icon, 0); + network_status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + network_status_text->setText(tr("Not Connected. Click here to find a room!")); statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux @@ -430,7 +432,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); - connect(network_status, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); + connect(network_status_text, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); + connect(network_status_icon, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); } void GMainWindow::ConnectMenuEvents() { @@ -931,13 +934,13 @@ void GMainWindow::OnMenuRecentFile() { void GMainWindow::OnNetworkStateChanged(const Network::RoomMember::State& state) { LOG_INFO(Frontend, "network state change"); if (state == Network::RoomMember::State::Joined) { - network_status->setPixmap(QIcon::fromTheme("connected").pixmap(16)); - network_status->setText(tr("Connected")); + network_status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); + network_status_text->setText(tr("Connected")); ui.action_Chat->setEnabled(true); return; } - network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); - network_status->setText(tr("Not Connected")); + network_status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + network_status_text->setText(tr("Not Connected")); ui.action_Chat->setDisabled(true); ChangeRoomState(); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 315631da2..27aad35ca 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -211,7 +211,8 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; - ClickableLabel* network_status = nullptr; + ClickableLabel* network_status_icon = nullptr; + ClickableLabel* network_status_text = nullptr; QTimer status_bar_update_timer; std::unique_ptr config; From f346a9d3728a20454a6e08ef3d47247a6951075b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 1 Apr 2018 00:06:48 -0600 Subject: [PATCH 03/16] Split multiplayer code into its own class --- src/citra_qt/CMakeLists.txt | 2 + src/citra_qt/main.cpp | 167 ++------------------ src/citra_qt/main.h | 36 +---- src/citra_qt/multiplayer/direct_connect.cpp | 3 +- src/citra_qt/multiplayer/host_room.cpp | 5 +- src/citra_qt/multiplayer/lobby.cpp | 11 +- src/citra_qt/multiplayer/lobby.h | 2 +- src/citra_qt/multiplayer/state.cpp | 157 ++++++++++++++++++ src/citra_qt/multiplayer/state.h | 65 ++++++++ 9 files changed, 253 insertions(+), 195 deletions(-) create mode 100644 src/citra_qt/multiplayer/state.cpp create mode 100644 src/citra_qt/multiplayer/state.h diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 699e7006b..1a150065f 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -69,6 +69,8 @@ add_executable(citra-qt multiplayer/lobby.cpp multiplayer/message.h multiplayer/message.cpp + multiplayer/state.cpp + multiplayer/state.h multiplayer/validation.h ui_settings.cpp ui_settings.h diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 47bc73d90..75d1fb0fb 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -31,11 +31,7 @@ #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" #include "citra_qt/main.h" -#include "citra_qt/multiplayer/client_room.h" -#include "citra_qt/multiplayer/direct_connect.h" -#include "citra_qt/multiplayer/host_room.h" -#include "citra_qt/multiplayer/lobby.h" -#include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/state.h" #include "citra_qt/ui_settings.h" #include "citra_qt/updater/updater.h" #include "citra_qt/util/clickable_label.h" @@ -137,16 +133,6 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { Network::Init(); - if (auto member = Network::GetRoomMember().lock()) { - // register the network structs to use in slots and signals - qRegisterMetaType(); - state_callback_handle = member->BindOnStateChanged( - [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); - connect(this, &GMainWindow::NetworkStateChanged, this, &GMainWindow::OnNetworkStateChanged); - } - - qRegisterMetaType(); - setWindowTitle(QString("Citra %1| %2-%3") .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); show(); @@ -173,12 +159,6 @@ GMainWindow::~GMainWindow() { delete render_window; Pica::g_debug_context.reset(); - - if (state_callback_handle) { - if (auto member = Network::GetRoomMember().lock()) { - member->Unbind(state_callback_handle); - } - } Network::Shutdown(); } @@ -192,6 +172,8 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(this); ui.horizontalLayout->addWidget(game_list); + multiplayer_state = new MultiplayerState(this, game_list->GetModel()); + // Setup updater updater = new Updater(this); UISettings::values.updater_found = updater->HasUpdater(); @@ -220,24 +202,14 @@ void GMainWindow::InitializeWidgets() { tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); - announce_multiplayer_session = std::make_shared(); - announce_multiplayer_session->BindErrorCallback( - [this](const Common::WebResult& result) { emit AnnounceFailed(result); }); - connect(this, &GMainWindow::AnnounceFailed, this, &GMainWindow::OnAnnounceFailed); - network_status_text = new ClickableLabel(this); - network_status_icon = new ClickableLabel(this); - network_status_text->setToolTip(tr("Current connection status")); - for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); label->setFrameStyle(QFrame::NoFrame); label->setContentsMargins(4, 0, 4, 0); statusBar()->addPermanentWidget(label, 0); } - statusBar()->addPermanentWidget(network_status_text, 0); - statusBar()->addPermanentWidget(network_status_icon, 0); - network_status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); - network_status_text->setText(tr("Not Connected. Click here to find a room!")); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux @@ -431,9 +403,6 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); - - connect(network_status_text, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); - connect(network_status_icon, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom); } void GMainWindow::ConnectMenuEvents() { @@ -462,12 +431,16 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); // Multiplayer - connect(ui.action_View_Lobby, &QAction::triggered, this, &GMainWindow::OnViewLobby); - connect(ui.action_Start_Room, &QAction::triggered, this, &GMainWindow::OnCreateRoom); - connect(ui.action_Stop_Room, &QAction::triggered, this, &GMainWindow::OnCloseRoom); - connect(ui.action_Connect_To_Room, &QAction::triggered, this, - &GMainWindow::OnDirectConnectToRoom); - connect(ui.action_Chat, &QAction::triggered, this, &GMainWindow::OnOpenNetworkRoom); + 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_Stop_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui.action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui.action_Chat, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key()); ui.action_Screen_Layout_Swap_Screens->setShortcut( @@ -931,30 +904,6 @@ void GMainWindow::OnMenuRecentFile() { } } -void GMainWindow::OnNetworkStateChanged(const Network::RoomMember::State& state) { - LOG_INFO(Frontend, "network state change"); - if (state == Network::RoomMember::State::Joined) { - network_status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); - network_status_text->setText(tr("Connected")); - ui.action_Chat->setEnabled(true); - return; - } - network_status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); - network_status_text->setText(tr("Not Connected")); - ui.action_Chat->setDisabled(true); - - ChangeRoomState(); -} - -void GMainWindow::OnAnnounceFailed(const Common::WebResult& result) { - announce_multiplayer_session->Stop(); - QMessageBox::warning( - this, tr("Error"), - tr("Announcing the room failed.\nThe room will not get listed publicly.\nError: ") + - QString::fromStdString(result.result_string), - QMessageBox::Ok); -} - void GMainWindow::OnStartGame() { emu_thread->SetRunning(true); qRegisterMetaType("Core::System::ResultStatus"); @@ -1129,80 +1078,6 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { graphicsSurfaceViewerWidget->show(); } -static void BringWidgetToFront(QWidget* widget) { - widget->show(); - widget->activateWindow(); - widget->raise(); -} - -void GMainWindow::OnViewLobby() { - if (lobby == nullptr) { - lobby = new Lobby(this, game_list->GetModel(), announce_multiplayer_session); - connect(lobby, &Lobby::Closed, [&] { - LOG_INFO(Frontend, "Destroying lobby"); - // lobby->close(); - lobby = nullptr; - }); - } - BringWidgetToFront(lobby); -} - -void GMainWindow::OnCreateRoom() { - if (host_room == nullptr) { - host_room = new HostRoomWindow(this, game_list->GetModel(), announce_multiplayer_session); - connect(host_room, &HostRoomWindow::Closed, [&] { - // host_room->close(); - LOG_INFO(Frontend, "Destroying host room"); - host_room = nullptr; - }); - } - BringWidgetToFront(host_room); -} - -void GMainWindow::OnCloseRoom() { - if (auto room = Network::GetRoom().lock()) { - if (room->GetState() == Network::Room::State::Open) { - if (NetworkMessage::WarnCloseRoom()) { - room->Destroy(); - announce_multiplayer_session->Stop(); - // host_room->close(); - } - } - } -} - -void GMainWindow::OnOpenNetworkRoom() { - if (auto member = Network::GetRoomMember().lock()) { - if (member->IsConnected()) { - if (client_room == nullptr) { - client_room = new ClientRoomWindow(this); - connect(client_room, &ClientRoomWindow::Closed, [&] { - LOG_INFO(Frontend, "Destroying client room"); - // client_room->close(); - client_room = nullptr; - }); - } - 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 GMainWindow::OnDirectConnectToRoom() { - if (direct_connect == nullptr) { - direct_connect = new DirectConnectWindow(this); - connect(direct_connect, &DirectConnectWindow::Closed, [&] { - LOG_INFO(Frontend, "Destroying direct connect"); - // direct_connect->close(); - direct_connect = nullptr; - }); - } - BringWidgetToFront(direct_connect); -} - void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); @@ -1335,17 +1210,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) { ShutdownGame(); render_window->close(); - - // Close Multiplayer windows - if (host_room) - host_room->close(); - if (direct_connect) - direct_connect->close(); - if (client_room) - client_room->close(); - if (lobby) - lobby->close(); - + multiplayer_state->Close(); QWidget::closeEvent(event); } diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 27aad35ca..a3f2371fa 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -29,6 +29,7 @@ class GraphicsTracingWidget; class GraphicsVertexShaderWidget; class GRenderWindow; class MicroProfileDialog; +class MultiplayerState; class ProfilerWidget; template class QFutureWatcher; @@ -37,16 +38,6 @@ class RegistersWidget; class Updater; class WaitTreeWidget; -// Multiplayer forward declarations -class Lobby; -class HostRoomWindow; -class ClientRoomWindow; -class DirectConnectWindow; - -namespace Core { -class AnnounceMultiplayerSession; -} - class GMainWindow : public QMainWindow { Q_OBJECT @@ -94,16 +85,6 @@ signals: // Signal that tells widgets to update icons to use the current theme void UpdateThemedIcons(); - void NetworkStateChanged(const Network::RoomMember::State&); - void AnnounceFailed(const Common::WebResult&); - -public slots: - void OnViewLobby(); - void OnCreateRoom(); - void OnCloseRoom(); - void OnOpenNetworkRoom(); - void OnDirectConnectToRoom(); - private: void InitializeWidgets(); void InitializeDebugWidgets(); @@ -173,8 +154,6 @@ private slots: /// Called whenever a user selects the "File->Select Game List Root" menu item void OnMenuSelectGameListRoot(); void OnMenuRecentFile(); - void OnNetworkStateChanged(const Network::RoomMember::State& state); - void OnAnnounceFailed(const Common::WebResult&); void OnConfigure(); void OnToggleFilterBar(); void OnDisplayTitleBars(bool); @@ -211,12 +190,10 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; - ClickableLabel* network_status_icon = nullptr; - ClickableLabel* network_status_text = nullptr; QTimer status_bar_update_timer; + MultiplayerState* multiplayer_state = nullptr; std::unique_ptr config; - std::shared_ptr announce_multiplayer_session; // Whether emulation is currently running in Citra. bool emulation_running = false; @@ -237,14 +214,6 @@ private: bool explicit_update_check = false; bool defer_update_prompt = false; - // Multiplayer windows - Lobby* lobby = nullptr; - HostRoomWindow* host_room = nullptr; - ClientRoomWindow* client_room = nullptr; - DirectConnectWindow* direct_connect = nullptr; - - Network::RoomMember::CallbackHandle state_callback_handle; - QAction* actions_recent_files[max_recent_files_item]; QTranslator translator; @@ -260,4 +229,3 @@ protected: Q_DECLARE_METATYPE(size_t); Q_DECLARE_METATYPE(Service::AM::InstallStatus); -Q_DECLARE_METATYPE(Common::WebResult); diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 9d68d96d1..785062d48 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -12,6 +12,7 @@ #include "citra_qt/multiplayer/client_room.h" #include "citra_qt/multiplayer/direct_connect.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 "core/settings.h" @@ -124,7 +125,7 @@ void DirectConnectWindow::OnConnection() { ShowError(NetworkMessage::USERNAME_IN_USE); break; case Network::RoomMember::State::Joining: - auto parent = static_cast(parentWidget()); + auto parent = static_cast(parentWidget()); parent->OnOpenNetworkRoom(); close(); } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 02122f445..df43b23b2 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -14,6 +14,7 @@ #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" @@ -136,8 +137,8 @@ void HostRoomWindow::OnConnection() { LOG_ERROR(Network, "Starting announce session failed"); } } - auto parent = static_cast(parentWidget()); - parent->ChangeRoomState(); + auto parent = static_cast(parentWidget()); + // parent->ChangeRoomState(); parent->OnOpenNetworkRoom(); close(); emit Closed(); diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index c20c7d59f..3e21738c2 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -11,6 +11,7 @@ #include "citra_qt/multiplayer/lobby.h" #include "citra_qt/multiplayer/lobby_p.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" @@ -51,9 +52,9 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->nickname->setText(UISettings::values.nickname); // UI Buttons - GMainWindow* p = reinterpret_cast(parent); + MultiplayerState* p = reinterpret_cast(parent); connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby); - connect(ui->chat, &QPushButton::pressed, p, &GMainWindow::OnOpenNetworkRoom); + connect(ui->chat, &QPushButton::pressed, p, &MultiplayerState::OnOpenNetworkRoom); connect(ui->games_owned, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterOwned); connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull); @@ -64,7 +65,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, // Actions connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby); // TODO(jroweboy): change this slot to OnConnected? - connect(this, &Lobby::Connected, p, &GMainWindow::OnOpenNetworkRoom); + connect(this, &Lobby::Connected, p, &MultiplayerState::OnOpenNetworkRoom); // setup the callbacks for network updates if (auto member = Network::GetRoomMember().lock()) { @@ -90,8 +91,6 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->chat->setDisabled(true); } -Lobby::~Lobby() {} - const QString Lobby::PasswordPrompt() { bool ok; const QString text = @@ -305,7 +304,7 @@ void Lobby::OnConnection() { ShowError(NetworkMessage::UNABLE_TO_CONNECT); break; case Network::RoomMember::State::Joining: - auto parent = static_cast(parentWidget()); + auto parent = static_cast(parentWidget()); parent->OnOpenNetworkRoom(); close(); break; diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index c8ec502f9..d2141b6ca 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -31,7 +31,7 @@ class Lobby : public QDialog { public: explicit Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session); - ~Lobby(); + ~Lobby() = default; public slots: /** diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp new file mode 100644 index 000000000..3b072f85c --- /dev/null +++ b/src/citra_qt/multiplayer/state.cpp @@ -0,0 +1,157 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/game_list.h" +#include "citra_qt/multiplayer/client_room.h" +#include "citra_qt/multiplayer/direct_connect.h" +#include "citra_qt/multiplayer/host_room.h" +#include "citra_qt/multiplayer/lobby.h" +#include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/state.h" +#include "citra_qt/util/clickable_label.h" +#include "common/announce_multiplayer_room.h" +#include "common/logging/log.h" + +MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model) + : QWidget(parent), game_list_model(game_list_model) { + if (auto member = Network::GetRoomMember().lock()) { + // register the network structs to use in slots and signals + qRegisterMetaType(); + state_callback_handle = member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &MultiplayerState::NetworkStateChanged, this, + &MultiplayerState::OnNetworkStateChanged); + } + + qRegisterMetaType(); + announce_multiplayer_session = std::make_shared(); + announce_multiplayer_session->BindErrorCallback( + [this](const Common::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("disconnected").pixmap(16)); + + connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); +} + +MultiplayerState::~MultiplayerState() { + if (state_callback_handle) { + if (auto member = Network::GetRoomMember().lock()) { + member->Unbind(state_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::OnNetworkStateChanged(const Network::RoomMember::State& state) { + NGLOG_DEBUG(Frontend, "Network state change"); + if (state == Network::RoomMember::State::Joined) { + status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); + status_text->setText(tr("Connected")); + return; + } + status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + status_text->setText(tr("Not Connected")); +} + +void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { + announce_multiplayer_session->Stop(); + QMessageBox::warning(this, tr("Error"), + tr("Failed to announce the room to the public lobby.\nThe room will not " + "get listed publicly.\nError: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); +} + +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); + connect(lobby, &Lobby::Closed, [&] { + LOG_INFO(Frontend, "Destroying lobby"); + // lobby->close(); + lobby = nullptr; + }); + } + BringWidgetToFront(lobby); +} + +void MultiplayerState::OnCreateRoom() { + if (host_room == nullptr) { + host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); + connect(host_room, &HostRoomWindow::Closed, [&] { + // host_room->close(); + LOG_INFO(Frontend, "Destroying host room"); + host_room = nullptr; + }); + } + BringWidgetToFront(host_room); +} + +void MultiplayerState::OnCloseRoom() { + if (auto room = Network::GetRoom().lock()) { + if (room->GetState() == Network::Room::State::Open) { + if (NetworkMessage::WarnCloseRoom()) { + room->Destroy(); + announce_multiplayer_session->Stop(); + // host_room->close(); + } + } + } +} + +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::Closed, [&] { + LOG_INFO(Frontend, "Destroying client room"); + // client_room->close(); + client_room = nullptr; + }); + } + 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); + connect(direct_connect, &DirectConnectWindow::Closed, [&] { + LOG_INFO(Frontend, "Destroying direct connect"); + // direct_connect->close(); + direct_connect = nullptr; + }); + } + BringWidgetToFront(direct_connect); +} diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h new file mode 100644 index 000000000..7bfff6d4e --- /dev/null +++ b/src/citra_qt/multiplayer/state.h @@ -0,0 +1,65 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "network/network.h" + +class QStandardItemModel; +class Lobby; +class HostRoomWindow; +class ClientRoomWindow; +class DirectConnectWindow; +class ClickableLabel; +namespace Core { +class AnnounceMultiplayerSession; +} + +class MultiplayerState : public QWidget { + Q_OBJECT; + +public: + explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list); + ~MultiplayerState(); + + /** + * Close all open multiplayer related dialogs + */ + void Close(); + + ClickableLabel* GetStatusText() const { + return status_text; + } + + ClickableLabel* GetStatusIcon() const { + return status_icon; + } + +public slots: + void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnViewLobby(); + void OnCreateRoom(); + void OnCloseRoom(); + void OnOpenNetworkRoom(); + void OnDirectConnectToRoom(); + void OnAnnounceFailed(const Common::WebResult&); + +signals: + void NetworkStateChanged(const Network::RoomMember::State&); + void AnnounceFailed(const Common::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; + std::shared_ptr announce_multiplayer_session; + Network::RoomMember::CallbackHandle state_callback_handle; +}; + +Q_DECLARE_METATYPE(Common::WebResult); From 2d1efcc36bd036f59095f65a65733f8b1d764bdd Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 5 Apr 2018 12:07:11 -0600 Subject: [PATCH 04/16] Add a member list expandable to the lobby. Fix issue with hosting more than once. --- src/citra_qt/main.cpp | 4 +- src/citra_qt/multiplayer/client_room.cpp | 2 + src/citra_qt/multiplayer/host_room.cpp | 2 + src/citra_qt/multiplayer/lobby.cpp | 54 +++++++++++++++----- src/citra_qt/multiplayer/lobby.h | 10 +++- src/citra_qt/multiplayer/lobby.ui | 2 +- src/citra_qt/multiplayer/lobby_p.h | 63 ++++++++++++++++++------ src/citra_qt/multiplayer/state.cpp | 23 +-------- 8 files changed, 106 insertions(+), 54 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 75d1fb0fb..b80775962 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -117,6 +117,8 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { default_theme_paths = QIcon::themeSearchPaths(); UpdateUITheme(); + Network::Init(); + InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); @@ -131,8 +133,6 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { SetupUIStrings(); - Network::Init(); - setWindowTitle(QString("Citra %1| %2-%3") .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); show(); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 8baafb418..b4737eb3b 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -96,6 +96,8 @@ void ClientRoomWindow::Disconnect() { if (auto member = Network::GetRoomMember().lock()) { member->Leave(); ui->chat->AppendStatusMessage(tr("Disconnected")); + close(); + emit Closed(); } } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index df43b23b2..09958e596 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -79,6 +79,8 @@ void HostRoomWindow::Host() { return; } else { member->Leave(); + auto parent = static_cast(parentWidget()); + parent->OnCloseRoom(); } } ui->host->setDisabled(true); diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 3e21738c2..91ba080fb 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -35,7 +35,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); proxy->setSortLocaleAware(true); ui->room_list->setModel(proxy); - ui->room_list->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive); ui->room_list->header()->stretchLastSection(); ui->room_list->setAlternatingRowColors(true); ui->room_list->setSelectionMode(QHeaderView::SingleSelection); @@ -45,7 +45,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->room_list->setSortingEnabled(true); ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers); ui->room_list->setExpandsOnDoubleClick(false); - ui->room_list->setUniformRowHeights(true); + // ui->room_list->setUniformRowHeights(true); ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); ui->nickname->setValidator(Validation::nickname); @@ -61,6 +61,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::setFilterFixedString); connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom); + connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom); // Actions connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby); @@ -99,6 +100,11 @@ const QString Lobby::PasswordPrompt() { 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& index) { if (!ui->nickname->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); @@ -123,7 +129,6 @@ void Lobby::OnJoinRoom(const QModelIndex& index) { // attempt to connect in a different thread QFuture f = QtConcurrent::run([&, password] { if (auto room_member = Network::GetRoomMember().lock()) { - QModelIndex connection_index = proxy->index(index.row(), Column::HOST); const std::string nickname = ui->nickname->text().toStdString(); const std::string ip = @@ -161,7 +166,6 @@ void Lobby::ResetModel() { 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); - ui->room_list->header()->stretchLastSection(); } void Lobby::RefreshLobby() { @@ -181,7 +185,7 @@ void Lobby::OnRefreshLobby() { // 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 = QModelIndex(game_list->index(r, 0)); + 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(); @@ -196,17 +200,41 @@ void Lobby::OnRefreshLobby() { members.append(var); } - model->appendRow(QList( - {new LobbyItemPassword(room.has_password), - new LobbyItemName(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), - new LobbyItemMemberList(members, room.max_player)})); + auto first_item = new LobbyItemPassword(room.has_password); + auto row = QList({ + first_item, + new LobbyItemName(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), + 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.members.size() > 0) { + first_item->appendRow(new LobbyItemExpandedMemberList(members)); + } } + ui->room_list->setModel(model); + + // 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 < model->rowCount(); i++) { + auto parent = model->item(i, 0); + if (parent->hasChildren()) { + ui->room_list->setFirstColumnSpanned(0, parent->index(), true); + } + } } LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list) diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index d2141b6ca..c68a7fd7b 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -46,6 +46,14 @@ private slots: */ 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. @@ -99,7 +107,7 @@ private: std::future room_list_future; std::weak_ptr announce_multiplayer_session; - std::unique_ptr ui; + Ui::Lobby* ui; QFutureWatcher* watcher; }; diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui index e5489b18a..b21a7c928 100644 --- a/src/citra_qt/multiplayer/lobby.ui +++ b/src/citra_qt/multiplayer/lobby.ui @@ -6,7 +6,7 @@ 0 0 - 707 + 850 487
diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index a8a429f12..7523d881d 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -23,16 +23,17 @@ enum List { class LobbyItem : public QStandardItem { public: - LobbyItem() : QStandardItem() {} - LobbyItem(const QString& string) : QStandardItem(string) {} + LobbyItem() = default; + explicit LobbyItem(const QString& string) : QStandardItem(string) {} virtual ~LobbyItem() override {} }; class LobbyItemPassword : public LobbyItem { public: static const int PasswordRole = Qt::UserRole + 1; - LobbyItemPassword() : LobbyItem() {} - LobbyItemPassword(const bool has_password) : LobbyItem() { + + LobbyItemPassword() = default; + explicit LobbyItemPassword(const bool has_password) : LobbyItem() { setData(has_password, PasswordRole); } @@ -52,8 +53,9 @@ public: class LobbyItemName : public LobbyItem { public: static const int NameRole = Qt::UserRole + 1; - LobbyItemName() : LobbyItem() {} - LobbyItemName(QString name) : LobbyItem() { + + LobbyItemName() = default; + explicit LobbyItemName(QString name) : LobbyItem() { setData(name, NameRole); } @@ -74,8 +76,8 @@ public: static const int GameNameRole = Qt::UserRole + 2; static const int GameIconRole = Qt::UserRole + 3; - LobbyItemGame() : LobbyItem() {} - LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) : LobbyItem() { + LobbyItemGame() = default; + explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) : LobbyItem() { setData(static_cast(title_id), TitleIDRole); setData(game_name, GameNameRole); if (!smdh_icon.isNull()) { @@ -109,8 +111,8 @@ public: static const int HostIPRole = Qt::UserRole + 2; static const int HostPortRole = Qt::UserRole + 3; - LobbyItemHost() : LobbyItem() {} - LobbyItemHost(QString username, QString ip, u16 port) : LobbyItem() { + LobbyItemHost() = default; + explicit LobbyItemHost(QString username, QString ip, u16 port) : LobbyItem() { setData(username, HostUsernameRole); setData(ip, HostIPRole); setData(port, HostPortRole); @@ -132,15 +134,15 @@ public: class LobbyMember { public: - LobbyMember() {} + LobbyMember() = default; LobbyMember(const LobbyMember& other) { username = other.username; title_id = other.title_id; game_name = other.game_name; } - LobbyMember(const QString username, u64 title_id, const QString game_name) + explicit LobbyMember(const QString username, u64 title_id, const QString game_name) : username(username), title_id(title_id), game_name(game_name) {} - ~LobbyMember() {} + ~LobbyMember() = default; QString GetUsername() const { return username; @@ -165,8 +167,8 @@ public: static const int MemberListRole = Qt::UserRole + 1; static const int MaxPlayerRole = Qt::UserRole + 2; - LobbyItemMemberList() : LobbyItem() {} - LobbyItemMemberList(QList members, u32 max_players) : LobbyItem() { + LobbyItemMemberList() = default; + explicit LobbyItemMemberList(QList members, u32 max_players) : LobbyItem() { setData(members, MemberListRole); setData(max_players, MaxPlayerRole); } @@ -187,3 +189,34 @@ public: 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) : LobbyItem() { + setData(members, MemberListRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + QString out = QObject::tr("Current Players in the room"); + for (const auto& member : members) { + const auto& m = member.value(); + if (m.GetGameName().isEmpty()) { + out += QString(QObject::tr("\n%1 is not playing a game")).arg(m.GetUsername()); + } else { + out += QString(QObject::tr("\n%1 is playing %2")) + .arg(m.GetUsername(), m.GetGameName()); + } + } + return out; + } +}; diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 3b072f85c..f0857af52 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -20,13 +20,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis : QWidget(parent), game_list_model(game_list_model) { if (auto member = Network::GetRoomMember().lock()) { // register the network structs to use in slots and signals - qRegisterMetaType(); state_callback_handle = member->BindOnStateChanged( [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); connect(this, &MultiplayerState::NetworkStateChanged, this, &MultiplayerState::OnNetworkStateChanged); } + qRegisterMetaType(); qRegisterMetaType(); announce_multiplayer_session = std::make_shared(); announce_multiplayer_session->BindErrorCallback( @@ -91,11 +91,6 @@ static void BringWidgetToFront(QWidget* widget) { void MultiplayerState::OnViewLobby() { if (lobby == nullptr) { lobby = new Lobby(this, game_list_model, announce_multiplayer_session); - connect(lobby, &Lobby::Closed, [&] { - LOG_INFO(Frontend, "Destroying lobby"); - // lobby->close(); - lobby = nullptr; - }); } BringWidgetToFront(lobby); } @@ -103,11 +98,6 @@ void MultiplayerState::OnViewLobby() { void MultiplayerState::OnCreateRoom() { if (host_room == nullptr) { host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); - connect(host_room, &HostRoomWindow::Closed, [&] { - // host_room->close(); - LOG_INFO(Frontend, "Destroying host room"); - host_room = nullptr; - }); } BringWidgetToFront(host_room); } @@ -118,7 +108,6 @@ void MultiplayerState::OnCloseRoom() { if (NetworkMessage::WarnCloseRoom()) { room->Destroy(); announce_multiplayer_session->Stop(); - // host_room->close(); } } } @@ -129,11 +118,6 @@ void MultiplayerState::OnOpenNetworkRoom() { if (member->IsConnected()) { if (client_room == nullptr) { client_room = new ClientRoomWindow(this); - connect(client_room, &ClientRoomWindow::Closed, [&] { - LOG_INFO(Frontend, "Destroying client room"); - // client_room->close(); - client_room = nullptr; - }); } BringWidgetToFront(client_room); return; @@ -147,11 +131,6 @@ void MultiplayerState::OnOpenNetworkRoom() { void MultiplayerState::OnDirectConnectToRoom() { if (direct_connect == nullptr) { direct_connect = new DirectConnectWindow(this); - connect(direct_connect, &DirectConnectWindow::Closed, [&] { - LOG_INFO(Frontend, "Destroying direct connect"); - // direct_connect->close(); - direct_connect = nullptr; - }); } BringWidgetToFront(direct_connect); } From 599eebf511492d9394279db90628a5bd485b10ff Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 5 Apr 2018 21:56:04 -0600 Subject: [PATCH 05/16] Remove the current players blurb --- src/citra_qt/multiplayer/lobby_p.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index 7523d881d..a177bc66d 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -207,15 +207,19 @@ public: return LobbyItem::data(role); } auto members = data(MemberListRole).toList(); - QString out = QObject::tr("Current Players in the room"); + QString out; + bool first = true; for (const auto& member : members) { + if (!first) + out += '\n'; const auto& m = member.value(); if (m.GetGameName().isEmpty()) { - out += QString(QObject::tr("\n%1 is not playing a game")).arg(m.GetUsername()); + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetUsername()); } else { - out += QString(QObject::tr("\n%1 is playing %2")) - .arg(m.GetUsername(), m.GetGameName()); + out += + QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName()); } + first = false; } return out; } From aa391ed60d4445643e6a50f97f72e9fdd49a3759 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 9 Apr 2018 09:49:51 -0600 Subject: [PATCH 06/16] Fixed and issue where multiplayer state was covering the File and Emulation menu items when it shouldn't even be visible --- src/citra_qt/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index b80775962..41528718b 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -173,6 +173,7 @@ void GMainWindow::InitializeWidgets() { ui.horizontalLayout->addWidget(game_list); multiplayer_state = new MultiplayerState(this, game_list->GetModel()); + multiplayer_state->setVisible(false); // Setup updater updater = new Updater(this); From 3be7aa2cfce6dfb3fb41da78c4fe7a248882109b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 9 Apr 2018 10:18:12 -0600 Subject: [PATCH 07/16] Moved the password icon to the room name. Also added a dark mode lock icon as well (and fixed a small bug preventing the lock icon from showing up) --- .../qt_themes/qdarkstyle/icons/16x16/lock.png | Bin 0 -> 304 bytes dist/qt_themes/qdarkstyle/style.qrc | 1 + src/citra_qt/multiplayer/lobby.cpp | 10 ++--- src/citra_qt/multiplayer/lobby_p.h | 37 ++++++------------ 4 files changed, 17 insertions(+), 31 deletions(-) create mode 100644 dist/qt_themes/qdarkstyle/icons/16x16/lock.png diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/lock.png b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..c750a39e855800fe4cde217a1bf28f841030e501 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPd3#R~&?yEVTSwNw^o-U3d7N_S%ktxROZ0mC!4`O#esdB1N*JU5{0l2e>jubge>mPSJ^&&?%8KcL>WvAo_9+JoM2kO zsn>V#-&Rc~i~Ae_tY5EP-=*8Ab(MpM;j*Q7#*{i)r8@>Yr!28i{4k+p>J|4L=dRCP zQYgN$Vq(bz-seA`PP+eLPWpo{(p_gRe3@1fbK~xheFC)y^e0Hmy!rd&s^O(iEb1#g wo=%oNY{h!`>vV^`LLZb)T(e+SzEHyuo5c9`w}kC2php-yUHx3vIVCg!0C@XzqyPW_ literal 0 HcmV?d00001 diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc index efbd0b9dc..54a96b680 100644 --- a/dist/qt_themes/qdarkstyle/style.qrc +++ b/dist/qt_themes/qdarkstyle/style.qrc @@ -1,6 +1,7 @@ icons/index.theme + icons/16x16/lock.png rc/up_arrow_disabled.png diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 91ba080fb..6780ac430 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -119,8 +119,8 @@ void Lobby::OnJoinRoom(const QModelIndex& index) { } // Get a password to pass if the room is password protected - QModelIndex password_index = proxy->index(index.row(), Column::PASSWORD); - bool has_password = proxy->data(password_index, LobbyItemPassword::PasswordRole).toBool(); + 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; @@ -161,7 +161,7 @@ void Lobby::OnStateChanged(const Network::RoomMember::State& state) { void Lobby::ResetModel() { model->clear(); model->insertColumns(0, Column::TOTAL); - model->setHeaderData(Column::PASSWORD, Qt::Horizontal, tr("Password"), Qt::DisplayRole); + model->setHeaderData(Column::EXPAND, Qt::Horizontal, "", 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); @@ -200,10 +200,10 @@ void Lobby::OnRefreshLobby() { members.append(var); } - auto first_item = new LobbyItemPassword(room.has_password); + auto first_item = new LobbyItem(); auto row = QList({ first_item, - new LobbyItemName(QString::fromStdString(room.name)), + 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), diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index a177bc66d..fa8580349 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -12,7 +12,7 @@ namespace Column { enum List { - PASSWORD, + EXPAND, ROOM_NAME, GAME_NAME, HOST, @@ -28,43 +28,28 @@ public: virtual ~LobbyItem() override {} }; -class LobbyItemPassword : public LobbyItem { +class LobbyItemName : public LobbyItem { public: - static const int PasswordRole = Qt::UserRole + 1; + static const int NameRole = Qt::UserRole + 1; + static const int PasswordRole = Qt::UserRole + 2; - LobbyItemPassword() = default; - explicit LobbyItemPassword(const bool has_password) : LobbyItem() { + 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) { - return LobbyItem::data(role); + if (role == Qt::DecorationRole) { + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon::fromTheme("lock").pixmap(16) : QIcon(); } - bool has_password = data(PasswordRole).toBool(); - return has_password ? QIcon(":/icons/lock.png") : QIcon(); - } - - bool operator<(const QStandardItem& other) const override { - return data(PasswordRole).toBool() < other.data(PasswordRole).toBool(); - } -}; - -class LobbyItemName : public LobbyItem { -public: - static const int NameRole = Qt::UserRole + 1; - - LobbyItemName() = default; - explicit LobbyItemName(QString name) : LobbyItem() { - setData(name, NameRole); - } - - QVariant data(int role) const override { 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; } From 601fd81d5cbb4608e21df843dbc3db10ae4866a0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 9 Apr 2018 11:00:56 -0600 Subject: [PATCH 08/16] Address review comments --- src/citra_qt/multiplayer/chat_room.cpp | 57 +++++++++++---------- src/citra_qt/multiplayer/chat_room.h | 2 +- src/citra_qt/multiplayer/direct_connect.cpp | 26 +++------- src/citra_qt/multiplayer/direct_connect.h | 1 - src/citra_qt/multiplayer/lobby_p.h | 22 ++++---- src/citra_qt/multiplayer/message.h | 4 +- src/citra_qt/multiplayer/state.cpp | 11 ++-- src/citra_qt/util/clickable_label.cpp | 2 - src/citra_qt/util/clickable_label.h | 5 +- 9 files changed, 58 insertions(+), 72 deletions(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 46c477eae..d55b39c22 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -10,7 +11,6 @@ #include #include #include - #include "citra_qt/game_list_p.h" #include "citra_qt/multiplayer/chat_room.h" #include "citra_qt/multiplayer/message.h" @@ -36,10 +36,10 @@ public: } private: - ChatMessage() {} - const QList player_color = { + static constexpr std::array player_color = { {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + QString timestamp; QString nickname; QString message; @@ -49,7 +49,7 @@ class StatusMessage { public: explicit StatusMessage(const QString& msg, QTime ts = {}) { /// Convert the time to their default locale defined format - static QLocale locale; + QLocale locale; timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); message = msg; } @@ -65,7 +65,7 @@ private: QString message; }; -ChatRoom::ChatRoom(QWidget* parent) : ui(new Ui::ChatRoom) { +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(new Ui::ChatRoom) { ui->setupUi(this); // set the item_model for player_view @@ -159,28 +159,29 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { void ChatRoom::OnSendChat() { if (auto room = Network::GetRoomMember().lock()) { - if (room->GetState() == Network::RoomMember::State::Joined) { - auto message = ui->chat_message->text().toStdString(); - if (!ValidateMessage(message)) { - return; - } - auto nick = room->GetNickname(); - Network::ChatEntry chat{nick, 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; - }); - if (it == members.end()) { - LOG_INFO(Network, "Chat message received from unknown player"); - } - auto player = std::distance(members.begin(), it); - ChatMessage m(chat); - room->SendChatMessage(message); - AppendChatMessage(m.GetPlayerChatMessage(player)); - ui->chat_message->clear(); + if (room->GetState() != Network::RoomMember::State::Joined) { + return; } + auto message = ui->chat_message->text().toStdString(); + if (!ValidateMessage(message)) { + return; + } + auto nick = room->GetNickname(); + Network::ChatEntry chat{nick, 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; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player"); + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat); + room->SendChatMessage(message); + AppendChatMessage(m.GetPlayerChatMessage(player)); + ui->chat_message->clear(); } } @@ -188,11 +189,11 @@ 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 == "") + if (member.nickname.empty()) continue; QList l; std::vector elements = {member.nickname, member.game_info.name}; - for (auto& item : elements) { + for (const auto& item : elements) { QStandardItem* child = new QStandardItem(QString::fromStdString(item)); child->setEditable(false); l.append(child); diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index 8c07654cc..c837c24e0 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -45,7 +45,7 @@ signals: void ChatReceived(const Network::ChatEntry&); private: - const u32 max_chat_lines = 1000; + static constexpr u32 max_chat_lines = 1000; void AppendChatMessage(const QString&); bool ValidateMessage(const std::string&); QStandardItemModel* player_list; diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 785062d48..c21fe5d75 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -19,7 +19,7 @@ #include "network/network.h" #include "ui_direct_connect.h" -enum class ConnectionType : u8 { TRAVERSAL_SERVER, IP }; +enum class ConnectionType : u8 { TraversalServer, IP }; DirectConnectWindow::DirectConnectWindow(QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), @@ -44,38 +44,30 @@ DirectConnectWindow::DirectConnectWindow(QWidget* parent) } void DirectConnectWindow::Connect() { - ClearAllError(); - bool isValid = true; if (!ui->nickname->hasAcceptableInput()) { - isValid = false; - ShowError(NetworkMessage::USERNAME_NOT_VALID); + NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); + return; } if (const auto member = Network::GetRoomMember().lock()) { - if (member->IsConnected()) { - if (!NetworkMessage::WarnDisconnect()) { - return; - } + if (member->IsConnected() && !NetworkMessage::WarnDisconnect()) { + return; } } switch (static_cast(ui->connection_type->currentIndex())) { - case ConnectionType::TRAVERSAL_SERVER: + case ConnectionType::TraversalServer: break; case ConnectionType::IP: if (!ui->ip->hasAcceptableInput()) { - isValid = false; NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID); + return; } if (!ui->port->hasAcceptableInput()) { - isValid = false; NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID); + return; } break; } - if (!isValid) { - return; - } - // Store settings UISettings::values.nickname = ui->nickname->text(); UISettings::values.ip = ui->ip->text(); @@ -98,8 +90,6 @@ void DirectConnectWindow::Connect() { BeginConnecting(); } -void DirectConnectWindow::ClearAllError() {} - void DirectConnectWindow::BeginConnecting() { ui->connect->setEnabled(false); ui->connect->setText(tr("Connecting")); diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h index 6d9266601..026484394 100644 --- a/src/citra_qt/multiplayer/direct_connect.h +++ b/src/citra_qt/multiplayer/direct_connect.h @@ -30,7 +30,6 @@ private slots: private: void Connect(); - void ClearAllError(); void BeginConnecting(); void EndConnecting(); diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h index fa8580349..3773f99de 100644 --- a/src/citra_qt/multiplayer/lobby_p.h +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -4,10 +4,10 @@ #pragma once +#include #include #include #include - #include "common/common_types.h" namespace Column { @@ -25,7 +25,7 @@ class LobbyItem : public QStandardItem { public: LobbyItem() = default; explicit LobbyItem(const QString& string) : QStandardItem(string) {} - virtual ~LobbyItem() override {} + virtual ~LobbyItem() override = default; }; class LobbyItemName : public LobbyItem { @@ -62,7 +62,7 @@ public: static const int GameIconRole = Qt::UserRole + 3; LobbyItemGame() = default; - explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) : LobbyItem() { + 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()) { @@ -97,7 +97,7 @@ public: static const int HostPortRole = Qt::UserRole + 3; LobbyItemHost() = default; - explicit LobbyItemHost(QString username, QString ip, u16 port) : LobbyItem() { + explicit LobbyItemHost(QString username, QString ip, u16 port) { setData(username, HostUsernameRole); setData(ip, HostIPRole); setData(port, HostPortRole); @@ -120,13 +120,9 @@ public: class LobbyMember { public: LobbyMember() = default; - LobbyMember(const LobbyMember& other) { - username = other.username; - title_id = other.title_id; - game_name = other.game_name; - } - explicit LobbyMember(const QString username, u64 title_id, const QString game_name) - : username(username), title_id(title_id), game_name(game_name) {} + LobbyMember(const LobbyMember& other) = default; + explicit LobbyMember(QString username, u64 title_id, QString game_name) + : username(std::move(username)), title_id(title_id), game_name(std::move(game_name)) {} ~LobbyMember() = default; QString GetUsername() const { @@ -153,7 +149,7 @@ public: static const int MaxPlayerRole = Qt::UserRole + 2; LobbyItemMemberList() = default; - explicit LobbyItemMemberList(QList members, u32 max_players) : LobbyItem() { + explicit LobbyItemMemberList(QList members, u32 max_players) { setData(members, MemberListRole); setData(max_players, MaxPlayerRole); } @@ -183,7 +179,7 @@ public: static const int MemberListRole = Qt::UserRole + 1; LobbyItemExpandedMemberList() = default; - explicit LobbyItemExpandedMemberList(QList members) : LobbyItem() { + explicit LobbyItemExpandedMemberList(QList members) { setData(members, MemberListRole); } diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h index 3a95c1081..3b8613199 100644 --- a/src/citra_qt/multiplayer/message.h +++ b/src/citra_qt/multiplayer/message.h @@ -4,12 +4,14 @@ #pragma once +#include + namespace NetworkMessage { class ConnectionError { public: - explicit ConnectionError(const std::string& str) : err(str) {} + explicit ConnectionError(std::string str) : err(std::move(str)) {} const std::string& GetString() const { return err; } diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index f0857af52..8882f1c53 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -104,11 +104,12 @@ void MultiplayerState::OnCreateRoom() { void MultiplayerState::OnCloseRoom() { if (auto room = Network::GetRoom().lock()) { - if (room->GetState() == Network::Room::State::Open) { - if (NetworkMessage::WarnCloseRoom()) { - room->Destroy(); - announce_multiplayer_session->Stop(); - } + if (room->GetState() != Network::Room::State::Open) { + return; + } + if (NetworkMessage::WarnCloseRoom()) { + room->Destroy(); + announce_multiplayer_session->Stop(); } } } diff --git a/src/citra_qt/util/clickable_label.cpp b/src/citra_qt/util/clickable_label.cpp index 010413271..e990423a9 100644 --- a/src/citra_qt/util/clickable_label.cpp +++ b/src/citra_qt/util/clickable_label.cpp @@ -6,8 +6,6 @@ ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent) {} -ClickableLabel::~ClickableLabel() {} - void ClickableLabel::mouseReleaseEvent(QMouseEvent* event) { emit clicked(); } diff --git a/src/citra_qt/util/clickable_label.h b/src/citra_qt/util/clickable_label.h index 780a01bf6..3c65a74be 100644 --- a/src/citra_qt/util/clickable_label.h +++ b/src/citra_qt/util/clickable_label.h @@ -6,14 +6,13 @@ #include #include -#include class ClickableLabel : public QLabel { Q_OBJECT public: - explicit ClickableLabel(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); - ~ClickableLabel(); + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; signals: void clicked(); From a85511cd7d783ab9fcf85ee6040f6c006cad0351 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 17 Apr 2018 12:01:14 -0600 Subject: [PATCH 09/16] Fix multiplayer dropdowns and proxy model * Filters in the lobby properly remove rooms * Multiplayer menu items for Show Room and Leave Room work as intended --- src/citra_qt/main.cpp | 19 +-- src/citra_qt/main.h | 2 - src/citra_qt/main.ui | 12 +- src/citra_qt/multiplayer/direct_connect.ui | 24 +--- src/citra_qt/multiplayer/lobby.cpp | 36 +---- src/citra_qt/multiplayer/lobby.h | 2 - src/citra_qt/multiplayer/lobby.ui | 153 ++++++++++----------- src/citra_qt/multiplayer/state.cpp | 25 +++- src/citra_qt/multiplayer/state.h | 5 +- 9 files changed, 107 insertions(+), 171 deletions(-) diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 41528718b..306c79ace 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -172,7 +172,8 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(this); ui.horizontalLayout->addWidget(game_list); - multiplayer_state = new MultiplayerState(this, game_list->GetModel()); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room, + ui.action_Show_Room); multiplayer_state->setVisible(false); // Setup updater @@ -436,11 +437,11 @@ void GMainWindow::ConnectMenuEvents() { &MultiplayerState::OnViewLobby); connect(ui.action_Start_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnCreateRoom); - connect(ui.action_Stop_Room, &QAction::triggered, multiplayer_state, + 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_Chat, &QAction::triggered, multiplayer_state, + connect(ui.action_Show_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnOpenNetworkRoom); ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key()); @@ -1331,18 +1332,6 @@ void GMainWindow::SyncMenuUISettings() { ui.action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen); } -void GMainWindow::ChangeRoomState() { - if (auto room = Network::GetRoom().lock()) { - if (room->GetState() == Network::Room::State::Open) { - ui.action_Start_Room->setDisabled(true); - ui.action_Stop_Room->setEnabled(true); - return; - } - ui.action_Start_Room->setEnabled(true); - ui.action_Stop_Room->setDisabled(true); - } -} - #ifdef main #undef main #endif diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index a3f2371fa..f0aaf9114 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -12,7 +12,6 @@ #include "common/announce_multiplayer_room.h" #include "core/core.h" #include "core/hle/service/am/am.h" -#include "network/network.h" #include "ui_main.h" class AboutDialog; @@ -55,7 +54,6 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - void ChangeRoomState(); GameList* game_list; GMainWindow(); diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index d7ffc6b06..86c4e46ed 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -116,10 +116,10 @@ - - + + @@ -259,12 +259,12 @@ Create Room
- + false - Close Room + Leave Room @@ -272,12 +272,12 @@ Direct Connect to Room - + false - Current Room + Show Current Room diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/citra_qt/multiplayer/direct_connect.ui index 81aac7431..681b6bf69 100644 --- a/src/citra_qt/multiplayer/direct_connect.ui +++ b/src/citra_qt/multiplayer/direct_connect.ui @@ -7,7 +7,7 @@ 0 0 455 - 239 + 161 @@ -18,28 +18,6 @@ - - - - Instructions - - - - - - <html><head/><body><p>Directly connect to a friend by <span style=" font-weight:600;">Traversal server</span> or by<span style=" font-weight:600;"> IP address</span>. </p><p>To use the <span style=" font-weight:600;">Traversal Server</span>, ask the game host for their &quot;<span style=" font-weight:600;">Host Code</span>&quot; which will be visible on the create room screen after it is created.</p></body></html> - - - Qt::RichText - - - true - - - - - - diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 6780ac430..b4e91d6ed 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -32,6 +32,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, model = new QStandardItemModel(ui->room_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); @@ -45,7 +46,6 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->room_list->setSortingEnabled(true); ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers); ui->room_list->setExpandsOnDoubleClick(false); - // ui->room_list->setUniformRowHeights(true); ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); ui->nickname->setValidator(Validation::nickname); @@ -54,7 +54,6 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, // UI Buttons MultiplayerState* p = reinterpret_cast(parent); connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby); - connect(ui->chat, &QPushButton::pressed, p, &MultiplayerState::OnOpenNetworkRoom); connect(ui->games_owned, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterOwned); connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull); @@ -68,28 +67,11 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, // TODO(jroweboy): change this slot to OnConnected? connect(this, &Lobby::Connected, p, &MultiplayerState::OnOpenNetworkRoom); - // setup the callbacks for network updates - if (auto member = Network::GetRoomMember().lock()) { - member->BindOnStateChanged( - [this](const Network::RoomMember::State& state) { emit StateChanged(state); }); - connect(this, &Lobby::StateChanged, this, &Lobby::OnStateChanged); - } else { - // TODO (jroweboy) network was not initialized? - } - // 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(); - - if (auto member = Network::GetRoomMember().lock()) { - if (member->IsConnected()) { - ui->chat->setEnabled(true); - return; - } - } - ui->chat->setDisabled(true); } const QString Lobby::PasswordPrompt() { @@ -148,16 +130,6 @@ void Lobby::OnJoinRoom(const QModelIndex& index) { Settings::Apply(); } -void Lobby::OnStateChanged(const Network::RoomMember::State& state) { - if (auto member = Network::GetRoomMember().lock()) { - if (member->IsConnected()) { - ui->chat->setEnabled(true); - return; - } - } - ui->chat->setDisabled(true); -} - void Lobby::ResetModel() { model->clear(); model->insertColumns(0, Column::TOTAL); @@ -218,7 +190,7 @@ void Lobby::OnRefreshLobby() { first_item->appendRow(new LobbyItemExpandedMemberList(members)); } } - ui->room_list->setModel(model); + proxy->setSourceModel(model); // Reenable the refresh button and resize the columns ui->refresh_list->setEnabled(true); @@ -311,12 +283,12 @@ void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) { void LobbyFilterProxyModel::SetFilterOwned(bool filter) { filter_owned = filter; - invalidateFilter(); + invalidate(); } void LobbyFilterProxyModel::SetFilterFull(bool filter) { filter_full = filter; - invalidateFilter(); + invalidate(); } void Lobby::OnConnection() { diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index c68a7fd7b..4fbc56b9a 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -68,8 +68,6 @@ private slots: */ void OnConnection(); - void OnStateChanged(const Network::RoomMember::State&); - signals: /** * Signalled when the latest lobby data is retrieved. diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui index b21a7c928..835d03238 100644 --- a/src/citra_qt/multiplayer/lobby.ui +++ b/src/citra_qt/multiplayer/lobby.ui @@ -6,7 +6,7 @@ 0 0 - 850 + 903 487 @@ -24,98 +24,83 @@ 6 - - - - Nickname - - - false - - - - 6 - - - 1 - - - 6 - - - 4 - - - - - Nickname - - - - - - - - - - Filters - - - false - - - - 6 - - - 1 - - - 6 - - - 4 - - - - - Search - - - true - - - - - - - Games I Own - - - - - - - Hide Full Games - - - - - - - + - Refresh Lobby + Nickname - + + + Nickname + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - Chat + Filters + + + + + + + Search + + + true + + + + + + + Games I Own + + + + + + + Hide Full Games + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh Lobby diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 8882f1c53..d90046c52 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -16,8 +17,10 @@ #include "common/announce_multiplayer_room.h" #include "common/logging/log.h" -MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model) - : QWidget(parent), game_list_model(game_list_model) { +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( @@ -67,10 +70,14 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s if (state == Network::RoomMember::State::Joined) { status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); status_text->setText(tr("Connected")); + leave_room->setEnabled(true); + show_room->setEnabled(true); return; } status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); status_text->setText(tr("Not Connected")); + leave_room->setEnabled(false); + show_room->setEnabled(false); } void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { @@ -103,14 +110,20 @@ void MultiplayerState::OnCreateRoom() { } void MultiplayerState::OnCloseRoom() { + if (!NetworkMessage::WarnCloseRoom()) + return; if (auto room = Network::GetRoom().lock()) { + // if you are in a room, leave it + if (auto member = Network::GetRoomMember().lock()) { + member->Leave(); + } + + // if you are hosting a room, also stop hosting if (room->GetState() != Network::Room::State::Open) { return; } - if (NetworkMessage::WarnCloseRoom()) { - room->Destroy(); - announce_multiplayer_session->Stop(); - } + room->Destroy(); + announce_multiplayer_session->Stop(); } } diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h index 7bfff6d4e..1829d19fb 100644 --- a/src/citra_qt/multiplayer/state.h +++ b/src/citra_qt/multiplayer/state.h @@ -21,7 +21,8 @@ class MultiplayerState : public QWidget { Q_OBJECT; public: - explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list); + explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room, + QAction* show_room); ~MultiplayerState(); /** @@ -58,6 +59,8 @@ private: 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::CallbackHandle state_callback_handle; }; From c635c7f40df0baf38cfe87e0ec9006edfc9c9576 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 17 Apr 2018 23:06:02 -0600 Subject: [PATCH 10/16] Address more review comments * Make Validation a singleton instead * Wording changes for error messages * Smart pointers for Ui members * Other minor nitpicks --- src/citra_qt/configuration/config.cpp | 8 +++- src/citra_qt/main.cpp | 2 - src/citra_qt/main.h | 5 +-- src/citra_qt/multiplayer/chat_room.cpp | 10 ++--- src/citra_qt/multiplayer/chat_room.h | 2 +- src/citra_qt/multiplayer/client_room.cpp | 20 ++++----- src/citra_qt/multiplayer/client_room.h | 2 +- src/citra_qt/multiplayer/direct_connect.cpp | 8 ++-- src/citra_qt/multiplayer/direct_connect.h | 2 +- src/citra_qt/multiplayer/host_room.cpp | 18 ++++---- src/citra_qt/multiplayer/host_room.h | 2 +- src/citra_qt/multiplayer/lobby.cpp | 6 +-- src/citra_qt/multiplayer/lobby.h | 4 +- src/citra_qt/multiplayer/message.cpp | 12 +++--- src/citra_qt/multiplayer/state.cpp | 13 +++--- src/citra_qt/multiplayer/validation.h | 48 ++++++++++++++------- src/citra_qt/ui_settings.h | 2 +- 17 files changed, 93 insertions(+), 71 deletions(-) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index fbc343037..868447a4a 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -238,8 +238,12 @@ void Config::ReadValues() { UISettings::values.port = qt_config->value("port", Network::DefaultRoomPort).toString(); UISettings::values.room_nickname = qt_config->value("room_nickname", "").toString(); UISettings::values.room_name = qt_config->value("room_name", "").toString(); - UISettings::values.room_port = qt_config->value("room_port", 24872).toString(); - UISettings::values.host_type = qt_config->value("host_type", 0).toString(); + UISettings::values.room_port = qt_config->value("room_port", "24872").toString(); + bool ok; + UISettings::values.host_type = qt_config->value("host_type", 0).toUInt(&ok); + if (!ok) { + UISettings::values.host_type = 0; + } UISettings::values.max_player = qt_config->value("max_player", 8).toUInt(); UISettings::values.game_id = qt_config->value("game_id", 0).toULongLong(); qt_config->endGroup(); diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 306c79ace..519d1e2e5 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -133,8 +133,6 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { SetupUIStrings(); - setWindowTitle(QString("Citra %1| %2-%3") - .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); show(); game_list->LoadCompatibilityList(); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index f0aaf9114..686624603 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -55,10 +55,11 @@ public: void filterBarSetChecked(bool state); void UpdateUITheme(); - GameList* game_list; GMainWindow(); ~GMainWindow(); + GameList* game_list; + signals: /** @@ -180,8 +181,6 @@ private: GRenderWindow* render_window; - QFutureWatcher* watcher = nullptr; - // Status bar elements QProgressBar* progress_bar = nullptr; QLabel* message_label = nullptr; diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index d55b39c22..36f15ba6b 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -22,7 +22,7 @@ class ChatMessage { public: explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) { /// Convert the time to their default locale defined format - static QLocale locale; + QLocale locale; timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); nickname = QString::fromStdString(chat.nickname); message = QString::fromStdString(chat.message); @@ -60,12 +60,12 @@ public: } private: - const QString system_color = "#888888"; + static constexpr const char system_color[] = "#888888"; QString timestamp; QString message; }; -ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(new Ui::ChatRoom) { +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique()) { ui->setupUi(this); // set the item_model for player_view @@ -148,7 +148,7 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { return member.nickname == chat.nickname; }); if (it == members.end()) { - LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); + NGLOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); return; } auto player = std::distance(members.begin(), it); @@ -175,7 +175,7 @@ void ChatRoom::OnSendChat() { return member.nickname == chat.nickname; }); if (it == members.end()) { - LOG_INFO(Network, "Chat message received from unknown player"); + NGLOG_INFO(Network, "Cannot find self in the player list when sending a message."); } auto player = std::distance(members.begin(), it); ChatMessage m(chat); diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index c837c24e0..0683b7a69 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -49,7 +49,7 @@ private: void AppendChatMessage(const QString&); bool ValidateMessage(const std::string&); QStandardItemModel* player_list; - Ui::ChatRoom* ui; + std::unique_ptr ui; }; Q_DECLARE_METATYPE(Network::ChatEntry); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index b4737eb3b..243b8adef 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -19,7 +19,7 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(new Ui::ClientRoom) { + ui(std::make_unique()) { ui->setupUi(this); // setup the callbacks for network updates @@ -49,39 +49,39 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { switch (state) { case Network::RoomMember::State::Idle: - LOG_INFO(Network, "State: Idle"); + NGLOG_INFO(Network, "State: Idle"); break; case Network::RoomMember::State::Joining: - LOG_INFO(Network, "State: Joining"); + NGLOG_INFO(Network, "State: Joining"); break; case Network::RoomMember::State::Joined: - LOG_INFO(Network, "State: Joined"); + NGLOG_INFO(Network, "State: Joined"); ui->chat->Clear(); ui->chat->AppendStatusMessage(tr("Connected")); break; case Network::RoomMember::State::LostConnection: NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); - LOG_INFO(Network, "State: LostConnection"); + NGLOG_INFO(Network, "State: LostConnection"); break; case Network::RoomMember::State::CouldNotConnect: NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - LOG_INFO(Network, "State: CouldNotConnect"); + NGLOG_INFO(Network, "State: CouldNotConnect"); break; case Network::RoomMember::State::NameCollision: NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); - LOG_INFO(Network, "State: NameCollision"); + NGLOG_INFO(Network, "State: NameCollision"); break; case Network::RoomMember::State::MacCollision: NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); - LOG_INFO(Network, "State: MacCollision"); + NGLOG_INFO(Network, "State: MacCollision"); break; case Network::RoomMember::State::WrongPassword: NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); - LOG_INFO(Network, "State: WrongPassword"); + NGLOG_INFO(Network, "State: WrongPassword"); break; case Network::RoomMember::State::WrongVersion: NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); - LOG_INFO(Network, "State: WrongVersion"); + NGLOG_INFO(Network, "State: WrongVersion"); break; default: break; diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index 8d282b754..a09212810 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -34,5 +34,5 @@ private: void UpdateView(); QStandardItemModel* player_list; - Ui::ClientRoom* ui; + std::unique_ptr ui; }; diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index c21fe5d75..218916f68 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -23,7 +23,7 @@ enum class ConnectionType : u8 { TraversalServer, IP }; DirectConnectWindow::DirectConnectWindow(QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(new Ui::DirectConnect) { + ui(std::make_unique()) { ui->setupUi(this); @@ -31,11 +31,11 @@ DirectConnectWindow::DirectConnectWindow(QWidget* parent) watcher = new QFutureWatcher; connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); - ui->nickname->setValidator(Validation::nickname); + ui->nickname->setValidator(Validation::get().nickname); ui->nickname->setText(UISettings::values.nickname); - ui->ip->setValidator(Validation::ip); + ui->ip->setValidator(Validation::get().ip); ui->ip->setText(UISettings::values.ip); - ui->port->setValidator(Validation::port); + ui->port->setValidator(Validation::get().port); ui->port->setText(UISettings::values.port); // TODO(jroweboy): Show or hide the connection options based on the current value of the combo diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h index 026484394..c824cb557 100644 --- a/src/citra_qt/multiplayer/direct_connect.h +++ b/src/citra_qt/multiplayer/direct_connect.h @@ -34,5 +34,5 @@ private: void EndConnecting(); QFutureWatcher* watcher; - Ui::DirectConnect* ui; + std::unique_ptr ui; }; diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 09958e596..8a3cd3d9f 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -26,13 +26,13 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(new Ui::HostRoom), announce_multiplayer_session(session), game_list(list) { + ui(std::make_unique()), announce_multiplayer_session(session), game_list(list) { ui->setupUi(this); // set up validation for all of the fields - ui->room_name->setValidator(Validation::room_name); - ui->username->setValidator(Validation::nickname); - ui->port->setValidator(Validation::port); + ui->room_name->setValidator(Validation::get().room_name); + ui->username->setValidator(Validation::get().nickname); + ui->port->setValidator(Validation::get().port); ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort)); // Create a proxy to the game list to display the list of preferred games @@ -49,8 +49,8 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, 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 = ui->host_type->findData(UISettings::values.host_type); - if (index != -1) { + 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); @@ -94,7 +94,7 @@ void HostRoomWindow::Host() { ui->max_player->value(), game_name.toStdString(), game_id); if (!created) { NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); - LOG_ERROR(Network, "Could not create room!"); + NGLOG_ERROR(Network, "Could not create room!"); ui->host->setEnabled(true); return; } @@ -109,7 +109,7 @@ void HostRoomWindow::Host() { ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); UISettings::values.max_player = ui->max_player->value(); - UISettings::values.host_type = ui->host_type->currentText(); + 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); @@ -136,7 +136,7 @@ void HostRoomWindow::OnConnection() { if (auto session = announce_multiplayer_session.lock()) { session->Start(); } else { - LOG_ERROR(Network, "Starting announce session failed"); + NGLOG_ERROR(Network, "Starting announce session failed"); } } auto parent = static_cast(parentWidget()); diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h index 289dbb040..64e6c0ab8 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/citra_qt/multiplayer/host_room.h @@ -52,7 +52,7 @@ private: std::weak_ptr announce_multiplayer_session; QStandardItemModel* game_list; ComboBoxProxyModel* proxy; - Ui::HostRoom* ui; + std::unique_ptr ui; }; /** diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index b4e91d6ed..66632cdcc 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -22,7 +22,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), - ui(new Ui::Lobby), announce_multiplayer_session(session), game_list(list) { + ui(std::make_unique()), announce_multiplayer_session(session), game_list(list) { ui->setupUi(this); // setup the watcher for background connections @@ -48,7 +48,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->room_list->setExpandsOnDoubleClick(false); ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); - ui->nickname->setValidator(Validation::nickname); + ui->nickname->setValidator(Validation::get().nickname); ui->nickname->setText(UISettings::values.nickname); // UI Buttons @@ -74,7 +74,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, RefreshLobby(); } -const QString Lobby::PasswordPrompt() { +QString Lobby::PasswordPrompt() { bool ok; const QString text = QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"), diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index 4fbc56b9a..c02febf57 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -97,7 +97,7 @@ private: * Prompts for a password. Returns an empty QString if the user either did not provide a * password or if the user closed the window. */ - const QString PasswordPrompt(); + QString PasswordPrompt(); QStandardItemModel* model; QStandardItemModel* game_list; @@ -105,7 +105,7 @@ private: std::future room_list_future; std::weak_ptr announce_multiplayer_session; - Ui::Lobby* ui; + std::unique_ptr ui; QFutureWatcher* watcher; }; diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index f74a57025..28d62bb81 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -19,20 +19,22 @@ const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 const ConnectionError NO_INTERNET( QT_TR_NOOP("Unable to find an internet connection. Check your internet settings.")); const ConnectionError UNABLE_TO_CONNECT( - QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct.")); + 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 COULD_NOT_CREATE_ROOM( QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); const ConnectionError 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 WRONG_VERSION( - QT_TR_NOOP("You are using a different version of Citra-Local-Wifi(tm) then the room " - "you are trying to connect to.")); -const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Wrong password.")); + QT_TR_NOOP("Version mismatch! Please update to the latest version of citra. If the problem " + "persists, contact the room host and ask them to update the server.")); +const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); const ConnectionError GENERIC_ERROR(QT_TR_NOOP("An error occured.")); const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect.")); const ConnectionError MAC_COLLISION( - QT_TR_NOOP("MAC-Address is already in use. Please choose another.")); + QT_TR_NOOP("MAC address is already in use. Please choose another.")); static bool WarnMessage(const std::string& title, const std::string& text) { return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index d90046c52..5339868fb 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -82,11 +82,14 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { announce_multiplayer_session->Stop(); - QMessageBox::warning(this, tr("Error"), - tr("Failed to announce the room to the public lobby.\nThe room will not " - "get listed publicly.\nError: ") + - QString::fromStdString(result.result_string), - QMessageBox::Ok); + 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.\n" + "Debug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); } static void BringWidgetToFront(QWidget* widget) { diff --git a/src/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h index fa6777beb..19c8fca75 100644 --- a/src/citra_qt/multiplayer/validation.h +++ b/src/citra_qt/multiplayer/validation.h @@ -7,22 +7,38 @@ #include #include -namespace Validation { -/// room name can be alphanumeric and " " "_" "." and "-" -static const QRegExp room_name_regex("^[a-zA-Z0-9._- ]+$"); -static const QValidator* room_name = new QRegExpValidator(room_name_regex); +class Validation { +public: + static Validation get() { + static Validation validation; + return validation; + } -/// nickname can be alphanumeric and " " "_" "." and "-" -static const QRegExp nickname_regex("^[a-zA-Z0-9._- ]+$"); -static const QValidator* nickname = new QRegExpValidator(nickname_regex); + ~Validation() { + delete room_name; + delete nickname; + delete ip; + delete port; + } -/// ipv4 address only -// TODO remove this when we support hostnames in direct connect -static const QRegExp ip_regex( - "(([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])"); -static const QValidator* ip = new QRegExpValidator(ip_regex); + /// room name can be alphanumeric and " " "_" "." and "-" + QRegExp room_name_regex = QRegExp("^[a-zA-Z0-9._- ]+$"); + const QValidator* room_name = new QRegExpValidator(room_name_regex); -/// port must be between 0 and 65535 -static const QValidator* port = new QIntValidator(0, 65535); -}; // namespace Validation + /// nickname can be alphanumeric and " " "_" "." and "-" + QRegExp nickname_regex = QRegExp("^[a-zA-Z0-9._- ]+$"); + const QValidator* nickname = new QRegExpValidator(nickname_regex); + + /// ipv4 address only + // TODO remove this when we support hostnames in direct connect + QRegExp ip_regex = QRegExp( + "(([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])"); + const QValidator* ip = new QRegExpValidator(ip_regex); + + /// port must be between 0 and 65535 + const QValidator* port = new QIntValidator(0, 65535); + +private: + Validation() = default; +}; diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index 895a24c1b..b102f560d 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -65,7 +65,7 @@ struct Values { QString room_name; quint32 max_player; QString room_port; - QString host_type; + uint host_type; qulonglong game_id; }; From 01b49b7e785fdf3532b68c3fe2f74af379cb3fa1 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 17 Apr 2018 23:33:54 -0600 Subject: [PATCH 11/16] Fix compilation issue where unique_ptr needs full class declaration --- src/citra_qt/multiplayer/chat_room.cpp | 1 - src/citra_qt/multiplayer/chat_room.h | 5 +---- src/citra_qt/multiplayer/client_room.cpp | 1 - src/citra_qt/multiplayer/client_room.h | 5 +---- src/citra_qt/multiplayer/direct_connect.cpp | 1 - src/citra_qt/multiplayer/direct_connect.h | 5 +---- src/citra_qt/multiplayer/host_room.cpp | 2 -- src/citra_qt/multiplayer/host_room.h | 5 +---- src/citra_qt/multiplayer/lobby.cpp | 1 - src/citra_qt/multiplayer/lobby.h | 5 +---- 10 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 36f15ba6b..877c99af4 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -16,7 +16,6 @@ #include "citra_qt/multiplayer/message.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" -#include "ui_chat_room.h" class ChatMessage { public: diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index 0683b7a69..440860459 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -10,10 +10,7 @@ #include #include #include "network/network.h" - -namespace Ui { -class ChatRoom; -} // namespace Ui +#include "ui_chat_room.h" namespace Core { class AnnounceMultiplayerSession; diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 243b8adef..2fff32275 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -15,7 +15,6 @@ #include "citra_qt/multiplayer/message.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" -#include "ui_client_room.h" ClientRoomWindow::ClientRoomWindow(QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index a09212810..f235c4b77 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -5,10 +5,7 @@ #pragma once #include "citra_qt/multiplayer/chat_room.h" - -namespace Ui { -class ClientRoom; -} +#include "ui_client_room.h" class ClientRoomWindow : public QDialog { Q_OBJECT diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 218916f68..d623054dd 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -17,7 +17,6 @@ #include "citra_qt/ui_settings.h" #include "core/settings.h" #include "network/network.h" -#include "ui_direct_connect.h" enum class ConnectionType : u8 { TraversalServer, IP }; diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h index c824cb557..86259dc3b 100644 --- a/src/citra_qt/multiplayer/direct_connect.h +++ b/src/citra_qt/multiplayer/direct_connect.h @@ -7,10 +7,7 @@ #include #include #include - -namespace Ui { -class DirectConnect; -} +#include "ui_direct_connect.h" class DirectConnectWindow : public QDialog { Q_OBJECT diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 8a3cd3d9f..e8fbd44f8 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -20,8 +20,6 @@ #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" #include "core/settings.h" -#include "ui_chat_room.h" -#include "ui_host_room.h" HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h index 64e6c0ab8..53362ddba 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/citra_qt/multiplayer/host_room.h @@ -11,10 +11,7 @@ #include #include "citra_qt/multiplayer/chat_room.h" #include "network/network.h" - -namespace Ui { -class HostRoom; -} // namespace Ui +#include "ui_host_room.h" namespace Core { class AnnounceMultiplayerSession; diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 66632cdcc..e4522af22 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -17,7 +17,6 @@ #include "common/logging/log.h" #include "core/settings.h" #include "network/network.h" -#include "ui_lobby.h" Lobby::Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index c02febf57..80c81a3a0 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -13,10 +13,7 @@ #include "common/announce_multiplayer_room.h" #include "core/announce_multiplayer_session.h" #include "network/network.h" - -namespace Ui { -class Lobby; -} +#include "ui_lobby.h" class LobbyModel; class LobbyFilterProxyModel; From 2be02f221d58ca5ea31ac94b758289224e76a466 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 18 Apr 2018 10:29:03 -0600 Subject: [PATCH 12/16] Fix player list not showing in lobby. Fix host and direct connect crashing citra --- src/citra_qt/multiplayer/chat_room.cpp | 3 ++ src/citra_qt/multiplayer/chat_room.h | 6 +++- src/citra_qt/multiplayer/client_room.cpp | 3 ++ src/citra_qt/multiplayer/client_room.h | 6 +++- src/citra_qt/multiplayer/direct_connect.cpp | 9 +++-- src/citra_qt/multiplayer/direct_connect.h | 8 ++++- src/citra_qt/multiplayer/host_room.cpp | 9 +++-- src/citra_qt/multiplayer/host_room.h | 8 ++++- src/citra_qt/multiplayer/lobby.cpp | 7 ++-- src/citra_qt/multiplayer/lobby.h | 2 ++ src/citra_qt/multiplayer/validation.h | 38 ++++++++++++--------- 11 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp index 877c99af4..5bb20bc12 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -16,6 +16,7 @@ #include "citra_qt/multiplayer/message.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" +#include "ui_chat_room.h" class ChatMessage { public: @@ -102,6 +103,8 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_uniquesend_message, &QPushButton::pressed, this, &ChatRoom::OnSendChat); } +ChatRoom::~ChatRoom() = default; + void ChatRoom::Clear() { ui->chat_history->clear(); } diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h index 440860459..4604c3395 100644 --- a/src/citra_qt/multiplayer/chat_room.h +++ b/src/citra_qt/multiplayer/chat_room.h @@ -10,7 +10,10 @@ #include #include #include "network/network.h" -#include "ui_chat_room.h" + +namespace Ui { +class ChatRoom; +} namespace Core { class AnnounceMultiplayerSession; @@ -29,6 +32,7 @@ public: void SetPlayerList(const Network::RoomMember::MemberList& member_list); void Clear(); void AppendStatusMessage(const QString& msg); + ~ChatRoom(); public slots: void OnRoomUpdate(const Network::RoomInformation& info); diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 2fff32275..8e37426bd 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -15,6 +15,7 @@ #include "citra_qt/multiplayer/message.h" #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" +#include "ui_client_room.h" ClientRoomWindow::ClientRoomWindow(QWidget* parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), @@ -41,6 +42,8 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) UpdateView(); } +ClientRoomWindow::~ClientRoomWindow() = default; + void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { UpdateView(); } diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index f235c4b77..960dbe97f 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -5,13 +5,17 @@ #pragma once #include "citra_qt/multiplayer/chat_room.h" -#include "ui_client_room.h" + +namespace Ui { +class ClientRoom; +} class ClientRoomWindow : public QDialog { Q_OBJECT public: explicit ClientRoomWindow(QWidget* parent); + ~ClientRoomWindow(); public slots: void OnRoomUpdate(const Network::RoomInformation&); diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index d623054dd..7a47918fb 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -17,6 +17,7 @@ #include "citra_qt/ui_settings.h" #include "core/settings.h" #include "network/network.h" +#include "ui_direct_connect.h" enum class ConnectionType : u8 { TraversalServer, IP }; @@ -30,11 +31,11 @@ DirectConnectWindow::DirectConnectWindow(QWidget* parent) watcher = new QFutureWatcher; connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection); - ui->nickname->setValidator(Validation::get().nickname); + ui->nickname->setValidator(validation.GetNickname()); ui->nickname->setText(UISettings::values.nickname); - ui->ip->setValidator(Validation::get().ip); + ui->ip->setValidator(validation.GetIP()); ui->ip->setText(UISettings::values.ip); - ui->port->setValidator(Validation::get().port); + 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 @@ -42,6 +43,8 @@ DirectConnectWindow::DirectConnectWindow(QWidget* parent) connect(ui->connect, &QPushButton::pressed, this, &DirectConnectWindow::Connect); } +DirectConnectWindow::~DirectConnectWindow() = default; + void DirectConnectWindow::Connect() { if (!ui->nickname->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h index 86259dc3b..de167c1f9 100644 --- a/src/citra_qt/multiplayer/direct_connect.h +++ b/src/citra_qt/multiplayer/direct_connect.h @@ -7,13 +7,18 @@ #include #include #include -#include "ui_direct_connect.h" +#include "citra_qt/multiplayer/validation.h" + +namespace Ui { +class DirectConnect; +} class DirectConnectWindow : public QDialog { Q_OBJECT public: explicit DirectConnectWindow(QWidget* parent = nullptr); + ~DirectConnectWindow(); signals: /** @@ -32,4 +37,5 @@ private: QFutureWatcher* watcher; std::unique_ptr ui; + Validation validation; }; diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index e8fbd44f8..3f53b3c8a 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -20,6 +20,7 @@ #include "common/logging/log.h" #include "core/announce_multiplayer_session.h" #include "core/settings.h" +#include "ui_host_room.h" HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session) @@ -28,9 +29,9 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, ui->setupUi(this); // set up validation for all of the fields - ui->room_name->setValidator(Validation::get().room_name); - ui->username->setValidator(Validation::get().nickname); - ui->port->setValidator(Validation::get().port); + 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 @@ -57,6 +58,8 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, } } +HostRoomWindow::~HostRoomWindow() = default; + void HostRoomWindow::Host() { if (!ui->username->hasAcceptableInput()) { NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h index 53362ddba..038b9f538 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/citra_qt/multiplayer/host_room.h @@ -10,8 +10,12 @@ #include #include #include "citra_qt/multiplayer/chat_room.h" +#include "citra_qt/multiplayer/validation.h" #include "network/network.h" -#include "ui_host_room.h" + +namespace Ui { +class HostRoom; +} namespace Core { class AnnounceMultiplayerSession; @@ -28,6 +32,7 @@ class HostRoomWindow : public QDialog { public: explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session); + ~HostRoomWindow(); signals: /** @@ -50,6 +55,7 @@ private: QStandardItemModel* game_list; ComboBoxProxyModel* proxy; std::unique_ptr ui; + Validation validation; }; /** diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index e4522af22..1c90ef4ae 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -47,7 +47,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, ui->room_list->setExpandsOnDoubleClick(false); ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); - ui->nickname->setValidator(Validation::get().nickname); + ui->nickname->setValidator(validation.GetNickname()); ui->nickname->setText(UISettings::values.nickname); // UI Buttons @@ -189,7 +189,6 @@ void Lobby::OnRefreshLobby() { first_item->appendRow(new LobbyItemExpandedMemberList(members)); } } - proxy->setSourceModel(model); // Reenable the refresh button and resize the columns ui->refresh_list->setEnabled(true); @@ -200,10 +199,10 @@ void Lobby::OnRefreshLobby() { } // Set the member list child items to span all columns - for (int i = 0; i < model->rowCount(); i++) { + for (int i = 0; i < proxy->rowCount(); i++) { auto parent = model->item(i, 0); if (parent->hasChildren()) { - ui->room_list->setFirstColumnSpanned(0, parent->index(), true); + ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true); } } } diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index 80c81a3a0..d3f753fcd 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -10,6 +10,7 @@ #include #include #include +#include "citra_qt/multiplayer/validation.h" #include "common/announce_multiplayer_room.h" #include "core/announce_multiplayer_session.h" #include "network/network.h" @@ -104,6 +105,7 @@ private: std::weak_ptr announce_multiplayer_session; std::unique_ptr ui; QFutureWatcher* watcher; + Validation validation; }; /** diff --git a/src/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h index 19c8fca75..4e8f6b9e9 100644 --- a/src/citra_qt/multiplayer/validation.h +++ b/src/citra_qt/multiplayer/validation.h @@ -9,36 +9,40 @@ class Validation { public: - static Validation get() { - static Validation validation; - return validation; - } - - ~Validation() { - delete room_name; - delete nickname; - delete ip; - delete port; + 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 "-" QRegExp room_name_regex = QRegExp("^[a-zA-Z0-9._- ]+$"); - const QValidator* room_name = new QRegExpValidator(room_name_regex); + QRegExpValidator room_name; /// nickname can be alphanumeric and " " "_" "." and "-" QRegExp nickname_regex = QRegExp("^[a-zA-Z0-9._- ]+$"); - const QValidator* nickname = new QRegExpValidator(nickname_regex); + QRegExpValidator nickname; /// ipv4 address only // TODO remove this when we support hostnames in direct connect QRegExp ip_regex = QRegExp( "(([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])"); - const QValidator* ip = new QRegExpValidator(ip_regex); + QRegExpValidator ip; /// port must be between 0 and 65535 - const QValidator* port = new QIntValidator(0, 65535); - -private: - Validation() = default; + QIntValidator port; }; From a5c8e07f466ac538f94d1ed91dd8e7130e1d4ef8 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 18 Apr 2018 11:04:32 -0600 Subject: [PATCH 13/16] Remove duplicated logic in HostRoom Fixes some issues with multiple warning messages --- src/citra_qt/multiplayer/host_room.cpp | 7 ++----- src/citra_qt/multiplayer/state.cpp | 7 ++++--- src/citra_qt/multiplayer/state.h | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 3f53b3c8a..946915378 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -75,13 +75,10 @@ void HostRoomWindow::Host() { } if (auto member = Network::GetRoomMember().lock()) { if (member->IsConnected()) { - if (!NetworkMessage::WarnDisconnect()) { + auto parent = static_cast(parentWidget()); + if (!parent->OnCloseRoom()) { close(); return; - } else { - member->Leave(); - auto parent = static_cast(parentWidget()); - parent->OnCloseRoom(); } } ui->host->setDisabled(true); diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 5339868fb..3c40a7d86 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -112,9 +112,9 @@ void MultiplayerState::OnCreateRoom() { BringWidgetToFront(host_room); } -void MultiplayerState::OnCloseRoom() { +bool MultiplayerState::OnCloseRoom() { if (!NetworkMessage::WarnCloseRoom()) - return; + return false; if (auto room = Network::GetRoom().lock()) { // if you are in a room, leave it if (auto member = Network::GetRoomMember().lock()) { @@ -123,11 +123,12 @@ void MultiplayerState::OnCloseRoom() { // if you are hosting a room, also stop hosting if (room->GetState() != Network::Room::State::Open) { - return; + return true; } room->Destroy(); announce_multiplayer_session->Stop(); } + return true; } void MultiplayerState::OnOpenNetworkRoom() { diff --git a/src/citra_qt/multiplayer/state.h b/src/citra_qt/multiplayer/state.h index 1829d19fb..673bc6ecf 100644 --- a/src/citra_qt/multiplayer/state.h +++ b/src/citra_qt/multiplayer/state.h @@ -42,7 +42,7 @@ public slots: void OnNetworkStateChanged(const Network::RoomMember::State& state); void OnViewLobby(); void OnCreateRoom(); - void OnCloseRoom(); + bool OnCloseRoom(); void OnOpenNetworkRoom(); void OnDirectConnectToRoom(); void OnAnnounceFailed(const Common::WebResult&); From 1f6791431d0bcb2b9ed0cf0c162b2121c324d96b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 19 Apr 2018 00:47:11 -0600 Subject: [PATCH 14/16] Move almost all state change tracking to MultiplayerState Each window can still watch for state changes to update the ui or to close the window as appropriate, but for any error announcements, they all belong in Multiplayer state now. --- src/citra_qt/multiplayer/client_room.cpp | 40 +-------------- src/citra_qt/multiplayer/client_room.h | 5 -- src/citra_qt/multiplayer/direct_connect.cpp | 14 +----- src/citra_qt/multiplayer/host_room.cpp | 18 +------ src/citra_qt/multiplayer/host_room.h | 7 --- src/citra_qt/multiplayer/message.cpp | 5 +- src/citra_qt/multiplayer/state.cpp | 54 ++++++++++++++++++--- 7 files changed, 55 insertions(+), 88 deletions(-) diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp index 8e37426bd..60f488587 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -49,44 +49,9 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { } void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { - switch (state) { - case Network::RoomMember::State::Idle: - NGLOG_INFO(Network, "State: Idle"); - break; - case Network::RoomMember::State::Joining: - NGLOG_INFO(Network, "State: Joining"); - break; - case Network::RoomMember::State::Joined: - NGLOG_INFO(Network, "State: Joined"); + if (state == Network::RoomMember::State::Joined) { ui->chat->Clear(); ui->chat->AppendStatusMessage(tr("Connected")); - break; - case Network::RoomMember::State::LostConnection: - NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); - NGLOG_INFO(Network, "State: LostConnection"); - break; - case Network::RoomMember::State::CouldNotConnect: - NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - NGLOG_INFO(Network, "State: CouldNotConnect"); - break; - case Network::RoomMember::State::NameCollision: - NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); - NGLOG_INFO(Network, "State: NameCollision"); - break; - case Network::RoomMember::State::MacCollision: - NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); - NGLOG_INFO(Network, "State: MacCollision"); - break; - case Network::RoomMember::State::WrongPassword: - NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); - NGLOG_INFO(Network, "State: WrongPassword"); - break; - case Network::RoomMember::State::WrongVersion: - NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); - NGLOG_INFO(Network, "State: WrongVersion"); - break; - default: - break; } UpdateView(); } @@ -99,7 +64,6 @@ void ClientRoomWindow::Disconnect() { member->Leave(); ui->chat->AppendStatusMessage(tr("Disconnected")); close(); - emit Closed(); } } @@ -120,6 +84,4 @@ void ClientRoomWindow::UpdateView() { } // TODO(B3N30): can't get RoomMember*, show error and close window close(); - emit Closed(); - return; } diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h index 960dbe97f..8e8ee24eb 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/citra_qt/multiplayer/client_room.h @@ -22,11 +22,6 @@ public slots: void OnStateChange(const Network::RoomMember::State&); signals: - /** - * Signalled by this widget when it is closing itself and destroying any state such as - * connections that it might have. - */ - void Closed(); void RoomInformationChanged(const Network::RoomInformation&); void StateChanged(const Network::RoomMember::State&); diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp index 7a47918fb..a9b64c98c 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -105,20 +105,8 @@ void DirectConnectWindow::EndConnecting() { void DirectConnectWindow::OnConnection() { EndConnecting(); - bool isConnected = true; if (auto room_member = Network::GetRoomMember().lock()) { - switch (room_member->GetState()) { - case Network::RoomMember::State::CouldNotConnect: - isConnected = false; - ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::NameCollision: - isConnected = false; - ShowError(NetworkMessage::USERNAME_IN_USE); - break; - case Network::RoomMember::State::Joining: - auto parent = static_cast(parentWidget()); - parent->OnOpenNetworkRoom(); + if (room_member->GetState() == Network::RoomMember::State::Joined) { close(); } } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 946915378..a57f31052 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -119,17 +119,8 @@ void HostRoomWindow::Host() { void HostRoomWindow::OnConnection() { ui->host->setEnabled(true); if (auto room_member = Network::GetRoomMember().lock()) { - switch (room_member->GetState()) { - case Network::RoomMember::State::CouldNotConnect: - ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::NameCollision: - ShowError(NetworkMessage::USERNAME_IN_USE); - break; - case Network::RoomMember::State::Error: - ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::Joining: + if (room_member->GetState() == Network::RoomMember::State::Joining) { + // Start the announce session if they chose Public if (ui->host_type->currentIndex() == 0) { if (auto session = announce_multiplayer_session.lock()) { session->Start(); @@ -137,12 +128,7 @@ void HostRoomWindow::OnConnection() { NGLOG_ERROR(Network, "Starting announce session failed"); } } - auto parent = static_cast(parentWidget()); - // parent->ChangeRoomState(); - parent->OnOpenNetworkRoom(); close(); - emit Closed(); - break; } } } diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h index 038b9f538..574dc2824 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/citra_qt/multiplayer/host_room.h @@ -34,13 +34,6 @@ public: std::shared_ptr session); ~HostRoomWindow(); -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: /** * Handler for connection status changes. Launches the chat window if successful or diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp index 28d62bb81..57cd7671c 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/citra_qt/multiplayer/message.cpp @@ -28,10 +28,11 @@ const ConnectionError 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 WRONG_VERSION( - QT_TR_NOOP("Version mismatch! Please update to the latest version of citra. If the problem " + QT_TR_NOOP("Version mismatch! Please update to the latest version of Citra. If the problem " "persists, contact the room host and ask them to update the server.")); const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); -const ConnectionError GENERIC_ERROR(QT_TR_NOOP("An error occured.")); +const ConnectionError GENERIC_ERROR( + QT_TR_NOOP("An unknown error occured. If this error continues to occur, please open an issue")); const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect.")); const ConnectionError MAC_COLLISION( QT_TR_NOOP("MAC address is already in use. Please choose another.")); diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index 3c40a7d86..f741b299b 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -67,17 +67,59 @@ void MultiplayerState::Close() { void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { NGLOG_DEBUG(Frontend, "Network state change"); - if (state == Network::RoomMember::State::Joined) { + bool is_connected = false; + switch (state) { + case Network::RoomMember::State::Idle: + NGLOG_DEBUG(Network, "State: Idle"); + break; + case Network::RoomMember::State::Joining: + NGLOG_DEBUG(Network, "State: Joining"); + break; + case Network::RoomMember::State::LostConnection: + NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); + NGLOG_DEBUG(Network, "State: LostConnection"); + break; + case Network::RoomMember::State::CouldNotConnect: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + NGLOG_DEBUG(Network, "State: CouldNotConnect"); + break; + case Network::RoomMember::State::NameCollision: + NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); + NGLOG_DEBUG(Network, "State: NameCollision"); + break; + case Network::RoomMember::State::MacCollision: + NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); + NGLOG_DEBUG(Network, "State: MacCollision"); + break; + case Network::RoomMember::State::WrongPassword: + NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); + NGLOG_DEBUG(Network, "State: WrongPassword"); + break; + case Network::RoomMember::State::WrongVersion: + NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); + NGLOG_DEBUG(Network, "State: WrongVersion"); + break; + case Network::RoomMember::State::Error: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + NGLOG_DEBUG(Network, "State: GenericError"); + break; + case Network::RoomMember::State::Joined: + NGLOG_DEBUG(Network, "State: Joined"); + is_connected = true; + OnOpenNetworkRoom(); + break; + } + if (is_connected) { status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); status_text->setText(tr("Connected")); leave_room->setEnabled(true); show_room->setEnabled(true); - return; + } else { + status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); + status_text->setText(tr("Not Connected")); + leave_room->setEnabled(false); + show_room->setEnabled(false); } - status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); - status_text->setText(tr("Not Connected")); - leave_room->setEnabled(false); - show_room->setEnabled(false); } void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { From 62257e0d79ba6365402b1f2d92876e3f4e72a026 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 19 Apr 2018 01:18:45 -0600 Subject: [PATCH 15/16] Fix Lobby filtering with player list * Make double clicking the player list open the correct room * Fix an issue where filtering with search broke the whos playing list --- src/citra_qt/multiplayer/lobby.cpp | 29 +++++++++++++++++++++-------- src/citra_qt/multiplayer/lobby.h | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index 1c90ef4ae..aca07f59c 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -56,8 +56,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, connect(ui->games_owned, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterOwned); connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull); - connect(ui->search, &QLineEdit::textChanged, proxy, - &LobbyFilterProxyModel::setFilterFixedString); + 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); @@ -86,7 +85,12 @@ void Lobby::OnExpandRoom(const QModelIndex& index) { auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList(); } -void Lobby::OnJoinRoom(const QModelIndex& index) { +void Lobby::OnJoinRoom(const QModelIndex& source) { + 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::ShowError(NetworkMessage::USERNAME_NOT_VALID); return; @@ -213,6 +217,11 @@ LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel 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); @@ -226,23 +235,22 @@ bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& s } // filter by search parameters - auto search_param = filterRegExp(); - if (!search_param.isEmpty()) { + 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(search_param); + .contains(filter_search, filterCaseSensitivity()); bool room_name_match = sourceModel() ->data(room_name, LobbyItemName::NameRole) .toString() - .contains(search_param); + .contains(filter_search, filterCaseSensitivity()); bool username_match = sourceModel() ->data(host_name, LobbyItemHost::HostUsernameRole) .toString() - .contains(search_param); + .contains(filter_search, filterCaseSensitivity()); if (!preferred_game_match && !room_name_match && !username_match) { return false; } @@ -289,6 +297,11 @@ void LobbyFilterProxyModel::SetFilterFull(bool filter) { invalidate(); } +void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { + filter_search = filter; + invalidate(); +} + void Lobby::OnConnection() { if (auto room_member = Network::GetRoomMember().lock()) { switch (room_member->GetState()) { diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index d3f753fcd..d44c7fcbc 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -122,9 +122,11 @@ public: 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; }; From d35693bbbc870265c41dacd02cce803f72ba6d9d Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 19 Apr 2018 10:23:39 -0600 Subject: [PATCH 16/16] More minor issue fixes * Move Joining state change sooner in the code to prevent an issue where failing to connect multiple times in a row doesn't change the state (as it goes from CouldNotConnect -> CouldNotConnect which doesn't trigger a state changed callback) * Prevent double clicking too fast on a room in the lobby from causing issues * Lobby no longer closes when joining a room --- src/citra_qt/multiplayer/lobby.cpp | 29 +++++------------------------ src/citra_qt/multiplayer/lobby.h | 18 +----------------- src/citra_qt/multiplayer/state.cpp | 16 +--------------- src/network/room_member.cpp | 3 ++- src/network/room_member.h | 26 ++++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index aca07f59c..0b58091cb 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -26,7 +26,7 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, // setup the watcher for background connections watcher = new QFutureWatcher; - connect(watcher, &QFutureWatcher::finished, this, &Lobby::OnConnection); + connect(watcher, &QFutureWatcher::finished, [&] { joining = false; }); model = new QStandardItemModel(ui->room_list); proxy = new LobbyFilterProxyModel(this, game_list); @@ -62,8 +62,6 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, // Actions connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby); - // TODO(jroweboy): change this slot to OnConnected? - connect(this, &Lobby::Connected, p, &MultiplayerState::OnOpenNetworkRoom); // 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 @@ -86,6 +84,10 @@ void Lobby::OnExpandRoom(const QModelIndex& index) { } void Lobby::OnJoinRoom(const QModelIndex& source) { + if (joining) { + return; + } + joining = true; 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()) { @@ -301,24 +303,3 @@ void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { filter_search = filter; invalidate(); } - -void Lobby::OnConnection() { - if (auto room_member = Network::GetRoomMember().lock()) { - switch (room_member->GetState()) { - case Network::RoomMember::State::CouldNotConnect: - ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::NameCollision: - ShowError(NetworkMessage::USERNAME_IN_USE); - break; - case Network::RoomMember::State::Error: - ShowError(NetworkMessage::UNABLE_TO_CONNECT); - break; - case Network::RoomMember::State::Joining: - auto parent = static_cast(parentWidget()); - parent->OnOpenNetworkRoom(); - close(); - break; - } - } -} diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h index d44c7fcbc..783225737 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/citra_qt/multiplayer/lobby.h @@ -60,29 +60,12 @@ private slots: */ void OnJoinRoom(const QModelIndex&); - /** - * Handler for connection status changes. Launches the client room window if successful or - * displays an error - */ - void OnConnection(); - signals: /** * Signalled when the latest lobby data is retrieved. */ void LobbyRefreshed(); - /** - * Signalled when the status for room connection changes. - */ - void Connected(); - - /** - * Signalled by this widget when it is closing itself and destroying any state such as - * connections that it might have. - */ - void Closed(); - void StateChanged(const Network::RoomMember::State&); private: @@ -106,6 +89,7 @@ private: std::unique_ptr ui; QFutureWatcher* watcher; Validation validation; + bool joining = false; }; /** diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp index f741b299b..dfd5ee66f 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/citra_qt/multiplayer/state.cpp @@ -66,45 +66,31 @@ void MultiplayerState::Close() { } void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { - NGLOG_DEBUG(Frontend, "Network state change"); + NGLOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); bool is_connected = false; switch (state) { - case Network::RoomMember::State::Idle: - NGLOG_DEBUG(Network, "State: Idle"); - break; - case Network::RoomMember::State::Joining: - NGLOG_DEBUG(Network, "State: Joining"); - break; case Network::RoomMember::State::LostConnection: NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); - NGLOG_DEBUG(Network, "State: LostConnection"); break; case Network::RoomMember::State::CouldNotConnect: NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - NGLOG_DEBUG(Network, "State: CouldNotConnect"); break; case Network::RoomMember::State::NameCollision: NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); - NGLOG_DEBUG(Network, "State: NameCollision"); break; case Network::RoomMember::State::MacCollision: NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); - NGLOG_DEBUG(Network, "State: MacCollision"); break; case Network::RoomMember::State::WrongPassword: NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); - NGLOG_DEBUG(Network, "State: WrongPassword"); break; case Network::RoomMember::State::WrongVersion: NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); - NGLOG_DEBUG(Network, "State: WrongVersion"); break; case Network::RoomMember::State::Error: NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); - NGLOG_DEBUG(Network, "State: GenericError"); break; case Network::RoomMember::State::Joined: - NGLOG_DEBUG(Network, "State: Joined"); is_connected = true; OnOpenNetworkRoom(); break; diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 3a143ce5d..e8d9094c5 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -406,6 +406,8 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); } + room_member_impl->SetState(State::Joining); + ENetAddress address{}; enet_address_set_host(&address, server_addr); address.port = server_port; @@ -421,7 +423,6 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs); if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { room_member_impl->nickname = nick; - room_member_impl->SetState(State::Joining); room_member_impl->StartLoop(); room_member_impl->SendJoinRequest(nick, preferred_mac, password); SendGameInfo(room_member_impl->current_game_info); diff --git a/src/network/room_member.h b/src/network/room_member.h index 77b73890d..d01eb7dcd 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -187,4 +187,30 @@ private: std::unique_ptr room_member_impl; }; +static const char* GetStateStr(const RoomMember::State& s) { + switch (s) { + case RoomMember::State::Idle: + return "Idle"; + case RoomMember::State::Error: + return "Error"; + case RoomMember::State::Joining: + return "Joining"; + case RoomMember::State::Joined: + return "Joined"; + case RoomMember::State::LostConnection: + return "LostConnection"; + case RoomMember::State::NameCollision: + return "NameCollision"; + case RoomMember::State::MacCollision: + return "MacCollision"; + case RoomMember::State::WrongVersion: + return "WrongVersion"; + case RoomMember::State::WrongPassword: + return "WrongPassword"; + case RoomMember::State::CouldNotConnect: + return "CouldNotConnect"; + } + return "Unknown"; +} + } // namespace Network