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 @@ <file alias="16x16/checked.png">icons/16x16/checked.png</file> <file alias="16x16/failed.png">icons/16x16/failed.png</file> - + + <file alias="16x16/connected.png">icons/16x16/connected.png</file> + + <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> + + <file alias="16x16/lock.png">icons/16x16/lock.png</file> + <file alias="256x256/citra.png">icons/256x256/citra.png</file> </qresource> </RCC> 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 000000000..afa797394 Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/connected.png differ 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 000000000..835b1f0d6 Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/disconnected.png differ 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 000000000..496b58078 Binary files /dev/null and b/dist/qt_themes/default/icons/16x16/lock.png differ 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 000000000..c750a39e8 Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/16x16/lock.png differ 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 @@ <RCC> <qresource prefix="icons/qdarkstyle"> <file alias="index.theme">icons/index.theme</file> + <file alias="16x16/lock.png">icons/16x16/lock.png</file> </qresource> <qresource prefix="qss_icons"> <file>rc/up_arrow_disabled.png</file> diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 4d08a622e..1a150065f 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -56,11 +56,29 @@ add_executable(citra-qt hotkeys.h main.cpp main.h + multiplayer/chat_room.h + multiplayer/chat_room.cpp + multiplayer/client_room.h + multiplayer/client_room.cpp + multiplayer/direct_connect.h + multiplayer/direct_connect.cpp + multiplayer/host_room.h + multiplayer/host_room.cpp + multiplayer/lobby.h + multiplayer/lobby_p.h + multiplayer/lobby.cpp + multiplayer/message.h + multiplayer/message.cpp + multiplayer/state.cpp + multiplayer/state.h + multiplayer/validation.h ui_settings.cpp ui_settings.h updater/updater.cpp updater/updater.h updater/updater_p.h + util/clickable_label.h + util/clickable_label.cpp util/spinbox.cpp util/spinbox.h util/util.cpp @@ -79,6 +97,11 @@ set(UIS configuration/configure_system.ui configuration/configure_web.ui debugger/registers.ui + multiplayer/direct_connect.ui + multiplayer/lobby.ui + multiplayer/chat_room.ui + multiplayer/client_room.ui + multiplayer/host_room.ui aboutdialog.ui hotkeys.ui main.ui diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 1691a5124..dc345ad57 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -108,12 +108,10 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread) setWindowTitle(QString::fromStdString(window_title)); InputCommon::Init(); - Network::Init(); } GRenderWindow::~GRenderWindow() { InputCommon::Shutdown(); - Network::Shutdown(); } void GRenderWindow::moveContext() { diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index a0dc0cd84..868447a4a 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -7,6 +7,7 @@ #include "citra_qt/ui_settings.h" #include "common/file_util.h" #include "input_common/main.h" +#include "network/network.h" Config::Config() { // TODO: Don't hardcode the path; let the frontend decide where to put the config files. @@ -162,6 +163,12 @@ void Config::ReadValues() { qt_config->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,22 @@ 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(); + 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(); + qt_config->endGroup(); } @@ -320,6 +343,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 +392,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..519d1e2e5 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -31,8 +31,10 @@ #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" #include "citra_qt/main.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" #include "common/logging/backend.h" #include "common/logging/filter.h" #include "common/logging/log.h" @@ -115,6 +117,8 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { default_theme_paths = QIcon::themeSearchPaths(); UpdateUITheme(); + Network::Init(); + InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); @@ -153,6 +157,7 @@ GMainWindow::~GMainWindow() { delete render_window; Pica::g_debug_context.reset(); + Network::Shutdown(); } void GMainWindow::InitializeWidgets() { @@ -165,6 +170,10 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(this); ui.horizontalLayout->addWidget(game_list); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room, + ui.action_Show_Room); + multiplayer_state->setVisible(false); + // Setup updater updater = new Updater(this); UISettings::values.updater_found = updater->HasUpdater(); @@ -199,6 +208,8 @@ void GMainWindow::InitializeWidgets() { label->setContentsMargins(4, 0, 4, 0); statusBar()->addPermanentWidget(label, 0); } + 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 @@ -418,6 +429,19 @@ 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, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui.action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + 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_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::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()); @@ -1186,7 +1210,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) { ShutdownGame(); render_window->close(); - + multiplayer_state->Close(); QWidget::closeEvent(event); } diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index c29c0ccfc..686624603 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -5,15 +5,18 @@ #pragma once #include <memory> +#include <QLabel> #include <QMainWindow> #include <QTimer> #include <QTranslator> +#include "common/announce_multiplayer_room.h" #include "core/core.h" #include "core/hle/service/am/am.h" #include "ui_main.h" class AboutDialog; class Config; +class ClickableLabel; class EmuThread; class GameList; enum class GameListOpenTarget; @@ -25,6 +28,7 @@ class GraphicsTracingWidget; class GraphicsVertexShaderWidget; class GRenderWindow; class MicroProfileDialog; +class MultiplayerState; class ProfilerWidget; template <typename> class QFutureWatcher; @@ -50,9 +54,12 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); + GMainWindow(); ~GMainWindow(); + GameList* game_list; + signals: /** @@ -173,7 +180,6 @@ private: Ui::MainWindow ui; GRenderWindow* render_window; - GameList* game_list; // Status bar elements QProgressBar* progress_bar = nullptr; @@ -183,6 +189,7 @@ private: QLabel* emu_frametime_label = nullptr; QTimer status_bar_update_timer; + MultiplayerState* multiplayer_state = nullptr; std::unique_ptr<Config> config; // Whether emulation is currently running in Citra. diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index c598c444f..86c4e46ed 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -45,7 +45,7 @@ <x>0</x> <y>0</y> <width>1081</width> - <height>26</height> + <height>21</height> </rect> </property> <widget class="QMenu" name="menu_File"> @@ -107,6 +107,20 @@ <addaction name="separator"/> <addaction name="menu_View_Debugging"/> </widget> + <widget class="QMenu" name="menu_Multiplayer"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Multiplayer</string> + </property> + <addaction name="action_View_Lobby"/> + <addaction name="action_Start_Room"/> + <addaction name="action_Connect_To_Room"/> + <addaction name="separator"/> + <addaction name="action_Show_Room"/> + <addaction name="action_Leave_Room"/> + </widget> <widget class="QMenu" name="menu_Help"> <property name="title"> <string>&Help</string> @@ -122,6 +136,7 @@ <addaction name="menu_File"/> <addaction name="menu_Emulation"/> <addaction name="menu_View"/> + <addaction name="menu_Multiplayer"/> <addaction name="menu_Help"/> </widget> <action name="action_Load_File"> @@ -228,6 +243,43 @@ <string>Create Pica Surface Viewer</string> </property> </action> + <action name="action_View_Lobby"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Browse Public Game Lobby</string> + </property> + </action> + <action name="action_Start_Room"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Create Room</string> + </property> + </action> + <action name="action_Leave_Room"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Leave Room</string> + </property> + </action> + <action name="action_Connect_To_Room"> + <property name="text"> + <string>Direct Connect to Room</string> + </property> + </action> + <action name="action_Show_Room"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Show Current Room</string> + </property> + </action> <action name="action_Fullscreen"> <property name="checkable"> <bool>true</bool> @@ -244,46 +296,46 @@ <string>Opens the maintenance tool to modify your Citra installation</string> </property> </action> - <action name="action_Screen_Layout_Default"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Default</string> - </property> - </action> - <action name="action_Screen_Layout_Single_Screen"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Single Screen</string> - </property> - </action> - <action name="action_Screen_Layout_Large_Screen"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Large Screen</string> - </property> - </action> - <action name="action_Screen_Layout_Side_by_Side"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Side by Side</string> - </property> - </action> - <action name="action_Screen_Layout_Swap_Screens"> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="text"> - <string>Swap Screens</string> - </property> - </action> + <action name="action_Screen_Layout_Default"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Default</string> + </property> + </action> + <action name="action_Screen_Layout_Single_Screen"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Single Screen</string> + </property> + </action> + <action name="action_Screen_Layout_Large_Screen"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Large Screen</string> + </property> + </action> + <action name="action_Screen_Layout_Side_by_Side"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Side by Side</string> + </property> + </action> + <action name="action_Screen_Layout_Swap_Screens"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Swap Screens</string> + </property> + </action> <action name="action_Check_For_Updates"> <property name="text"> <string>Check for Updates</string> diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp new file mode 100644 index 000000000..5bb20bc12 --- /dev/null +++ b/src/citra_qt/multiplayer/chat_room.cpp @@ -0,0 +1,211 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <array> +#include <future> +#include <QColor> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMetaType> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#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 + 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] <font color='%2'><%3></font> %4") + .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped()); + } + +private: + static constexpr std::array<const char*, 16> 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 + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + message = msg; + } + + QString GetSystemChatMessage() const { + return QString("[%1] <font color='%2'><i>%3</i></font>") + .arg(timestamp, system_color, message); + } + +private: + static constexpr const char system_color[] = "#888888"; + QString timestamp; + QString message; +}; + +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<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<Network::ChatEntry>(); + qRegisterMetaType<Network::RoomInformation>(); + qRegisterMetaType<Network::RoomMember::State>(); + + // 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); +} + +ChatRoom::~ChatRoom() = default; + +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()) { + NGLOG_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) { + 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()) { + NGLOG_INFO(Network, "Cannot find self in the player list when sending a message."); + } + 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.empty()) + continue; + QList<QStandardItem*> l; + std::vector<std::string> elements = {member.nickname, member.game_info.name}; + for (const 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..4604c3395 --- /dev/null +++ b/src/citra_qt/multiplayer/chat_room.h @@ -0,0 +1,58 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QDialog> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include <QVariant> +#include "network/network.h" + +namespace Ui { +class ChatRoom; +} + +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); + ~ChatRoom(); + +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: + static constexpr u32 max_chat_lines = 1000; + void AppendChatMessage(const QString&); + bool ValidateMessage(const std::string&); + QStandardItemModel* player_list; + std::unique_ptr<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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ChatRoom</class> + <widget class="QWidget" name="ChatRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>607</width> + <height>432</height> + </rect> + </property> + <property name="windowTitle"> + <string>Room Window</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QTreeView" name="player_view"/> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QTextEdit" name="chat_history"> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLineEdit" name="chat_message"> + <property name="placeholderText"> + <string>Send Chat Message</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="send_message"> + <property name="text"> + <string>Send Message</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp new file mode 100644 index 000000000..60f488587 --- /dev/null +++ b/src/citra_qt/multiplayer/client_room.cpp @@ -0,0 +1,87 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <future> +#include <QColor> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMetaType> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#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(std::make_unique<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(); +} + +ClientRoomWindow::~ClientRoomWindow() = default; + +void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { + UpdateView(); +} + +void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { + if (state == Network::RoomMember::State::Joined) { + ui->chat->Clear(); + ui->chat->AppendStatusMessage(tr("Connected")); + } + UpdateView(); +} + +void ClientRoomWindow::Disconnect() { + if (!NetworkMessage::WarnDisconnect()) { + return; + } + if (auto member = Network::GetRoomMember().lock()) { + member->Leave(); + ui->chat->AppendStatusMessage(tr("Disconnected")); + close(); + } +} + +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(); +} diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h new file mode 100644 index 000000000..8e8ee24eb --- /dev/null +++ b/src/citra_qt/multiplayer/client_room.h @@ -0,0 +1,34 @@ +// 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); + ~ClientRoomWindow(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation&); + void OnStateChange(const Network::RoomMember::State&); + +signals: + void RoomInformationChanged(const Network::RoomInformation&); + void StateChanged(const Network::RoomMember::State&); + +private: + void Disconnect(); + void UpdateView(); + + QStandardItemModel* player_list; + std::unique_ptr<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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ClientRoom</class> + <widget class="QWidget" name="ClientRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>607</width> + <height>432</height> + </rect> + </property> + <property name="windowTitle"> + <string>Room Window</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="disconnect"> + <property name="text"> + <string>Leave Room</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="ChatRoom" name="chat" native="true"/> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ChatRoom</class> + <extends>QWidget</extends> + <header>multiplayer/chat_room.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp new file mode 100644 index 000000000..a9b64c98c --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.cpp @@ -0,0 +1,113 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QComboBox> +#include <QFuture> +#include <QIntValidator> +#include <QRegExpValidator> +#include <QString> +#include <QtConcurrent/QtConcurrentRun> +#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/state.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 { TraversalServer, IP }; + +DirectConnectWindow::DirectConnectWindow(QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::DirectConnect>()) { + + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher<void>; + connect(watcher, &QFutureWatcher<void>::finished, this, &DirectConnectWindow::OnConnection); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + ui->ip->setValidator(validation.GetIP()); + ui->ip->setText(UISettings::values.ip); + 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 + // box. Add this back in when the traversal server support is added. + connect(ui->connect, &QPushButton::pressed, this, &DirectConnectWindow::Connect); +} + +DirectConnectWindow::~DirectConnectWindow() = default; + +void DirectConnectWindow::Connect() { + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); + return; + } + if (const auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected() && !NetworkMessage::WarnDisconnect()) { + return; + } + } + switch (static_cast<ConnectionType>(ui->connection_type->currentIndex())) { + case ConnectionType::TraversalServer: + break; + case ConnectionType::IP: + if (!ui->ip->hasAcceptableInput()) { + NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID); + return; + } + break; + } + + // 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<void> 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::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(); + + if (auto room_member = Network::GetRoomMember().lock()) { + if (room_member->GetState() == Network::RoomMember::State::Joined) { + 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..de167c1f9 --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.h @@ -0,0 +1,41 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QDialog> +#include <QFutureWatcher> +#include "citra_qt/multiplayer/validation.h" + +namespace Ui { +class DirectConnect; +} + +class DirectConnectWindow : public QDialog { + Q_OBJECT + +public: + explicit DirectConnectWindow(QWidget* parent = nullptr); + ~DirectConnectWindow(); + +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 BeginConnecting(); + void EndConnecting(); + + QFutureWatcher<void>* watcher; + std::unique_ptr<Ui::DirectConnect> ui; + Validation validation; +}; diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/citra_qt/multiplayer/direct_connect.ui new file mode 100644 index 000000000..681b6bf69 --- /dev/null +++ b/src/citra_qt/multiplayer/direct_connect.ui @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DirectConnect</class> + <widget class="QWidget" name="DirectConnect"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>455</width> + <height>161</height> + </rect> + </property> + <property name="windowTitle"> + <string>Direct Connect</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QComboBox" name="connection_type"> + <item> + <property name="text"> + <string>IP Address</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QWidget" name="ip_container" native="true"> + <layout class="QHBoxLayout" name="ip_layout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>IP</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="ip"> + <property name="toolTip"> + <string><html><head/><body><p>IPv4 address of the host</p></body></html></string> + </property> + <property name="maxLength"> + <number>16</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Port</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="port"> + <property name="toolTip"> + <string><html><head/><body><p>Port number the host is listening on</p></body></html></string> + </property> + <property name="maxLength"> + <number>5</number> + </property> + <property name="placeholderText"> + <string>24872</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nickname"> + <property name="maxLength"> + <number>20</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="password"/> + </item> + </layout> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connect"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp new file mode 100644 index 000000000..a57f31052 --- /dev/null +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -0,0 +1,157 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <future> +#include <QColor> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMetaType> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#include "citra_qt/game_list_p.h" +#include "citra_qt/main.h" +#include "citra_qt/multiplayer/host_room.h" +#include "citra_qt/multiplayer/message.h" +#include "citra_qt/multiplayer/state.h" +#include "citra_qt/multiplayer/validation.h" +#include "citra_qt/ui_settings.h" +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "core/settings.h" +#include "ui_host_room.h" + +HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<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.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 + 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 = UISettings::values.host_type; + if (index < ui->host_type->count()) { + ui->host_type->setCurrentIndex(index); + } + index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole); + if (index != -1) { + ui->game_list->setCurrentIndex(index); + } +} + +HostRoomWindow::~HostRoomWindow() = default; + +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()) { + auto parent = static_cast<MultiplayerState*>(parentWidget()); + if (!parent->OnCloseRoom()) { + close(); + return; + } + } + ui->host->setDisabled(true); + + auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString(); + auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; + auto password = ui->password->text().toStdString(); + 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); + NGLOG_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->currentIndex(); + 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()) { + 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(); + } else { + NGLOG_ERROR(Network, "Starting announce session failed"); + } + } + close(); + } + } +} + +QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { + if (role != Qt::DisplayRole) { + auto val = QSortFilterProxyModel::data(idx, role); + // If its the icon, shrink it to 16x16 + if (role == Qt::DecorationRole) + val = val.value<QImage>().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..574dc2824 --- /dev/null +++ b/src/citra_qt/multiplayer/host_room.h @@ -0,0 +1,69 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <QDialog> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include <QVariant> +#include "citra_qt/multiplayer/chat_room.h" +#include "citra_qt/multiplayer/validation.h" +#include "network/network.h" + +namespace Ui { +class HostRoom; +} + +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<Core::AnnounceMultiplayerSession> session); + ~HostRoomWindow(); + +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<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + QStandardItemModel* game_list; + ComboBoxProxyModel* proxy; + std::unique_ptr<Ui::HostRoom> ui; + Validation validation; +}; + +/** + * 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>HostRoom</class> + <widget class="QWidget" name="HostRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>607</width> + <height>165</height> + </rect> + </property> + <property name="windowTitle"> + <string>Create Room</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QWidget" name="settings" native="true"> + <layout class="QHBoxLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <layout class="QFormLayout" name="formLayout_2"> + <property name="labelAlignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Room Name</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="room_name"> + <property name="maxLength"> + <number>50</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Preferred Game</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="game_list"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Max Players</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="max_player"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>16</number> + </property> + <property name="value"> + <number>8</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QFormLayout" name="formLayout"> + <property name="labelAlignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <item row="0" column="1"> + <widget class="QLineEdit" name="username"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="password"> + <property name="echoMode"> + <enum>QLineEdit::PasswordEchoOnEdit</enum> + </property> + <property name="placeholderText"> + <string>(Leave blank for open game)</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="port"> + <property name="inputMethodHints"> + <set>Qt::ImhDigitsOnly</set> + </property> + <property name="maxLength"> + <number>5</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Port</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QComboBox" name="host_type"> + <item> + <property name="text"> + <string>Public</string> + </property> + </item> + <item> + <property name="text"> + <string>Unlisted</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QPushButton" name="host"> + <property name="text"> + <string>Host Room</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp new file mode 100644 index 000000000..0b58091cb --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -0,0 +1,305 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QInputDialog> +#include <QList> +#include <QtConcurrent/QtConcurrentRun> +#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/state.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" + +Lobby::Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::Lobby>()), announce_multiplayer_session(session), game_list(list) { + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher<void>; + connect(watcher, &QFutureWatcher<void>::finished, [&] { joining = false; }); + + 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); + ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive); + 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->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.nickname); + + // UI Buttons + MultiplayerState* p = reinterpret_cast<MultiplayerState*>(parent); + connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby); + 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::SetFilterSearch); + 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); + + // 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(); +} + +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::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& 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()) { + index = source.parent(); + } + 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::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; + } + + // attempt to connect in a different thread + QFuture<void> 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::ResetModel() { + model->clear(); + model->insertColumns(0, Column::TOTAL); + 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); + model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole); +} + +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 = 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<QPixmap>(); + } + } + + QList<QVariant> 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); + } + + auto first_item = new LobbyItem(); + auto row = QList<QStandardItem*>({ + first_item, + 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), + 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)); + } + } + + // 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 < proxy->rowCount(); i++) { + auto parent = model->item(i, 0); + if (parent->hasChildren()) { + ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true); + } + } +} + +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 + + // 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); + 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 + 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(filter_search, filterCaseSensitivity()); + bool room_name_match = sourceModel() + ->data(room_name, LobbyItemName::NameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool username_match = sourceModel() + ->data(host_name, LobbyItemHost::HostUsernameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + 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<QModelIndex> 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; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterFull(bool filter) { + filter_full = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { + filter_search = filter; + invalidate(); +} diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h new file mode 100644 index 000000000..783225737 --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.h @@ -0,0 +1,116 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <future> +#include <memory> +#include <QDialog> +#include <QFutureWatcher> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include "citra_qt/multiplayer/validation.h" +#include "common/announce_multiplayer_room.h" +#include "core/announce_multiplayer_session.h" +#include "network/network.h" +#include "ui_lobby.h" + +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<Core::AnnounceMultiplayerSession> session); + ~Lobby() = default; + +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 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. + * + * index - The row of the proxy model that the user wants to join. + */ + void OnJoinRoom(const QModelIndex&); + +signals: + /** + * Signalled when the latest lobby data is retrieved. + */ + void LobbyRefreshed(); + + 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. + */ + QString PasswordPrompt(); + + QStandardItemModel* model; + QStandardItemModel* game_list; + LobbyFilterProxyModel* proxy; + + std::future<AnnounceMultiplayerRoom::RoomList> room_list_future; + std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + std::unique_ptr<Ui::Lobby> ui; + QFutureWatcher<void>* watcher; + Validation validation; + bool joining = false; +}; + +/** + * 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); + void SetFilterSearch(const QString&); + +private: + QStandardItemModel* game_list; + bool filter_owned = false; + bool filter_full = false; + QString filter_search; +}; diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui new file mode 100644 index 000000000..835d03238 --- /dev/null +++ b/src/citra_qt/multiplayer/lobby.ui @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Lobby</class> + <widget class="QWidget" name="Lobby"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>903</width> + <height>487</height> + </rect> + </property> + <property name="windowTitle"> + <string>Public Room Browser</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>3</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>6</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nickname"> + <property name="placeholderText"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Filters</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="search"> + <property name="placeholderText"> + <string>Search</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="games_owned"> + <property name="text"> + <string>Games I Own</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="hide_full"> + <property name="text"> + <string>Hide Full Games</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refresh_list"> + <property name="text"> + <string>Refresh Lobby</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="QTreeView" name="room_list"/> + </item> + <item> + <widget class="QWidget" name="widget" native="true"/> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h new file mode 100644 index 000000000..3773f99de --- /dev/null +++ b/src/citra_qt/multiplayer/lobby_p.h @@ -0,0 +1,207 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <utility> +#include <QPixmap> +#include <QStandardItem> +#include <QStandardItemModel> +#include "common/common_types.h" + +namespace Column { +enum List { + EXPAND, + ROOM_NAME, + GAME_NAME, + HOST, + MEMBER, + TOTAL, +}; +} + +class LobbyItem : public QStandardItem { +public: + LobbyItem() = default; + explicit LobbyItem(const QString& string) : QStandardItem(string) {} + virtual ~LobbyItem() override = default; +}; + +class LobbyItemName : public LobbyItem { +public: + static const int NameRole = Qt::UserRole + 1; + static const int PasswordRole = Qt::UserRole + 2; + + 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) { + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon::fromTheme("lock").pixmap(16) : QIcon(); + } + 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() = default; + explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) { + setData(static_cast<unsigned long long>(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<QPixmap>().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() = default; + explicit LobbyItemHost(QString username, QString ip, u16 port) { + 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() = default; + 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 { + 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() = default; + explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) { + 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; + } +}; + +/** + * 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<QVariant> members) { + setData(members, MemberListRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + QString out; + bool first = true; + for (const auto& member : members) { + if (!first) + out += '\n'; + const auto& m = member.value<LobbyMember>(); + if (m.GetGameName().isEmpty()) { + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetUsername()); + } else { + out += + QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName()); + } + first = false; + } + return out; + } +}; diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp new file mode 100644 index 000000000..57cd7671c --- /dev/null +++ b/src/citra_qt/multiplayer/message.cpp @@ -0,0 +1,62 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QMessageBox> +#include <QString> + +#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. 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("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 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.")); + +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..3b8613199 --- /dev/null +++ b/src/citra_qt/multiplayer/message.h @@ -0,0 +1,55 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <utility> + +namespace NetworkMessage { + +class ConnectionError { + +public: + explicit ConnectionError(std::string str) : err(std::move(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/state.cpp b/src/citra_qt/multiplayer/state.cpp new file mode 100644 index 000000000..dfd5ee66f --- /dev/null +++ b/src/citra_qt/multiplayer/state.cpp @@ -0,0 +1,182 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QAction> +#include <QIcon> +#include <QMessageBox> +#include <QStandardItemModel> +#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, + 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( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &MultiplayerState::NetworkStateChanged, this, + &MultiplayerState::OnNetworkStateChanged); + } + + qRegisterMetaType<Network::RoomMember::State>(); + qRegisterMetaType<Common::WebResult>(); + announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(); + 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: {}", Network::GetStateStr(state)); + bool is_connected = false; + switch (state) { + case Network::RoomMember::State::LostConnection: + NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); + break; + case Network::RoomMember::State::CouldNotConnect: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::State::NameCollision: + NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE); + break; + case Network::RoomMember::State::MacCollision: + NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); + break; + case Network::RoomMember::State::WrongPassword: + NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); + break; + case Network::RoomMember::State::WrongVersion: + NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); + break; + case Network::RoomMember::State::Error: + NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::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); + } else { + 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) { + announce_multiplayer_session->Stop(); + 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) { + widget->show(); + widget->activateWindow(); + widget->raise(); +} + +void MultiplayerState::OnViewLobby() { + if (lobby == nullptr) { + lobby = new Lobby(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(lobby); +} + +void MultiplayerState::OnCreateRoom() { + if (host_room == nullptr) { + host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session); + } + BringWidgetToFront(host_room); +} + +bool MultiplayerState::OnCloseRoom() { + if (!NetworkMessage::WarnCloseRoom()) + return false; + 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 true; + } + room->Destroy(); + announce_multiplayer_session->Stop(); + } + return true; +} + +void MultiplayerState::OnOpenNetworkRoom() { + if (auto member = Network::GetRoomMember().lock()) { + if (member->IsConnected()) { + if (client_room == nullptr) { + client_room = new ClientRoomWindow(this); + } + 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); + } + 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..673bc6ecf --- /dev/null +++ b/src/citra_qt/multiplayer/state.h @@ -0,0 +1,68 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QWidget> +#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, QAction* leave_room, + QAction* show_room); + ~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(); + bool 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; + QAction* leave_room; + QAction* show_room; + std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle; +}; + +Q_DECLARE_METATYPE(Common::WebResult); diff --git a/src/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h new file mode 100644 index 000000000..4e8f6b9e9 --- /dev/null +++ b/src/citra_qt/multiplayer/validation.h @@ -0,0 +1,48 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QRegExp> +#include <QValidator> + +class Validation { +public: + 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._- ]+$"); + QRegExpValidator room_name; + + /// nickname can be alphanumeric and " " "_" "." and "-" + QRegExp nickname_regex = QRegExp("^[a-zA-Z0-9._- ]+$"); + 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])"); + QRegExpValidator ip; + + /// port must be between 0 and 65535 + QIntValidator port; +}; diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index caf6aea6a..b102f560d 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -56,8 +56,18 @@ struct Values { std::vector<Shortcut> 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; + uint 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..e990423a9 --- /dev/null +++ b/src/citra_qt/util/clickable_label.cpp @@ -0,0 +1,11 @@ +// 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) {} + +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..3c65a74be --- /dev/null +++ b/src/citra_qt/util/clickable_label.h @@ -0,0 +1,22 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QLabel> +#include <QWidget> + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; 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<RoomMemberImpl> 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