mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-27 09:43:51 +00:00
Some genius decided to put the entire MainWindow class into main.h and main.cpp, which is not only horrific practice but also completely destroys clangd beyond repair. Please, just don't do this. (this will probably merge conflict to hell and back) Also, fixes a bunch of issues with Ryujinx save data link: - Paths with spaces would cause mklink to fail - Add support for portable directories - Symlink detection was incorrect sometimes(????) - Some other stuff I'm forgetting Furthermore, when selecting "From Eden" and attempting to save in Ryujinx, Ryujinx would destroy the link for... some reason? So to get around this we just copy the Eden data to Ryujinx then treat it like a "From Ryujinx" op Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2929 Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
442 lines
18 KiB
C++
442 lines
18 KiB
C++
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <QInputDialog>
|
|
#include <QList>
|
|
#include <QtConcurrentRun>
|
|
#include "common/logging/log.h"
|
|
#include "common/settings.h"
|
|
#include "core/core.h"
|
|
#include "core/hle/service/acc/profile_manager.h"
|
|
#include "core/internal_network/network_interface.h"
|
|
#include "network/network.h"
|
|
#include "ui_lobby.h"
|
|
#include "yuzu/game_list_p.h"
|
|
#include "yuzu/main_window.h"
|
|
#include "yuzu/multiplayer/client_room.h"
|
|
#include "yuzu/multiplayer/lobby.h"
|
|
#include "yuzu/multiplayer/lobby_p.h"
|
|
#include "yuzu/multiplayer/message.h"
|
|
#include "yuzu/multiplayer/state.h"
|
|
#include "yuzu/multiplayer/validation.h"
|
|
#include "qt_common/config/uisettings.h"
|
|
#ifdef ENABLE_WEB_SERVICE
|
|
#include "web_service/web_backend.h"
|
|
#endif
|
|
|
|
Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
|
|
std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
|
|
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
|
|
ui(std::make_unique<Ui::Lobby>()),
|
|
announce_multiplayer_session(session), system{system_} {
|
|
ui->setupUi(this);
|
|
|
|
// setup the watcher for background connections
|
|
watcher = new QFutureWatcher<void>;
|
|
|
|
model = new QStandardItemModel(ui->room_list);
|
|
|
|
// Create a proxy to the game list to get the list of games owned
|
|
game_list = new QStandardItemModel;
|
|
UpdateGameList(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(
|
|
QString::fromStdString(UISettings::values.multiplayer_nickname.GetValue()));
|
|
|
|
// Try find the best nickname by default
|
|
if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("Eden")) {
|
|
if (!Settings::values.eden_username.GetValue().empty()) {
|
|
ui->nickname->setText(
|
|
QString::fromStdString(Settings::values.eden_username.GetValue()));
|
|
} else if (!GetProfileUsername().empty()) {
|
|
ui->nickname->setText(QString::fromStdString(GetProfileUsername()));
|
|
} else {
|
|
ui->nickname->setText(QStringLiteral("Eden"));
|
|
}
|
|
}
|
|
|
|
// UI Buttons
|
|
connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby);
|
|
connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
|
|
connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned);
|
|
connect(ui->hide_empty, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterEmpty);
|
|
connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull);
|
|
connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
|
|
connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
|
|
|
|
// Actions
|
|
connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
|
|
&Lobby::OnRefreshLobby);
|
|
|
|
// Load persistent filters after events are connected to make sure they apply
|
|
ui->search->setText(
|
|
QString::fromStdString(UISettings::values.multiplayer_filter_text.GetValue()));
|
|
ui->games_owned->setChecked(UISettings::values.multiplayer_filter_games_owned.GetValue());
|
|
ui->hide_empty->setChecked(UISettings::values.multiplayer_filter_hide_empty.GetValue());
|
|
ui->hide_full->setChecked(UISettings::values.multiplayer_filter_hide_full.GetValue());
|
|
}
|
|
|
|
Lobby::~Lobby() = default;
|
|
|
|
void Lobby::UpdateGameList(QStandardItemModel* list) {
|
|
game_list->clear();
|
|
for (int i = 0; i < list->rowCount(); i++) {
|
|
auto parent = list->item(i, 0);
|
|
for (int j = 0; j < parent->rowCount(); j++) {
|
|
game_list->appendRow(parent->child(j)->clone());
|
|
}
|
|
}
|
|
if (proxy)
|
|
proxy->UpdateGameList(game_list);
|
|
ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
|
|
}
|
|
|
|
void Lobby::RetranslateUi() {
|
|
ui->retranslateUi(this);
|
|
}
|
|
|
|
QString Lobby::PasswordPrompt() {
|
|
bool ok;
|
|
const QString text =
|
|
QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
|
|
QLineEdit::Password, QString(), &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 (!Network::GetSelectedNetworkInterface()) {
|
|
LOG_INFO(WebService, "Automatically selected network interface for room network.");
|
|
Network::SelectFirstNetworkInterface();
|
|
}
|
|
|
|
if (!Network::GetSelectedNetworkInterface()) {
|
|
NetworkMessage::ErrorManager::ShowError(
|
|
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
|
|
return;
|
|
}
|
|
|
|
if (system.IsPoweredOn()) {
|
|
if (!NetworkMessage::WarnGameRunning()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (const auto member = Network::GetRoomMember().lock()) {
|
|
// Prevent the user from trying to join a room while they are already joining.
|
|
if (member->GetState() == Network::RoomMember::State::Joining) {
|
|
return;
|
|
} else if (member->IsConnected()) {
|
|
// And ask if they want to leave the room if they are already in one.
|
|
if (!NetworkMessage::WarnDisconnect()) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
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::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
|
|
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;
|
|
}
|
|
|
|
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();
|
|
const std::string verify_uid =
|
|
proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString();
|
|
|
|
// attempt to connect in a different thread
|
|
QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid] {
|
|
std::string token;
|
|
#ifdef ENABLE_WEB_SERVICE
|
|
if (!Settings::values.eden_username.GetValue().empty() &&
|
|
!Settings::values.eden_token.GetValue().empty()) {
|
|
WebService::Client client(Settings::values.web_api_url.GetValue(),
|
|
Settings::values.eden_username.GetValue(),
|
|
Settings::values.eden_token.GetValue());
|
|
token = client.GetExternalJWT(verify_uid).returned_data;
|
|
if (token.empty()) {
|
|
LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
|
|
} else {
|
|
LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
|
|
}
|
|
}
|
|
#endif
|
|
if (auto room_member = Network::GetRoomMember().lock()) {
|
|
room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredIP, password,
|
|
token);
|
|
}
|
|
});
|
|
watcher->setFuture(f);
|
|
|
|
// TODO(jroweboy): disable widgets and display a connecting while we wait
|
|
|
|
// Save settings
|
|
UISettings::values.multiplayer_nickname = ui->nickname->text().toStdString();
|
|
UISettings::values.multiplayer_filter_text = ui->search->text().toStdString();
|
|
UISettings::values.multiplayer_filter_games_owned = ui->games_owned->isChecked();
|
|
UISettings::values.multiplayer_filter_hide_empty = ui->hide_empty->isChecked();
|
|
UISettings::values.multiplayer_filter_hide_full = ui->hide_full->isChecked();
|
|
UISettings::values.multiplayer_ip =
|
|
proxy->data(connection_index, LobbyItemHost::HostIPRole).value<QString>().toStdString();
|
|
UISettings::values.multiplayer_port =
|
|
proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
|
|
emit SaveConfig();
|
|
}
|
|
|
|
void Lobby::ResetModel() {
|
|
model->clear();
|
|
model->insertColumns(0, Column::TOTAL);
|
|
model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), 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);
|
|
}
|
|
|
|
void Lobby::RefreshLobby() {
|
|
if (auto session = announce_multiplayer_session.lock()) {
|
|
ResetModel();
|
|
ui->refresh_list->setEnabled(false);
|
|
ui->refresh_list->setText(tr("Refreshing"));
|
|
room_list_watcher.setFuture(
|
|
QtConcurrent::run([session]() { return session->GetRoomList(); }));
|
|
} else {
|
|
// TODO(jroweboy): Display an error box about announce couldn't be started
|
|
}
|
|
}
|
|
|
|
void Lobby::OnRefreshLobby() {
|
|
AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result();
|
|
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.information.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.username),
|
|
QString::fromStdString(member.nickname), member.game.id,
|
|
QString::fromStdString(member.game.name)});
|
|
members.append(var);
|
|
}
|
|
|
|
auto first_item = new LobbyItemGame(
|
|
room.information.preferred_game.id,
|
|
QString::fromStdString(room.information.preferred_game.name), smdh_icon);
|
|
auto row = QList<QStandardItem*>({
|
|
first_item,
|
|
new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)),
|
|
new LobbyItemMemberList(members, room.information.member_slots),
|
|
new LobbyItemHost(QString::fromStdString(room.information.host_username),
|
|
QString::fromStdString(room.ip), room.information.port,
|
|
QString::fromStdString(room.verify_uid)),
|
|
});
|
|
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.information.description.empty()) {
|
|
first_item->appendRow(
|
|
new LobbyItemDescription(QString::fromStdString(room.information.description)));
|
|
}
|
|
if (!room.members.empty()) {
|
|
first_item->appendRow(new LobbyItemExpandedMemberList(members));
|
|
}
|
|
}
|
|
|
|
// Re-enable 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);
|
|
for (int j = 0; j < parent->rowCount(); j++) {
|
|
ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true);
|
|
}
|
|
}
|
|
|
|
ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
|
|
}
|
|
|
|
std::string Lobby::GetProfileUsername() {
|
|
const auto& current_user =
|
|
system.GetProfileManager().GetUser(Settings::values.current_user.GetValue());
|
|
Service::Account::ProfileBase profile{};
|
|
|
|
if (!current_user.has_value()) {
|
|
return "";
|
|
}
|
|
|
|
if (!system.GetProfileManager().GetProfileBase(*current_user, profile)) {
|
|
return "";
|
|
}
|
|
|
|
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
|
reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
|
|
|
|
return text;
|
|
}
|
|
|
|
LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
|
|
: QSortFilterProxyModel(parent), game_list(list) {}
|
|
|
|
void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) {
|
|
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 empty rooms
|
|
if (filter_empty) {
|
|
QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
|
|
int player_count =
|
|
sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
|
|
if (player_count == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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::SetFilterEmpty(bool filter) {
|
|
filter_empty = filter;
|
|
invalidate();
|
|
}
|
|
|
|
void LobbyFilterProxyModel::SetFilterFull(bool filter) {
|
|
filter_full = filter;
|
|
invalidate();
|
|
}
|
|
|
|
void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
|
|
filter_search = filter;
|
|
invalidate();
|
|
}
|