lobby.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project & 2024 suyu Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <QInputDialog>
  4. #include <QList>
  5. #include <QtConcurrent/QtConcurrentRun>
  6. #include "common/logging/log.h"
  7. #include "common/settings.h"
  8. #include "core/core.h"
  9. #include "core/hle/service/acc/profile_manager.h"
  10. #include "core/internal_network/network_interface.h"
  11. #include "network/network.h"
  12. #include "suyu/game_list_p.h"
  13. #include "suyu/main.h"
  14. #include "suyu/multiplayer/client_room.h"
  15. #include "suyu/multiplayer/lobby.h"
  16. #include "suyu/multiplayer/lobby_p.h"
  17. #include "suyu/multiplayer/message.h"
  18. #include "suyu/multiplayer/state.h"
  19. #include "suyu/multiplayer/validation.h"
  20. #include "suyu/uisettings.h"
  21. #include "ui_lobby.h"
  22. #ifdef ENABLE_WEB_SERVICE
  23. #include "web_service/web_backend.h"
  24. #endif
  25. Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
  26. std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
  27. : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
  28. ui(std::make_unique<Ui::Lobby>()), announce_multiplayer_session(session), system{system_},
  29. room_network{system.GetRoomNetwork()} {
  30. ui->setupUi(this);
  31. // setup the watcher for background connections
  32. watcher = new QFutureWatcher<void>;
  33. model = new QStandardItemModel(ui->room_list);
  34. // Create a proxy to the game list to get the list of games owned
  35. game_list = new QStandardItemModel;
  36. UpdateGameList(list);
  37. proxy = new LobbyFilterProxyModel(this, game_list);
  38. proxy->setSourceModel(model);
  39. proxy->setDynamicSortFilter(true);
  40. proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
  41. proxy->setSortLocaleAware(true);
  42. ui->room_list->setModel(proxy);
  43. ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive);
  44. ui->room_list->header()->stretchLastSection();
  45. ui->room_list->setAlternatingRowColors(true);
  46. ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
  47. ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
  48. ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
  49. ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
  50. ui->room_list->setSortingEnabled(true);
  51. ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
  52. ui->room_list->setExpandsOnDoubleClick(false);
  53. ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);
  54. ui->nickname->setValidator(validation.GetNickname());
  55. ui->nickname->setText(
  56. QString::fromStdString(UISettings::values.multiplayer_nickname.GetValue()));
  57. // Try find the best nickname by default
  58. if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("suyu")) {
  59. if (!Settings::values.suyu_username.GetValue().empty()) {
  60. ui->nickname->setText(
  61. QString::fromStdString(Settings::values.suyu_username.GetValue()));
  62. } else if (!GetProfileUsername().empty()) {
  63. ui->nickname->setText(QString::fromStdString(GetProfileUsername()));
  64. } else {
  65. ui->nickname->setText(QStringLiteral("suyu"));
  66. }
  67. }
  68. // UI Buttons
  69. connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby);
  70. connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
  71. connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned);
  72. connect(ui->hide_empty, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterEmpty);
  73. connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull);
  74. connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
  75. connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
  76. // Actions
  77. connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
  78. &Lobby::OnRefreshLobby);
  79. // Load persistent filters after events are connected to make sure they apply
  80. ui->search->setText(
  81. QString::fromStdString(UISettings::values.multiplayer_filter_text.GetValue()));
  82. ui->games_owned->setChecked(UISettings::values.multiplayer_filter_games_owned.GetValue());
  83. ui->hide_empty->setChecked(UISettings::values.multiplayer_filter_hide_empty.GetValue());
  84. ui->hide_full->setChecked(UISettings::values.multiplayer_filter_hide_full.GetValue());
  85. }
  86. Lobby::~Lobby() = default;
  87. void Lobby::UpdateGameList(QStandardItemModel* list) {
  88. game_list->clear();
  89. for (int i = 0; i < list->rowCount(); i++) {
  90. auto parent = list->item(i, 0);
  91. for (int j = 0; j < parent->rowCount(); j++) {
  92. game_list->appendRow(parent->child(j)->clone());
  93. }
  94. }
  95. if (proxy)
  96. proxy->UpdateGameList(game_list);
  97. ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
  98. }
  99. void Lobby::RetranslateUi() {
  100. ui->retranslateUi(this);
  101. }
  102. QString Lobby::PasswordPrompt() {
  103. bool ok;
  104. const QString text =
  105. QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
  106. QLineEdit::Password, QString(), &ok);
  107. return ok ? text : QString();
  108. }
  109. void Lobby::OnExpandRoom(const QModelIndex& index) {
  110. QModelIndex member_index = proxy->index(index.row(), Column::MEMBER);
  111. auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList();
  112. }
  113. void Lobby::OnJoinRoom(const QModelIndex& source) {
  114. if (!Network::GetSelectedNetworkInterface()) {
  115. LOG_INFO(WebService, "Automatically selected network interface for room network.");
  116. Network::SelectFirstNetworkInterface();
  117. }
  118. if (!Network::GetSelectedNetworkInterface()) {
  119. NetworkMessage::ErrorManager::ShowError(
  120. NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
  121. return;
  122. }
  123. if (system.IsPoweredOn()) {
  124. if (!NetworkMessage::WarnGameRunning()) {
  125. return;
  126. }
  127. }
  128. if (const auto member = room_network.GetRoomMember().lock()) {
  129. // Prevent the user from trying to join a room while they are already joining.
  130. if (member->GetState() == Network::RoomMember::State::Joining) {
  131. return;
  132. } else if (member->IsConnected()) {
  133. // And ask if they want to leave the room if they are already in one.
  134. if (!NetworkMessage::WarnDisconnect()) {
  135. return;
  136. }
  137. }
  138. }
  139. QModelIndex index = source;
  140. // If the user double clicks on a child row (aka the player list) then use the parent instead
  141. if (source.parent() != QModelIndex()) {
  142. index = source.parent();
  143. }
  144. if (!ui->nickname->hasAcceptableInput()) {
  145. NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
  146. return;
  147. }
  148. // Get a password to pass if the room is password protected
  149. QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME);
  150. bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool();
  151. const std::string password = has_password ? PasswordPrompt().toStdString() : "";
  152. if (has_password && password.empty()) {
  153. return;
  154. }
  155. QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
  156. const std::string nickname = ui->nickname->text().toStdString();
  157. const std::string ip =
  158. proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
  159. int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
  160. const std::string verify_uid =
  161. proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString();
  162. // attempt to connect in a different thread
  163. QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] {
  164. std::string token;
  165. #ifdef ENABLE_WEB_SERVICE
  166. if (!Settings::values.suyu_username.GetValue().empty() &&
  167. !Settings::values.suyu_token.GetValue().empty()) {
  168. WebService::Client client(Settings::values.web_api_url.GetValue(),
  169. Settings::values.suyu_username.GetValue(),
  170. Settings::values.suyu_token.GetValue());
  171. token = client.GetExternalJWT(verify_uid).returned_data;
  172. if (token.empty()) {
  173. LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
  174. } else {
  175. LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
  176. }
  177. }
  178. #endif
  179. if (auto room_member = room_network.GetRoomMember().lock()) {
  180. room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredIP, password,
  181. token);
  182. }
  183. });
  184. watcher->setFuture(f);
  185. // TODO(jroweboy): disable widgets and display a connecting while we wait
  186. // Save settings
  187. UISettings::values.multiplayer_nickname = ui->nickname->text().toStdString();
  188. UISettings::values.multiplayer_filter_text = ui->search->text().toStdString();
  189. UISettings::values.multiplayer_filter_games_owned = ui->games_owned->isChecked();
  190. UISettings::values.multiplayer_filter_hide_empty = ui->hide_empty->isChecked();
  191. UISettings::values.multiplayer_filter_hide_full = ui->hide_full->isChecked();
  192. UISettings::values.multiplayer_ip =
  193. proxy->data(connection_index, LobbyItemHost::HostIPRole).value<QString>().toStdString();
  194. UISettings::values.multiplayer_port =
  195. proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
  196. emit SaveConfig();
  197. }
  198. void Lobby::ResetModel() {
  199. model->clear();
  200. model->insertColumns(0, Column::TOTAL);
  201. model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
  202. model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
  203. model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
  204. model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
  205. }
  206. void Lobby::RefreshLobby() {
  207. if (auto session = announce_multiplayer_session.lock()) {
  208. ResetModel();
  209. ui->refresh_list->setEnabled(false);
  210. ui->refresh_list->setText(tr("Refreshing"));
  211. room_list_watcher.setFuture(
  212. QtConcurrent::run([session]() { return session->GetRoomList(); }));
  213. } else {
  214. // TODO(jroweboy): Display an error box about announce couldn't be started
  215. }
  216. }
  217. void Lobby::OnRefreshLobby() {
  218. AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result();
  219. for (auto room : new_room_list) {
  220. // find the icon for the game if this person owns that game.
  221. QPixmap smdh_icon;
  222. for (int r = 0; r < game_list->rowCount(); ++r) {
  223. auto index = game_list->index(r, 0);
  224. auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
  225. if (game_id != 0 && room.information.preferred_game.id == game_id) {
  226. smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
  227. }
  228. }
  229. QList<QVariant> members;
  230. for (auto member : room.members) {
  231. QVariant var;
  232. var.setValue(LobbyMember{QString::fromStdString(member.username),
  233. QString::fromStdString(member.nickname), member.game.id,
  234. QString::fromStdString(member.game.name)});
  235. members.append(var);
  236. }
  237. auto first_item = new LobbyItemGame(
  238. room.information.preferred_game.id,
  239. QString::fromStdString(room.information.preferred_game.name), smdh_icon);
  240. auto row = QList<QStandardItem*>({
  241. first_item,
  242. new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)),
  243. new LobbyItemMemberList(members, room.information.member_slots),
  244. new LobbyItemHost(QString::fromStdString(room.information.host_username),
  245. QString::fromStdString(room.ip), room.information.port,
  246. QString::fromStdString(room.verify_uid)),
  247. });
  248. model->appendRow(row);
  249. // To make the rows expandable, add the member data as a child of the first column of the
  250. // rows with people in them and have qt set them to colspan after the model is finished
  251. // resetting
  252. if (!room.information.description.empty()) {
  253. first_item->appendRow(
  254. new LobbyItemDescription(QString::fromStdString(room.information.description)));
  255. }
  256. if (!room.members.empty()) {
  257. first_item->appendRow(new LobbyItemExpandedMemberList(members));
  258. }
  259. }
  260. // Re-enable the refresh button and resize the columns
  261. ui->refresh_list->setEnabled(true);
  262. ui->refresh_list->setText(tr("Refresh List"));
  263. ui->room_list->header()->stretchLastSection();
  264. for (int i = 0; i < Column::TOTAL - 1; ++i) {
  265. ui->room_list->resizeColumnToContents(i);
  266. }
  267. // Set the member list child items to span all columns
  268. for (int i = 0; i < proxy->rowCount(); i++) {
  269. auto parent = model->item(i, 0);
  270. for (int j = 0; j < parent->rowCount(); j++) {
  271. ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true);
  272. }
  273. }
  274. ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
  275. }
  276. std::string Lobby::GetProfileUsername() {
  277. const auto& current_user =
  278. system.GetProfileManager().GetUser(Settings::values.current_user.GetValue());
  279. Service::Account::ProfileBase profile{};
  280. if (!current_user.has_value()) {
  281. return "";
  282. }
  283. if (!system.GetProfileManager().GetProfileBase(*current_user, profile)) {
  284. return "";
  285. }
  286. const auto text = Common::StringFromFixedZeroTerminatedBuffer(
  287. reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
  288. return text;
  289. }
  290. LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
  291. : QSortFilterProxyModel(parent), game_list(list) {}
  292. void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) {
  293. game_list = list;
  294. }
  295. bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
  296. // Prioritize filters by fastest to compute
  297. // pass over any child rows (aka row that shows the players in the room)
  298. if (sourceParent != QModelIndex()) {
  299. return true;
  300. }
  301. // filter by empty rooms
  302. if (filter_empty) {
  303. QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
  304. int player_count =
  305. sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
  306. if (player_count == 0) {
  307. return false;
  308. }
  309. }
  310. // filter by filled rooms
  311. if (filter_full) {
  312. QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
  313. int player_count =
  314. sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
  315. int max_players =
  316. sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
  317. if (player_count >= max_players) {
  318. return false;
  319. }
  320. }
  321. // filter by search parameters
  322. if (!filter_search.isEmpty()) {
  323. QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
  324. QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
  325. QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
  326. bool preferred_game_match = sourceModel()
  327. ->data(game_name, LobbyItemGame::GameNameRole)
  328. .toString()
  329. .contains(filter_search, filterCaseSensitivity());
  330. bool room_name_match = sourceModel()
  331. ->data(room_name, LobbyItemName::NameRole)
  332. .toString()
  333. .contains(filter_search, filterCaseSensitivity());
  334. bool username_match = sourceModel()
  335. ->data(host_name, LobbyItemHost::HostUsernameRole)
  336. .toString()
  337. .contains(filter_search, filterCaseSensitivity());
  338. if (!preferred_game_match && !room_name_match && !username_match) {
  339. return false;
  340. }
  341. }
  342. // filter by game owned
  343. if (filter_owned) {
  344. QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
  345. QList<QModelIndex> owned_games;
  346. for (int r = 0; r < game_list->rowCount(); ++r) {
  347. owned_games.append(QModelIndex(game_list->index(r, 0)));
  348. }
  349. auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
  350. if (current_id == 0) {
  351. // TODO(jroweboy): homebrew often doesn't have a game id and this hides them
  352. return false;
  353. }
  354. bool owned = false;
  355. for (const auto& game : owned_games) {
  356. auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
  357. if (current_id == game_id) {
  358. owned = true;
  359. }
  360. }
  361. if (!owned) {
  362. return false;
  363. }
  364. }
  365. return true;
  366. }
  367. void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
  368. sourceModel()->sort(column, order);
  369. }
  370. void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
  371. filter_owned = filter;
  372. invalidate();
  373. }
  374. void LobbyFilterProxyModel::SetFilterEmpty(bool filter) {
  375. filter_empty = filter;
  376. invalidate();
  377. }
  378. void LobbyFilterProxyModel::SetFilterFull(bool filter) {
  379. filter_full = filter;
  380. invalidate();
  381. }
  382. void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
  383. filter_search = filter;
  384. invalidate();
  385. }