mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	Merge pull request #4468 from citra-emu/multiplayer-v4/main
Multiplayer version 4
This commit is contained in:
		
						commit
						eabc9727d8
					
				
					 60 changed files with 2395 additions and 308 deletions
				
			
		|  | @ -99,6 +99,8 @@ add_executable(citra-qt | |||
|     multiplayer/lobby.cpp | ||||
|     multiplayer/message.h | ||||
|     multiplayer/message.cpp | ||||
|     multiplayer/moderation_dialog.cpp | ||||
|     multiplayer/moderation_dialog.h | ||||
|     multiplayer/state.cpp | ||||
|     multiplayer/state.h | ||||
|     multiplayer/validation.h | ||||
|  | @ -135,6 +137,7 @@ set(UIS | |||
|     multiplayer/chat_room.ui | ||||
|     multiplayer/client_room.ui | ||||
|     multiplayer/host_room.ui | ||||
|     multiplayer/moderation_dialog.ui | ||||
|     aboutdialog.ui | ||||
|     cheats.ui | ||||
|     hotkeys.ui | ||||
|  | @ -228,6 +231,10 @@ if (USE_DISCORD_PRESENCE) | |||
|     target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE) | ||||
| endif() | ||||
| 
 | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     target_compile_definitions(citra-qt PRIVATE -DENABLE_WEB_SERVICE) | ||||
| endif() | ||||
| 
 | ||||
| if(UNIX AND NOT APPLE) | ||||
|     install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") | ||||
| endif() | ||||
|  |  | |||
|  | @ -329,6 +329,22 @@ void Config::ReadValues() { | |||
|     } | ||||
|     UISettings::values.max_player = ReadSetting("max_player", 8).toUInt(); | ||||
|     UISettings::values.game_id = ReadSetting("game_id", 0).toULongLong(); | ||||
|     UISettings::values.room_description = ReadSetting("room_description", "").toString(); | ||||
|     // Read ban list back
 | ||||
|     size = qt_config->beginReadArray("username_ban_list"); | ||||
|     UISettings::values.ban_list.first.resize(size); | ||||
|     for (int i = 0; i < size; ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         UISettings::values.ban_list.first[i] = ReadSetting("username").toString().toStdString(); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     size = qt_config->beginReadArray("ip_ban_list"); | ||||
|     UISettings::values.ban_list.second.resize(size); | ||||
|     for (int i = 0; i < size; ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         UISettings::values.ban_list.second[i] = ReadSetting("ip").toString().toStdString(); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     qt_config->endGroup(); | ||||
| 
 | ||||
|     qt_config->endGroup(); | ||||
|  | @ -533,6 +549,20 @@ void Config::SaveValues() { | |||
|     WriteSetting("host_type", UISettings::values.host_type, 0); | ||||
|     WriteSetting("max_player", UISettings::values.max_player, 8); | ||||
|     WriteSetting("game_id", UISettings::values.game_id, 0); | ||||
|     WriteSetting("room_description", UISettings::values.room_description, ""); | ||||
|     // Write ban list
 | ||||
|     qt_config->beginWriteArray("username_ban_list"); | ||||
|     for (std::size_t i = 0; i < UISettings::values.ban_list.first.size(); ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         WriteSetting("username", QString::fromStdString(UISettings::values.ban_list.first[i])); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     qt_config->beginWriteArray("ip_ban_list"); | ||||
|     for (std::size_t i = 0; i < UISettings::values.ban_list.second.size(); ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|         WriteSetting("ip", QString::fromStdString(UISettings::values.ban_list.second[i])); | ||||
|     } | ||||
|     qt_config->endArray(); | ||||
|     qt_config->endGroup(); | ||||
| 
 | ||||
|     qt_config->endGroup(); | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| #include <array> | ||||
| #include <future> | ||||
| #include <QColor> | ||||
| #include <QDesktopServices> | ||||
| #include <QFutureWatcher> | ||||
| #include <QImage> | ||||
| #include <QList> | ||||
| #include <QLocale> | ||||
|  | @ -12,6 +14,7 @@ | |||
| #include <QMessageBox> | ||||
| #include <QMetaType> | ||||
| #include <QTime> | ||||
| #include <QUrl> | ||||
| #include <QtConcurrent/QtConcurrentRun> | ||||
| #include "citra_qt/game_list_p.h" | ||||
| #include "citra_qt/multiplayer/chat_room.h" | ||||
|  | @ -19,6 +22,9 @@ | |||
| #include "common/logging/log.h" | ||||
| #include "core/announce_multiplayer_session.h" | ||||
| #include "ui_chat_room.h" | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/web_backend.h" | ||||
| #endif | ||||
| 
 | ||||
| class ChatMessage { | ||||
| public: | ||||
|  | @ -27,24 +33,60 @@ public: | |||
|         QLocale locale; | ||||
|         timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); | ||||
|         nickname = QString::fromStdString(chat.nickname); | ||||
|         username = QString::fromStdString(chat.username); | ||||
|         message = QString::fromStdString(chat.message); | ||||
| 
 | ||||
|         // Check for user pings
 | ||||
|         QString cur_nickname, cur_username; | ||||
|         if (auto room = Network::GetRoomMember().lock()) { | ||||
|             cur_nickname = QString::fromStdString(room->GetNickname()); | ||||
|             cur_username = QString::fromStdString(room->GetUsername()); | ||||
|         } | ||||
|         if (message.contains(QString("@").append(cur_nickname)) || | ||||
|             (!cur_username.isEmpty() && message.contains(QString("@").append(cur_username)))) { | ||||
| 
 | ||||
|             contains_ping = true; | ||||
|         } else { | ||||
|             contains_ping = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     bool ContainsPing() const { | ||||
|         return contains_ping; | ||||
|     } | ||||
| 
 | ||||
|     /// 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()); | ||||
|         QString name; | ||||
|         if (username.isEmpty() || username == nickname) { | ||||
|             name = nickname; | ||||
|         } else { | ||||
|             name = QString("%1 (%2)").arg(nickname, username); | ||||
|         } | ||||
| 
 | ||||
|         QString style; | ||||
|         if (ContainsPing()) { | ||||
|             // Add a background color to these messages
 | ||||
|             style = QString("background-color: %1").arg(ping_color); | ||||
|         } | ||||
| 
 | ||||
|         return QString("[%1] <font color='%2'><%3></font> <font style='%4' " | ||||
|                        "color='#000000'>%5</font>") | ||||
|             .arg(timestamp, color, name.toHtmlEscaped(), style, 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"}}; | ||||
|     static constexpr char ping_color[] = "#FFFF00"; | ||||
| 
 | ||||
|     QString timestamp; | ||||
|     QString nickname; | ||||
|     QString username; | ||||
|     QString message; | ||||
|     bool contains_ping; | ||||
| }; | ||||
| 
 | ||||
| class StatusMessage { | ||||
|  | @ -57,37 +99,69 @@ public: | |||
|     } | ||||
| 
 | ||||
|     QString GetSystemChatMessage() const { | ||||
|         return QString("[%1] <font color='%2'><i>%3</i></font>") | ||||
|             .arg(timestamp, system_color, message); | ||||
|         return QString("[%1] <font color='%2'>* %3</font>").arg(timestamp, system_color, message); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     static constexpr const char system_color[] = "#888888"; | ||||
|     static constexpr const char system_color[] = "#FF8C00"; | ||||
|     QString timestamp; | ||||
|     QString message; | ||||
| }; | ||||
| 
 | ||||
| class PlayerListItem : public QStandardItem { | ||||
| public: | ||||
|     static const int NicknameRole = Qt::UserRole + 1; | ||||
|     static const int UsernameRole = Qt::UserRole + 2; | ||||
|     static const int AvatarUrlRole = Qt::UserRole + 3; | ||||
|     static const int GameNameRole = Qt::UserRole + 4; | ||||
| 
 | ||||
|     PlayerListItem() = default; | ||||
|     explicit PlayerListItem(const std::string& nickname, const std::string& username, | ||||
|                             const std::string& avatar_url, const std::string& game_name) { | ||||
|         setEditable(false); | ||||
|         setData(QString::fromStdString(nickname), NicknameRole); | ||||
|         setData(QString::fromStdString(username), UsernameRole); | ||||
|         setData(QString::fromStdString(avatar_url), AvatarUrlRole); | ||||
|         if (game_name.empty()) { | ||||
|             setData(QObject::tr("Not playing a game"), GameNameRole); | ||||
|         } else { | ||||
|             setData(QString::fromStdString(game_name), GameNameRole); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     QVariant data(int role) const override { | ||||
|         if (role != Qt::DisplayRole) { | ||||
|             return QStandardItem::data(role); | ||||
|         } | ||||
|         QString name; | ||||
|         const QString nickname = data(NicknameRole).toString(); | ||||
|         const QString username = data(UsernameRole).toString(); | ||||
|         if (username.isEmpty() || username == nickname) { | ||||
|             name = nickname; | ||||
|         } else { | ||||
|             name = QString("%1 (%2)").arg(nickname, username); | ||||
|         } | ||||
|         return QString("%1\n      %2").arg(name, data(GameNameRole).toString()); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| 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); | ||||
|     ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); | ||||
|     player_list->insertColumns(0, COLUMN_COUNT); | ||||
|     player_list->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); | ||||
|     player_list->setHeaderData(COLUMN_GAME, Qt::Horizontal, tr("Game")); | ||||
|     // set a header to make it look better though there is only one column
 | ||||
|     player_list->insertColumns(0, 1); | ||||
|     player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); | ||||
| 
 | ||||
|     ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); | ||||
| 
 | ||||
|     // register the network structs to use in slots and signals
 | ||||
|     qRegisterMetaType<Network::ChatEntry>(); | ||||
|     qRegisterMetaType<Network::StatusMessageEntry>(); | ||||
|     qRegisterMetaType<Network::RoomInformation>(); | ||||
|     qRegisterMetaType<Network::RoomMember::State>(); | ||||
| 
 | ||||
|  | @ -95,7 +169,12 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::C | |||
|     if (auto member = Network::GetRoomMember().lock()) { | ||||
|         member->BindOnChatMessageRecieved( | ||||
|             [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); | ||||
|         member->BindOnStatusMessageReceived( | ||||
|             [this](const Network::StatusMessageEntry& status_message) { | ||||
|                 emit StatusMessageReceived(status_message); | ||||
|             }); | ||||
|         connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); | ||||
|         connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); | ||||
|     } else { | ||||
|         // TODO (jroweboy) network was not initialized?
 | ||||
|     } | ||||
|  | @ -110,6 +189,10 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::C | |||
| 
 | ||||
| ChatRoom::~ChatRoom() = default; | ||||
| 
 | ||||
| void ChatRoom::SetModPerms(bool is_mod) { | ||||
|     has_mod_perms = is_mod; | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::RetranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
| } | ||||
|  | @ -127,6 +210,21 @@ void ChatRoom::AppendChatMessage(const QString& msg) { | |||
|     ui->chat_history->append(msg); | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         auto members = room->GetMemberInformation(); | ||||
|         auto it = std::find_if(members.begin(), members.end(), | ||||
|                                [&nickname](const Network::RoomMember::MemberInformation& member) { | ||||
|                                    return member.nickname == nickname; | ||||
|                                }); | ||||
|         if (it == members.end()) { | ||||
|             NetworkMessage::ShowError(NetworkMessage::NO_SUCH_USER); | ||||
|             return; | ||||
|         } | ||||
|         room->SendModerationRequest(type, nickname); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool ChatRoom::ValidateMessage(const std::string& msg) { | ||||
|     return !msg.empty(); | ||||
| } | ||||
|  | @ -157,7 +255,8 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { | |||
|         auto members = room->GetMemberInformation(); | ||||
|         auto it = std::find_if(members.begin(), members.end(), | ||||
|                                [&chat](const Network::RoomMember::MemberInformation& member) { | ||||
|                                    return member.nickname == chat.nickname; | ||||
|                                    return member.nickname == chat.nickname && | ||||
|                                           member.username == chat.username; | ||||
|                                }); | ||||
|         if (it == members.end()) { | ||||
|             LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); | ||||
|  | @ -170,13 +269,48 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { | |||
|         } | ||||
|         auto player = std::distance(members.begin(), it); | ||||
|         ChatMessage m(chat); | ||||
|         if (m.ContainsPing()) { | ||||
|             emit UserPinged(); | ||||
|         } | ||||
|         AppendChatMessage(m.GetPlayerChatMessage(player)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) { | ||||
|     QString name; | ||||
|     if (status_message.username.empty() || status_message.username == status_message.nickname) { | ||||
|         name = QString::fromStdString(status_message.nickname); | ||||
|     } else { | ||||
|         name = QString("%1 (%2)").arg(QString::fromStdString(status_message.nickname), | ||||
|                                       QString::fromStdString(status_message.username)); | ||||
|     } | ||||
|     QString message; | ||||
|     switch (status_message.type) { | ||||
|     case Network::IdMemberJoin: | ||||
|         message = tr("%1 has joined").arg(name); | ||||
|         break; | ||||
|     case Network::IdMemberLeave: | ||||
|         message = tr("%1 has left").arg(name); | ||||
|         break; | ||||
|     case Network::IdMemberKicked: | ||||
|         message = tr("%1 has been kicked").arg(name); | ||||
|         break; | ||||
|     case Network::IdMemberBanned: | ||||
|         message = tr("%1 has been banned").arg(name); | ||||
|         break; | ||||
|     case Network::IdAddressUnbanned: | ||||
|         message = tr("%1 has been unbanned").arg(name); | ||||
|         break; | ||||
|     } | ||||
|     if (!message.isEmpty()) | ||||
|         AppendStatusMessage(message); | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::OnSendChat() { | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         if (room->GetState() != Network::RoomMember::State::Joined) { | ||||
|         if (room->GetState() != Network::RoomMember::State::Joined && | ||||
|             room->GetState() != Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         auto message = ui->chat_message->text().toStdString(); | ||||
|  | @ -184,12 +318,14 @@ void ChatRoom::OnSendChat() { | |||
|             return; | ||||
|         } | ||||
|         auto nick = room->GetNickname(); | ||||
|         Network::ChatEntry chat{nick, message}; | ||||
|         auto username = room->GetUsername(); | ||||
|         Network::ChatEntry chat{nick, username, 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; | ||||
|                                    return member.nickname == chat.nickname && | ||||
|                                           member.username == chat.username; | ||||
|                                }); | ||||
|         if (it == members.end()) { | ||||
|             LOG_INFO(Network, "Cannot find self in the player list when sending a message."); | ||||
|  | @ -202,20 +338,64 @@ void ChatRoom::OnSendChat() { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void ChatRoom::UpdateIconDisplay() { | ||||
|     for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) { | ||||
|         QStandardItem* item = player_list->invisibleRootItem()->child(row); | ||||
|         const std::string avatar_url = | ||||
|             item->data(PlayerListItem::AvatarUrlRole).toString().toStdString(); | ||||
|         if (icon_cache.count(avatar_url)) { | ||||
|             item->setData(icon_cache.at(avatar_url), Qt::DecorationRole); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
|         QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, | ||||
|                                                       member.avatar_url, member.game_info.name); | ||||
| 
 | ||||
|         if (!icon_cache.count(member.avatar_url)) { | ||||
|             // Emplace a default question mark icon as avatar
 | ||||
|             icon_cache.emplace(member.avatar_url, QIcon::fromTheme("no_avatar").pixmap(48)); | ||||
|             if (!member.avatar_url.empty()) { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|                 // Start a request to get the member's avatar
 | ||||
|                 const QUrl url(QString::fromStdString(member.avatar_url)); | ||||
|                 QFuture<std::string> future = QtConcurrent::run([url] { | ||||
|                     WebService::Client client( | ||||
|                         QString("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", ""); | ||||
|                     auto result = client.GetImage(url.path().toStdString(), true); | ||||
|                     if (result.returned_data.empty()) { | ||||
|                         LOG_ERROR(WebService, "Failed to get avatar"); | ||||
|                     } | ||||
|                     return result.returned_data; | ||||
|                 }); | ||||
|                 auto* future_watcher = new QFutureWatcher<std::string>(this); | ||||
|                 connect(future_watcher, &QFutureWatcher<std::string>::finished, this, | ||||
|                         [this, future_watcher, avatar_url = member.avatar_url] { | ||||
|                             const std::string result = future_watcher->result(); | ||||
|                             if (result.empty()) | ||||
|                                 return; | ||||
|                             QPixmap pixmap; | ||||
|                             if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()), | ||||
|                                                      result.size())) | ||||
|                                 return; | ||||
|                             icon_cache[avatar_url] = pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, | ||||
|                                                                    Qt::SmoothTransformation); | ||||
|                             // Update all the displayed icons with the new icon_cache
 | ||||
|                             UpdateIconDisplay(); | ||||
|                         }); | ||||
|                 future_watcher->setFuture(future); | ||||
| #endif | ||||
|             } | ||||
|         } | ||||
|         player_list->invisibleRootItem()->appendRow(l); | ||||
|         name_item->setData(icon_cache.at(member.avatar_url), Qt::DecorationRole); | ||||
| 
 | ||||
|         player_list->invisibleRootItem()->appendRow(name_item); | ||||
|     } | ||||
|     // TODO(B3N30): Restore row selection
 | ||||
| } | ||||
|  | @ -230,33 +410,73 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) { | |||
|     if (!item.isValid()) | ||||
|         return; | ||||
| 
 | ||||
|     std::string nickname = player_list->item(item.row())->text().toStdString(); | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         // You can't block yourself
 | ||||
|         if (nickname == room->GetNickname()) | ||||
|             return; | ||||
|     } | ||||
|     std::string nickname = | ||||
|         player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); | ||||
| 
 | ||||
|     QMenu context_menu; | ||||
|     QAction* block_action = context_menu.addAction(tr("Block Player")); | ||||
| 
 | ||||
|     block_action->setCheckable(true); | ||||
|     block_action->setChecked(block_list.count(nickname) > 0); | ||||
|     QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString(); | ||||
|     if (!username.isEmpty()) { | ||||
|         QAction* view_profile_action = context_menu.addAction(tr("View Profile")); | ||||
|         connect(view_profile_action, &QAction::triggered, [username] { | ||||
|             QDesktopServices::openUrl( | ||||
|                 QString("https://community.citra-emu.org/u/%1").arg(username)); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     connect(block_action, &QAction::triggered, [this, nickname] { | ||||
|         if (block_list.count(nickname)) { | ||||
|             block_list.erase(nickname); | ||||
|         } else { | ||||
|     std::string cur_nickname; | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         cur_nickname = room->GetNickname(); | ||||
|     } | ||||
| 
 | ||||
|     if (nickname != cur_nickname) { // You can't block yourself
 | ||||
|         QAction* block_action = context_menu.addAction(tr("Block Player")); | ||||
| 
 | ||||
|         block_action->setCheckable(true); | ||||
|         block_action->setChecked(block_list.count(nickname) > 0); | ||||
| 
 | ||||
|         connect(block_action, &QAction::triggered, [this, nickname] { | ||||
|             if (block_list.count(nickname)) { | ||||
|                 block_list.erase(nickname); | ||||
|             } else { | ||||
|                 QMessageBox::StandardButton result = QMessageBox::question( | ||||
|                     this, tr("Block Player"), | ||||
|                     tr("When you block a player, you will no longer receive chat messages from " | ||||
|                        "them.<br><br>Are you sure you would like to block %1?") | ||||
|                         .arg(QString::fromStdString(nickname)), | ||||
|                     QMessageBox::Yes | QMessageBox::No); | ||||
|                 if (result == QMessageBox::Yes) | ||||
|                     block_list.emplace(nickname); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself
 | ||||
|         context_menu.addSeparator(); | ||||
| 
 | ||||
|         QAction* kick_action = context_menu.addAction(tr("Kick")); | ||||
|         QAction* ban_action = context_menu.addAction(tr("Ban")); | ||||
| 
 | ||||
|         connect(kick_action, &QAction::triggered, [this, nickname] { | ||||
|             QMessageBox::StandardButton result = | ||||
|                 QMessageBox::question(this, tr("Kick Player"), | ||||
|                                       tr("Are you sure you would like to <b>kick</b> %1?") | ||||
|                                           .arg(QString::fromStdString(nickname)), | ||||
|                                       QMessageBox::Yes | QMessageBox::No); | ||||
|             if (result == QMessageBox::Yes) | ||||
|                 SendModerationRequest(Network::IdModKick, nickname); | ||||
|         }); | ||||
|         connect(ban_action, &QAction::triggered, [this, nickname] { | ||||
|             QMessageBox::StandardButton result = QMessageBox::question( | ||||
|                 this, tr("Block Player"), | ||||
|                 tr("When you block a player, you will no longer receive chat messages from " | ||||
|                    "them.<br><br>Are you sure you would like to block %1?") | ||||
|                 this, tr("Ban Player"), | ||||
|                 tr("Are you sure you would like to <b>kick and ban</b> %1?\n\nThis would " | ||||
|                    "ban both their forum username and their IP address.") | ||||
|                     .arg(QString::fromStdString(nickname)), | ||||
|                 QMessageBox::Yes | QMessageBox::No); | ||||
|             if (result == QMessageBox::Yes) | ||||
|                 block_list.emplace(nickname); | ||||
|         } | ||||
|     }); | ||||
|                 SendModerationRequest(Network::IdModBan, nickname); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); | ||||
| } | ||||
|  |  | |||
|  | @ -36,9 +36,12 @@ public: | |||
|     void AppendStatusMessage(const QString& msg); | ||||
|     ~ChatRoom(); | ||||
| 
 | ||||
|     void SetModPerms(bool is_mod); | ||||
| 
 | ||||
| public slots: | ||||
|     void OnRoomUpdate(const Network::RoomInformation& info); | ||||
|     void OnChatReceive(const Network::ChatEntry&); | ||||
|     void OnStatusMessageReceive(const Network::StatusMessageEntry&); | ||||
|     void OnSendChat(); | ||||
|     void OnChatTextChanged(); | ||||
|     void PopupContextMenu(const QPoint& menu_location); | ||||
|  | @ -47,16 +50,25 @@ public slots: | |||
| 
 | ||||
| signals: | ||||
|     void ChatReceived(const Network::ChatEntry&); | ||||
|     void StatusMessageReceived(const Network::StatusMessageEntry&); | ||||
|     void UserPinged(); | ||||
| 
 | ||||
| private: | ||||
|     static constexpr u32 max_chat_lines = 1000; | ||||
|     void AppendChatMessage(const QString&); | ||||
|     bool ValidateMessage(const std::string&); | ||||
|     void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); | ||||
|     void UpdateIconDisplay(); | ||||
| 
 | ||||
|     bool has_mod_perms = false; | ||||
|     QStandardItemModel* player_list; | ||||
|     std::unique_ptr<Ui::ChatRoom> ui; | ||||
|     std::unordered_set<std::string> block_list; | ||||
|     std::unordered_map<std::string, QPixmap> icon_cache; | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(Network::ChatEntry); | ||||
| Q_DECLARE_METATYPE(Network::StatusMessageEntry); | ||||
| Q_DECLARE_METATYPE(Network::RoomInformation); | ||||
| Q_DECLARE_METATYPE(Network::RoomMember::State); | ||||
| Q_DECLARE_METATYPE(Network::RoomMember::Error); | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <width>807</width> | ||||
|     <height>432</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| #include "citra_qt/game_list_p.h" | ||||
| #include "citra_qt/multiplayer/client_room.h" | ||||
| #include "citra_qt/multiplayer/message.h" | ||||
| #include "citra_qt/multiplayer/moderation_dialog.h" | ||||
| #include "citra_qt/multiplayer/state.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/announce_multiplayer_session.h" | ||||
|  | @ -33,6 +34,8 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) | |||
|         connect(this, &ClientRoomWindow::RoomInformationChanged, this, | ||||
|                 &ClientRoomWindow::OnRoomUpdate); | ||||
|         connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); | ||||
|         // Update the state
 | ||||
|         OnStateChange(member->GetState()); | ||||
|     } else { | ||||
|         // TODO (jroweboy) network was not initialized?
 | ||||
|     } | ||||
|  | @ -40,11 +43,25 @@ ClientRoomWindow::ClientRoomWindow(QWidget* parent) | |||
|     connect(ui->disconnect, &QPushButton::pressed, [this] { Disconnect(); }); | ||||
|     ui->disconnect->setDefault(false); | ||||
|     ui->disconnect->setAutoDefault(false); | ||||
|     connect(ui->moderation, &QPushButton::clicked, [this] { | ||||
|         ModerationDialog dialog(this); | ||||
|         dialog.exec(); | ||||
|     }); | ||||
|     ui->moderation->setDefault(false); | ||||
|     ui->moderation->setAutoDefault(false); | ||||
|     connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); | ||||
|     UpdateView(); | ||||
| } | ||||
| 
 | ||||
| ClientRoomWindow::~ClientRoomWindow() = default; | ||||
| 
 | ||||
| void ClientRoomWindow::SetModPerms(bool is_mod) { | ||||
|     ui->chat->SetModPerms(is_mod); | ||||
|     ui->moderation->setVisible(is_mod); | ||||
|     ui->moderation->setDefault(false); | ||||
|     ui->moderation->setAutoDefault(false); | ||||
| } | ||||
| 
 | ||||
| void ClientRoomWindow::RetranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
|     ui->chat->RetranslateUi(); | ||||
|  | @ -55,9 +72,12 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { | |||
| } | ||||
| 
 | ||||
| void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { | ||||
|     if (state == Network::RoomMember::State::Joined) { | ||||
|     if (state == Network::RoomMember::State::Joined || | ||||
|         state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         ui->chat->Clear(); | ||||
|         ui->chat->AppendStatusMessage(tr("Connected")); | ||||
|         SetModPerms(state == Network::RoomMember::State::Moderator); | ||||
|     } | ||||
|     UpdateView(); | ||||
| } | ||||
|  | @ -82,6 +102,7 @@ void ClientRoomWindow::UpdateView() { | |||
|                                .arg(QString::fromStdString(information.name)) | ||||
|                                .arg(memberlist.size()) | ||||
|                                .arg(information.member_slots)); | ||||
|             ui->description->setText(QString::fromStdString(information.description)); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -26,10 +26,12 @@ public slots: | |||
| signals: | ||||
|     void RoomInformationChanged(const Network::RoomInformation&); | ||||
|     void StateChanged(const Network::RoomMember::State&); | ||||
|     void ShowNotification(); | ||||
| 
 | ||||
| private: | ||||
|     void Disconnect(); | ||||
|     void UpdateView(); | ||||
|     void SetModPerms(bool is_mod); | ||||
| 
 | ||||
|     QStandardItemModel* player_list; | ||||
|     std::unique_ptr<Ui::ClientRoom> ui; | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <width>807</width> | ||||
|     <height>432</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|  | @ -21,6 +21,13 @@ | |||
|        <property name="rightMargin"> | ||||
|         <number>0</number> | ||||
|        </property> | ||||
|        <item> | ||||
|         <widget class="QLabel" name="description"> | ||||
|          <property name="text"> | ||||
|           <string>Room Description</string> | ||||
|          </property> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item> | ||||
|         <spacer name="horizontalSpacer"> | ||||
|          <property name="orientation"> | ||||
|  | @ -34,6 +41,16 @@ | |||
|          </property> | ||||
|         </spacer> | ||||
|        </item> | ||||
|        <item> | ||||
|         <widget class="QPushButton" name="moderation"> | ||||
|          <property name="text"> | ||||
|           <string>Moderation...</string> | ||||
|          </property> | ||||
|          <property name="visible"> | ||||
|           <bool>false</bool> | ||||
|          </property> | ||||
|         </widget> | ||||
|        </item> | ||||
|        <item> | ||||
|         <widget class="QPushButton" name="disconnect"> | ||||
|          <property name="text"> | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| #include "citra_qt/multiplayer/state.h" | ||||
| #include "citra_qt/multiplayer/validation.h" | ||||
| #include "citra_qt/ui_settings.h" | ||||
| #include "core/hle/service/cfg/cfg.h" | ||||
| #include "core/settings.h" | ||||
| #include "network/network.h" | ||||
| #include "ui_direct_connect.h" | ||||
|  | @ -62,7 +63,7 @@ void DirectConnectWindow::Connect() { | |||
|         // 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->GetState() == Network::RoomMember::State::Joined) { | ||||
|         } else if (member->IsConnected()) { | ||||
|             // And ask if they want to leave the room if they are already in one.
 | ||||
|             if (!NetworkMessage::WarnDisconnect()) { | ||||
|                 return; | ||||
|  | @ -97,6 +98,7 @@ void DirectConnectWindow::Connect() { | |||
|         if (auto room_member = Network::GetRoomMember().lock()) { | ||||
|             auto port = UISettings::values.port.toUInt(); | ||||
|             room_member->Join(ui->nickname->text().toStdString(), | ||||
|                               Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), | ||||
|                               ui->ip->text().toStdString().c_str(), port, 0, | ||||
|                               Network::NoPreferredMac, ui->password->text().toStdString().c_str()); | ||||
|         } | ||||
|  | @ -120,7 +122,9 @@ void DirectConnectWindow::OnConnection() { | |||
|     EndConnecting(); | ||||
| 
 | ||||
|     if (auto room_member = Network::GetRoomMember().lock()) { | ||||
|         if (room_member->GetState() == Network::RoomMember::State::Joined) { | ||||
|         if (room_member->GetState() == Network::RoomMember::State::Joined || | ||||
|             room_member->GetState() == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|             close(); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -19,8 +19,12 @@ | |||
| #include "citra_qt/ui_settings.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/announce_multiplayer_session.h" | ||||
| #include "core/hle/service/cfg/cfg.h" | ||||
| #include "core/settings.h" | ||||
| #include "ui_host_room.h" | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/verify_user_jwt.h" | ||||
| #endif | ||||
| 
 | ||||
| HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | ||||
|                                std::shared_ptr<Core::AnnounceMultiplayerSession> session) | ||||
|  | @ -69,6 +73,7 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, | |||
|     if (index != -1) { | ||||
|         ui->game_list->setCurrentIndex(index); | ||||
|     } | ||||
|     ui->room_description->setText(UISettings::values.room_description); | ||||
| } | ||||
| 
 | ||||
| HostRoomWindow::~HostRoomWindow() = default; | ||||
|  | @ -77,6 +82,21 @@ void HostRoomWindow::RetranslateUi() { | |||
|     ui->retranslateUi(this); | ||||
| } | ||||
| 
 | ||||
| std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBackend( | ||||
|     bool use_validation) const { | ||||
|     std::unique_ptr<Network::VerifyUser::Backend> verify_backend; | ||||
|     if (use_validation) { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|         verify_backend = std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url); | ||||
| #else | ||||
|         verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); | ||||
| #endif | ||||
|     } else { | ||||
|         verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); | ||||
|     } | ||||
|     return verify_backend; | ||||
| } | ||||
| 
 | ||||
| void HostRoomWindow::Host() { | ||||
|     if (!ui->username->hasAcceptableInput()) { | ||||
|         NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID); | ||||
|  | @ -93,7 +113,7 @@ void HostRoomWindow::Host() { | |||
|     if (auto member = Network::GetRoomMember().lock()) { | ||||
|         if (member->GetState() == Network::RoomMember::State::Joining) { | ||||
|             return; | ||||
|         } else if (member->GetState() == Network::RoomMember::State::Joined) { | ||||
|         } else if (member->IsConnected()) { | ||||
|             auto parent = static_cast<MultiplayerState*>(parentWidget()); | ||||
|             if (!parent->OnCloseRoom()) { | ||||
|                 close(); | ||||
|  | @ -106,9 +126,17 @@ void HostRoomWindow::Host() { | |||
|         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(); | ||||
|         const bool is_public = ui->host_type->currentIndex() == 0; | ||||
|         Network::Room::BanList ban_list{}; | ||||
|         if (ui->load_ban_list->isChecked()) { | ||||
|             ban_list = UISettings::values.ban_list; | ||||
|         } | ||||
|         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); | ||||
|             bool created = room->Create(ui->room_name->text().toStdString(), | ||||
|                                         ui->room_description->toPlainText().toStdString(), "", port, | ||||
|                                         password, ui->max_player->value(), | ||||
|                                         Settings::values.citra_username, game_name.toStdString(), | ||||
|                                         game_id, CreateVerifyBackend(is_public), ban_list); | ||||
|             if (!created) { | ||||
|                 NetworkMessage::ShowError(NetworkMessage::COULD_NOT_CREATE_ROOM); | ||||
|                 LOG_ERROR(Network, "Could not create room!"); | ||||
|  | @ -116,8 +144,34 @@ void HostRoomWindow::Host() { | |||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         member->Join(ui->username->text().toStdString(), "127.0.0.1", port, 0, | ||||
|                      Network::NoPreferredMac, password); | ||||
|         // Start the announce session if they chose Public
 | ||||
|         if (is_public) { | ||||
|             if (auto session = announce_multiplayer_session.lock()) { | ||||
|                 // Register the room first to ensure verify_UID is present when we connect
 | ||||
|                 session->Register(); | ||||
|                 session->Start(); | ||||
|             } else { | ||||
|                 LOG_ERROR(Network, "Starting announce session failed"); | ||||
|             } | ||||
|         } | ||||
|         std::string token; | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|         if (is_public) { | ||||
|             WebService::Client client(Settings::values.web_api_url, Settings::values.citra_username, | ||||
|                                       Settings::values.citra_token); | ||||
|             if (auto room = Network::GetRoom().lock()) { | ||||
|                 token = client.GetExternalJWT(room->GetVerifyUID()).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 | ||||
|         member->Join(ui->username->text().toStdString(), | ||||
|                      Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), "127.0.0.1", port, | ||||
|                      0, Network::NoPreferredMac, password, token); | ||||
| 
 | ||||
|         // Store settings
 | ||||
|         UISettings::values.room_nickname = ui->username->text(); | ||||
|  | @ -130,25 +184,10 @@ void HostRoomWindow::Host() { | |||
|         UISettings::values.room_port = (ui->port->isModified() && !ui->port->text().isEmpty()) | ||||
|                                            ? ui->port->text() | ||||
|                                            : QString::number(Network::DefaultRoomPort); | ||||
|         UISettings::values.room_description = ui->room_description->toPlainText(); | ||||
|         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 { | ||||
|                     LOG_ERROR(Network, "Starting announce session failed"); | ||||
|                 } | ||||
|             } | ||||
|             close(); | ||||
|         } | ||||
|         ui->host->setEnabled(true); | ||||
|         close(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,6 +26,10 @@ class ComboBoxProxyModel; | |||
| 
 | ||||
| class ChatMessage; | ||||
| 
 | ||||
| namespace Network::VerifyUser { | ||||
| class Backend; | ||||
| }; | ||||
| 
 | ||||
| class HostRoomWindow : public QDialog { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
|  | @ -36,15 +40,9 @@ public: | |||
| 
 | ||||
|     void RetranslateUi(); | ||||
| 
 | ||||
| private slots: | ||||
|     /**
 | ||||
|      * Handler for connection status changes. Launches the chat window if successful or | ||||
|      * displays an error | ||||
|      */ | ||||
|     void OnConnection(); | ||||
| 
 | ||||
| private: | ||||
|     void Host(); | ||||
|     std::unique_ptr<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const; | ||||
| 
 | ||||
|     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||
|     QStandardItemModel* game_list; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>607</width> | ||||
|     <height>165</height> | ||||
|     <height>211</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|  | @ -131,6 +131,34 @@ | |||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout_3"> | ||||
|      <item> | ||||
|       <widget class="QLabel" name="label_7"> | ||||
|        <property name="text"> | ||||
|         <string>Room Description</string> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <widget class="QTextEdit" name="room_description"/> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout"> | ||||
|      <item> | ||||
|       <widget class="QCheckBox" name="load_ban_list"> | ||||
|        <property name="text"> | ||||
|         <string>Load Previous Ban List</string> | ||||
|        </property> | ||||
|        <property name="checked"> | ||||
|         <bool>true</bool> | ||||
|        </property> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||
|      <property name="rightMargin"> | ||||
|  |  | |||
|  | @ -15,8 +15,12 @@ | |||
| #include "citra_qt/multiplayer/validation.h" | ||||
| #include "citra_qt/ui_settings.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/hle/service/cfg/cfg.h" | ||||
| #include "core/settings.h" | ||||
| #include "network/network.h" | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/web_backend.h" | ||||
| #endif | ||||
| 
 | ||||
| Lobby::Lobby(QWidget* parent, QStandardItemModel* list, | ||||
|              std::shared_ptr<Core::AnnounceMultiplayerSession> session) | ||||
|  | @ -105,7 +109,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { | |||
|         // 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->GetState() == Network::RoomMember::State::Joined) { | ||||
|         } else if (member->IsConnected()) { | ||||
|             // And ask if they want to leave the room if they are already in one.
 | ||||
|             if (!NetworkMessage::WarnDisconnect()) { | ||||
|                 return; | ||||
|  | @ -135,11 +139,27 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { | |||
|     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] { | ||||
|     QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_UID] { | ||||
|         std::string token; | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|         if (!Settings::values.citra_username.empty() && !Settings::values.citra_token.empty()) { | ||||
|             WebService::Client client(Settings::values.web_api_url, Settings::values.citra_username, | ||||
|                                       Settings::values.citra_token); | ||||
|             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::NoPreferredMac, password); | ||||
|             room_member->Join(nickname, Service::CFG::GetConsoleIdHash(Core::System::GetInstance()), | ||||
|                               ip.c_str(), port, 0, Network::NoPreferredMac, password, token); | ||||
|         } | ||||
|     }); | ||||
|     watcher->setFuture(f); | ||||
|  | @ -191,7 +211,8 @@ void Lobby::OnRefreshLobby() { | |||
|         QList<QVariant> members; | ||||
|         for (auto member : room.members) { | ||||
|             QVariant var; | ||||
|             var.setValue(LobbyMember{QString::fromStdString(member.name), member.game_id, | ||||
|             var.setValue(LobbyMember{QString::fromStdString(member.username), | ||||
|                                      QString::fromStdString(member.nickname), member.game_id, | ||||
|                                      QString::fromStdString(member.game_name)}); | ||||
|             members.append(var); | ||||
|         } | ||||
|  | @ -203,14 +224,18 @@ void Lobby::OnRefreshLobby() { | |||
|             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), | ||||
|                               room.port, QString::fromStdString(room.verify_UID)), | ||||
|             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) { | ||||
|         if (!room.description.empty()) { | ||||
|             first_item->appendRow( | ||||
|                 new LobbyItemDescription(QString::fromStdString(room.description))); | ||||
|         } | ||||
|         if (!room.members.empty()) { | ||||
|             first_item->appendRow(new LobbyItemExpandedMemberList(members)); | ||||
|         } | ||||
|     } | ||||
|  | @ -226,8 +251,8 @@ void Lobby::OnRefreshLobby() { | |||
|     // 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); | ||||
|         for (int j = 0; j < parent->rowCount(); j++) { | ||||
|             ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -55,6 +55,31 @@ public: | |||
|     } | ||||
| }; | ||||
| 
 | ||||
| class LobbyItemDescription : public LobbyItem { | ||||
| public: | ||||
|     static const int DescriptionRole = Qt::UserRole + 1; | ||||
| 
 | ||||
|     LobbyItemDescription() = default; | ||||
|     explicit LobbyItemDescription(QString description) { | ||||
|         setData(description, DescriptionRole); | ||||
|     } | ||||
| 
 | ||||
|     QVariant data(int role) const override { | ||||
|         if (role != Qt::DisplayRole) { | ||||
|             return LobbyItem::data(role); | ||||
|         } | ||||
|         auto description = data(DescriptionRole).toString(); | ||||
|         description.prepend("Description: "); | ||||
|         return description; | ||||
|     } | ||||
| 
 | ||||
|     bool operator<(const QStandardItem& other) const override { | ||||
|         return data(DescriptionRole) | ||||
|                    .toString() | ||||
|                    .localeAwareCompare(other.data(DescriptionRole).toString()) < 0; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| class LobbyItemGame : public LobbyItem { | ||||
| public: | ||||
|     static const int TitleIDRole = Qt::UserRole + 1; | ||||
|  | @ -95,12 +120,14 @@ public: | |||
|     static const int HostUsernameRole = Qt::UserRole + 1; | ||||
|     static const int HostIPRole = Qt::UserRole + 2; | ||||
|     static const int HostPortRole = Qt::UserRole + 3; | ||||
|     static const int HostVerifyUIDRole = Qt::UserRole + 4; | ||||
| 
 | ||||
|     LobbyItemHost() = default; | ||||
|     explicit LobbyItemHost(QString username, QString ip, u16 port) { | ||||
|     explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_UID) { | ||||
|         setData(username, HostUsernameRole); | ||||
|         setData(ip, HostIPRole); | ||||
|         setData(port, HostPortRole); | ||||
|         setData(verify_UID, HostVerifyUIDRole); | ||||
|     } | ||||
| 
 | ||||
|     QVariant data(int role) const override { | ||||
|  | @ -121,12 +148,17 @@ 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)) {} | ||||
|     explicit LobbyMember(QString username, QString nickname, u64 title_id, QString game_name) | ||||
|         : username(std::move(username)), nickname(std::move(nickname)), title_id(title_id), | ||||
|           game_name(std::move(game_name)) {} | ||||
|     ~LobbyMember() = default; | ||||
| 
 | ||||
|     QString GetUsername() const { | ||||
|         return username; | ||||
|     QString GetName() const { | ||||
|         if (username.isEmpty() || username == nickname) { | ||||
|             return nickname; | ||||
|         } else { | ||||
|             return QString("%1 (%2)").arg(nickname, username); | ||||
|         } | ||||
|     } | ||||
|     u64 GetTitleId() const { | ||||
|         return title_id; | ||||
|  | @ -137,6 +169,7 @@ public: | |||
| 
 | ||||
| private: | ||||
|     QString username; | ||||
|     QString nickname; | ||||
|     u64 title_id; | ||||
|     QString game_name; | ||||
| }; | ||||
|  | @ -195,10 +228,9 @@ public: | |||
|                 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()); | ||||
|                 out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName()); | ||||
|             } else { | ||||
|                 out += | ||||
|                     QString(QObject::tr("%1 is playing %2")).arg(m.GetUsername(), m.GetGameName()); | ||||
|                 out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); | ||||
|             } | ||||
|             first = false; | ||||
|         } | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ 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 USERNAME_NOT_VALID_SERVER( | ||||
|     QT_TR_NOOP("Username is already in use or not valid. 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( | ||||
|  | @ -22,6 +22,8 @@ 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 ROOM_IS_FULL( | ||||
|     QT_TR_NOOP("Unable to connect to the room because it is already full.")); | ||||
| const ConnectionError COULD_NOT_CREATE_ROOM( | ||||
|     QT_TR_NOOP("Creating a room failed. Please retry. Restarting Citra might be necessary.")); | ||||
| const ConnectionError HOST_BANNED( | ||||
|  | @ -34,8 +36,16 @@ 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 HOST_KICKED(QT_TR_NOOP("You have been kicked by the room host.")); | ||||
| const ConnectionError MAC_COLLISION( | ||||
|     QT_TR_NOOP("MAC address is already in use. Please choose another.")); | ||||
| const ConnectionError CONSOLE_ID_COLLISION(QT_TR_NOOP( | ||||
|     "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " | ||||
|     "> Configure > System to regenerate your Console ID.")); | ||||
| const ConnectionError PERMISSION_DENIED( | ||||
|     QT_TR_NOOP("You do not have enough permission to perform this action.")); | ||||
| const ConnectionError NO_SUCH_USER(QT_TR_NOOP( | ||||
|     "The user you are trying to kick/ban could not be found.\nThey may have left the room.")); | ||||
| 
 | ||||
| static bool WarnMessage(const std::string& title, const std::string& text) { | ||||
|     return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), | ||||
|  |  | |||
|  | @ -20,20 +20,27 @@ private: | |||
|     std::string err; | ||||
| }; | ||||
| 
 | ||||
| /// When the nickname is considered invalid by the client
 | ||||
| extern const ConnectionError USERNAME_NOT_VALID; | ||||
| extern const ConnectionError ROOMNAME_NOT_VALID; | ||||
| extern const ConnectionError USERNAME_IN_USE; | ||||
| /// When the nickname is considered invalid by the room server
 | ||||
| extern const ConnectionError USERNAME_NOT_VALID_SERVER; | ||||
| 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 ROOM_IS_FULL; | ||||
| 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 HOST_KICKED; | ||||
| extern const ConnectionError MAC_COLLISION; | ||||
| extern const ConnectionError CONSOLE_ID_COLLISION; | ||||
| extern const ConnectionError PERMISSION_DENIED; | ||||
| extern const ConnectionError NO_SUCH_USER; | ||||
| 
 | ||||
| /**
 | ||||
|  *  Shows a standard QMessageBox with a error message | ||||
|  |  | |||
							
								
								
									
										113
									
								
								src/citra_qt/multiplayer/moderation_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/citra_qt/multiplayer/moderation_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| // Copyright 2018 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QStandardItem> | ||||
| #include <QStandardItemModel> | ||||
| #include "citra_qt/multiplayer/moderation_dialog.h" | ||||
| #include "network/network.h" | ||||
| #include "network/room_member.h" | ||||
| #include "ui_moderation_dialog.h" | ||||
| 
 | ||||
| namespace Column { | ||||
| enum { | ||||
|     SUBJECT, | ||||
|     TYPE, | ||||
|     COUNT, | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| ModerationDialog::ModerationDialog(QWidget* parent) | ||||
|     : QDialog(parent), ui(std::make_unique<Ui::ModerationDialog>()) { | ||||
|     ui->setupUi(this); | ||||
| 
 | ||||
|     qRegisterMetaType<Network::Room::BanList>(); | ||||
| 
 | ||||
|     if (auto member = Network::GetRoomMember().lock()) { | ||||
|         callback_handle_status_message = member->BindOnStatusMessageReceived( | ||||
|             [this](const Network::StatusMessageEntry& status_message) { | ||||
|                 emit StatusMessageReceived(status_message); | ||||
|             }); | ||||
|         connect(this, &ModerationDialog::StatusMessageReceived, this, | ||||
|                 &ModerationDialog::OnStatusMessageReceived); | ||||
|         callback_handle_ban_list = member->BindOnBanListReceived( | ||||
|             [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); }); | ||||
|         connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList); | ||||
|     } | ||||
| 
 | ||||
|     // Initialize the UI
 | ||||
|     model = new QStandardItemModel(ui->ban_list_view); | ||||
|     model->insertColumns(0, Column::COUNT); | ||||
|     model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject")); | ||||
|     model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type")); | ||||
| 
 | ||||
|     ui->ban_list_view->setModel(model); | ||||
| 
 | ||||
|     // Load the ban list in background
 | ||||
|     LoadBanList(); | ||||
| 
 | ||||
|     connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); }); | ||||
|     connect(ui->unban, &QPushButton::clicked, this, [this] { | ||||
|         auto index = ui->ban_list_view->currentIndex(); | ||||
|         SendUnbanRequest(model->item(index.row(), 0)->text()); | ||||
|     }); | ||||
|     connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); }); | ||||
| } | ||||
| 
 | ||||
| ModerationDialog::~ModerationDialog() { | ||||
|     if (callback_handle_status_message) { | ||||
|         if (auto room = Network::GetRoomMember().lock()) { | ||||
|             room->Unbind(callback_handle_status_message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (callback_handle_ban_list) { | ||||
|         if (auto room = Network::GetRoomMember().lock()) { | ||||
|             room->Unbind(callback_handle_ban_list); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void ModerationDialog::LoadBanList() { | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         ui->refresh->setEnabled(false); | ||||
|         ui->refresh->setText(tr("Refreshing")); | ||||
|         ui->unban->setEnabled(false); | ||||
|         room->RequestBanList(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { | ||||
|     model->removeRows(0, model->rowCount()); | ||||
|     for (const auto& username : ban_list.first) { | ||||
|         QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username)); | ||||
|         QStandardItem* type_item = new QStandardItem(tr("Forum Username")); | ||||
|         model->invisibleRootItem()->appendRow({subject_item, type_item}); | ||||
|     } | ||||
|     for (const auto& ip : ban_list.second) { | ||||
|         QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip)); | ||||
|         QStandardItem* type_item = new QStandardItem(tr("IP Address")); | ||||
|         model->invisibleRootItem()->appendRow({subject_item, type_item}); | ||||
|     } | ||||
|     for (int i = 0; i < Column::COUNT - 1; ++i) { | ||||
|         ui->ban_list_view->resizeColumnToContents(i); | ||||
|     } | ||||
|     ui->refresh->setEnabled(true); | ||||
|     ui->refresh->setText(tr("Refresh")); | ||||
|     ui->unban->setEnabled(false); | ||||
| } | ||||
| 
 | ||||
| void ModerationDialog::SendUnbanRequest(const QString& subject) { | ||||
|     if (auto room = Network::GetRoomMember().lock()) { | ||||
|         room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) { | ||||
|     if (status_message.type != Network::IdMemberBanned && | ||||
|         status_message.type != Network::IdAddressUnbanned) | ||||
|         return; | ||||
| 
 | ||||
|     // Update the ban list for ban/unban
 | ||||
|     LoadBanList(); | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/citra_qt/multiplayer/moderation_dialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/citra_qt/multiplayer/moderation_dialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| // Copyright 2018 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <optional> | ||||
| #include <QDialog> | ||||
| #include "network/room.h" | ||||
| #include "network/room_member.h" | ||||
| 
 | ||||
| namespace Ui { | ||||
| class ModerationDialog; | ||||
| } | ||||
| 
 | ||||
| class QStandardItemModel; | ||||
| 
 | ||||
| class ModerationDialog : public QDialog { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|     explicit ModerationDialog(QWidget* parent = nullptr); | ||||
|     ~ModerationDialog(); | ||||
| 
 | ||||
| signals: | ||||
|     void StatusMessageReceived(const Network::StatusMessageEntry&); | ||||
|     void BanListReceived(const Network::Room::BanList&); | ||||
| 
 | ||||
| private: | ||||
|     void LoadBanList(); | ||||
|     void PopulateBanList(const Network::Room::BanList& ban_list); | ||||
|     void SendUnbanRequest(const QString& subject); | ||||
|     void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message); | ||||
| 
 | ||||
|     std::unique_ptr<Ui::ModerationDialog> ui; | ||||
|     QStandardItemModel* model; | ||||
|     Network::RoomMember::CallbackHandle<Network::StatusMessageEntry> callback_handle_status_message; | ||||
|     Network::RoomMember::CallbackHandle<Network::Room::BanList> callback_handle_ban_list; | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(Network::Room::BanList); | ||||
							
								
								
									
										84
									
								
								src/citra_qt/multiplayer/moderation_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/citra_qt/multiplayer/moderation_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ModerationDialog</class> | ||||
|  <widget class="QDialog" name="ModerationDialog"> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Moderation</string> | ||||
|   </property> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>500</width> | ||||
|     <height>300</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout"> | ||||
|    <item> | ||||
|     <widget class="QGroupBox" name="ban_list_group_box"> | ||||
|      <property name="title"> | ||||
|       <string>Ban List</string> | ||||
|      </property> | ||||
|      <layout class="QVBoxLayout"> | ||||
|       <item> | ||||
|        <layout class="QHBoxLayout"> | ||||
|         <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"> | ||||
|           <property name="text"> | ||||
|            <string>Refreshing</string> | ||||
|           </property> | ||||
|           <property name="enabled"> | ||||
|            <bool>false</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item> | ||||
|          <widget class="QPushButton" name="unban"> | ||||
|           <property name="text"> | ||||
|            <string>Unban</string> | ||||
|           </property> | ||||
|           <property name="enabled"> | ||||
|            <bool>false</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|        </layout> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QTreeView" name="ban_list_view"/> | ||||
|       </item> | ||||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <widget class="QDialogButtonBox" name="buttonBox"> | ||||
|      <property name="standardButtons"> | ||||
|       <set>QDialogButtonBox::Ok</set> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <connections> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>accepted()</signal> | ||||
|    <receiver>ModerationDialog</receiver> | ||||
|    <slot>accept()</slot> | ||||
|   </connection> | ||||
|  </connections> | ||||
|  <resources/> | ||||
| </ui> | ||||
|  | @ -3,6 +3,7 @@ | |||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <QAction> | ||||
| #include <QApplication> | ||||
| #include <QIcon> | ||||
| #include <QMessageBox> | ||||
| #include <QStandardItemModel> | ||||
|  | @ -13,6 +14,7 @@ | |||
| #include "citra_qt/multiplayer/lobby.h" | ||||
| #include "citra_qt/multiplayer/message.h" | ||||
| #include "citra_qt/multiplayer/state.h" | ||||
| #include "citra_qt/ui_settings.h" | ||||
| #include "citra_qt/util/clickable_label.h" | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "common/logging/log.h" | ||||
|  | @ -27,9 +29,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis | |||
|             [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); | ||||
|         connect(this, &MultiplayerState::NetworkStateChanged, this, | ||||
|                 &MultiplayerState::OnNetworkStateChanged); | ||||
|         error_callback_handle = member->BindOnError( | ||||
|             [this](const Network::RoomMember::Error& error) { emit NetworkError(error); }); | ||||
|         connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError); | ||||
|     } | ||||
| 
 | ||||
|     qRegisterMetaType<Network::RoomMember::State>(); | ||||
|     qRegisterMetaType<Network::RoomMember::Error>(); | ||||
|     qRegisterMetaType<Common::WebResult>(); | ||||
|     announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(); | ||||
|     announce_multiplayer_session->BindErrorCallback( | ||||
|  | @ -44,6 +50,13 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis | |||
| 
 | ||||
|     connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); | ||||
|     connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); | ||||
| 
 | ||||
|     connect(static_cast<QApplication*>(QApplication::instance()), &QApplication::focusChanged, this, | ||||
|             [this](QWidget* /*old*/, QWidget* now) { | ||||
|                 if (client_room && client_room->isAncestorOf(now)) { | ||||
|                     HideNotification(); | ||||
|                 } | ||||
|             }); | ||||
| } | ||||
| 
 | ||||
| MultiplayerState::~MultiplayerState() { | ||||
|  | @ -52,6 +65,12 @@ MultiplayerState::~MultiplayerState() { | |||
|             member->Unbind(state_callback_handle); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (error_callback_handle) { | ||||
|         if (auto member = Network::GetRoomMember().lock()) { | ||||
|             member->Unbind(error_callback_handle); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::Close() { | ||||
|  | @ -70,7 +89,9 @@ void MultiplayerState::retranslateUi() { | |||
| 
 | ||||
|     if (current_state == Network::RoomMember::State::Uninitialized) { | ||||
|         status_text->setText(tr("Not Connected. Click here to find a room!")); | ||||
|     } else if (current_state == Network::RoomMember::State::Joined) { | ||||
|     } else if (current_state == Network::RoomMember::State::Joined || | ||||
|                current_state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         status_text->setText(tr("Connected")); | ||||
|     } else { | ||||
|         status_text->setText(tr("Not Connected")); | ||||
|  | @ -88,35 +109,10 @@ void MultiplayerState::retranslateUi() { | |||
| 
 | ||||
| void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { | ||||
|     LOG_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; | ||||
|     if (state == Network::RoomMember::State::Joined || | ||||
|         state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         OnOpenNetworkRoom(); | ||||
|         break; | ||||
|     } | ||||
|     if (is_connected) { | ||||
|         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||
|         status_text->setText(tr("Connected")); | ||||
|         leave_room->setEnabled(true); | ||||
|  | @ -131,6 +127,51 @@ void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& s | |||
|     current_state = state; | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) { | ||||
|     LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error)); | ||||
|     switch (error) { | ||||
|     case Network::RoomMember::Error::LostConnection: | ||||
|         NetworkMessage::ShowError(NetworkMessage::LOST_CONNECTION); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::HostKicked: | ||||
|         NetworkMessage::ShowError(NetworkMessage::HOST_KICKED); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::CouldNotConnect: | ||||
|         NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::NameCollision: | ||||
|         NetworkMessage::ShowError(NetworkMessage::USERNAME_NOT_VALID_SERVER); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::MacCollision: | ||||
|         NetworkMessage::ShowError(NetworkMessage::MAC_COLLISION); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::ConsoleIdCollision: | ||||
|         NetworkMessage::ShowError(NetworkMessage::CONSOLE_ID_COLLISION); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::RoomIsFull: | ||||
|         NetworkMessage::ShowError(NetworkMessage::ROOM_IS_FULL); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::WrongPassword: | ||||
|         NetworkMessage::ShowError(NetworkMessage::WRONG_PASSWORD); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::WrongVersion: | ||||
|         NetworkMessage::ShowError(NetworkMessage::WRONG_VERSION); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::HostBanned: | ||||
|         NetworkMessage::ShowError(NetworkMessage::HOST_BANNED); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::UnknownError: | ||||
|         NetworkMessage::ShowError(NetworkMessage::UNABLE_TO_CONNECT); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::PermissionDenied: | ||||
|         NetworkMessage::ShowError(NetworkMessage::PERMISSION_DENIED); | ||||
|         break; | ||||
|     case Network::RoomMember::Error::NoSuchUser: | ||||
|         NetworkMessage::ShowError(NetworkMessage::NO_SUCH_USER); | ||||
|         break; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { | ||||
|     announce_multiplayer_session->Stop(); | ||||
|     QMessageBox::warning( | ||||
|  | @ -144,7 +185,11 @@ void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) { | |||
| } | ||||
| 
 | ||||
| void MultiplayerState::UpdateThemedIcons() { | ||||
|     if (current_state == Network::RoomMember::State::Joined) { | ||||
|     if (show_notification) { | ||||
|         status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16)); | ||||
|     } else if (current_state == Network::RoomMember::State::Joined || | ||||
|                current_state == Network::RoomMember::State::Moderator) { | ||||
| 
 | ||||
|         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||
|     } else { | ||||
|         status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16)); | ||||
|  | @ -185,6 +230,10 @@ bool MultiplayerState::OnCloseRoom() { | |||
|         if (room->GetState() != Network::Room::State::Open) { | ||||
|             return true; | ||||
|         } | ||||
|         // Save ban list
 | ||||
|         if (auto room = Network::GetRoom().lock()) { | ||||
|             UISettings::values.ban_list = std::move(room->GetBanList()); | ||||
|         } | ||||
|         room->Destroy(); | ||||
|         announce_multiplayer_session->Stop(); | ||||
|         LOG_DEBUG(Frontend, "Closed the room (as a server)"); | ||||
|  | @ -192,11 +241,28 @@ bool MultiplayerState::OnCloseRoom() { | |||
|     return true; | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::ShowNotification() { | ||||
|     if (client_room && client_room->isAncestorOf(QApplication::focusWidget())) | ||||
|         return; // Do not show notification if the chat window currently has focus
 | ||||
|     show_notification = true; | ||||
|     QApplication::alert(nullptr); | ||||
|     status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16)); | ||||
|     status_text->setText(tr("New Messages Received")); | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::HideNotification() { | ||||
|     show_notification = false; | ||||
|     status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16)); | ||||
|     status_text->setText(tr("Connected")); | ||||
| } | ||||
| 
 | ||||
| void MultiplayerState::OnOpenNetworkRoom() { | ||||
|     if (auto member = Network::GetRoomMember().lock()) { | ||||
|         if (member->IsConnected()) { | ||||
|             if (client_room == nullptr) { | ||||
|                 client_room = new ClientRoomWindow(this); | ||||
|                 connect(client_room, &ClientRoomWindow::ShowNotification, this, | ||||
|                         &MultiplayerState::ShowNotification); | ||||
|             } | ||||
|             BringWidgetToFront(client_room); | ||||
|             return; | ||||
|  |  | |||
|  | @ -40,6 +40,7 @@ public: | |||
| 
 | ||||
| public slots: | ||||
|     void OnNetworkStateChanged(const Network::RoomMember::State& state); | ||||
|     void OnNetworkError(const Network::RoomMember::Error& error); | ||||
|     void OnViewLobby(); | ||||
|     void OnCreateRoom(); | ||||
|     bool OnCloseRoom(); | ||||
|  | @ -47,9 +48,12 @@ public slots: | |||
|     void OnDirectConnectToRoom(); | ||||
|     void OnAnnounceFailed(const Common::WebResult&); | ||||
|     void UpdateThemedIcons(); | ||||
|     void ShowNotification(); | ||||
|     void HideNotification(); | ||||
| 
 | ||||
| signals: | ||||
|     void NetworkStateChanged(const Network::RoomMember::State&); | ||||
|     void NetworkError(const Network::RoomMember::Error&); | ||||
|     void AnnounceFailed(const Common::WebResult&); | ||||
| 
 | ||||
| private: | ||||
|  | @ -64,7 +68,11 @@ private: | |||
|     QAction* show_room; | ||||
|     std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; | ||||
|     Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; | ||||
|     bool has_mod_perms = false; | ||||
|     Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle; | ||||
|     Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle; | ||||
| 
 | ||||
|     bool show_notification = false; | ||||
| }; | ||||
| 
 | ||||
| Q_DECLARE_METATYPE(Common::WebResult); | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <array> | ||||
| #include <string> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #include <QByteArray> | ||||
| #include <QMetaType> | ||||
|  | @ -109,6 +111,8 @@ struct Values { | |||
|     QString room_port; | ||||
|     uint host_type; | ||||
|     qulonglong game_id; | ||||
|     QString room_description; | ||||
|     std::pair<std::vector<std::string>, std::vector<std::string>> ban_list; | ||||
| 
 | ||||
|     // logging
 | ||||
|     bool show_console; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue