Citra-qt: Add multiplayer ui

master
James Rowe 2018-01-19 13:42:21 +07:00
parent bba2a60b22
commit 871196bc10
34 changed files with 2653 additions and 46 deletions

@ -6,6 +6,12 @@
<file alias="16x16/failed.png">icons/16x16/failed.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> <file alias="256x256/citra.png">icons/256x256/citra.png</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

@ -56,11 +56,27 @@ add_executable(citra-qt
hotkeys.h hotkeys.h
main.cpp main.cpp
main.h 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.cpp
ui_settings.h ui_settings.h
updater/updater.cpp updater/updater.cpp
updater/updater.h updater/updater.h
updater/updater_p.h updater/updater_p.h
util/clickable_label.h
util/clickable_label.cpp
util/spinbox.cpp util/spinbox.cpp
util/spinbox.h util/spinbox.h
util/util.cpp util/util.cpp
@ -79,6 +95,11 @@ set(UIS
configuration/configure_system.ui configuration/configure_system.ui
configuration/configure_web.ui configuration/configure_web.ui
debugger/registers.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 aboutdialog.ui
hotkeys.ui hotkeys.ui
main.ui main.ui

@ -108,12 +108,10 @@ GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread)
setWindowTitle(QString::fromStdString(window_title)); setWindowTitle(QString::fromStdString(window_title));
InputCommon::Init(); InputCommon::Init();
Network::Init();
} }
GRenderWindow::~GRenderWindow() { GRenderWindow::~GRenderWindow() {
InputCommon::Shutdown(); InputCommon::Shutdown();
Network::Shutdown();
} }
void GRenderWindow::moveContext() { void GRenderWindow::moveContext() {

@ -7,6 +7,7 @@
#include "citra_qt/ui_settings.h" #include "citra_qt/ui_settings.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "network/network.h"
Config::Config() { Config::Config() {
// TODO: Don't hardcode the path; let the frontend decide where to put the config files. // 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") qt_config->value("verify_endpoint_url", "https://services.citra-emu.org/api/profile")
.toString() .toString()
.toStdString(); .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_username = qt_config->value("citra_username").toString().toStdString();
Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString(); Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString();
qt_config->endGroup(); qt_config->endGroup();
@ -225,6 +232,18 @@ void Config::ReadValues() {
UISettings::values.first_start = qt_config->value("firstStart", true).toBool(); UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt(); 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(); qt_config->endGroup();
} }
@ -320,6 +339,9 @@ void Config::SaveValues() {
QString::fromStdString(Settings::values.telemetry_endpoint_url)); QString::fromStdString(Settings::values.telemetry_endpoint_url));
qt_config->setValue("verify_endpoint_url", qt_config->setValue("verify_endpoint_url",
QString::fromStdString(Settings::values.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_username", QString::fromStdString(Settings::values.citra_username));
qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token)); qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
qt_config->endGroup(); qt_config->endGroup();
@ -366,6 +388,18 @@ void Config::SaveValues() {
qt_config->setValue("firstStart", UISettings::values.first_start); qt_config->setValue("firstStart", UISettings::values.first_start);
qt_config->setValue("calloutFlags", UISettings::values.callout_flags); 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(); qt_config->endGroup();
} }

@ -374,6 +374,10 @@ void GameList::LoadCompatibilityList() {
} }
} }
QStandardItemModel* GameList::GetModel() const {
return item_model;
}
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
if (!FileUtil::Exists(dir_path.toStdString()) || if (!FileUtil::Exists(dir_path.toStdString()) ||
!FileUtil::IsDirectory(dir_path.toStdString())) { !FileUtil::IsDirectory(dir_path.toStdString())) {

@ -76,6 +76,8 @@ public:
void SaveInterfaceLayout(); void SaveInterfaceLayout();
void LoadInterfaceLayout(); void LoadInterfaceLayout();
QStandardItemModel* GetModel() const;
static const QStringList supported_file_extensions; static const QStringList supported_file_extensions;
signals: signals:

@ -31,8 +31,14 @@
#include "citra_qt/game_list.h" #include "citra_qt/game_list.h"
#include "citra_qt/hotkeys.h" #include "citra_qt/hotkeys.h"
#include "citra_qt/main.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/ui_settings.h"
#include "citra_qt/updater/updater.h" #include "citra_qt/updater/updater.h"
#include "citra_qt/util/clickable_label.h"
#include "common/logging/backend.h" #include "common/logging/backend.h"
#include "common/logging/filter.h" #include "common/logging/filter.h"
#include "common/logging/log.h" #include "common/logging/log.h"
@ -129,6 +135,20 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
SetupUIStrings(); SetupUIStrings();
Network::Init();
if (auto member = Network::GetRoomMember().lock()) {
// register the network structs to use in slots and signals
qRegisterMetaType<Network::RoomMember::State>();
state_callback_handle = member->BindOnStateChanged(
[this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
connect(this, &GMainWindow::NetworkStateChanged, this, &GMainWindow::OnNetworkStateChanged);
}
qRegisterMetaType<Common::WebResult>();
setWindowTitle(QString("Citra %1| %2-%3")
.arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
show(); show();
game_list->LoadCompatibilityList(); game_list->LoadCompatibilityList();
@ -153,6 +173,13 @@ GMainWindow::~GMainWindow() {
delete render_window; delete render_window;
Pica::g_debug_context.reset(); 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() { 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 " 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.")); "full-speed emulation this should be at most 16.67 ms."));
announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
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}) { for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false); label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame); label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0); label->setContentsMargins(4, 0, 4, 0);
statusBar()->addPermanentWidget(label, 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); statusBar()->setVisible(true);
// Removes an ugly inner border from the status bar widgets under Linux // 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::UpdateProgress, this, &GMainWindow::OnUpdateProgress);
connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport);
connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished);
connect(network_status, &ClickableLabel::clicked, this, &GMainWindow::OnOpenNetworkRoom);
} }
void GMainWindow::ConnectMenuEvents() { void GMainWindow::ConnectMenuEvents() {
@ -418,6 +457,15 @@ void GMainWindow::ConnectMenuEvents() {
ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F")); ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar); connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); 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_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
ui.action_Screen_Layout_Swap_Screens->setShortcut( ui.action_Screen_Layout_Swap_Screens->setShortcut(
GetHotkey("Main Window", "Swap Screens", this)->key()); 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() { void GMainWindow::OnStartGame() {
emu_thread->SetRunning(true); emu_thread->SetRunning(true);
qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus"); qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
@ -1054,6 +1126,80 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
graphicsSurfaceViewerWidget->show(); 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() { void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) { if (emu_thread == nullptr) {
status_bar_update_timer.stop(); status_bar_update_timer.stop();
@ -1187,6 +1333,16 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
render_window->close(); 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); QWidget::closeEvent(event);
} }
@ -1306,6 +1462,18 @@ void GMainWindow::SyncMenuUISettings() {
ui.action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen); 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 #ifdef main
#undef main #undef main
#endif #endif

@ -5,15 +5,19 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <QLabel>
#include <QMainWindow> #include <QMainWindow>
#include <QTimer> #include <QTimer>
#include <QTranslator> #include <QTranslator>
#include "common/announce_multiplayer_room.h"
#include "core/core.h" #include "core/core.h"
#include "core/hle/service/am/am.h" #include "core/hle/service/am/am.h"
#include "network/network.h"
#include "ui_main.h" #include "ui_main.h"
class AboutDialog; class AboutDialog;
class Config; class Config;
class ClickableLabel;
class EmuThread; class EmuThread;
class GameList; class GameList;
enum class GameListOpenTarget; enum class GameListOpenTarget;
@ -33,6 +37,16 @@ class RegistersWidget;
class Updater; class Updater;
class WaitTreeWidget; class WaitTreeWidget;
// Multiplayer forward declarations
class Lobby;
class HostRoomWindow;
class ClientRoomWindow;
class DirectConnectWindow;
namespace Core {
class AnnounceMultiplayerSession;
}
class GMainWindow : public QMainWindow { class GMainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
@ -50,6 +64,9 @@ class GMainWindow : public QMainWindow {
public: public:
void filterBarSetChecked(bool state); void filterBarSetChecked(bool state);
void UpdateUITheme(); void UpdateUITheme();
void ChangeRoomState();
GameList* game_list;
GMainWindow(); GMainWindow();
~GMainWindow(); ~GMainWindow();
@ -77,6 +94,16 @@ signals:
// Signal that tells widgets to update icons to use the current theme // Signal that tells widgets to update icons to use the current theme
void UpdateThemedIcons(); 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: private:
void InitializeWidgets(); void InitializeWidgets();
void InitializeDebugWidgets(); void InitializeDebugWidgets();
@ -146,6 +173,8 @@ private slots:
/// Called whenever a user selects the "File->Select Game List Root" menu item /// Called whenever a user selects the "File->Select Game List Root" menu item
void OnMenuSelectGameListRoot(); void OnMenuSelectGameListRoot();
void OnMenuRecentFile(); void OnMenuRecentFile();
void OnNetworkStateChanged(const Network::RoomMember::State& state);
void OnAnnounceFailed(const Common::WebResult&);
void OnConfigure(); void OnConfigure();
void OnToggleFilterBar(); void OnToggleFilterBar();
void OnDisplayTitleBars(bool); void OnDisplayTitleBars(bool);
@ -173,7 +202,8 @@ private:
Ui::MainWindow ui; Ui::MainWindow ui;
GRenderWindow* render_window; GRenderWindow* render_window;
GameList* game_list;
QFutureWatcher<Service::AM::InstallStatus>* watcher = nullptr;
// Status bar elements // Status bar elements
QProgressBar* progress_bar = nullptr; QProgressBar* progress_bar = nullptr;
@ -181,9 +211,11 @@ private:
QLabel* emu_speed_label = nullptr; QLabel* emu_speed_label = nullptr;
QLabel* game_fps_label = nullptr; QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr; QLabel* emu_frametime_label = nullptr;
ClickableLabel* network_status = nullptr;
QTimer status_bar_update_timer; QTimer status_bar_update_timer;
std::unique_ptr<Config> config; std::unique_ptr<Config> config;
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
// Whether emulation is currently running in Citra. // Whether emulation is currently running in Citra.
bool emulation_running = false; bool emulation_running = false;
@ -204,6 +236,14 @@ private:
bool explicit_update_check = false; bool explicit_update_check = false;
bool defer_update_prompt = 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<Network::RoomMember::State> state_callback_handle;
QAction* actions_recent_files[max_recent_files_item]; QAction* actions_recent_files[max_recent_files_item];
QTranslator translator; QTranslator translator;
@ -219,3 +259,4 @@ protected:
Q_DECLARE_METATYPE(size_t); Q_DECLARE_METATYPE(size_t);
Q_DECLARE_METATYPE(Service::AM::InstallStatus); Q_DECLARE_METATYPE(Service::AM::InstallStatus);
Q_DECLARE_METATYPE(Common::WebResult);

@ -45,7 +45,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1081</width> <width>1081</width>
<height>26</height> <height>21</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menu_File"> <widget class="QMenu" name="menu_File">
@ -107,6 +107,20 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menu_View_Debugging"/> <addaction name="menu_View_Debugging"/>
</widget> </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_Stop_Room"/>
<addaction name="action_Connect_To_Room"/>
<addaction name="separator"/>
<addaction name="action_Chat"/>
</widget>
<widget class="QMenu" name="menu_Help"> <widget class="QMenu" name="menu_Help">
<property name="title"> <property name="title">
<string>&amp;Help</string> <string>&amp;Help</string>
@ -122,6 +136,7 @@
<addaction name="menu_File"/> <addaction name="menu_File"/>
<addaction name="menu_Emulation"/> <addaction name="menu_Emulation"/>
<addaction name="menu_View"/> <addaction name="menu_View"/>
<addaction name="menu_Multiplayer"/>
<addaction name="menu_Help"/> <addaction name="menu_Help"/>
</widget> </widget>
<action name="action_Load_File"> <action name="action_Load_File">
@ -228,6 +243,43 @@
<string>Create Pica Surface Viewer</string> <string>Create Pica Surface Viewer</string>
</property> </property>
</action> </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_Stop_Room">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Close Room</string>
</property>
</action>
<action name="action_Connect_To_Room">
<property name="text">
<string>Direct Connect to Room</string>
</property>
</action>
<action name="action_Chat">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Current Room</string>
</property>
</action>
<action name="action_Fullscreen"> <action name="action_Fullscreen">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

@ -0,0 +1,208 @@
// 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/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] <font color='%2'>&lt;%3&gt;</font> %4")
.arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped());
}
private:
ChatMessage() {}
const QList<QString> 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] <font color='%2'><i>%3</i></font>")
.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<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);
}
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<QStandardItem*> l;
std::vector<std::string> 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));
}

