mirror of
https://github.com/Lime3DS/Lime3DS
synced 2024-12-27 01:22:37 -06:00
Merge pull request #3073 from jroweboy/multiplayer_pr
Citra-qt: Add multiplayer ui
This commit is contained in:
commit
c8d4ca8915
40 changed files with 2678 additions and 48 deletions
6
dist/qt_themes/default/default.qrc
vendored
6
dist/qt_themes/default/default.qrc
vendored
|
@ -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>
|
||||||
|
|
BIN
dist/qt_themes/default/icons/16x16/connected.png
vendored
Normal file
BIN
dist/qt_themes/default/icons/16x16/connected.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 269 B |
BIN
dist/qt_themes/default/icons/16x16/disconnected.png
vendored
Normal file
BIN
dist/qt_themes/default/icons/16x16/disconnected.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 306 B |
BIN
dist/qt_themes/default/icons/16x16/lock.png
vendored
Normal file
BIN
dist/qt_themes/default/icons/16x16/lock.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 279 B |
BIN
dist/qt_themes/qdarkstyle/icons/16x16/lock.png
vendored
Normal file
BIN
dist/qt_themes/qdarkstyle/icons/16x16/lock.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 304 B |
1
dist/qt_themes/qdarkstyle/style.qrc
vendored
1
dist/qt_themes/qdarkstyle/style.qrc
vendored
|
@ -1,6 +1,7 @@
|
||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icons/qdarkstyle">
|
<qresource prefix="icons/qdarkstyle">
|
||||||
<file alias="index.theme">icons/index.theme</file>
|
<file alias="index.theme">icons/index.theme</file>
|
||||||
|
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="qss_icons">
|
<qresource prefix="qss_icons">
|
||||||
<file>rc/up_arrow_disabled.png</file>
|
<file>rc/up_arrow_disabled.png</file>
|
||||||
|
|
|
@ -56,11 +56,29 @@ 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/state.cpp
|
||||||
|
multiplayer/state.h
|
||||||
|
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 +97,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,22 @@ 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();
|
||||||
|
bool ok;
|
||||||
|
UISettings::values.host_type = qt_config->value("host_type", 0).toUInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
UISettings::values.host_type = 0;
|
||||||
|
}
|
||||||
|
UISettings::values.max_player = qt_config->value("max_player", 8).toUInt();
|
||||||
|
UISettings::values.game_id = qt_config->value("game_id", 0).toULongLong();
|
||||||
|
qt_config->endGroup();
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,6 +343,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 +392,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,10 @@
|
||||||
#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/state.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"
|
||||||
|
@ -115,6 +117,8 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
|
||||||
default_theme_paths = QIcon::themeSearchPaths();
|
default_theme_paths = QIcon::themeSearchPaths();
|
||||||
UpdateUITheme();
|
UpdateUITheme();
|
||||||
|
|
||||||
|
Network::Init();
|
||||||
|
|
||||||
InitializeWidgets();
|
InitializeWidgets();
|
||||||
InitializeDebugWidgets();
|
InitializeDebugWidgets();
|
||||||
InitializeRecentFileMenuActions();
|
InitializeRecentFileMenuActions();
|
||||||
|
@ -153,6 +157,7 @@ GMainWindow::~GMainWindow() {
|
||||||
delete render_window;
|
delete render_window;
|
||||||
|
|
||||||
Pica::g_debug_context.reset();
|
Pica::g_debug_context.reset();
|
||||||
|
Network::Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::InitializeWidgets() {
|
void GMainWindow::InitializeWidgets() {
|
||||||
|
@ -165,6 +170,10 @@ void GMainWindow::InitializeWidgets() {
|
||||||
game_list = new GameList(this);
|
game_list = new GameList(this);
|
||||||
ui.horizontalLayout->addWidget(game_list);
|
ui.horizontalLayout->addWidget(game_list);
|
||||||
|
|
||||||
|
multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
|
||||||
|
ui.action_Show_Room);
|
||||||
|
multiplayer_state->setVisible(false);
|
||||||
|
|
||||||
// Setup updater
|
// Setup updater
|
||||||
updater = new Updater(this);
|
updater = new Updater(this);
|
||||||
UISettings::values.updater_found = updater->HasUpdater();
|
UISettings::values.updater_found = updater->HasUpdater();
|
||||||
|
@ -199,6 +208,8 @@ void GMainWindow::InitializeWidgets() {
|
||||||
label->setContentsMargins(4, 0, 4, 0);
|
label->setContentsMargins(4, 0, 4, 0);
|
||||||
statusBar()->addPermanentWidget(label, 0);
|
statusBar()->addPermanentWidget(label, 0);
|
||||||
}
|
}
|
||||||
|
statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
|
||||||
|
statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
|
||||||
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
|
||||||
|
@ -418,6 +429,19 @@ 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, multiplayer_state,
|
||||||
|
&MultiplayerState::OnViewLobby);
|
||||||
|
connect(ui.action_Start_Room, &QAction::triggered, multiplayer_state,
|
||||||
|
&MultiplayerState::OnCreateRoom);
|
||||||
|
connect(ui.action_Leave_Room, &QAction::triggered, multiplayer_state,
|
||||||
|
&MultiplayerState::OnCloseRoom);
|
||||||
|
connect(ui.action_Connect_To_Room, &QAction::triggered, multiplayer_state,
|
||||||
|
&MultiplayerState::OnDirectConnectToRoom);
|
||||||
|
connect(ui.action_Show_Room, &QAction::triggered, multiplayer_state,
|
||||||
|
&MultiplayerState::OnOpenNetworkRoom);
|
||||||
|
|
||||||
ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
|
ui.action_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());
|
||||||
|
@ -1186,7 +1210,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
|
||||||
ShutdownGame();
|
ShutdownGame();
|
||||||
|
|
||||||
render_window->close();
|
render_window->close();
|
||||||
|
multiplayer_state->Close();
|
||||||
QWidget::closeEvent(event);
|
QWidget::closeEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,18 @@
|
||||||
#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 "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;
|
||||||
|
@ -25,6 +28,7 @@ class GraphicsTracingWidget;
|
||||||
class GraphicsVertexShaderWidget;
|
class GraphicsVertexShaderWidget;
|
||||||
class GRenderWindow;
|
class GRenderWindow;
|
||||||
class MicroProfileDialog;
|
class MicroProfileDialog;
|
||||||
|
class MultiplayerState;
|
||||||
class ProfilerWidget;
|
class ProfilerWidget;
|
||||||
template <typename>
|
template <typename>
|
||||||
class QFutureWatcher;
|
class QFutureWatcher;
|
||||||
|
@ -50,9 +54,12 @@ class GMainWindow : public QMainWindow {
|
||||||
public:
|
public:
|
||||||
void filterBarSetChecked(bool state);
|
void filterBarSetChecked(bool state);
|
||||||
void UpdateUITheme();
|
void UpdateUITheme();
|
||||||
|
|
||||||
GMainWindow();
|
GMainWindow();
|
||||||
~GMainWindow();
|
~GMainWindow();
|
||||||
|
|
||||||
|
GameList* game_list;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -173,7 +180,6 @@ private:
|
||||||
Ui::MainWindow ui;
|
Ui::MainWindow ui;
|
||||||
|
|
||||||
GRenderWindow* render_window;
|
GRenderWindow* render_window;
|
||||||
GameList* game_list;
|
|
||||||
|
|
||||||
// Status bar elements
|
// Status bar elements
|
||||||
QProgressBar* progress_bar = nullptr;
|
QProgressBar* progress_bar = nullptr;
|
||||||
|
@ -183,6 +189,7 @@ private:
|
||||||
QLabel* emu_frametime_label = nullptr;
|
QLabel* emu_frametime_label = nullptr;
|
||||||
QTimer status_bar_update_timer;
|
QTimer status_bar_update_timer;
|
||||||
|
|
||||||
|
MultiplayerState* multiplayer_state = nullptr;
|
||||||
std::unique_ptr<Config> config;
|
std::unique_ptr<Config> config;
|
||||||
|
|
||||||
// Whether emulation is currently running in Citra.
|
// Whether emulation is currently running in Citra.
|
||||||
|
|
|
@ -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_Connect_To_Room"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="action_Show_Room"/>
|
||||||
|
<addaction name="action_Leave_Room"/>
|
||||||
|
</widget>
|
||||||
<widget class="QMenu" name="menu_Help">
|
<widget class="QMenu" name="menu_Help">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&Help</string>
|
<string>&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_Leave_Room">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Leave Room</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_Connect_To_Room">
|
||||||
|
<property name="text">
|
||||||
|
<string>Direct Connect to Room</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_Show_Room">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Show Current Room</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="action_Fullscreen">
|
<action name="action_Fullscreen">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -244,46 +296,46 @@
|
||||||
<string>Opens the maintenance tool to modify your Citra installation</string>
|
<string>Opens the maintenance tool to modify your Citra installation</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Screen_Layout_Default">
|
<action name="action_Screen_Layout_Default">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Default</string>
|
<string>Default</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Screen_Layout_Single_Screen">
|
<action name="action_Screen_Layout_Single_Screen">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Single Screen</string>
|
<string>Single Screen</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Screen_Layout_Large_Screen">
|
<action name="action_Screen_Layout_Large_Screen">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Large Screen</string>
|
<string>Large Screen</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Screen_Layout_Side_by_Side">
|
<action name="action_Screen_Layout_Side_by_Side">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Side by Side</string>
|
<string>Side by Side</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Screen_Layout_Swap_Screens">
|
<action name="action_Screen_Layout_Swap_Screens">
|
||||||
<property name="checkable">
|
<property name="checkable">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Swap Screens</string>
|
<string>Swap Screens</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Check_For_Updates">
|
<action name="action_Check_For_Updates">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Check for Updates</string>
|
<string>Check for Updates</string>
|
||||||
|
|
211
src/citra_qt/multiplayer/chat_room.cpp
Normal file
211
src/citra_qt/multiplayer/chat_room.cpp
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <future>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QList>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QTime>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include "citra_qt/game_list_p.h"
|
||||||
|
#include "citra_qt/multiplayer/chat_room.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/announce_multiplayer_session.h"
|
||||||
|
#include "ui_chat_room.h"
|
||||||
|
|
||||||
|
class ChatMessage {
|
||||||
|
public:
|
||||||
|
explicit ChatMessage(const Network::ChatEntry& chat, QTime ts = {}) {
|
||||||
|
/// Convert the time to their default locale defined format
|
||||||
|
QLocale locale;
|
||||||
|
timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
|
||||||
|
nickname = QString::fromStdString(chat.nickname);
|
||||||
|
message = QString::fromStdString(chat.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the message using the players color
|
||||||
|
QString GetPlayerChatMessage(u16 player) const {
|
||||||
|
auto color = player_color[player % 16];
|
||||||
|
return QString("[%1] <font color='%2'><%3></font> %4")
|
||||||
|
.arg(timestamp, color, nickname.toHtmlEscaped(), message.toHtmlEscaped());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr std::array<const char*, 16> player_color = {
|
||||||
|
{"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222",
|
||||||
|
"#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}};
|
||||||
|
|
||||||
|
QString timestamp;
|
||||||
|
QString nickname;
|
||||||
|
QString message;
|
||||||
|
};
|
||||||
|
|
||||||
|
class StatusMessage {
|
||||||
|
public:
|
||||||
|
explicit StatusMessage(const QString& msg, QTime ts = {}) {
|
||||||
|
/// Convert the time to their default locale defined format
|
||||||
|
QLocale locale;
|
||||||
|
timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
|
||||||
|
message = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GetSystemChatMessage() const {
|
||||||
|
return QString("[%1] <font color='%2'><i>%3</i></font>")
|
||||||
|
.arg(timestamp, system_color, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr const char system_color[] = "#888888";
|
||||||
|
QString timestamp;
|
||||||
|
QString message;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
// set the item_model for player_view
|
||||||
|
enum {
|
||||||
|
COLUMN_NAME,
|
||||||
|
COLUMN_GAME,
|
||||||
|
COLUMN_COUNT, // Number of columns
|
||||||
|
};
|
||||||
|
|
||||||
|
player_list = new QStandardItemModel(ui->player_view);
|
||||||
|
ui->player_view->setModel(player_list);
|
||||||
|
player_list->insertColumns(0, COLUMN_COUNT);
|
||||||
|
player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name"));
|
||||||
|
player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game"));
|
||||||
|
|
||||||
|
ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
|
||||||
|
|
||||||
|
// register the network structs to use in slots and signals
|
||||||
|
qRegisterMetaType<Network::ChatEntry>();
|
||||||
|
qRegisterMetaType<Network::RoomInformation>();
|
||||||
|
qRegisterMetaType<Network::RoomMember::State>();
|
||||||
|
|
||||||
|
// setup the callbacks for network updates
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
member->BindOnChatMessageRecieved(
|
||||||
|
[this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
|
||||||
|
connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
|
||||||
|
} else {
|
||||||
|
// TODO (jroweboy) network was not initialized?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect all the widgets to the appropriate events
|
||||||
|
connect(ui->chat_message, &QLineEdit::returnPressed, ui->send_message, &QPushButton::pressed);
|
||||||
|
connect(ui->chat_message, &QLineEdit::textChanged, this, &::ChatRoom::OnChatTextChanged);
|
||||||
|
connect(ui->send_message, &QPushButton::pressed, this, &ChatRoom::OnSendChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom::~ChatRoom() = default;
|
||||||
|
|
||||||
|
void ChatRoom::Clear() {
|
||||||
|
ui->chat_history->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::AppendStatusMessage(const QString& msg) {
|
||||||
|
ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::AppendChatMessage(const QString& msg) {
|
||||||
|
ui->chat_history->append(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatRoom::ValidateMessage(const std::string& msg) {
|
||||||
|
return !msg.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
|
||||||
|
// TODO(B3N30): change title
|
||||||
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
|
SetPlayerList(room_member->GetMemberInformation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::Disable() {
|
||||||
|
ui->send_message->setDisabled(true);
|
||||||
|
ui->chat_message->setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::Enable() {
|
||||||
|
ui->send_message->setEnabled(true);
|
||||||
|
ui->chat_message->setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
|
||||||
|
if (!ValidateMessage(chat.message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auto room = Network::GetRoomMember().lock()) {
|
||||||
|
// get the id of the player
|
||||||
|
auto members = room->GetMemberInformation();
|
||||||
|
auto it = std::find_if(members.begin(), members.end(),
|
||||||
|
[&chat](const Network::RoomMember::MemberInformation& member) {
|
||||||
|
return member.nickname == chat.nickname;
|
||||||
|
});
|
||||||
|
if (it == members.end()) {
|
||||||
|
NGLOG_INFO(Network, "Chat message received from unknown player. Ignoring it.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto player = std::distance(members.begin(), it);
|
||||||
|
ChatMessage m(chat);
|
||||||
|
AppendChatMessage(m.GetPlayerChatMessage(player));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::OnSendChat() {
|
||||||
|
if (auto room = Network::GetRoomMember().lock()) {
|
||||||
|
if (room->GetState() != Network::RoomMember::State::Joined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto message = ui->chat_message->text().toStdString();
|
||||||
|
if (!ValidateMessage(message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto nick = room->GetNickname();
|
||||||
|
Network::ChatEntry chat{nick, message};
|
||||||
|
|
||||||
|
auto members = room->GetMemberInformation();
|
||||||
|
auto it = std::find_if(members.begin(), members.end(),
|
||||||
|
[&chat](const Network::RoomMember::MemberInformation& member) {
|
||||||
|
return member.nickname == chat.nickname;
|
||||||
|
});
|
||||||
|
if (it == members.end()) {
|
||||||
|
NGLOG_INFO(Network, "Cannot find self in the player list when sending a message.");
|
||||||
|
}
|
||||||
|
auto player = std::distance(members.begin(), it);
|
||||||
|
ChatMessage m(chat);
|
||||||
|
room->SendChatMessage(message);
|
||||||
|
AppendChatMessage(m.GetPlayerChatMessage(player));
|
||||||
|
ui->chat_message->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
|
||||||
|
// TODO(B3N30): Remember which row is selected
|
||||||
|
player_list->removeRows(0, player_list->rowCount());
|
||||||
|
for (const auto& member : member_list) {
|
||||||
|
if (member.nickname.empty())
|
||||||
|
continue;
|
||||||
|
QList<QStandardItem*> l;
|
||||||
|
std::vector<std::string> elements = {member.nickname, member.game_info.name};
|
||||||
|
for (const auto& item : elements) {
|
||||||
|
QStandardItem* child = new QStandardItem(QString::fromStdString(item));
|
||||||
|
child->setEditable(false);
|
||||||
|
l.append(child);
|
||||||
|
}
|
||||||
|
player_list->invisibleRootItem()->appendRow(l);
|
||||||
|
}
|
||||||
|
// TODO(B3N30): Restore row selection
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRoom::OnChatTextChanged() {
|
||||||
|
if (ui->chat_message->text().length() > Network::MaxMessageSize)
|
||||||
|
ui->chat_message->setText(ui->chat_message->text().left(Network::MaxMessageSize));
|
||||||
|
}
|
58
src/citra_qt/multiplayer/chat_room.h
Normal file
58
src/citra_qt/multiplayer/chat_room.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QVariant>
|
||||||
|
#include "network/network.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class ChatRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class AnnounceMultiplayerSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionError;
|
||||||
|
class ComboBoxProxyModel;
|
||||||
|
|
||||||
|
class ChatMessage;
|
||||||
|
|
||||||
|
class ChatRoom : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatRoom(QWidget* parent);
|
||||||
|
void SetPlayerList(const Network::RoomMember::MemberList& member_list);
|
||||||
|
void Clear();
|
||||||
|
void AppendStatusMessage(const QString& msg);
|
||||||
|
~ChatRoom();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void OnRoomUpdate(const Network::RoomInformation& info);
|
||||||
|
void OnChatReceive(const Network::ChatEntry&);
|
||||||
|
void OnSendChat();
|
||||||
|
void OnChatTextChanged();
|
||||||
|
void Disable();
|
||||||
|
void Enable();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void ChatReceived(const Network::ChatEntry&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr u32 max_chat_lines = 1000;
|
||||||
|
void AppendChatMessage(const QString&);
|
||||||
|
bool ValidateMessage(const std::string&);
|
||||||
|
QStandardItemModel* player_list;
|
||||||
|
std::unique_ptr<Ui::ChatRoom> ui;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(Network::ChatEntry);
|
||||||
|
Q_DECLARE_METATYPE(Network::RoomInformation);
|
||||||
|
Q_DECLARE_METATYPE(Network::RoomMember::State);
|
59
src/citra_qt/multiplayer/chat_room.ui
Normal file
59
src/citra_qt/multiplayer/chat_room.ui
Normal file
|
@ -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>
|
87
src/citra_qt/multiplayer/client_room.cpp
Normal file
87
src/citra_qt/multiplayer/client_room.cpp
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QList>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QTime>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include "citra_qt/game_list_p.h"
|
||||||
|
#include "citra_qt/multiplayer/client_room.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/announce_multiplayer_session.h"
|
||||||
|
#include "ui_client_room.h"
|
||||||
|
|
||||||
|
ClientRoomWindow::ClientRoomWindow(QWidget* parent)
|
||||||
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
||||||
|
ui(std::make_unique<Ui::ClientRoom>()) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
// setup the callbacks for network updates
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
member->BindOnRoomInformationChanged(
|
||||||
|
[this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); });
|
||||||
|
member->BindOnStateChanged(
|
||||||
|
[this](const Network::RoomMember::State& state) { emit StateChanged(state); });
|
||||||
|
|
||||||
|
connect(this, &ClientRoomWindow::RoomInformationChanged, this,
|
||||||
|
&ClientRoomWindow::OnRoomUpdate);
|
||||||
|
connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange);
|
||||||
|
} else {
|
||||||
|
// TODO (jroweboy) network was not initialized?
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); });
|
||||||
|
ui->disconnect->setDefault(false);
|
||||||
|
ui->disconnect->setAutoDefault(false);
|
||||||
|
UpdateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientRoomWindow::~ClientRoomWindow() = default;
|
||||||
|
|
||||||
|
void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) {
|
||||||
|
UpdateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) {
|
||||||
|
if (state == Network::RoomMember::State::Joined) {
|
||||||
|
ui->chat->Clear();
|
||||||
|
ui->chat->AppendStatusMessage(tr("Connected"));
|
||||||
|
}
|
||||||
|
UpdateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientRoomWindow::Disconnect() {
|
||||||
|
if (!NetworkMessage::WarnDisconnect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
member->Leave();
|
||||||
|
ui->chat->AppendStatusMessage(tr("Disconnected"));
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientRoomWindow::UpdateView() {
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
if (member->IsConnected()) {
|
||||||
|
ui->chat->Enable();
|
||||||
|
ui->disconnect->setEnabled(true);
|
||||||
|
auto memberlist = member->GetMemberInformation();
|
||||||
|
ui->chat->SetPlayerList(memberlist);
|
||||||
|
const auto information = member->GetRoomInformation();
|
||||||
|
setWindowTitle(QString(tr("%1 (%2/%3 members) - connected"))
|
||||||
|
.arg(QString::fromStdString(information.name))
|
||||||
|
.arg(memberlist.size())
|
||||||
|
.arg(information.member_slots));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(B3N30): can't get RoomMember*, show error and close window
|
||||||
|
close();
|
||||||
|
}
|
34
src/citra_qt/multiplayer/client_room.h
Normal file
34
src/citra_qt/multiplayer/client_room.h
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "citra_qt/multiplayer/chat_room.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class ClientRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientRoomWindow : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ClientRoomWindow(QWidget* parent);
|
||||||
|
~ClientRoomWindow();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void OnRoomUpdate(const Network::RoomInformation&);
|
||||||
|
void OnStateChange(const Network::RoomMember::State&);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void RoomInformationChanged(const Network::RoomInformation&);
|
||||||
|
void StateChanged(const Network::RoomMember::State&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Disconnect();
|
||||||
|
void UpdateView();
|
||||||
|
|
||||||
|
QStandardItemModel* player_list;
|
||||||
|
std::unique_ptr<Ui::ClientRoom> ui;
|
||||||
|
};
|
63
src/citra_qt/multiplayer/client_room.ui
Normal file
63
src/citra_qt/multiplayer/client_room.ui
Normal file
|
@ -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>
|
113
src/citra_qt/multiplayer/direct_connect.cpp
Normal file
113
src/citra_qt/multiplayer/direct_connect.cpp
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QIntValidator>
|
||||||
|
#include <QRegExpValidator>
|
||||||
|
#include <QString>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include "citra_qt/main.h"
|
||||||
|
#include "citra_qt/multiplayer/client_room.h"
|
||||||
|
#include "citra_qt/multiplayer/direct_connect.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "citra_qt/multiplayer/state.h"
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "network/network.h"
|
||||||
|
#include "ui_direct_connect.h"
|
||||||
|
|
||||||
|
enum class ConnectionType : u8 { TraversalServer, IP };
|
||||||
|
|
||||||
|
DirectConnectWindow::DirectConnectWindow(QWidget* parent)
|
||||||
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
||||||
|
ui(std::make_unique<Ui::DirectConnect>()) {
|
||||||
|
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
// setup the watcher for background connections
|
||||||
|
watcher = new QFutureWatcher<void>;
|
||||||
|
connect(watcher, &QFutureWatcher<void>::finished, this, &DirectConnectWindow::OnConnection);
|
||||||
|
|
||||||
|
ui->nickname->setValidator(validation.GetNickname());
|
||||||
|
ui->nickname->setText(UISettings::values.nickname);
|
||||||
|
ui->ip->setValidator(validation.GetIP());
|
||||||
|
ui->ip->setText(UISettings::values.ip);
|
||||||
|
ui->port->setValidator(validation.GetPort());
|
||||||
|
ui->port->setText(UISettings::values.port);
|
||||||
|
|
||||||
|
// TODO(jroweboy): Show or hide the connection options based on the current value of the combo
|
||||||
|
// box. Add this back in when the traversal server support is added.
|
||||||
|
connect(ui->connect, &QPushButton::pressed, this, &DirectConnectWindow::Connect);
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectConnectWindow::~DirectConnectWindow() = default;
|
||||||
|
|
||||||
|
void DirectConnectWindow::Connect() {
|
||||||
|
if (!ui->nickname->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto member = Network::GetRoomMember().lock()) {
|
||||||
|
if (member->IsConnected() && !NetworkMessage::WarnDisconnect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (static_cast<ConnectionType>(ui->connection_type->currentIndex())) {
|
||||||
|
case ConnectionType::TraversalServer:
|
||||||
|
break;
|
||||||
|
case ConnectionType::IP:
|
||||||
|
if (!ui->ip->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::IP_ADDRESS_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ui->port->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store settings
|
||||||
|
UISettings::values.nickname = ui->nickname->text();
|
||||||
|
UISettings::values.ip = ui->ip->text();
|
||||||
|
UISettings::values.port = (ui->port->isModified() && !ui->port->text().isEmpty())
|
||||||
|
? ui->port->text()
|
||||||
|
: UISettings::values.port;
|
||||||
|
Settings::Apply();
|
||||||
|
|
||||||
|
// attempt to connect in a different thread
|
||||||
|
QFuture<void> f = QtConcurrent::run([&] {
|
||||||
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
|
auto port = UISettings::values.port.toUInt();
|
||||||
|
room_member->Join(ui->nickname->text().toStdString(),
|
||||||
|
ui->ip->text().toStdString().c_str(), port, 0,
|
||||||
|
Network::NoPreferredMac, ui->password->text().toStdString().c_str());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watcher->setFuture(f);
|
||||||
|
// and disable widgets and display a connecting while we wait
|
||||||
|
BeginConnecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DirectConnectWindow::BeginConnecting() {
|
||||||
|
ui->connect->setEnabled(false);
|
||||||
|
ui->connect->setText(tr("Connecting"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DirectConnectWindow::EndConnecting() {
|
||||||
|
ui->connect->setEnabled(true);
|
||||||
|
ui->connect->setText(tr("Connect"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DirectConnectWindow::OnConnection() {
|
||||||
|
EndConnecting();
|
||||||
|
|
||||||
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
|
if (room_member->GetState() == Network::RoomMember::State::Joined) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/citra_qt/multiplayer/direct_connect.h
Normal file
41
src/citra_qt/multiplayer/direct_connect.h
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class DirectConnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectConnectWindow : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DirectConnectWindow(QWidget* parent = nullptr);
|
||||||
|
~DirectConnectWindow();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* Signalled by this widget when it is closing itself and destroying any state such as
|
||||||
|
* connections that it might have.
|
||||||
|
*/
|
||||||
|
void Closed();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void OnConnection();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Connect();
|
||||||
|
void BeginConnecting();
|
||||||
|
void EndConnecting();
|
||||||
|
|
||||||
|
QFutureWatcher<void>* watcher;
|
||||||
|
std::unique_ptr<Ui::DirectConnect> ui;
|
||||||
|
Validation validation;
|
||||||
|
};
|
168
src/citra_qt/multiplayer/direct_connect.ui
Normal file
168
src/citra_qt/multiplayer/direct_connect.ui
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>DirectConnect</class>
|
||||||
|
<widget class="QWidget" name="DirectConnect">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>455</width>
|
||||||
|
<height>161</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Direct Connect</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="connection_type">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>IP Address</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="ip_container" native="true">
|
||||||
|
<layout class="QHBoxLayout" name="ip_layout">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>IP</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="ip">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>IPv4 address of the host</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="maxLength">
|
||||||
|
<number>16</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Port</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="port">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Port number the host is listening on</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="maxLength">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>24872</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Nickname</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="nickname">
|
||||||
|
<property name="maxLength">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Password</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="password"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="connect">
|
||||||
|
<property name="text">
|
||||||
|
<string>Connect</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
157
src/citra_qt/multiplayer/host_room.cpp
Normal file
157
src/citra_qt/multiplayer/host_room.cpp
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QList>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QTime>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include "citra_qt/game_list_p.h"
|
||||||
|
#include "citra_qt/main.h"
|
||||||
|
#include "citra_qt/multiplayer/host_room.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "citra_qt/multiplayer/state.h"
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/announce_multiplayer_session.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "ui_host_room.h"
|
||||||
|
|
||||||
|
HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
|
||||||
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session)
|
||||||
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
||||||
|
ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session), game_list(list) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
// set up validation for all of the fields
|
||||||
|
ui->room_name->setValidator(validation.GetRoomName());
|
||||||
|
ui->username->setValidator(validation.GetNickname());
|
||||||
|
ui->port->setValidator(validation.GetPort());
|
||||||
|
ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
|
||||||
|
|
||||||
|
// Create a proxy to the game list to display the list of preferred games
|
||||||
|
proxy = new ComboBoxProxyModel;
|
||||||
|
proxy->setSourceModel(game_list);
|
||||||
|
proxy->sort(0, Qt::AscendingOrder);
|
||||||
|
ui->game_list->setModel(proxy);
|
||||||
|
|
||||||
|
// Connect all the widgets to the appropriate events
|
||||||
|
connect(ui->host, &QPushButton::pressed, this, &HostRoomWindow::Host);
|
||||||
|
|
||||||
|
// Restore the settings:
|
||||||
|
ui->username->setText(UISettings::values.room_nickname);
|
||||||
|
ui->room_name->setText(UISettings::values.room_name);
|
||||||
|
ui->port->setText(UISettings::values.room_port);
|
||||||
|
ui->max_player->setValue(UISettings::values.max_player);
|
||||||
|
int index = UISettings::values.host_type;
|
||||||
|
if (index < ui->host_type->count()) {
|
||||||
|
ui->host_type->setCurrentIndex(index);
|
||||||
|
}
|
||||||
|
index = ui->game_list->findData(UISettings::values.game_id, GameListItemPath::ProgramIdRole);
|
||||||
|
if (index != -1) {
|
||||||
|
ui->game_list->setCurrentIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HostRoomWindow::~HostRoomWindow() = default;
|
||||||
|
|
||||||
|
void HostRoomWindow::Host() {
|
||||||
|
if (!ui->username->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ui->room_name->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::ROOMNAME_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ui->port->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::PORT_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
if (member->IsConnected()) {
|
||||||
|
auto parent = static_cast<MultiplayerState*>(parentWidget());
|
||||||
|
if (!parent->OnCloseRoom()) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui->host->setDisabled(true);
|
||||||
|
|
||||||
|
auto game_name = ui->game_list->currentData(Qt::DisplayRole).toString();
|
||||||
|
auto game_id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
|
||||||
|
auto port = ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort;
|
||||||
|
auto password = ui->password->text().toStdString();
|
||||||
|
if (auto room = Network::GetRoom().lock()) {
|
||||||
|
bool created = room->Create(ui->room_name->text().toStdString(), "", port, password,
|
||||||
|
ui->max_player->value(), game_name.toStdString(), game_id);
|
||||||
|
if (!created) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM);
|
||||||
|
NGLOG_ERROR(Network, "Could not create room!");
|
||||||
|
ui->host->setEnabled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0,
|
||||||
|
Network::NoPreferredMac, password);
|
||||||
|
|
||||||
|
// Store settings
|
||||||
|
UISettings::values.room_nickname = ui->username->text();
|
||||||
|
UISettings::values.room_name = ui->room_name->text();
|
||||||
|
UISettings::values.game_id =
|
||||||
|
ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
|
||||||
|
UISettings::values.max_player = ui->max_player->value();
|
||||||
|
|
||||||
|
UISettings::values.host_type = ui->host_type->currentIndex();
|
||||||
|
UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty())
|
||||||
|
? ui->port->text()
|
||||||
|
: QString::number(Network::DefaultRoomPort);
|
||||||
|
Settings::Apply();
|
||||||
|
OnConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HostRoomWindow::OnConnection() {
|
||||||
|
ui->host->setEnabled(true);
|
||||||
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
|
if (room_member->GetState() == Network::RoomMember::State::Joining) {
|
||||||
|
// Start the announce session if they chose Public
|
||||||
|
if (ui->host_type->currentIndex() == 0) {
|
||||||
|
if (auto session = announce_multiplayer_session.lock()) {
|
||||||
|
session->Start();
|
||||||
|
} else {
|
||||||
|
NGLOG_ERROR(Network, "Starting announce session failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
|
||||||
|
if (role != Qt::DisplayRole) {
|
||||||
|
auto val = QSortFilterProxyModel::data(idx, role);
|
||||||
|
// If its the icon, shrink it to 16x16
|
||||||
|
if (role == Qt::DecorationRole)
|
||||||
|
val = val.value<QImage>().scaled(16, 16, Qt::KeepAspectRatio);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
std::string filename;
|
||||||
|
Common::SplitPath(
|
||||||
|
QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(),
|
||||||
|
nullptr, &filename, nullptr);
|
||||||
|
QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString();
|
||||||
|
return title.isEmpty() ? QString::fromStdString(filename) : title;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
|
||||||
|
// TODO(jroweboy): Sort by game title not filename
|
||||||
|
auto leftData = left.data(Qt::DisplayRole).toString();
|
||||||
|
auto rightData = right.data(Qt::DisplayRole).toString();
|
||||||
|
return leftData.compare(rightData) < 0;
|
||||||
|
}
|
69
src/citra_qt/multiplayer/host_room.h
Normal file
69
src/citra_qt/multiplayer/host_room.h
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QVariant>
|
||||||
|
#include "citra_qt/multiplayer/chat_room.h"
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
#include "network/network.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class HostRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class AnnounceMultiplayerSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionError;
|
||||||
|
class ComboBoxProxyModel;
|
||||||
|
|
||||||
|
class ChatMessage;
|
||||||
|
|
||||||
|
class HostRoomWindow : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list,
|
||||||
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session);
|
||||||
|
~HostRoomWindow();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
/**
|
||||||
|
* Handler for connection status changes. Launches the chat window if successful or
|
||||||
|
* displays an error
|
||||||
|
*/
|
||||||
|
void OnConnection();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Host();
|
||||||
|
|
||||||
|
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
||||||
|
QStandardItemModel* game_list;
|
||||||
|
ComboBoxProxyModel* proxy;
|
||||||
|
std::unique_ptr<Ui::HostRoom> ui;
|
||||||
|
Validation validation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy Model for the game list combo box so we can reuse the game list model while still
|
||||||
|
* displaying the fields slightly differently
|
||||||
|
*/
|
||||||
|
class ComboBoxProxyModel : public QSortFilterProxyModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
int columnCount(const QModelIndex& idx) const override {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex& idx, int role) const override;
|
||||||
|
|
||||||
|
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
|
||||||
|
};
|
179
src/citra_qt/multiplayer/host_room.ui
Normal file
179
src/citra_qt/multiplayer/host_room.ui
Normal file
|
@ -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>
|
305
src/citra_qt/multiplayer/lobby.cpp
Normal file
305
src/citra_qt/multiplayer/lobby.cpp
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QList>
|
||||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include "citra_qt/game_list_p.h"
|
||||||
|
#include "citra_qt/main.h"
|
||||||
|
#include "citra_qt/multiplayer/client_room.h"
|
||||||
|
#include "citra_qt/multiplayer/lobby.h"
|
||||||
|
#include "citra_qt/multiplayer/lobby_p.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "citra_qt/multiplayer/state.h"
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "network/network.h"
|
||||||
|
|
||||||
|
Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
|
||||||
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session)
|
||||||
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
||||||
|
ui(std::make_unique<Ui::Lobby>()), announce_multiplayer_session(session), game_list(list) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
// setup the watcher for background connections
|
||||||
|
watcher = new QFutureWatcher<void>;
|
||||||
|
connect(watcher, &QFutureWatcher<void>::finished, [&] { joining = false; });
|
||||||
|
|
||||||
|
model = new QStandardItemModel(ui->room_list);
|
||||||
|
proxy = new LobbyFilterProxyModel(this, game_list);
|
||||||
|
proxy->setSourceModel(model);
|
||||||
|
proxy->setDynamicSortFilter(true);
|
||||||
|
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
proxy->setSortLocaleAware(true);
|
||||||
|
ui->room_list->setModel(proxy);
|
||||||
|
ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive);
|
||||||
|
ui->room_list->header()->stretchLastSection();
|
||||||
|
ui->room_list->setAlternatingRowColors(true);
|
||||||
|
ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
|
||||||
|
ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
|
||||||
|
ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
|
||||||
|
ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
|
||||||
|
ui->room_list->setSortingEnabled(true);
|
||||||
|
ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
|
||||||
|
ui->room_list->setExpandsOnDoubleClick(false);
|
||||||
|
ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
|
||||||
|
ui->nickname->setValidator(validation.GetNickname());
|
||||||
|
ui->nickname->setText(UISettings::values.nickname);
|
||||||
|
|
||||||
|
// UI Buttons
|
||||||
|
MultiplayerState* p = reinterpret_cast<MultiplayerState*>(parent);
|
||||||
|
connect(ui->refresh_list, &QPushButton::pressed, this, &Lobby::RefreshLobby);
|
||||||
|
connect(ui->games_owned, &QCheckBox::stateChanged, proxy,
|
||||||
|
&LobbyFilterProxyModel::SetFilterOwned);
|
||||||
|
connect(ui->hide_full, &QCheckBox::stateChanged, proxy, &LobbyFilterProxyModel::SetFilterFull);
|
||||||
|
connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
|
||||||
|
connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
|
||||||
|
connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
connect(this, &Lobby::LobbyRefreshed, this, &Lobby::OnRefreshLobby);
|
||||||
|
|
||||||
|
// manually start a refresh when the window is opening
|
||||||
|
// TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
|
||||||
|
// part of the constructor, but offload the refresh until after the window shown. perhaps emit a
|
||||||
|
// refreshroomlist signal from places that open the lobby
|
||||||
|
RefreshLobby();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Lobby::PasswordPrompt() {
|
||||||
|
bool ok;
|
||||||
|
const QString text =
|
||||||
|
QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
|
||||||
|
QLineEdit::Normal, tr("Password"), &ok);
|
||||||
|
return ok ? text : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::OnExpandRoom(const QModelIndex& index) {
|
||||||
|
QModelIndex member_index = proxy->index(index.row(), Column::MEMBER);
|
||||||
|
auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::OnJoinRoom(const QModelIndex& source) {
|
||||||
|
if (joining) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
joining = true;
|
||||||
|
QModelIndex index = source;
|
||||||
|
// If the user double clicks on a child row (aka the player list) then use the parent instead
|
||||||
|
if (source.parent() != QModelIndex()) {
|
||||||
|
index = source.parent();
|
||||||
|
}
|
||||||
|
if (!ui->nickname->hasAcceptableInput()) {
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto member = Network::GetRoomMember().lock()) {
|
||||||
|
if (member->IsConnected()) {
|
||||||
|
if (!NetworkMessage::WarnDisconnect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a password to pass if the room is password protected
|
||||||
|
QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME);
|
||||||
|
bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool();
|
||||||
|
const std::string password = has_password ? PasswordPrompt().toStdString() : "";
|
||||||
|
if (has_password && password.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to connect in a different thread
|
||||||
|
QFuture<void> f = QtConcurrent::run([&, password] {
|
||||||
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
||||||
|
QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
|
||||||
|
const std::string nickname = ui->nickname->text().toStdString();
|
||||||
|
const std::string ip =
|
||||||
|
proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
|
||||||
|
int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
|
||||||
|
room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredMac, password);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watcher->setFuture(f);
|
||||||
|
// and disable widgets and display a connecting while we wait
|
||||||
|
QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
UISettings::values.nickname = ui->nickname->text();
|
||||||
|
UISettings::values.ip = proxy->data(connection_index, LobbyItemHost::HostIPRole).toString();
|
||||||
|
UISettings::values.port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toString();
|
||||||
|
Settings::Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::ResetModel() {
|
||||||
|
model->clear();
|
||||||
|
model->insertColumns(0, Column::TOTAL);
|
||||||
|
model->setHeaderData(Column::EXPAND, Qt::Horizontal, "", Qt::DisplayRole);
|
||||||
|
model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
|
||||||
|
model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
|
||||||
|
model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
|
||||||
|
model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::RefreshLobby() {
|
||||||
|
if (auto session = announce_multiplayer_session.lock()) {
|
||||||
|
ResetModel();
|
||||||
|
room_list_future = session->GetRoomList([&]() { emit LobbyRefreshed(); });
|
||||||
|
ui->refresh_list->setEnabled(false);
|
||||||
|
ui->refresh_list->setText(tr("Refreshing"));
|
||||||
|
} else {
|
||||||
|
// TODO(jroweboy): Display an error box about announce couldn't be started
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobby::OnRefreshLobby() {
|
||||||
|
AnnounceMultiplayerRoom::RoomList new_room_list = room_list_future.get();
|
||||||
|
for (auto room : new_room_list) {
|
||||||
|
// find the icon for the game if this person owns that game.
|
||||||
|
QPixmap smdh_icon;
|
||||||
|
for (int r = 0; r < game_list->rowCount(); ++r) {
|
||||||
|
auto index = game_list->index(r, 0);
|
||||||
|
auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
|
||||||
|
if (game_id != 0 && room.preferred_game_id == game_id) {
|
||||||
|
smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QVariant> members;
|
||||||
|
for (auto member : room.members) {
|
||||||
|
QVariant var;
|
||||||
|
var.setValue(LobbyMember{QString::fromStdString(member.name), member.game_id,
|
||||||
|
QString::fromStdString(member.game_name)});
|
||||||
|
members.append(var);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto first_item = new LobbyItem();
|
||||||
|
auto row = QList<QStandardItem*>({
|
||||||
|
first_item,
|
||||||
|
new LobbyItemName(room.has_password, QString::fromStdString(room.name)),
|
||||||
|
new LobbyItemGame(room.preferred_game_id, QString::fromStdString(room.preferred_game),
|
||||||
|
smdh_icon),
|
||||||
|
new LobbyItemHost(QString::fromStdString(room.owner), QString::fromStdString(room.ip),
|
||||||
|
room.port),
|
||||||
|
new LobbyItemMemberList(members, room.max_player),
|
||||||
|
});
|
||||||
|
model->appendRow(row);
|
||||||
|
// To make the rows expandable, add the member data as a child of the first column of the
|
||||||
|
// rows with people in them and have qt set them to colspan after the model is finished
|
||||||
|
// resetting
|
||||||
|
if (room.members.size() > 0) {
|
||||||
|
first_item->appendRow(new LobbyItemExpandedMemberList(members));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reenable the refresh button and resize the columns
|
||||||
|
ui->refresh_list->setEnabled(true);
|
||||||
|
ui->refresh_list->setText(tr("Refresh List"));
|
||||||
|
ui->room_list->header()->stretchLastSection();
|
||||||
|
for (int i = 0; i < Column::TOTAL - 1; ++i) {
|
||||||
|
ui->room_list->resizeColumnToContents(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the member list child items to span all columns
|
||||||
|
for (int i = 0; i < proxy->rowCount(); i++) {
|
||||||
|
auto parent = model->item(i, 0);
|
||||||
|
if (parent->hasChildren()) {
|
||||||
|
ui->room_list->setFirstColumnSpanned(0, proxy->index(i, 0), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
|
||||||
|
: QSortFilterProxyModel(parent), game_list(list) {}
|
||||||
|
|
||||||
|
bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
|
||||||
|
// Prioritize filters by fastest to compute
|
||||||
|
|
||||||
|
// pass over any child rows (aka row that shows the players in the room)
|
||||||
|
if (sourceParent != QModelIndex()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by filled rooms
|
||||||
|
if (filter_full) {
|
||||||
|
QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
|
||||||
|
int player_count =
|
||||||
|
sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
|
||||||
|
int max_players =
|
||||||
|
sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
|
||||||
|
if (player_count >= max_players) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by search parameters
|
||||||
|
if (!filter_search.isEmpty()) {
|
||||||
|
QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
|
||||||
|
QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
|
||||||
|
QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
|
||||||
|
bool preferred_game_match = sourceModel()
|
||||||
|
->data(game_name, LobbyItemGame::GameNameRole)
|
||||||
|
.toString()
|
||||||
|
.contains(filter_search, filterCaseSensitivity());
|
||||||
|
bool room_name_match = sourceModel()
|
||||||
|
->data(room_name, LobbyItemName::NameRole)
|
||||||
|
.toString()
|
||||||
|
.contains(filter_search, filterCaseSensitivity());
|
||||||
|
bool username_match = sourceModel()
|
||||||
|
->data(host_name, LobbyItemHost::HostUsernameRole)
|
||||||
|
.toString()
|
||||||
|
.contains(filter_search, filterCaseSensitivity());
|
||||||
|
if (!preferred_game_match && !room_name_match && !username_match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by game owned
|
||||||
|
if (filter_owned) {
|
||||||
|
QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
|
||||||
|
QList<QModelIndex> owned_games;
|
||||||
|
for (int r = 0; r < game_list->rowCount(); ++r) {
|
||||||
|
owned_games.append(QModelIndex(game_list->index(r, 0)));
|
||||||
|
}
|
||||||
|
auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
|
||||||
|
if (current_id == 0) {
|
||||||
|
// TODO(jroweboy): homebrew often doesn't have a game id and this hides them
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool owned = false;
|
||||||
|
for (const auto& game : owned_games) {
|
||||||
|
auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
|
||||||
|
if (current_id == game_id) {
|
||||||
|
owned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!owned) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
|
||||||
|
sourceModel()->sort(column, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
|
||||||
|
filter_owned = filter;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LobbyFilterProxyModel::SetFilterFull(bool filter) {
|
||||||
|
filter_full = filter;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
|
||||||
|
filter_search = filter;
|
||||||
|
invalidate();
|
||||||
|
}
|
116
src/citra_qt/multiplayer/lobby.h
Normal file
116
src/citra_qt/multiplayer/lobby.h
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include "citra_qt/multiplayer/validation.h"
|
||||||
|
#include "common/announce_multiplayer_room.h"
|
||||||
|
#include "core/announce_multiplayer_session.h"
|
||||||
|
#include "network/network.h"
|
||||||
|
#include "ui_lobby.h"
|
||||||
|
|
||||||
|
class LobbyModel;
|
||||||
|
class LobbyFilterProxyModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listing of all public games pulled from services. The lobby should be simple enough for users to
|
||||||
|
* find the game they want to play, and join it.
|
||||||
|
*/
|
||||||
|
class Lobby : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Lobby(QWidget* parent, QStandardItemModel* list,
|
||||||
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session);
|
||||||
|
~Lobby() = default;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/**
|
||||||
|
* Begin the process to pull the latest room list from web services. After the listing is
|
||||||
|
* returned from web services, `LobbyRefreshed` will be signalled
|
||||||
|
*/
|
||||||
|
void RefreshLobby();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
/**
|
||||||
|
* Pulls the list of rooms from network and fills out the lobby model with the results
|
||||||
|
*/
|
||||||
|
void OnRefreshLobby();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for single clicking on a room in the list. Expands the treeitem to show player
|
||||||
|
* information for the people in the room
|
||||||
|
*
|
||||||
|
* index - The row of the proxy model that the user wants to join.
|
||||||
|
*/
|
||||||
|
void OnExpandRoom(const QModelIndex&);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for double clicking on a room in the list. Gathers the host ip and port and attempts
|
||||||
|
* to connect. Will also prompt for a password in case one is required.
|
||||||
|
*
|
||||||
|
* index - The row of the proxy model that the user wants to join.
|
||||||
|
*/
|
||||||
|
void OnJoinRoom(const QModelIndex&);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* Signalled when the latest lobby data is retrieved.
|
||||||
|
*/
|
||||||
|
void LobbyRefreshed();
|
||||||
|
|
||||||
|
void StateChanged(const Network::RoomMember::State&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Removes all entries in the Lobby before refreshing.
|
||||||
|
*/
|
||||||
|
void ResetModel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts for a password. Returns an empty QString if the user either did not provide a
|
||||||
|
* password or if the user closed the window.
|
||||||
|
*/
|
||||||
|
QString PasswordPrompt();
|
||||||
|
|
||||||
|
QStandardItemModel* model;
|
||||||
|
QStandardItemModel* game_list;
|
||||||
|
LobbyFilterProxyModel* proxy;
|
||||||
|
|
||||||
|
std::future<AnnounceMultiplayerRoom::RoomList> room_list_future;
|
||||||
|
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
||||||
|
std::unique_ptr<Ui::Lobby> ui;
|
||||||
|
QFutureWatcher<void>* watcher;
|
||||||
|
Validation validation;
|
||||||
|
bool joining = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy Model for filtering the lobby
|
||||||
|
*/
|
||||||
|
class LobbyFilterProxyModel : public QSortFilterProxyModel {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list);
|
||||||
|
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
|
||||||
|
void sort(int column, Qt::SortOrder order) override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void SetFilterOwned(bool);
|
||||||
|
void SetFilterFull(bool);
|
||||||
|
void SetFilterSearch(const QString&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QStandardItemModel* game_list;
|
||||||
|
bool filter_owned = false;
|
||||||
|
bool filter_full = false;
|
||||||
|
QString filter_search;
|
||||||
|
};
|
123
src/citra_qt/multiplayer/lobby.ui
Normal file
123
src/citra_qt/multiplayer/lobby.ui
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Lobby</class>
|
||||||
|
<widget class="QWidget" name="Lobby">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>903</width>
|
||||||
|
<height>487</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Public Room Browser</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>3</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Nickname</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="nickname">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Nickname</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Filters</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="search">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Search</string>
|
||||||
|
</property>
|
||||||
|
<property name="clearButtonEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="games_owned">
|
||||||
|
<property name="text">
|
||||||
|
<string>Games I Own</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="hide_full">
|
||||||
|
<property name="text">
|
||||||
|
<string>Hide Full Games</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="refresh_list">
|
||||||
|
<property name="text">
|
||||||
|
<string>Refresh Lobby</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeView" name="room_list"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="widget" native="true"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
207
src/citra_qt/multiplayer/lobby_p.h
Normal file
207
src/citra_qt/multiplayer/lobby_p.h
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QStandardItem>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Column {
|
||||||
|
enum List {
|
||||||
|
EXPAND,
|
||||||
|
ROOM_NAME,
|
||||||
|
GAME_NAME,
|
||||||
|
HOST,
|
||||||
|
MEMBER,
|
||||||
|
TOTAL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class LobbyItem : public QStandardItem {
|
||||||
|
public:
|
||||||
|
LobbyItem() = default;
|
||||||
|
explicit LobbyItem(const QString& string) : QStandardItem(string) {}
|
||||||
|
virtual ~LobbyItem() override = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LobbyItemName : public LobbyItem {
|
||||||
|
public:
|
||||||
|
static const int NameRole = Qt::UserRole + 1;
|
||||||
|
static const int PasswordRole = Qt::UserRole + 2;
|
||||||
|
|
||||||
|
LobbyItemName() = default;
|
||||||
|
explicit LobbyItemName(bool has_password, QString name) : LobbyItem() {
|
||||||
|
setData(name, NameRole);
|
||||||
|
setData(has_password, PasswordRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(int role) const override {
|
||||||
|
if (role == Qt::DecorationRole) {
|
||||||
|
bool has_password = data(PasswordRole).toBool();
|
||||||
|
return has_password ? QIcon::fromTheme("lock").pixmap(16) : QIcon();
|
||||||
|
}
|
||||||
|
if (role != Qt::DisplayRole) {
|
||||||
|
return LobbyItem::data(role);
|
||||||
|
}
|
||||||
|
return data(NameRole).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const QStandardItem& other) const override {
|
||||||
|
return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LobbyItemGame : public LobbyItem {
|
||||||
|
public:
|
||||||
|
static const int TitleIDRole = Qt::UserRole + 1;
|
||||||
|
static const int GameNameRole = Qt::UserRole + 2;
|
||||||
|
static const int GameIconRole = Qt::UserRole + 3;
|
||||||
|
|
||||||
|
LobbyItemGame() = default;
|
||||||
|
explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) {
|
||||||
|
setData(static_cast<unsigned long long>(title_id), TitleIDRole);
|
||||||
|
setData(game_name, GameNameRole);
|
||||||
|
if (!smdh_icon.isNull()) {
|
||||||
|
setData(smdh_icon, GameIconRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(int role) const override {
|
||||||
|
if (role == Qt::DecorationRole) {
|
||||||
|
auto val = data(GameIconRole);
|
||||||
|
if (val.isValid()) {
|
||||||
|
val = val.value<QPixmap>().scaled(16, 16, Qt::KeepAspectRatio);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
} else if (role != Qt::DisplayRole) {
|
||||||
|
return LobbyItem::data(role);
|
||||||
|
}
|
||||||
|
return data(GameNameRole).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const QStandardItem& other) const override {
|
||||||
|
return data(GameNameRole)
|
||||||
|
.toString()
|
||||||
|
.localeAwareCompare(other.data(GameNameRole).toString()) < 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LobbyItemHost : public LobbyItem {
|
||||||
|
public:
|
||||||
|
static const int HostUsernameRole = Qt::UserRole + 1;
|
||||||
|
static const int HostIPRole = Qt::UserRole + 2;
|
||||||
|
static const int HostPortRole = Qt::UserRole + 3;
|
||||||
|
|
||||||
|
LobbyItemHost() = default;
|
||||||
|
explicit LobbyItemHost(QString username, QString ip, u16 port) {
|
||||||
|
setData(username, HostUsernameRole);
|
||||||
|
setData(ip, HostIPRole);
|
||||||
|
setData(port, HostPortRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(int role) const override {
|
||||||
|
if (role != Qt::DisplayRole) {
|
||||||
|
return LobbyItem::data(role);
|
||||||
|
}
|
||||||
|
return data(HostUsernameRole).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const QStandardItem& other) const override {
|
||||||
|
return data(HostUsernameRole)
|
||||||
|
.toString()
|
||||||
|
.localeAwareCompare(other.data(HostUsernameRole).toString()) < 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class LobbyMember {
|
||||||
|
public:
|
||||||
|
LobbyMember() = default;
|
||||||
|
LobbyMember(const LobbyMember& other) = default;
|
||||||
|
explicit LobbyMember(QString username, u64 title_id, QString game_name)
|
||||||
|
: username(std::move(username)), title_id(title_id), game_name(std::move(game_name)) {}
|
||||||
|
~LobbyMember() = default;
|
||||||
|
|
||||||
|
QString GetUsername() const {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
u64 GetTitleId() const {
|
||||||
|
return title_id;
|
||||||
|
}
|
||||||
|
QString GetGameName() const {
|
||||||
|
return game_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString username;
|
||||||
|
u64 title_id;
|
||||||
|
QString game_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(LobbyMember);
|
||||||
|
|
||||||
|
class LobbyItemMemberList : public LobbyItem {
|
||||||
|
public:
|
||||||
|
static const int MemberListRole = Qt::UserRole + 1;
|
||||||
|
static const int MaxPlayerRole = Qt::UserRole + 2;
|
||||||
|
|
||||||
|
LobbyItemMemberList() = default;
|
||||||
|
explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) {
|
||||||
|
setData(members, MemberListRole);
|
||||||
|
setData(max_players, MaxPlayerRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(int role) const override {
|
||||||
|
if (role != Qt::DisplayRole) {
|
||||||
|
return LobbyItem::data(role);
|
||||||
|
}
|
||||||
|
auto members = data(MemberListRole).toList();
|
||||||
|
return QString("%1 / %2").arg(QString::number(members.size()),
|
||||||
|
data(MaxPlayerRole).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const QStandardItem& other) const override {
|
||||||
|
// sort by rooms that have the most players
|
||||||
|
int left_members = data(MemberListRole).toList().size();
|
||||||
|
int right_members = other.data(MemberListRole).toList().size();
|
||||||
|
return left_members < right_members;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member information for when a lobby is expanded in the UI
|
||||||
|
*/
|
||||||
|
class LobbyItemExpandedMemberList : public LobbyItem {
|
||||||
|
public:
|
||||||
|
static const int MemberListRole = Qt::UserRole + 1;
|
||||||
|
|
||||||
|
LobbyItemExpandedMemberList() = default;
|
||||||
|
explicit LobbyItemExpandedMemberList(QList<QVariant> members) {
|
||||||
|
setData(members, MemberListRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant data(int role) const override {
|
||||||
|
if (role != Qt::DisplayRole) {
|
||||||
|
return LobbyItem::data(role);
|
||||||
|
}
|
||||||
|
auto members = data(MemberListRole).toList();
|
||||||
|
QString out;
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& member : members) {
|
||||||
|
if (!first)
|
||||||
|
out += '\n';
|
||||||
|
const auto& m = member.value<LobbyMember>();
|
||||||
|
if (m.GetGameName().isEmpty()) {
|
||||||
|
out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetUsername());
|
||||||
|
} else {
|
||||||
|
out +=
|
||||||
|
QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName());
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
62
src/citra_qt/multiplayer/message.cpp
Normal file
62
src/citra_qt/multiplayer/message.cpp
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
|
||||||
|
namespace NetworkMessage {
|
||||||
|
const ConnectionError USERNAME_NOT_VALID(
|
||||||
|
QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters."));
|
||||||
|
const ConnectionError ROOMNAME_NOT_VALID(
|
||||||
|
QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters."));
|
||||||
|
const ConnectionError USERNAME_IN_USE(
|
||||||
|
QT_TR_NOOP("Username is already in use. Please choose another."));
|
||||||
|
const ConnectionError IP_ADDRESS_NOT_VALID(QT_TR_NOOP("IP is not a valid IPv4 address."));
|
||||||
|
const ConnectionError PORT_NOT_VALID(QT_TR_NOOP("Port must be a number between 0 to 65535."));
|
||||||
|
const ConnectionError NO_INTERNET(
|
||||||
|
QT_TR_NOOP("Unable to find an internet connection. Check your internet settings."));
|
||||||
|
const ConnectionError UNABLE_TO_CONNECT(
|
||||||
|
QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If "
|
||||||
|
"you still cannot connect, contact the room host and verify that the host is "
|
||||||
|
"properly configured with the external port forwarded."));
|
||||||
|
const ConnectionError COULD_NOT_CREATE_ROOM(
|
||||||
|
QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary."));
|
||||||
|
const ConnectionError HOST_BANNED(
|
||||||
|
QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you "
|
||||||
|
"or try a different room."));
|
||||||
|
const ConnectionError WRONG_VERSION(
|
||||||
|
QT_TR_NOOP("Version mismatch! Please update to the latest version of Citra. If the problem "
|
||||||
|
"persists, contact the room host and ask them to update the server."));
|
||||||
|
const ConnectionError WRONG_PASSWORD(QT_TR_NOOP("Incorrect password."));
|
||||||
|
const ConnectionError GENERIC_ERROR(
|
||||||
|
QT_TR_NOOP("An unknown error occured. If this error continues to occur, please open an issue"));
|
||||||
|
const ConnectionError LOST_CONNECTION(QT_TR_NOOP("Connection to room lost. Try to reconnect."));
|
||||||
|
const ConnectionError MAC_COLLISION(
|
||||||
|
QT_TR_NOOP("MAC address is already in use. Please choose another."));
|
||||||
|
|
||||||
|
static bool WarnMessage(const std::string& title, const std::string& text) {
|
||||||
|
return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
|
||||||
|
QObject::tr(text.c_str()),
|
||||||
|
QMessageBox::Ok | QMessageBox::Cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowError(const ConnectionError& e) {
|
||||||
|
QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromStdString(e.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WarnCloseRoom() {
|
||||||
|
return WarnMessage(
|
||||||
|
QT_TR_NOOP("Leave Room"),
|
||||||
|
QT_TR_NOOP("You are about to close the room. Any network connections will be closed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WarnDisconnect() {
|
||||||
|
return WarnMessage(
|
||||||
|
QT_TR_NOOP("Disconnect"),
|
||||||
|
QT_TR_NOOP("You are about to leave the room. Any network connections will be closed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace NetworkMessage
|
55
src/citra_qt/multiplayer/message.h
Normal file
55
src/citra_qt/multiplayer/message.h
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace NetworkMessage {
|
||||||
|
|
||||||
|
class ConnectionError {
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ConnectionError(std::string str) : err(std::move(str)) {}
|
||||||
|
const std::string& GetString() const {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string err;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const ConnectionError USERNAME_NOT_VALID;
|
||||||
|
extern const ConnectionError ROOMNAME_NOT_VALID;
|
||||||
|
extern const ConnectionError USERNAME_IN_USE;
|
||||||
|
extern const ConnectionError IP_ADDRESS_NOT_VALID;
|
||||||
|
extern const ConnectionError PORT_NOT_VALID;
|
||||||
|
extern const ConnectionError NO_INTERNET;
|
||||||
|
extern const ConnectionError UNABLE_TO_CONNECT;
|
||||||
|
extern const ConnectionError COULD_NOT_CREATE_ROOM;
|
||||||
|
extern const ConnectionError HOST_BANNED;
|
||||||
|
extern const ConnectionError WRONG_VERSION;
|
||||||
|
extern const ConnectionError WRONG_PASSWORD;
|
||||||
|
extern const ConnectionError GENERIC_ERROR;
|
||||||
|
extern const ConnectionError LOST_CONNECTION;
|
||||||
|
extern const ConnectionError MAC_COLLISION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a standard QMessageBox with a error message
|
||||||
|
*/
|
||||||
|
void ShowError(const ConnectionError& e);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a standard QMessageBox with a warning message about leaving the room
|
||||||
|
* return true if the user wants to close the network connection
|
||||||
|
*/
|
||||||
|
bool WarnCloseRoom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a standard QMessageBox with a warning message about disconnecting from the room
|
||||||
|
* return true if the user wants to disconnect
|
||||||
|
*/
|
||||||
|
bool WarnDisconnect();
|
||||||
|
|
||||||
|
} // namespace NetworkMessage
|
182
src/citra_qt/multiplayer/state.cpp
Normal file
182
src/citra_qt/multiplayer/state.cpp
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include "citra_qt/game_list.h"
|
||||||
|
#include "citra_qt/multiplayer/client_room.h"
|
||||||
|
#include "citra_qt/multiplayer/direct_connect.h"
|
||||||
|
#include "citra_qt/multiplayer/host_room.h"
|
||||||
|
#include "citra_qt/multiplayer/lobby.h"
|
||||||
|
#include "citra_qt/multiplayer/message.h"
|
||||||
|
#include "citra_qt/multiplayer/state.h"
|
||||||
|
#include "citra_qt/util/clickable_label.h"
|
||||||
|
#include "common/announce_multiplayer_room.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model,
|
||||||
|
QAction* leave_room, QAction* show_room)
|
||||||
|
: QWidget(parent), game_list_model(game_list_model), leave_room(leave_room),
|
||||||
|
show_room(show_room) {
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
// register the network structs to use in slots and signals
|
||||||
|
state_callback_handle = member->BindOnStateChanged(
|
||||||
|
[this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
|
||||||
|
connect(this, &MultiplayerState::NetworkStateChanged, this,
|
||||||
|
&MultiplayerState::OnNetworkStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
qRegisterMetaType<Network::RoomMember::State>();
|
||||||
|
qRegisterMetaType<Common::WebResult>();
|
||||||
|
announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
|
||||||
|
announce_multiplayer_session->BindErrorCallback(
|
||||||
|
[this](const Common::WebResult& result) { emit AnnounceFailed(result); });
|
||||||
|
connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed);
|
||||||
|
|
||||||
|
status_text = new ClickableLabel(this);
|
||||||
|
status_icon = new ClickableLabel(this);
|
||||||
|
status_text->setToolTip(tr("Current connection status"));
|
||||||
|
status_text->setText(tr("Not Connected. Click here to find a room!"));
|
||||||
|
status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
|
||||||
|
|
||||||
|
connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
|
||||||
|
connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiplayerState::~MultiplayerState() {
|
||||||
|
if (state_callback_handle) {
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
member->Unbind(state_callback_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::Close() {
|
||||||
|
if (host_room)
|
||||||
|
host_room->close();
|
||||||
|
if (direct_connect)
|
||||||
|
direct_connect->close();
|
||||||
|
if (client_room)
|
||||||
|
client_room->close();
|
||||||
|
if (lobby)
|
||||||
|
lobby->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
|
||||||
|
NGLOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
|
||||||
|
bool is_connected = false;
|
||||||
|
switch (state) {
|
||||||
|
case Network::RoomMember::State::LostConnection:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::CouldNotConnect:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::NameCollision:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::USERNAME_IN_USE);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::MacCollision:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::WrongPassword:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::WrongVersion:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::Error:
|
||||||
|
NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT);
|
||||||
|
break;
|
||||||
|
case Network::RoomMember::State::Joined:
|
||||||
|
is_connected = true;
|
||||||
|
OnOpenNetworkRoom();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (is_connected) {
|
||||||
|
status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16));
|
||||||
|
status_text->setText(tr("Connected"));
|
||||||
|
leave_room->setEnabled(true);
|
||||||
|
show_room->setEnabled(true);
|
||||||
|
} else {
|
||||||
|
status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
|
||||||
|
status_text->setText(tr("Not Connected"));
|
||||||
|
leave_room->setEnabled(false);
|
||||||
|
show_room->setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) {
|
||||||
|
announce_multiplayer_session->Stop();
|
||||||
|
QMessageBox::warning(
|
||||||
|
this, tr("Error"),
|
||||||
|
tr("Failed to announce the room to the public lobby. In order to host a room publicly, you "
|
||||||
|
"must have a valid Citra account configured in Emulation -> Configure -> Web. If you do "
|
||||||
|
"not want to publish a room in the public lobby, then select Unlisted instead.\n"
|
||||||
|
"Debug Message: ") +
|
||||||
|
QString::fromStdString(result.result_string),
|
||||||
|
QMessageBox::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BringWidgetToFront(QWidget* widget) {
|
||||||
|
widget->show();
|
||||||
|
widget->activateWindow();
|
||||||
|
widget->raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnViewLobby() {
|
||||||
|
if (lobby == nullptr) {
|
||||||
|
lobby = new Lobby(this, game_list_model, announce_multiplayer_session);
|
||||||
|
}
|
||||||
|
BringWidgetToFront(lobby);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnCreateRoom() {
|
||||||
|
if (host_room == nullptr) {
|
||||||
|
host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session);
|
||||||
|
}
|
||||||
|
BringWidgetToFront(host_room);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MultiplayerState::OnCloseRoom() {
|
||||||
|
if (!NetworkMessage::WarnCloseRoom())
|
||||||
|
return false;
|
||||||
|
if (auto room = Network::GetRoom().lock()) {
|
||||||
|
// if you are in a room, leave it
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
member->Leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if you are hosting a room, also stop hosting
|
||||||
|
if (room->GetState() != Network::Room::State::Open) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
room->Destroy();
|
||||||
|
announce_multiplayer_session->Stop();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnOpenNetworkRoom() {
|
||||||
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
|
if (member->IsConnected()) {
|
||||||
|
if (client_room == nullptr) {
|
||||||
|
client_room = new ClientRoomWindow(this);
|
||||||
|
}
|
||||||
|
BringWidgetToFront(client_room);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the user is not a member of a room, show the lobby instead.
|
||||||
|
// This is currently only used on the clickable label in the status bar
|
||||||
|
OnViewLobby();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerState::OnDirectConnectToRoom() {
|
||||||
|
if (direct_connect == nullptr) {
|
||||||
|
direct_connect = new DirectConnectWindow(this);
|
||||||
|
}
|
||||||
|
BringWidgetToFront(direct_connect);
|
||||||
|
}
|
68
src/citra_qt/multiplayer/state.h
Normal file
68
src/citra_qt/multiplayer/state.h
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include "network/network.h"
|
||||||
|
|
||||||
|
class QStandardItemModel;
|
||||||
|
class Lobby;
|
||||||
|
class HostRoomWindow;
|
||||||
|
class ClientRoomWindow;
|
||||||
|
class DirectConnectWindow;
|
||||||
|
class ClickableLabel;
|
||||||
|
namespace Core {
|
||||||
|
class AnnounceMultiplayerSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiplayerState : public QWidget {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
|
||||||
|
QAction* show_room);
|
||||||
|
~MultiplayerState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all open multiplayer related dialogs
|
||||||
|
*/
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
ClickableLabel* GetStatusText() const {
|
||||||
|
return status_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableLabel* GetStatusIcon() const {
|
||||||
|
return status_icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void OnNetworkStateChanged(const Network::RoomMember::State& state);
|
||||||
|
void OnViewLobby();
|
||||||
|
void OnCreateRoom();
|
||||||
|
bool OnCloseRoom();
|
||||||
|
void OnOpenNetworkRoom();
|
||||||
|
void OnDirectConnectToRoom();
|
||||||
|
void OnAnnounceFailed(const Common::WebResult&);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void NetworkStateChanged(const Network::RoomMember::State&);
|
||||||
|
void AnnounceFailed(const Common::WebResult&);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Lobby* lobby = nullptr;
|
||||||
|
HostRoomWindow* host_room = nullptr;
|
||||||
|
ClientRoomWindow* client_room = nullptr;
|
||||||
|
DirectConnectWindow* direct_connect = nullptr;
|
||||||
|
ClickableLabel* status_icon = nullptr;
|
||||||
|
ClickableLabel* status_text = nullptr;
|
||||||
|
QStandardItemModel* game_list_model = nullptr;
|
||||||
|
QAction* leave_room;
|
||||||
|
QAction* show_room;
|
||||||
|
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
||||||
|
Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(Common::WebResult);
|
48
src/citra_qt/multiplayer/validation.h
Normal file
48
src/citra_qt/multiplayer/validation.h
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QRegExp>
|
||||||
|
#include <QValidator>
|
||||||
|
|
||||||
|
class Validation {
|
||||||
|
public:
|
||||||
|
Validation()
|
||||||
|
: room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {}
|
||||||
|
|
||||||
|
~Validation() = default;
|
||||||
|
|
||||||
|
const QValidator* GetRoomName() const {
|
||||||
|
return &room_name;
|
||||||
|
}
|
||||||
|
const QValidator* GetNickname() const {
|
||||||
|
return &nickname;
|
||||||
|
}
|
||||||
|
const QValidator* GetIP() const {
|
||||||
|
return &ip;
|
||||||
|
}
|
||||||
|
const QValidator* GetPort() const {
|
||||||
|
return &port;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// room name can be alphanumeric and " " "_" "." and "-"
|
||||||
|
QRegExp room_name_regex = QRegExp("^[a-zA-Z0-9._- ]+$");
|
||||||
|
QRegExpValidator room_name;
|
||||||
|
|
||||||
|
/// nickname can be alphanumeric and " " "_" "." and "-"
|
||||||
|
QRegExp nickname_regex = QRegExp("^[a-zA-Z0-9._- ]+$");
|
||||||
|
QRegExpValidator nickname;
|
||||||
|
|
||||||
|
/// ipv4 address only
|
||||||
|
// TODO remove this when we support hostnames in direct connect
|
||||||
|
QRegExp ip_regex = QRegExp(
|
||||||
|
"(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|"
|
||||||
|
"2[0-4][0-9]|25[0-5])");
|
||||||
|
QRegExpValidator ip;
|
||||||
|
|
||||||
|
/// port must be between 0 and 65535
|
||||||
|
QIntValidator port;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
uint host_type;
|
||||||
|
qulonglong game_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
|
|
||||||
} // namespace UISettings
|
} // namespace UISettings
|
||||||
|
|
11
src/citra_qt/util/clickable_label.cpp
Normal file
11
src/citra_qt/util/clickable_label.cpp
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "citra_qt/util/clickable_label.h"
|
||||||
|
|
||||||
|
ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent) {}
|
||||||
|
|
||||||
|
void ClickableLabel::mouseReleaseEvent(QMouseEvent* event) {
|
||||||
|
emit clicked();
|
||||||
|
}
|
22
src/citra_qt/util/clickable_label.h
Normal file
22
src/citra_qt/util/clickable_label.h
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2017 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class ClickableLabel : public QLabel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
|
||||||
|
~ClickableLabel() = default;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void clicked();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mouseReleaseEvent(QMouseEvent* event);
|
||||||
|
};
|
|
@ -406,6 +406,8 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv
|
||||||
ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
|
ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room_member_impl->SetState(State::Joining);
|
||||||
|
|
||||||
ENetAddress address{};
|
ENetAddress address{};
|
||||||
enet_address_set_host(&address, server_addr);
|
enet_address_set_host(&address, server_addr);
|
||||||
address.port = server_port;
|
address.port = server_port;
|
||||||
|
@ -421,7 +423,6 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv
|
||||||
int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
|
int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
|
||||||
if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
|
if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
|
||||||
room_member_impl->nickname = nick;
|
room_member_impl->nickname = nick;
|
||||||
room_member_impl->SetState(State::Joining);
|
|
||||||
room_member_impl->StartLoop();
|
room_member_impl->StartLoop();
|
||||||
room_member_impl->SendJoinRequest(nick, preferred_mac, password);
|
room_member_impl->SendJoinRequest(nick, preferred_mac, password);
|
||||||
SendGameInfo(room_member_impl->current_game_info);
|
SendGameInfo(room_member_impl->current_game_info);
|
||||||
|
|
|
@ -187,4 +187,30 @@ private:
|
||||||
std::unique_ptr<RoomMemberImpl> room_member_impl;
|
std::unique_ptr<RoomMemberImpl> room_member_impl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const char* GetStateStr(const RoomMember::State& s) {
|
||||||
|
switch (s) {
|
||||||
|
case RoomMember::State::Idle:
|
||||||
|
return "Idle";
|
||||||
|
case RoomMember::State::Error:
|
||||||
|
return "Error";
|
||||||
|
case RoomMember::State::Joining:
|
||||||
|
return "Joining";
|
||||||
|
case RoomMember::State::Joined:
|
||||||
|
return "Joined";
|
||||||
|
case RoomMember::State::LostConnection:
|
||||||
|
return "LostConnection";
|
||||||
|
case RoomMember::State::NameCollision:
|
||||||
|
return "NameCollision";
|
||||||
|
case RoomMember::State::MacCollision:
|
||||||
|
return "MacCollision";
|
||||||
|
case RoomMember::State::WrongVersion:
|
||||||
|
return "WrongVersion";
|
||||||
|
case RoomMember::State::WrongPassword:
|
||||||
|
return "WrongPassword";
|
||||||
|
case RoomMember::State::CouldNotConnect:
|
||||||
|
return "CouldNotConnect";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Network
|
} // namespace Network
|
||||||
|
|
Loading…
Reference in a new issue