diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index a3e21645a..a524a17e7 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -5,7 +5,13 @@
icons/16x16/checked.png
icons/16x16/failed.png
-
+
+ icons/16x16/connected.png
+
+ icons/16x16/disconnected.png
+
+ icons/16x16/lock.png
+
icons/256x256/citra.png
diff --git a/dist/qt_themes/default/icons/16x16/connected.png b/dist/qt_themes/default/icons/16x16/connected.png
new file mode 100644
index 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/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index 4d08a622e..699e7006b 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -56,11 +56,27 @@ 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/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 +95,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..fbc343037 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,18 @@ void Config::ReadValues() {
UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
+ qt_config->beginGroup("Multiplayer");
+ UISettings::values.nickname = qt_config->value("nickname", "").toString();
+ UISettings::values.ip = qt_config->value("ip", "").toString();
+ UISettings::values.port = qt_config->value("port", Network::DefaultRoomPort).toString();
+ UISettings::values.room_nickname = qt_config->value("room_nickname", "").toString();
+ UISettings::values.room_name = qt_config->value("room_name", "").toString();
+ UISettings::values.room_port = qt_config->value("room_port", 24872).toString();
+ UISettings::values.host_type = qt_config->value("host_type", 0).toString();
+ UISettings::values.max_player = qt_config->value("max_player", 8).toUInt();
+ UISettings::values.game_id = qt_config->value("game_id", 0).toULongLong();
+ qt_config->endGroup();
+
qt_config->endGroup();
}
@@ -320,6 +339,9 @@ void Config::SaveValues() {
QString::fromStdString(Settings::values.telemetry_endpoint_url));
qt_config->setValue("verify_endpoint_url",
QString::fromStdString(Settings::values.verify_endpoint_url));
+ qt_config->setValue(
+ "announce_multiplayer_room_endpoint_url",
+ QString::fromStdString(Settings::values.announce_multiplayer_room_endpoint_url));
qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username));
qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
qt_config->endGroup();
@@ -366,6 +388,18 @@ void Config::SaveValues() {
qt_config->setValue("firstStart", UISettings::values.first_start);
qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
+ qt_config->beginGroup("Multiplayer");
+ qt_config->setValue("nickname", UISettings::values.nickname);
+ qt_config->setValue("ip", UISettings::values.ip);
+ qt_config->setValue("port", UISettings::values.port);
+ qt_config->setValue("room_nickname", UISettings::values.room_nickname);
+ qt_config->setValue("room_name", UISettings::values.room_name);
+ qt_config->setValue("room_port", UISettings::values.room_port);
+ qt_config->setValue("host_type", UISettings::values.host_type);
+ qt_config->setValue("max_player", UISettings::values.max_player);
+ qt_config->setValue("game_id", UISettings::values.game_id);
+ qt_config->endGroup();
+
qt_config->endGroup();
}
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index 401cd9d98..1bd387b75 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -374,6 +374,10 @@ void GameList::LoadCompatibilityList() {
}
}
+QStandardItemModel* GameList::GetModel() const {
+ return item_model;
+}
+
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
if (!FileUtil::Exists(dir_path.toStdString()) ||
!FileUtil::IsDirectory(dir_path.toStdString())) {
diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h
index c01dc1d21..376dc8474 100644
--- a/src/citra_qt/game_list.h
+++ b/src/citra_qt/game_list.h
@@ -76,6 +76,8 @@ public:
void SaveInterfaceLayout();
void LoadInterfaceLayout();
+ QStandardItemModel* GetModel() const;
+
static const QStringList supported_file_extensions;
signals:
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 6709874d5..475093b13 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -31,8 +31,14 @@
#include "citra_qt/game_list.h"
#include "citra_qt/hotkeys.h"
#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/direct_connect.h"
+#include "citra_qt/multiplayer/host_room.h"
+#include "citra_qt/multiplayer/lobby.h"
+#include "citra_qt/multiplayer/message.h"
#include "citra_qt/ui_settings.h"
#include "citra_qt/updater/updater.h"
+#include "citra_qt/util/clickable_label.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
@@ -129,6 +135,20 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
SetupUIStrings();
+ Network::Init();
+
+ if (auto member = Network::GetRoomMember().lock()) {
+ // register the network structs to use in slots and signals
+ qRegisterMetaType();
+ state_callback_handle = member->BindOnStateChanged(
+ [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
+ connect(this, &GMainWindow::NetworkStateChanged, this, &GMainWindow::OnNetworkStateChanged);
+ }
+
+ qRegisterMetaType();
+
+ setWindowTitle(QString("Citra %1| %2-%3")
+ .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
show();
game_list->LoadCompatibilityList();
@@ -153,6 +173,13 @@ GMainWindow::~GMainWindow() {
delete render_window;
Pica::g_debug_context.reset();
+
+ if (state_callback_handle) {
+ if (auto member = Network::GetRoomMember().lock()) {
+ member->Unbind(state_callback_handle);
+ }
+ }
+ Network::Shutdown();
}
void GMainWindow::InitializeWidgets() {
@@ -193,12 +220,22 @@ void GMainWindow::InitializeWidgets() {
tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
+ announce_multiplayer_session = std::make_shared();
+ announce_multiplayer_session->BindErrorCallback(
+ [this](const Common::WebResult& result) { emit AnnounceFailed(result); });
+ connect(this, &GMainWindow::AnnounceFailed, this, &GMainWindow::OnAnnounceFailed);
+ network_status = new ClickableLabel();
+ network_status->setToolTip(tr("Current connection status"));
+
for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
statusBar()->addPermanentWidget(label, 0);
}
+ statusBar()->addPermanentWidget(network_status, 0);
+ network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
+ network_status->setText(tr("Not Connected. Join a room for online play!"));
statusBar()->setVisible(true);
// Removes an ugly inner border from the status bar widgets under Linux
@@ -392,6 +429,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress);
connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport);
connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished);
+
+ connect(network_status, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom);
}
void GMainWindow::ConnectMenuEvents() {
@@ -418,6 +457,15 @@ void GMainWindow::ConnectMenuEvents() {
ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
+
+ // Multiplayer
+ connect(ui.action_View_Lobby, &QAction::triggered, this, &GMainWindow::OnViewLobby);
+ connect(ui.action_Start_Room, &QAction::triggered, this, &GMainWindow::OnCreateRoom);
+ connect(ui.action_Stop_Room, &QAction::triggered, this, &GMainWindow::OnCloseRoom);
+ connect(ui.action_Connect_To_Room, &QAction::triggered, this,
+ &GMainWindow::OnDirectConnectToRoom);
+ connect(ui.action_Chat, &QAction::triggered, this, &GMainWindow::OnOpenNetworkRoom);
+
ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
ui.action_Screen_Layout_Swap_Screens->setShortcut(
GetHotkey("Main Window", "Swap Screens", this)->key());
@@ -880,6 +928,30 @@ void GMainWindow::OnMenuRecentFile() {
}
}
+void GMainWindow::OnNetworkStateChanged(const Network::RoomMember::State& state) {
+ LOG_INFO(Frontend, "network state change");
+ if (state == Network::RoomMember::State::Joined) {
+ network_status->setPixmap(QIcon::fromTheme("connected").pixmap(16));
+ network_status->setText(tr("Connected"));
+ ui.action_Chat->setEnabled(true);
+ return;
+ }
+ network_status->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
+ network_status->setText(tr("Not Connected"));
+ ui.action_Chat->setDisabled(true);
+
+ ChangeRoomState();
+}
+
+void GMainWindow::OnAnnounceFailed(const Common::WebResult& result) {
+ announce_multiplayer_session->Stop();
+ QMessageBox::warning(
+ this, tr("Error"),
+ tr("Announcing the room failed.\nThe room will not get listed publicly.\nError: ") +
+ QString::fromStdString(result.result_string),
+ QMessageBox::Ok);
+}
+
void GMainWindow::OnStartGame() {
emu_thread->SetRunning(true);
qRegisterMetaType("Core::System::ResultStatus");
@@ -1054,6 +1126,80 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
graphicsSurfaceViewerWidget->show();
}
+static void BringWidgetToFront(QWidget* widget) {
+ widget->show();
+ widget->activateWindow();
+ widget->raise();
+}
+
+void GMainWindow::OnViewLobby() {
+ if (lobby == nullptr) {
+ lobby = new Lobby(this, game_list->GetModel(), announce_multiplayer_session);
+ connect(lobby, &Lobby::Closed, [&] {
+ LOG_INFO(Frontend, "Destroying lobby");
+ // lobby->close();
+ lobby = nullptr;
+ });
+ }
+ BringWidgetToFront(lobby);
+}
+
+void GMainWindow::OnCreateRoom() {
+ if (host_room == nullptr) {
+ host_room = new HostRoomWindow(this, game_list->GetModel(), announce_multiplayer_session);
+ connect(host_room, &HostRoomWindow::Closed, [&] {
+ // host_room->close();
+ LOG_INFO(Frontend, "Destroying host room");
+ host_room = nullptr;
+ });
+ }
+ BringWidgetToFront(host_room);
+}
+
+void GMainWindow::OnCloseRoom() {
+ if (auto room = Network::GetRoom().lock()) {
+ if (room->GetState() == Network::Room::State::Open) {
+ if (NetworkMessage::WarnCloseRoom()) {
+ room->Destroy();
+ announce_multiplayer_session->Stop();
+ // host_room->close();
+ }
+ }
+ }
+}
+
+void GMainWindow::OnOpenNetworkRoom() {
+ if (auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ if (client_room == nullptr) {
+ client_room = new ClientRoomWindow(this);
+ connect(client_room, &ClientRoomWindow::Closed, [&] {
+ LOG_INFO(Frontend, "Destroying client room");
+ // client_room->close();
+ client_room = nullptr;
+ });
+ }
+ BringWidgetToFront(client_room);
+ return;
+ }
+ }
+ // If the user is not a member of a room, show the lobby instead.
+ // This is currently only used on the clickable label in the status bar
+ OnViewLobby();
+}
+
+void GMainWindow::OnDirectConnectToRoom() {
+ if (direct_connect == nullptr) {
+ direct_connect = new DirectConnectWindow(this);
+ connect(direct_connect, &DirectConnectWindow::Closed, [&] {
+ LOG_INFO(Frontend, "Destroying direct connect");
+ // direct_connect->close();
+ direct_connect = nullptr;
+ });
+ }
+ BringWidgetToFront(direct_connect);
+}
+
void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) {
status_bar_update_timer.stop();
@@ -1187,6 +1333,16 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
render_window->close();
+ // Close Multiplayer windows
+ if (host_room)
+ host_room->close();
+ if (direct_connect)
+ direct_connect->close();
+ if (client_room)
+ client_room->close();
+ if (lobby)
+ lobby->close();
+
QWidget::closeEvent(event);
}
@@ -1306,6 +1462,18 @@ void GMainWindow::SyncMenuUISettings() {
ui.action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen);
}
+void GMainWindow::ChangeRoomState() {
+ if (auto room = Network::GetRoom().lock()) {
+ if (room->GetState() == Network::Room::State::Open) {
+ ui.action_Start_Room->setDisabled(true);
+ ui.action_Stop_Room->setEnabled(true);
+ return;
+ }
+ ui.action_Start_Room->setEnabled(true);
+ ui.action_Stop_Room->setDisabled(true);
+ }
+}
+
#ifdef main
#undef main
#endif
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index c29c0ccfc..315631da2 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -5,15 +5,19 @@
#pragma once
#include
+#include
#include
#include
#include
+#include "common/announce_multiplayer_room.h"
#include "core/core.h"
#include "core/hle/service/am/am.h"
+#include "network/network.h"
#include "ui_main.h"
class AboutDialog;
class Config;
+class ClickableLabel;
class EmuThread;
class GameList;
enum class GameListOpenTarget;
@@ -33,6 +37,16 @@ class RegistersWidget;
class Updater;
class WaitTreeWidget;
+// Multiplayer forward declarations
+class Lobby;
+class HostRoomWindow;
+class ClientRoomWindow;
+class DirectConnectWindow;
+
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
class GMainWindow : public QMainWindow {
Q_OBJECT
@@ -50,6 +64,9 @@ class GMainWindow : public QMainWindow {
public:
void filterBarSetChecked(bool state);
void UpdateUITheme();
+ void ChangeRoomState();
+
+ GameList* game_list;
GMainWindow();
~GMainWindow();
@@ -77,6 +94,16 @@ signals:
// Signal that tells widgets to update icons to use the current theme
void UpdateThemedIcons();
+ void NetworkStateChanged(const Network::RoomMember::State&);
+ void AnnounceFailed(const Common::WebResult&);
+
+public slots:
+ void OnViewLobby();
+ void OnCreateRoom();
+ void OnCloseRoom();
+ void OnOpenNetworkRoom();
+ void OnDirectConnectToRoom();
+
private:
void InitializeWidgets();
void InitializeDebugWidgets();
@@ -146,6 +173,8 @@ private slots:
/// Called whenever a user selects the "File->Select Game List Root" menu item
void OnMenuSelectGameListRoot();
void OnMenuRecentFile();
+ void OnNetworkStateChanged(const Network::RoomMember::State& state);
+ void OnAnnounceFailed(const Common::WebResult&);
void OnConfigure();
void OnToggleFilterBar();
void OnDisplayTitleBars(bool);
@@ -173,7 +202,8 @@ private:
Ui::MainWindow ui;
GRenderWindow* render_window;
- GameList* game_list;
+
+ QFutureWatcher* watcher = nullptr;
// Status bar elements
QProgressBar* progress_bar = nullptr;
@@ -181,9 +211,11 @@ private:
QLabel* emu_speed_label = nullptr;
QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr;
+ ClickableLabel* network_status = nullptr;
QTimer status_bar_update_timer;
std::unique_ptr config;
+ std::shared_ptr announce_multiplayer_session;
// Whether emulation is currently running in Citra.
bool emulation_running = false;
@@ -204,6 +236,14 @@ private:
bool explicit_update_check = false;
bool defer_update_prompt = false;
+ // Multiplayer windows
+ Lobby* lobby = nullptr;
+ HostRoomWindow* host_room = nullptr;
+ ClientRoomWindow* client_room = nullptr;
+ DirectConnectWindow* direct_connect = nullptr;
+
+ Network::RoomMember::CallbackHandle state_callback_handle;
+
QAction* actions_recent_files[max_recent_files_item];
QTranslator translator;
@@ -219,3 +259,4 @@ protected:
Q_DECLARE_METATYPE(size_t);
Q_DECLARE_METATYPE(Service::AM::InstallStatus);
+Q_DECLARE_METATYPE(Common::WebResult);
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index c598c444f..d7ffc6b06 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -45,7 +45,7 @@
0
0
1081
- 26
+ 21
+
@@ -228,6 +243,43 @@
Create Pica Surface Viewer
+
+
+ true
+
+
+ Browse Public Game Lobby
+
+
+
+
+ true
+
+
+ Create Room
+
+
+
+
+ false
+
+
+ Close Room
+
+
+
+
+ Direct Connect to Room
+
+
+
+
+ false
+
+
+ Current Room
+
+
true
@@ -244,46 +296,46 @@
Opens the maintenance tool to modify your Citra installation
-
-
- true
-
-
- Default
-
-
-
-
- true
-
-
- Single Screen
-
-
-
-
- true
-
-
- Large Screen
-
-
-
-
- true
-
-
- Side by Side
-
-
-
-
- true
-
-
- Swap Screens
-
-
+
+
+ true
+
+
+ Default
+
+
+
+
+ true
+
+
+ Single Screen
+
+
+
+
+ true
+
+
+ Large Screen
+
+
+
+
+ true
+
+
+ Side by Side
+
+
+
+
+ true
+
+
+ Swap Screens
+
+
Check for Updates
diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp
new file mode 100644
index 000000000..46c477eae
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.cpp
@@ -0,0 +1,208 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/multiplayer/chat_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "ui_chat_room.h"
+
+class ChatMessage {
+public:
+ explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) {
+ /// Convert the time to their default locale defined format
+ static QLocale locale;
+ timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
+ nickname = QString::fromStdString(chat.nickname);
+ message = QString::fromStdString(chat.message);
+ }
+
+ /// Format the message using the players color
+ QString GetPlayerChatMessage(u16 player) const {
+ auto color = player_color[player % 16];
+ return QString("[%1] <%3> %4")
+ .arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped());
+ }
+
+private:
+ ChatMessage() {}
+ const QList player_color = {
+ {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222",
+ "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}};
+ QString timestamp;
+ QString nickname;
+ QString message;
+};
+
+class StatusMessage {
+public:
+ explicit StatusMessage(const QString& msg, QTime ts = {}) {
+ /// Convert the time to their default locale defined format
+ static QLocale locale;
+ timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
+ message = msg;
+ }
+
+ QString GetSystemChatMessage() const {
+ return QString("[%1] %3")
+ .arg(timestamp, system_color, message);
+ }
+
+private:
+ const QString system_color = "#888888";
+ QString timestamp;
+ QString message;
+};
+
+ChatRoom::ChatRoom(QWidget* parent) : ui(new Ui::ChatRoom) {
+ ui->setupUi(this);
+
+ // set the item_model for player_view
+ enum {
+ COLUMN_NAME,
+ COLUMN_GAME,
+ COLUMN_COUNT, // Number of columns
+ };
+
+ player_list = new QStandardItemModel(ui->player_view);
+ ui->player_view->setModel(player_list);
+ player_list->insertColumns(0, COLUMN_COUNT);
+ player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name"));
+ player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game"));
+
+ ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
+
+ // register the network structs to use in slots and signals
+ qRegisterMetaType();
+ qRegisterMetaType();
+ qRegisterMetaType();
+
+ // setup the callbacks for network updates
+ if (auto member = Network::GetRoomMember().lock()) {
+ member->BindOnChatMessageRecieved(
+ [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
+ connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
+ } else {
+ // TODO (jroweboy) network was not initialized?
+ }
+
+ // Connect all the widgets to the appropriate events
+ connect(ui->chat_message, &QLineEdit::returnPressed, ui->send_message, &QPushButton::pressed);
+ connect(ui->chat_message, &QLineEdit::textChanged, this, &::ChatRoom::OnChatTextChanged);
+ connect(ui->send_message, &QPushButton::pressed, this, &ChatRoom::OnSendChat);
+}
+
+void ChatRoom::Clear() {
+ ui->chat_history->clear();
+}
+
+void ChatRoom::AppendStatusMessage(const QString& msg) {
+ ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage());
+}
+
+void ChatRoom::AppendChatMessage(const QString& msg) {
+ ui->chat_history->append(msg);
+}
+
+bool ChatRoom::ValidateMessage(const std::string& msg) {
+ return !msg.empty();
+}
+
+void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
+ // TODO(B3N30): change title
+ if (auto room_member = Network::GetRoomMember().lock()) {
+ SetPlayerList(room_member->GetMemberInformation());
+ }
+}
+
+void ChatRoom::Disable() {
+ ui->send_message->setDisabled(true);
+ ui->chat_message->setDisabled(true);
+}
+
+void ChatRoom::Enable() {
+ ui->send_message->setEnabled(true);
+ ui->chat_message->setEnabled(true);
+}
+
+void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
+ if (!ValidateMessage(chat.message)) {
+ return;
+ }
+ if (auto room = Network::GetRoomMember().lock()) {
+ // get the id of the player
+ auto members = room->GetMemberInformation();
+ auto it = std::find_if(members.begin(), members.end(),
+ [&chat](const Network::RoomMember::MemberInformation& member) {
+ return member.nickname == chat.nickname;
+ });
+ if (it == members.end()) {
+ LOG_INFO(Network, "Chat message received from unknown player. Ignoring it.");
+ return;
+ }
+ auto player = std::distance(members.begin(), it);
+ ChatMessage m(chat);
+ AppendChatMessage(m.GetPlayerChatMessage(player));
+ }
+}
+
+void ChatRoom::OnSendChat() {
+ if (auto room = Network::GetRoomMember().lock()) {
+ if (room->GetState() == Network::RoomMember::State::Joined) {
+ auto message = ui->chat_message->text().toStdString();
+ if (!ValidateMessage(message)) {
+ return;
+ }
+ auto nick = room->GetNickname();
+ Network::ChatEntry chat{nick, message};
+
+ auto members = room->GetMemberInformation();
+ auto it = std::find_if(members.begin(), members.end(),
+ [&chat](const Network::RoomMember::MemberInformation& member) {
+ return member.nickname == chat.nickname;
+ });
+ if (it == members.end()) {
+ LOG_INFO(Network, "Chat message received from unknown player");
+ }
+ auto player = std::distance(members.begin(), it);
+ ChatMessage m(chat);
+ room->SendChatMessage(message);
+ AppendChatMessage(m.GetPlayerChatMessage(player));
+ ui->chat_message->clear();
+ }
+ }
+}
+
+void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
+ // TODO(B3N30): Remember which row is selected
+ player_list->removeRows(0, player_list->rowCount());
+ for (const auto& member : member_list) {
+ if (member.nickname == "")
+ continue;
+ QList l;
+ std::vector elements = {member.nickname, member.game_info.name};
+ for (auto& item : elements) {
+ QStandardItem* child = new QStandardItem(QString::fromStdString(item));
+ child->setEditable(false);
+ l.append(child);
+ }
+ player_list->invisibleRootItem()->appendRow(l);
+ }
+ // TODO(B3N30): Restore row selection
+}
+
+void ChatRoom::OnChatTextChanged() {
+ if (ui->chat_message->text().length() > Network::MaxMessageSize)
+ ui->chat_message->setText(ui->chat_message->text().left(Network::MaxMessageSize));
+}
diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h
new file mode 100644
index 000000000..8c07654cc
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.h
@@ -0,0 +1,57 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include "network/network.h"
+
+namespace Ui {
+class ChatRoom;
+} // namespace Ui
+
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
+class ConnectionError;
+class ComboBoxProxyModel;
+
+class ChatMessage;
+
+class ChatRoom : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit ChatRoom(QWidget* parent);
+ void SetPlayerList(const Network::RoomMember::MemberList& member_list);
+ void Clear();
+ void AppendStatusMessage(const QString& msg);
+
+public slots:
+ void OnRoomUpdate(const Network::RoomInformation& info);
+ void OnChatReceive(const Network::ChatEntry&);
+ void OnSendChat();
+ void OnChatTextChanged();
+ void Disable();
+ void Enable();
+
+signals:
+ void ChatReceived(const Network::ChatEntry&);
+
+private:
+ const u32 max_chat_lines = 1000;
+ void AppendChatMessage(const QString&);
+ bool ValidateMessage(const std::string&);
+ QStandardItemModel* player_list;
+ Ui::ChatRoom* ui;
+};
+
+Q_DECLARE_METATYPE(Network::ChatEntry);
+Q_DECLARE_METATYPE(Network::RoomInformation);
+Q_DECLARE_METATYPE(Network::RoomMember::State);
diff --git a/src/citra_qt/multiplayer/chat_room.ui b/src/citra_qt/multiplayer/chat_room.ui
new file mode 100644
index 000000000..8bb1899c0
--- /dev/null
+++ b/src/citra_qt/multiplayer/chat_room.ui
@@ -0,0 +1,59 @@
+
+
+ ChatRoom
+
+
+
+ 0
+ 0
+ 607
+ 432
+
+
+
+ Room Window
+
+
+ -
+
+
+ -
+
+
-
+
+
+ false
+
+
+ true
+
+
+ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+ -
+
+
-
+
+
+ Send Chat Message
+
+
+
+ -
+
+
+ Send Message
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp
new file mode 100644
index 000000000..8baafb418
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.cpp
@@ -0,0 +1,121 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "ui_client_room.h"
+
+ClientRoomWindow::ClientRoomWindow(QWidget* parent)
+ : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+ ui(new Ui::ClientRoom) {
+ ui->setupUi(this);
+
+ // setup the callbacks for network updates
+ if (auto member = Network::GetRoomMember().lock()) {
+ member->BindOnRoomInformationChanged(
+ [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); });
+ member->BindOnStateChanged(
+ [this](const Network::RoomMember::State& state) { emit StateChanged(state); });
+
+ connect(this, &ClientRoomWindow::RoomInformationChanged, this,
+ &ClientRoomWindow::OnRoomUpdate);
+ connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange);
+ } else {
+ // TODO (jroweboy) network was not initialized?
+ }
+
+ connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); });
+ ui->disconnect->setDefault(false);
+ ui->disconnect->setAutoDefault(false);
+ UpdateView();
+}
+
+void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) {
+ UpdateView();
+}
+
+void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) {
+ switch (state) {
+ case Network::RoomMember::State::Idle:
+ LOG_INFO(Network, "State: Idle");
+ break;
+ case Network::RoomMember::State::Joining:
+ LOG_INFO(Network, "State: Joining");
+ break;
+ case Network::RoomMember::State::Joined:
+ LOG_INFO(Network, "State: Joined");
+ ui->chat->Clear();
+ ui->chat->AppendStatusMessage(tr("Connected"));
+ break;
+ case Network::RoomMember::State::LostConnection:
+ NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION);
+ LOG_INFO(Network, "State: LostConnection");
+ break;
+ case Network::RoomMember::State::CouldNotConnect:
+ NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ LOG_INFO(Network, "State: CouldNotConnect");
+ break;
+ case Network::RoomMember::State::NameCollision:
+ NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE);
+ LOG_INFO(Network, "State: NameCollision");
+ break;
+ case Network::RoomMember::State::MacCollision:
+ NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION);
+ LOG_INFO(Network, "State: MacCollision");
+ break;
+ case Network::RoomMember::State::WrongPassword:
+ NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD);
+ LOG_INFO(Network, "State: WrongPassword");
+ break;
+ case Network::RoomMember::State::WrongVersion:
+ NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION);
+ LOG_INFO(Network, "State: WrongVersion");
+ break;
+ default:
+ break;
+ }
+ UpdateView();
+}
+
+void ClientRoomWindow::Disconnect() {
+ if (!NetworkMessage::WarnDisconnect()) {
+ return;
+ }
+ if (auto member = Network::GetRoomMember().lock()) {
+ member->Leave();
+ ui->chat->AppendStatusMessage(tr("Disconnected"));
+ }
+}
+
+void ClientRoomWindow::UpdateView() {
+ if (auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ ui->chat->Enable();
+ ui->disconnect->setEnabled(true);
+ auto memberlist = member->GetMemberInformation();
+ ui->chat->SetPlayerList(memberlist);
+ const auto information = member->GetRoomInformation();
+ setWindowTitle(QString(tr("%1 (%2/%3 members) - connected"))
+ .arg(QString::fromStdString(information.name))
+ .arg(memberlist.size())
+ .arg(information.member_slots));
+ return;
+ }
+ }
+ // TODO(B3N30): can't get RoomMember*, show error and close window
+ close();
+ emit Closed();
+ return;
+}
diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h
new file mode 100644
index 000000000..8d282b754
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.h
@@ -0,0 +1,38 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "citra_qt/multiplayer/chat_room.h"
+
+namespace Ui {
+class ClientRoom;
+}
+
+class ClientRoomWindow : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit ClientRoomWindow(QWidget* parent);
+
+public slots:
+ void OnRoomUpdate(const Network::RoomInformation&);
+ void OnStateChange(const Network::RoomMember::State&);
+
+signals:
+ /**
+ * Signalled by this widget when it is closing itself and destroying any state such as
+ * connections that it might have.
+ */
+ void Closed();
+ void RoomInformationChanged(const Network::RoomInformation&);
+ void StateChanged(const Network::RoomMember::State&);
+
+private:
+ void Disconnect();
+ void UpdateView();
+
+ QStandardItemModel* player_list;
+ Ui::ClientRoom* ui;
+};
diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/citra_qt/multiplayer/client_room.ui
new file mode 100644
index 000000000..d83c088c2
--- /dev/null
+++ b/src/citra_qt/multiplayer/client_room.ui
@@ -0,0 +1,63 @@
+
+
+ ClientRoom
+
+
+
+ 0
+ 0
+ 607
+ 432
+
+
+
+ Room Window
+
+
+ -
+
+
-
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Leave Room
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ ChatRoom
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp
new file mode 100644
index 000000000..9d68d96d1
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.cpp
@@ -0,0 +1,132 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/direct_connect.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "core/settings.h"
+#include "network/network.h"
+#include "ui_direct_connect.h"
+
+enum class ConnectionType : u8 { TRAVERSAL_SERVER, IP };
+
+DirectConnectWindow::DirectConnectWindow(QWidget* parent)
+ : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+ ui(new Ui::DirectConnect) {
+
+ ui->setupUi(this);
+
+ // setup the watcher for background connections
+ watcher = new QFutureWatcher;
+ connect(watcher, &QFutureWatcher::finished, this, &DirectConnectWindow::OnConnection);
+
+ ui->nickname->setValidator(Validation::nickname);
+ ui->nickname->setText(UISettings::values.nickname);
+ ui->ip->setValidator(Validation::ip);
+ ui->ip->setText(UISettings::values.ip);
+ ui->port->setValidator(Validation::port);
+ ui->port->setText(UISettings::values.port);
+
+ // TODO(jroweboy): Show or hide the connection options based on the current value of the combo
+ // box. Add this back in when the traversal server support is added.
+ connect(ui->connect, &QPushButton::pressed, this, &DirectConnectWindow::Connect);
+}
+
+void DirectConnectWindow::Connect() {
+ ClearAllError();
+ bool isValid = true;
+ if (!ui->nickname->hasAcceptableInput()) {
+ isValid = false;
+ ShowError(NetworkMessage::USERNAME_NOT_VALID);
+ }
+ if (const auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ if (!NetworkMessage::WarnDisconnect()) {
+ return;
+ }
+ }
+ }
+ switch (static_cast(ui->connection_type->currentIndex())) {
+ case ConnectionType::TRAVERSAL_SERVER:
+ break;
+ case ConnectionType::IP:
+ if (!ui->ip->hasAcceptableInput()) {
+ isValid = false;
+ NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID);
+ }
+ if (!ui->port->hasAcceptableInput()) {
+ isValid = false;
+ NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
+ }
+ break;
+ }
+
+ if (!isValid) {
+ return;
+ }
+
+ // Store settings
+ UISettings::values.nickname = ui->nickname->text();
+ UISettings::values.ip = ui->ip->text();
+ UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty())
+ ? ui->port->text()
+ : UISettings::values.port;
+ Settings::Apply();
+
+ // attempt to connect in a different thread
+ QFuture f = QtConcurrent::run([&] {
+ if (auto room_member = Network::GetRoomMember().lock()) {
+ auto port = UISettings::values.port.toUInt();
+ room_member->Join(ui->nickname->text().toStdString(),
+ ui->ip->text().toStdString().c_str(), port, 0,
+ Network::NoPreferredMac, ui->password->text().toStdString().c_str());
+ }
+ });
+ watcher->setFuture(f);
+ // and disable widgets and display a connecting while we wait
+ BeginConnecting();
+}
+
+void DirectConnectWindow::ClearAllError() {}
+
+void DirectConnectWindow::BeginConnecting() {
+ ui->connect->setEnabled(false);
+ ui->connect->setText(tr("Connecting"));
+}
+
+void DirectConnectWindow::EndConnecting() {
+ ui->connect->setEnabled(true);
+ ui->connect->setText(tr("Connect"));
+}
+
+void DirectConnectWindow::OnConnection() {
+ EndConnecting();
+
+ bool isConnected = true;
+ if (auto room_member = Network::GetRoomMember().lock()) {
+ switch (room_member->GetState()) {
+ case Network::RoomMember::State::CouldNotConnect:
+ isConnected = false;
+ ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ break;
+ case Network::RoomMember::State::NameCollision:
+ isConnected = false;
+ ShowError(NetworkMessage::USERNAME_IN_USE);
+ break;
+ case Network::RoomMember::State::Joining:
+ auto parent = static_cast(parentWidget());
+ parent->OnOpenNetworkRoom();
+ close();
+ }
+ }
+}
diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/citra_qt/multiplayer/direct_connect.h
new file mode 100644
index 000000000..6d9266601
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.h
@@ -0,0 +1,39 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace Ui {
+class DirectConnect;
+}
+
+class DirectConnectWindow : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit DirectConnectWindow(QWidget* parent = nullptr);
+
+signals:
+ /**
+ * Signalled by this widget when it is closing itself and destroying any state such as
+ * connections that it might have.
+ */
+ void Closed();
+
+private slots:
+ void OnConnection();
+
+private:
+ void Connect();
+ void ClearAllError();
+ void BeginConnecting();
+ void EndConnecting();
+
+ QFutureWatcher* watcher;
+ Ui::DirectConnect* ui;
+};
diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/citra_qt/multiplayer/direct_connect.ui
new file mode 100644
index 000000000..81aac7431
--- /dev/null
+++ b/src/citra_qt/multiplayer/direct_connect.ui
@@ -0,0 +1,190 @@
+
+
+ DirectConnect
+
+
+
+ 0
+ 0
+ 455
+ 239
+
+
+
+ Direct Connect
+
+
+ -
+
+
-
+
+
-
+
+
+ Instructions
+
+
+
-
+
+
+ <html><head/><body><p>Directly connect to a friend by <span style=" font-weight:600;">Traversal server</span> or by<span style=" font-weight:600;"> IP address</span>. </p><p>To use the <span style=" font-weight:600;">Traversal Server</span>, ask the game host for their "<span style=" font-weight:600;">Host Code</span>" which will be visible on the create room screen after it is created.</p></body></html>
+
+
+ Qt::RichText
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+
-
+
+
-
+
+ IP Address
+
+
+
+
+ -
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ IP
+
+
+
+ -
+
+
+ <html><head/><body><p>IPv4 address of the host</p></body></html>
+
+
+ 16
+
+
+
+ -
+
+
+ Port
+
+
+
+ -
+
+
+ <html><head/><body><p>Port number the host is listening on</p></body></html>
+
+
+ 5
+
+
+ 24872
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Nickname
+
+
+
+ -
+
+
+ 20
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 20
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Connect
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp
new file mode 100644
index 000000000..02122f445
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.cpp
@@ -0,0 +1,170 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/host_room.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "common/logging/log.h"
+#include "core/announce_multiplayer_session.h"
+#include "core/settings.h"
+#include "ui_chat_room.h"
+#include "ui_host_room.h"
+
+HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
+ std::shared_ptr session)
+ : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+ ui(new Ui::HostRoom), announce_multiplayer_session(session), game_list(list) {
+ ui->setupUi(this);
+
+ // set up validation for all of the fields
+ ui->room_name->setValidator(Validation::room_name);
+ ui->username->setValidator(Validation::nickname);
+ ui->port->setValidator(Validation::port);
+ ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
+
+ // Create a proxy to the game list to display the list of preferred games
+ proxy = new ComboBoxProxyModel;
+ proxy->setSourceModel(game_list);
+ proxy->sort(0, Qt::AscendingOrder);
+ ui->game_list->setModel(proxy);
+
+ // Connect all the widgets to the appropriate events
+ connect(ui->host, &QPushButton::pressed, this, &HostRoomWindow::Host);
+
+ // Restore the settings:
+ ui->username->setText(UISettings::values.room_nickname);
+ ui->room_name->setText(UISettings::values.room_name);
+ ui->port->setText(UISettings::values.room_port);
+ ui->max_player->setValue(UISettings::values.max_player);
+ int index = ui->host_type->findData(UISettings::values.host_type);
+ if (index != -1) {
+ ui->host_type->setCurrentIndex(index);
+ }
+ index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole);
+ if (index != -1) {
+ ui->game_list->setCurrentIndex(index);
+ }
+}
+
+void HostRoomWindow::Host() {
+ if (!ui->username->hasAcceptableInput()) {
+ NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
+ return;
+ }
+ if (!ui->room_name->hasAcceptableInput()) {
+ NetworkMessage::ShowError(NetworkMessage::ROOMNAME_NOT_VALID);
+ return;
+ }
+ if (!ui->port->hasAcceptableInput()) {
+ NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
+ return;
+ }
+ if (auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ if (!NetworkMessage::WarnDisconnect()) {
+ close();
+ return;
+ } else {
+ member->Leave();
+ }
+ }
+ ui->host->setDisabled(true);
+
+ auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString();
+ auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
+ auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort;
+ auto password = ui->password->text().toStdString();
+ if (auto room = Network::GetRoom().lock()) {
+ bool created = room->Create(ui->room_name->text().toStdString(), "", port, password,
+ ui->max_player->value(), game_name.toStdString(), game_id);
+ if (!created) {
+ NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM);
+ LOG_ERROR(Network, "Could not create room!");
+ ui->host->setEnabled(true);
+ return;
+ }
+ }
+ member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0,
+ Network::NoPreferredMac, password);
+
+ // Store settings
+ UISettings::values.room_nickname = ui->username->text();
+ UISettings::values.room_name = ui->room_name->text();
+ UISettings::values.game_id =
+ ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
+ UISettings::values.max_player = ui->max_player->value();
+
+ UISettings::values.host_type = ui->host_type->currentText();
+ UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty())
+ ? ui->port->text()
+ : QString::number(Network::DefaultRoomPort);
+ Settings::Apply();
+ OnConnection();
+ }
+}
+
+void HostRoomWindow::OnConnection() {
+ ui->host->setEnabled(true);
+ if (auto room_member = Network::GetRoomMember().lock()) {
+ switch (room_member->GetState()) {
+ case Network::RoomMember::State::CouldNotConnect:
+ ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ break;
+ case Network::RoomMember::State::NameCollision:
+ ShowError(NetworkMessage::USERNAME_IN_USE);
+ break;
+ case Network::RoomMember::State::Error:
+ ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ break;
+ case Network::RoomMember::State::Joining:
+ if (ui->host_type->currentIndex() == 0) {
+ if (auto session = announce_multiplayer_session.lock()) {
+ session->Start();
+ } else {
+ LOG_ERROR(Network, "Starting announce session failed");
+ }
+ }
+ auto parent = static_cast(parentWidget());
+ parent->ChangeRoomState();
+ parent->OnOpenNetworkRoom();
+ close();
+ emit Closed();
+ break;
+ }
+ }
+}
+
+QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
+ if (role != Qt::DisplayRole) {
+ auto val = QSortFilterProxyModel::data(idx, role);
+ // If its the icon, shrink it to 16x16
+ if (role == Qt::DecorationRole)
+ val = val.value().scaled(16, 16, Qt::KeepAspectRatio);
+ return val;
+ }
+ std::string filename;
+ Common::SplitPath(
+ QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(),
+ nullptr, &filename, nullptr);
+ QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString();
+ return title.isEmpty() ? QString::fromStdString(filename) : title;
+}
+
+bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
+ // TODO(jroweboy): Sort by game title not filename
+ auto leftData = left.data(Qt::DisplayRole).toString();
+ auto rightData = right.data(Qt::DisplayRole).toString();
+ return leftData.compare(rightData) < 0;
+}
diff --git a/src/citra_qt/multiplayer/host_room.h b/src/citra_qt/multiplayer/host_room.h
new file mode 100644
index 000000000..289dbb040
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.h
@@ -0,0 +1,73 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include "citra_qt/multiplayer/chat_room.h"
+#include "network/network.h"
+
+namespace Ui {
+class HostRoom;
+} // namespace Ui
+
+namespace Core {
+class AnnounceMultiplayerSession;
+}
+
+class ConnectionError;
+class ComboBoxProxyModel;
+
+class ChatMessage;
+
+class HostRoomWindow : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list,
+ std::shared_ptr session);
+
+signals:
+ /**
+ * Signalled by this widget when it is closing itself and destroying any state such as
+ * connections that it might have.
+ */
+ void Closed();
+
+private slots:
+ /**
+ * Handler for connection status changes. Launches the chat window if successful or
+ * displays an error
+ */
+ void OnConnection();
+
+private:
+ void Host();
+
+ std::weak_ptr announce_multiplayer_session;
+ QStandardItemModel* game_list;
+ ComboBoxProxyModel* proxy;
+ Ui::HostRoom* ui;
+};
+
+/**
+ * Proxy Model for the game list combo box so we can reuse the game list model while still
+ * displaying the fields slightly differently
+ */
+class ComboBoxProxyModel : public QSortFilterProxyModel {
+ Q_OBJECT
+
+public:
+ int columnCount(const QModelIndex& idx) const override {
+ return 1;
+ }
+
+ QVariant data(const QModelIndex& idx, int role) const override;
+
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
+};
diff --git a/src/citra_qt/multiplayer/host_room.ui b/src/citra_qt/multiplayer/host_room.ui
new file mode 100644
index 000000000..7edf90628
--- /dev/null
+++ b/src/citra_qt/multiplayer/host_room.ui
@@ -0,0 +1,179 @@
+
+
+ HostRoom
+
+
+
+ 0
+ 0
+ 607
+ 165
+
+
+
+ Create Room
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
-
+
+
+ Room Name
+
+
+
+ -
+
+
+ 50
+
+
+
+ -
+
+
+ Preferred Game
+
+
+
+ -
+
+
+ -
+
+
+ Max Players
+
+
+
+ -
+
+
+ 1
+
+
+ 16
+
+
+ 8
+
+
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
-
+
+
+ -
+
+
+ Username
+
+
+
+ -
+
+
+ QLineEdit::PasswordEchoOnEdit
+
+
+ (Leave blank for open game)
+
+
+
+ -
+
+
+ Qt::ImhDigitsOnly
+
+
+ 5
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ Port
+
+
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
-
+
+ Public
+
+
+ -
+
+ Unlisted
+
+
+
+
+ -
+
+
+ Host Room
+
+
+
+
+
+
+
+
+
+
diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp
new file mode 100644
index 000000000..c20c7d59f
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.cpp
@@ -0,0 +1,314 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include "citra_qt/game_list_p.h"
+#include "citra_qt/main.h"
+#include "citra_qt/multiplayer/client_room.h"
+#include "citra_qt/multiplayer/lobby.h"
+#include "citra_qt/multiplayer/lobby_p.h"
+#include "citra_qt/multiplayer/message.h"
+#include "citra_qt/multiplayer/validation.h"
+#include "citra_qt/ui_settings.h"
+#include "common/logging/log.h"
+#include "core/settings.h"
+#include "network/network.h"
+#include "ui_lobby.h"
+
+Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
+ std::shared_ptr session)
+ : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+ ui(new Ui::Lobby), announce_multiplayer_session(session), game_list(list) {
+ ui->setupUi(this);
+
+ // setup the watcher for background connections
+ watcher = new QFutureWatcher;
+ connect(watcher, &QFutureWatcher::finished, this, &Lobby::OnConnection);
+
+ model = new QStandardItemModel(ui->room_list);
+ proxy = new LobbyFilterProxyModel(this, game_list);
+ proxy->setSourceModel(model);
+ proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ proxy->setSortLocaleAware(true);
+ ui->room_list->setModel(proxy);
+ ui->room_list->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ ui->room_list->header()->stretchLastSection();
+ ui->room_list->setAlternatingRowColors(true);
+ ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
+ ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
+ ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
+ ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
+ ui->room_list->setSortingEnabled(true);
+ ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
+ ui->room_list->setExpandsOnDoubleClick(false);
+ ui->room_list->setUniformRowHeights(true);
+ ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ ui->nickname->setValidator(Validation::nickname);
+ ui->nickname->setText(UISettings::values.nickname);
+
+ // UI Buttons
+ GMainWindow* p = reinterpret_cast(parent);
+ connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby);
+ connect(ui->chat, &QPushButton::pressed, p, &GMainWindow::OnOpenNetworkRoom);
+ connect(ui->games_owned, &QCheckBox::stateChanged, proxy,
+ &LobbyFilterProxyModel::SetFilterOwned);
+ connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull);
+ connect(ui->search, &QLineEdit::textChanged, proxy,
+ &LobbyFilterProxyModel::setFilterFixedString);
+ connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
+
+ // Actions
+ connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby);
+ // TODO(jroweboy): change this slot to OnConnected?
+ connect(this, &Lobby::Connected, p, &GMainWindow::OnOpenNetworkRoom);
+
+ // setup the callbacks for network updates
+ if (auto member = Network::GetRoomMember().lock()) {
+ member->BindOnStateChanged(
+ [this](const Network::RoomMember::State& state) { emit StateChanged(state); });
+ connect(this, &Lobby::StateChanged, this, &Lobby::OnStateChanged);
+ } else {
+ // TODO (jroweboy) network was not initialized?
+ }
+
+ // manually start a refresh when the window is opening
+ // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
+ // part of the constructor, but offload the refresh until after the window shown. perhaps emit a
+ // refreshroomlist signal from places that open the lobby
+ RefreshLobby();
+
+ if (auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ ui->chat->setEnabled(true);
+ return;
+ }
+ }
+ ui->chat->setDisabled(true);
+}
+
+Lobby::~Lobby() {}
+
+const QString Lobby::PasswordPrompt() {
+ bool ok;
+ const QString text =
+ QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
+ QLineEdit::Normal, tr("Password"), &ok);
+ return ok ? text : QString();
+}
+
+void Lobby::OnJoinRoom(const QModelIndex& index) {
+ if (!ui->nickname->hasAcceptableInput()) {
+ NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
+ return;
+ }
+ if (const auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ if (!NetworkMessage::WarnDisconnect()) {
+ return;
+ }
+ }
+ }
+
+ // Get a password to pass if the room is password protected
+ QModelIndex password_index = proxy->index(index.row(), Column::PASSWORD);
+ bool has_password = proxy->data(password_index, LobbyItemPassword::PasswordRole).toBool();
+ const std::string password = has_password ? PasswordPrompt().toStdString() : "";
+ if (has_password && password.empty()) {
+ return;
+ }
+
+ // attempt to connect in a different thread
+ QFuture f = QtConcurrent::run([&, password] {
+ if (auto room_member = Network::GetRoomMember().lock()) {
+
+ QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
+ const std::string nickname = ui->nickname->text().toStdString();
+ const std::string ip =
+ proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
+ int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
+ room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredMac, password);
+ }
+ });
+ watcher->setFuture(f);
+ // and disable widgets and display a connecting while we wait
+ QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
+
+ // Save settings
+ UISettings::values.nickname = ui->nickname->text();
+ UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString();
+ UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString();
+ Settings::Apply();
+}
+
+void Lobby::OnStateChanged(const Network::RoomMember::State& state) {
+ if (auto member = Network::GetRoomMember().lock()) {
+ if (member->IsConnected()) {
+ ui->chat->setEnabled(true);
+ return;
+ }
+ }
+ ui->chat->setDisabled(true);
+}
+
+void Lobby::ResetModel() {
+ model->clear();
+ model->insertColumns(0, Column::TOTAL);
+ model->setHeaderData(Column::PASSWORD, Qt::Horizontal, tr("Password"), Qt::DisplayRole);
+ model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
+ model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
+ model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
+ model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
+ ui->room_list->header()->stretchLastSection();
+}
+
+void Lobby::RefreshLobby() {
+ if (auto session = announce_multiplayer_session.lock()) {
+ ResetModel();
+ room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); });
+ ui->refresh_list->setEnabled(false);
+ ui->refresh_list->setText(tr("Refreshing"));
+ } else {
+ // TODO(jroweboy): Display an error box about announce couldn't be started
+ }
+}
+
+void Lobby::OnRefreshLobby() {
+ AnnounceMultiplayerRoom::RoomList new_room_list = room_list_future.get();
+ for (auto room : new_room_list) {
+ // find the icon for the game if this person owns that game.
+ QPixmap smdh_icon;
+ for (int r = 0; r < game_list->rowCount(); ++r) {
+ auto index = QModelIndex(game_list->index(r, 0));
+ auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
+ if (game_id != 0 && room.preferred_game_id == game_id) {
+ smdh_icon = game_list->data(index, Qt::DecorationRole).value();
+ }
+ }
+
+ QList members;
+ for (auto member : room.members) {
+ QVariant var;
+ var.setValue(LobbyMember{QString::fromStdString(member.name), member.game_id,
+ QString::fromStdString(member.game_name)});
+ members.append(var);
+ }
+
+ model->appendRow(QList(
+ {new LobbyItemPassword(room.has_password),
+ new LobbyItemName(QString::fromStdString(room.name)),
+ new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game),
+ smdh_icon),
+ new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip),
+ room.port),
+ new LobbyItemMemberList(members, room.max_player)}));
+ }
+ ui->refresh_list->setEnabled(true);
+ ui->refresh_list->setText(tr("Refresh List"));
+}
+
+LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
+ : QSortFilterProxyModel(parent), game_list(list) {}
+
+bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
+ // Prioritize filters by fastest to compute
+
+ // filter by filled rooms
+ if (filter_full) {
+ QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
+ int player_count =
+ sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
+ int max_players =
+ sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
+ if (player_count >= max_players) {
+ return false;
+ }
+ }
+
+ // filter by search parameters
+ auto search_param = filterRegExp();
+ if (!search_param.isEmpty()) {
+ QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
+ QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
+ QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
+ bool preferred_game_match = sourceModel()
+ ->data(game_name, LobbyItemGame::GameNameRole)
+ .toString()
+ .contains(search_param);
+ bool room_name_match = sourceModel()
+ ->data(room_name, LobbyItemName::NameRole)
+ .toString()
+ .contains(search_param);
+ bool username_match = sourceModel()
+ ->data(host_name, LobbyItemHost::HostUsernameRole)
+ .toString()
+ .contains(search_param);
+ if (!preferred_game_match && !room_name_match && !username_match) {
+ return false;
+ }
+ }
+
+ // filter by game owned
+ if (filter_owned) {
+ QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
+ QList owned_games;
+ for (int r = 0; r < game_list->rowCount(); ++r) {
+ owned_games.append(QModelIndex(game_list->index(r, 0)));
+ }
+ auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
+ if (current_id == 0) {
+ // TODO(jroweboy): homebrew often doesn't have a game id and this hides them
+ return false;
+ }
+ bool owned = false;
+ for (const auto& game : owned_games) {
+ auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
+ if (current_id == game_id) {
+ owned = true;
+ }
+ }
+ if (!owned) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
+ sourceModel()->sort(column, order);
+}
+
+void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
+ filter_owned = filter;
+ invalidateFilter();
+}
+
+void LobbyFilterProxyModel::SetFilterFull(bool filter) {
+ filter_full = filter;
+ invalidateFilter();
+}
+
+void Lobby::OnConnection() {
+ if (auto room_member = Network::GetRoomMember().lock()) {
+ switch (room_member->GetState()) {
+ case Network::RoomMember::State::CouldNotConnect:
+ ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ break;
+ case Network::RoomMember::State::NameCollision:
+ ShowError(NetworkMessage::USERNAME_IN_USE);
+ break;
+ case Network::RoomMember::State::Error:
+ ShowError(NetworkMessage::UNABLE_TO_CONNECT);
+ break;
+ case Network::RoomMember::State::Joining:
+ auto parent = static_cast(parentWidget());
+ parent->OnOpenNetworkRoom();
+ close();
+ break;
+ }
+ }
+}
diff --git a/src/citra_qt/multiplayer/lobby.h b/src/citra_qt/multiplayer/lobby.h
new file mode 100644
index 000000000..c8ec502f9
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.h
@@ -0,0 +1,125 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "common/announce_multiplayer_room.h"
+#include "core/announce_multiplayer_session.h"
+#include "network/network.h"
+
+namespace Ui {
+class Lobby;
+}
+
+class LobbyModel;
+class LobbyFilterProxyModel;
+
+/**
+ * Listing of all public games pulled from services. The lobby should be simple enough for users to
+ * find the game they want to play, and join it.
+ */
+class Lobby : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit Lobby(QWidget* parent, QStandardItemModel* list,
+ std::shared_ptr session);
+ ~Lobby();
+
+public slots:
+ /**
+ * Begin the process to pull the latest room list from web services. After the listing is
+ * returned from web services, `LobbyRefreshed` will be signalled
+ */
+ void RefreshLobby();
+
+private slots:
+ /**
+ * Pulls the list of rooms from network and fills out the lobby model with the results
+ */
+ void OnRefreshLobby();
+
+ /**
+ * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts
+ * to connect. Will also prompt for a password in case one is required.
+ *
+ * index - The row of the proxy model that the user wants to join.
+ */
+ void OnJoinRoom(const QModelIndex&);
+
+ /**
+ * Handler for connection status changes. Launches the client room window if successful or
+ * displays an error
+ */
+ void OnConnection();
+
+ void OnStateChanged(const Network::RoomMember::State&);
+
+signals:
+ /**
+ * Signalled when the latest lobby data is retrieved.
+ */
+ void LobbyRefreshed();
+
+ /**
+ * Signalled when the status for room connection changes.
+ */
+ void Connected();
+
+ /**
+ * Signalled by this widget when it is closing itself and destroying any state such as
+ * connections that it might have.
+ */
+ void Closed();
+
+ void StateChanged(const Network::RoomMember::State&);
+
+private:
+ /**
+ * Removes all entries in the Lobby before refreshing.
+ */
+ void ResetModel();
+
+ /**
+ * Prompts for a password. Returns an empty QString if the user either did not provide a
+ * password or if the user closed the window.
+ */
+ const QString PasswordPrompt();
+
+ QStandardItemModel* model;
+ QStandardItemModel* game_list;
+ LobbyFilterProxyModel* proxy;
+
+ std::future room_list_future;
+ std::weak_ptr announce_multiplayer_session;
+ std::unique_ptr ui;
+ QFutureWatcher* watcher;
+};
+
+/**
+ * Proxy Model for filtering the lobby
+ */
+class LobbyFilterProxyModel : public QSortFilterProxyModel {
+ Q_OBJECT;
+
+public:
+ explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list);
+ bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
+ void sort(int column, Qt::SortOrder order) override;
+
+public slots:
+ void SetFilterOwned(bool);
+ void SetFilterFull(bool);
+
+private:
+ QStandardItemModel* game_list;
+ bool filter_owned = false;
+ bool filter_full = false;
+};
diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/citra_qt/multiplayer/lobby.ui
new file mode 100644
index 000000000..e5489b18a
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby.ui
@@ -0,0 +1,138 @@
+
+
+ Lobby
+
+
+
+ 0
+ 0
+ 707
+ 487
+
+
+
+ Public Room Browser
+
+
+ -
+
+
+ 3
+
+
-
+
+
+ 6
+
+
-
+
+
+ Nickname
+
+
+ false
+
+
+
+ 6
+
+
+ 1
+
+
+ 6
+
+
+ 4
+
+
-
+
+
+ Nickname
+
+
+
+
+
+
+ -
+
+
+ Filters
+
+
+ false
+
+
+
+ 6
+
+
+ 1
+
+
+ 6
+
+
+ 4
+
+
-
+
+
+ Search
+
+
+ true
+
+
+
+ -
+
+
+ Games I Own
+
+
+
+ -
+
+
+ Hide Full Games
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Refresh Lobby
+
+
+
+ -
+
+
+ Chat
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/citra_qt/multiplayer/lobby_p.h
new file mode 100644
index 000000000..a8a429f12
--- /dev/null
+++ b/src/citra_qt/multiplayer/lobby_p.h
@@ -0,0 +1,189 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "common/common_types.h"
+
+namespace Column {
+enum List {
+ PASSWORD,
+ ROOM_NAME,
+ GAME_NAME,
+ HOST,
+ MEMBER,
+ TOTAL,
+};
+}
+
+class LobbyItem : public QStandardItem {
+public:
+ LobbyItem() : QStandardItem() {}
+ LobbyItem(const QString& string) : QStandardItem(string) {}
+ virtual ~LobbyItem() override {}
+};
+
+class LobbyItemPassword : public LobbyItem {
+public:
+ static const int PasswordRole = Qt::UserRole + 1;
+ LobbyItemPassword() : LobbyItem() {}
+ LobbyItemPassword(const bool has_password) : LobbyItem() {
+ setData(has_password, PasswordRole);
+ }
+
+ QVariant data(int role) const override {
+ if (role != Qt::DecorationRole) {
+ return LobbyItem::data(role);
+ }
+ bool has_password = data(PasswordRole).toBool();
+ return has_password ? QIcon(":/icons/lock.png") : QIcon();
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return data(PasswordRole).toBool() < other.data(PasswordRole).toBool();
+ }
+};
+
+class LobbyItemName : public LobbyItem {
+public:
+ static const int NameRole = Qt::UserRole + 1;
+ LobbyItemName() : LobbyItem() {}
+ LobbyItemName(QString name) : LobbyItem() {
+ setData(name, NameRole);
+ }
+
+ QVariant data(int role) const override {
+ if (role != Qt::DisplayRole) {
+ return LobbyItem::data(role);
+ }
+ return data(NameRole).toString();
+ }
+ bool operator<(const QStandardItem& other) const override {
+ return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0;
+ }
+};
+
+class LobbyItemGame : public LobbyItem {
+public:
+ static const int TitleIDRole = Qt::UserRole + 1;
+ static const int GameNameRole = Qt::UserRole + 2;
+ static const int GameIconRole = Qt::UserRole + 3;
+
+ LobbyItemGame() : LobbyItem() {}
+ LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) : LobbyItem() {
+ setData(static_cast(title_id), TitleIDRole);
+ setData(game_name, GameNameRole);
+ if (!smdh_icon.isNull()) {
+ setData(smdh_icon, GameIconRole);
+ }
+ }
+
+ QVariant data(int role) const override {
+ if (role == Qt::DecorationRole) {
+ auto val = data(GameIconRole);
+ if (val.isValid()) {
+ val = val.value().scaled(16, 16, Qt::KeepAspectRatio);
+ }
+ return val;
+ } else if (role != Qt::DisplayRole) {
+ return LobbyItem::data(role);
+ }
+ return data(GameNameRole).toString();
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return data(GameNameRole)
+ .toString()
+ .localeAwareCompare(other.data(GameNameRole).toString()) < 0;
+ }
+};
+
+class LobbyItemHost : public LobbyItem {
+public:
+ static const int HostUsernameRole = Qt::UserRole + 1;
+ static const int HostIPRole = Qt::UserRole + 2;
+ static const int HostPortRole = Qt::UserRole + 3;
+
+ LobbyItemHost() : LobbyItem() {}
+ LobbyItemHost(QString username, QString ip, u16 port) : LobbyItem() {
+ setData(username, HostUsernameRole);
+ setData(ip, HostIPRole);
+ setData(port, HostPortRole);
+ }
+
+ QVariant data(int role) const override {
+ if (role != Qt::DisplayRole) {
+ return LobbyItem::data(role);
+ }
+ return data(HostUsernameRole).toString();
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ return data(HostUsernameRole)
+ .toString()
+ .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0;
+ }
+};
+
+class LobbyMember {
+public:
+ LobbyMember() {}
+ LobbyMember(const LobbyMember& other) {
+ username = other.username;
+ title_id = other.title_id;
+ game_name = other.game_name;
+ }
+ LobbyMember(const QString username, u64 title_id, const QString game_name)
+ : username(username), title_id(title_id), game_name(game_name) {}
+ ~LobbyMember() {}
+
+ QString GetUsername() const {
+ return username;
+ }
+ u64 GetTitleId() const {
+ return title_id;
+ }
+ QString GetGameName() const {
+ return game_name;
+ }
+
+private:
+ QString username;
+ u64 title_id;
+ QString game_name;
+};
+
+Q_DECLARE_METATYPE(LobbyMember);
+
+class LobbyItemMemberList : public LobbyItem {
+public:
+ static const int MemberListRole = Qt::UserRole + 1;
+ static const int MaxPlayerRole = Qt::UserRole + 2;
+
+ LobbyItemMemberList() : LobbyItem() {}
+ LobbyItemMemberList(QList members, u32 max_players) : LobbyItem() {
+ setData(members, MemberListRole);
+ setData(max_players, MaxPlayerRole);
+ }
+
+ QVariant data(int role) const override {
+ if (role != Qt::DisplayRole) {
+ return LobbyItem::data(role);
+ }
+ auto members = data(MemberListRole).toList();
+ return QString("%1 / %2").arg(QString::number(members.size()),
+ data(MaxPlayerRole).toString());
+ }
+
+ bool operator<(const QStandardItem& other) const override {
+ // sort by rooms that have the most players
+ int left_members = data(MemberListRole).toList().size();
+ int right_members = other.data(MemberListRole).toList().size();
+ return left_members < right_members;
+ }
+};
diff --git a/src/citra_qt/multiplayer/message.cpp b/src/citra_qt/multiplayer/message.cpp
new file mode 100644
index 000000000..f74a57025
--- /dev/null
+++ b/src/citra_qt/multiplayer/message.cpp
@@ -0,0 +1,59 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+
+#include "citra_qt/multiplayer/message.h"
+
+namespace NetworkMessage {
+const ConnectionError USERNAME_NOT_VALID(
+ QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters."));
+const ConnectionError ROOMNAME_NOT_VALID(
+ QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters."));
+const ConnectionError USERNAME_IN_USE(
+ QT_TR_NOOP("Username is already in use. Please choose another."));
+const ConnectionError IP_ADDRESS_NOT_VALID(QT_TR_NOOP("IP is not a valid IPv4 address."));
+const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535."));
+const ConnectionError NO_INTERNET(
+ QT_TR_NOOP("Unable to find an internet connection. Check your internet settings."));
+const ConnectionError UNABLE_TO_CONNECT(
+ QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct."));
+const ConnectionError COULD_NOT_CREATE_ROOM(
+ QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary."));
+const ConnectionError HOST_BANNED(
+ QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you "
+ "or try a different room."));
+const ConnectionError WRONG_VERSION(
+ QT_TR_NOOP("You are using a different version of Citra-Local-Wifi(tm) then the room "
+ "you are trying to connect to."));
+const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Wrong password."));
+const ConnectionError GENERIC_ERROR(QT_TR_NOOP("An error occured."));
+const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect."));
+const ConnectionError MAC_COLLISION(
+ QT_TR_NOOP("MAC-Address is already in use. Please choose another."));
+
+static bool WarnMessage(const std::string& title, const std::string& text) {
+ return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
+ QObject::tr(text.c_str()),
+ QMessageBox::Ok | QMessageBox::Cancel);
+}
+
+void ShowError(const ConnectionError& e) {
+ QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromStdString(e.GetString()));
+}
+
+bool WarnCloseRoom() {
+ return WarnMessage(
+ QT_TR_NOOP("Leave Room"),
+ QT_TR_NOOP("You are about to close the room. Any network connections will be closed."));
+}
+
+bool WarnDisconnect() {
+ return WarnMessage(
+ QT_TR_NOOP("Disconnect"),
+ QT_TR_NOOP("You are about to leave the room. Any network connections will be closed."));
+}
+
+} // namespace NetworkMessage
diff --git a/src/citra_qt/multiplayer/message.h b/src/citra_qt/multiplayer/message.h
new file mode 100644
index 000000000..3a95c1081
--- /dev/null
+++ b/src/citra_qt/multiplayer/message.h
@@ -0,0 +1,53 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+namespace NetworkMessage {
+
+class ConnectionError {
+
+public:
+ explicit ConnectionError(const std::string& str) : err(str) {}
+ const std::string& GetString() const {
+ return err;
+ }
+
+private:
+ std::string err;
+};
+
+extern const ConnectionError USERNAME_NOT_VALID;
+extern const ConnectionError ROOMNAME_NOT_VALID;
+extern const ConnectionError USERNAME_IN_USE;
+extern const ConnectionError IP_ADDRESS_NOT_VALID;
+extern const ConnectionError PORT_NOT_VALID;
+extern const ConnectionError NO_INTERNET;
+extern const ConnectionError UNABLE_TO_CONNECT;
+extern const ConnectionError COULD_NOT_CREATE_ROOM;
+extern const ConnectionError HOST_BANNED;
+extern const ConnectionError WRONG_VERSION;
+extern const ConnectionError WRONG_PASSWORD;
+extern const ConnectionError GENERIC_ERROR;
+extern const ConnectionError LOST_CONNECTION;
+extern const ConnectionError MAC_COLLISION;
+
+/**
+ * Shows a standard QMessageBox with a error message
+ */
+void ShowError(const ConnectionError& e);
+
+/**
+ * Show a standard QMessageBox with a warning message about leaving the room
+ * return true if the user wants to close the network connection
+ */
+bool WarnCloseRoom();
+
+/**
+ * Show a standard QMessageBox with a warning message about disconnecting from the room
+ * return true if the user wants to disconnect
+ */
+bool WarnDisconnect();
+
+} // namespace NetworkMessage
diff --git a/src/citra_qt/multiplayer/validation.h b/src/citra_qt/multiplayer/validation.h
new file mode 100644
index 000000000..fa6777beb
--- /dev/null
+++ b/src/citra_qt/multiplayer/validation.h
@@ -0,0 +1,28 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+
+namespace Validation {
+/// room name can be alphanumeric and " " "_" "." and "-"
+static const QRegExp room_name_regex("^[a-zA-Z0-9._- ]+$");
+static const QValidator* room_name = new QRegExpValidator(room_name_regex);
+
+/// nickname can be alphanumeric and " " "_" "." and "-"
+static const QRegExp nickname_regex("^[a-zA-Z0-9._- ]+$");
+static const QValidator* nickname = new QRegExpValidator(nickname_regex);
+
+/// ipv4 address only
+// TODO remove this when we support hostnames in direct connect
+static const QRegExp ip_regex(
+ "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|"
+ "2[0-4][0-9]|25[0-5])");
+static const QValidator* ip = new QRegExpValidator(ip_regex);
+
+/// port must be between 0 and 65535
+static const QValidator* port = new QIntValidator(0, 65535);
+}; // namespace Validation
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index caf6aea6a..895a24c1b 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -56,8 +56,18 @@ struct Values {
std::vector shortcuts;
uint32_t callout_flags;
+
+ // multiplayer settings
+ QString nickname;
+ QString ip;
+ QString port;
+ QString room_nickname;
+ QString room_name;
+ quint32 max_player;
+ QString room_port;
+ QString host_type;
+ qulonglong game_id;
};
extern Values values;
-
} // namespace UISettings
diff --git a/src/citra_qt/util/clickable_label.cpp b/src/citra_qt/util/clickable_label.cpp
new file mode 100644
index 000000000..010413271
--- /dev/null
+++ b/src/citra_qt/util/clickable_label.cpp
@@ -0,0 +1,13 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "citra_qt/util/clickable_label.h"
+
+ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent) {}
+
+ClickableLabel::~ClickableLabel() {}
+
+void ClickableLabel::mouseReleaseEvent(QMouseEvent* event) {
+ emit clicked();
+}
diff --git a/src/citra_qt/util/clickable_label.h b/src/citra_qt/util/clickable_label.h
new file mode 100644
index 000000000..780a01bf6
--- /dev/null
+++ b/src/citra_qt/util/clickable_label.h
@@ -0,0 +1,23 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+class ClickableLabel : public QLabel {
+ Q_OBJECT
+
+public:
+ explicit ClickableLabel(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
+ ~ClickableLabel();
+
+signals:
+ void clicked();
+
+protected:
+ void mouseReleaseEvent(QMouseEvent* event);
+};