@ -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 <memory>
#include <QDialog>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QVariant>
#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);

@ -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>

@ -0,0 +1,121 @@
// 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(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;
}

@ -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;
};

@ -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>

@ -0,0 +1,132 @@
// 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/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<void>;
connect(watcher, &QFutureWatcher<void>::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<ConnectionType>(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<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::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<GMainWindow*>(parentWidget());
parent->OnOpenNetworkRoom();
close();
}
}
}

@ -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 <memory>
#include <QDialog>
#include <QFutureWatcher>
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<void>* watcher;
Ui::DirectConnect* ui;
};

@ -0,0 +1,190 @@
<?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>239</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>
<widget class="QGroupBox" name="instructions_2">
<property name="title">
<string>Instructions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="instructions">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Directly connect to a friend by &lt;span style=&quot; font-weight:600;&quot;&gt;Traversal server&lt;/span&gt; or by&lt;span style=&quot; font-weight:600;&quot;&gt; IP address&lt;/span&gt;. &lt;/p&gt;&lt;p&gt;To use the &lt;span style=&quot; font-weight:600;&quot;&gt;Traversal Server&lt;/span&gt;, ask the game host for their &amp;quot;&lt;span style=&quot; font-weight:600;&quot;&gt;Host Code&lt;/span&gt;&amp;quot; which will be visible on the create room screen after it is created.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;IPv4 address of the host&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Port number the host is listening on&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

