lobby.cpp 15 KB

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