@ -0,0 +1,170 @@
// 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/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<Core::AnnounceMultiplayerSession> 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<GMainWindow*>(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<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;
}

@ -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 <memory>
#include <QDialog>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QVariant>
#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<Core::AnnounceMultiplayerSession> 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<Core::AnnounceMultiplayerSession> 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;
};

@ -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>

@ -0,0 +1,314 @@
// 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/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<Core::AnnounceMultiplayerSession> 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<void>;
connect(watcher, &QFutureWatcher<void>::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<GMainWindow*>(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<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::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<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);
}
model->appendRow(QList<QStandardItem*>(
{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<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;
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<GMainWindow*>(parentWidget());
parent->OnOpenNetworkRoom();
close();
break;
}
}
}

@ -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 <future>
#include <memory>
#include <QDialog>
#include <QFutureWatcher>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#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<Core::AnnounceMultiplayerSession> 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<AnnounceMultiplayerRoom::RoomList> room_list_future;
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
std::unique_ptr<Ui::Lobby> ui;
QFutureWatcher<void>* 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;
};

@ -0,0 +1,138 @@
<?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>707</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>
<widget class="QGroupBox" name="general">
<property name="title">
<string>Nickname</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QLineEdit" name="nickname">
<property name="placeholderText">
<string>Nickname</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="filters">
<property name="title">
<string>Filters</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<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>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="refresh_list">
<property name="text">
<string>Refresh Lobby</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="chat">
<property name="text">
<string>Chat</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>

@ -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 <QPixmap>
#include <QStandardItem>
#include <QStandardItemModel>
#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<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() : 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<QVariant> 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;
}
};

@ -0,0 +1,59 @@
// 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."));
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

@ -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

@ -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 <QRegExp>
#include <QValidator>
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

@ -56,8 +56,18 @@ struct Values {
std::vector<Shortcut> shortcuts; std::vector<Shortcut> shortcuts;
uint32_t callout_flags; 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; extern Values values;
} // namespace UISettings } // namespace UISettings

@ -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();
}

@ -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 <QLabel>
#include <QWidget>
#include <Qt>
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);
};