code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QInputDialog>
#include <QList>
#include <QtConcurrent/QtConcurrentRun>
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/internal_network/network_interface.h"
#include "network/network.h"
#include "ui_lobby.h"
#include "yuzu/game_list_p.h"
#include "yuzu/main.h"
#include "yuzu/multiplayer/client_room.h"
#include "yuzu/multiplayer/lobby.h"
#include "yuzu/multiplayer/lobby_p.h"
#include "yuzu/multiplayer/message.h"
#include "yuzu/multiplayer/state.h"
#include "yuzu/multiplayer/validation.h"
#include "yuzu/uisettings.h"
#ifdef ENABLE_WEB_SERVICE
#include "web_service/web_backend.h"
#endif

Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
             std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
    : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
      ui(std::make_unique<Ui::Lobby>()),
      announce_multiplayer_session(session), system{system_}, room_network{
                                                                  system.GetRoomNetwork()} {
    ui->setupUi(this);

    // setup the watcher for background connections
    watcher = new QFutureWatcher<void>;

    model = new QStandardItemModel(ui->room_list);

    // Create a proxy to the game list to get the list of games owned
    game_list = new QStandardItemModel;
    UpdateGameList(list);

    proxy = new LobbyFilterProxyModel(this, game_list);
    proxy->setSourceModel(model);
    proxy->setDynamicSortFilter(true);
    proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
    proxy->setSortLocaleAware(true);
    ui->room_list->setModel(proxy);
    ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive);
    ui->room_list->header()->stretchLastSection();
    ui->room_list->setAlternatingRowColors(true);
    ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
    ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
    ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
    ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
    ui->room_list->setSortingEnabled(true);
    ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
    ui->room_list->setExpandsOnDoubleClick(false);
    ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);

    ui->nickname->setValidator(validation.GetNickname());
    ui->nickname->setText(
        QString::fromStdString(UISettings::values.multiplayer_nickname.GetValue()));

    // Try find the best nickname by default
    if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("yuzu")) {
        if (!Settings::values.yuzu_username.GetValue().empty()) {
            ui->nickname->setText(
                QString::fromStdString(Settings::values.yuzu_username.GetValue()));
        } else if (!GetProfileUsername().empty()) {
            ui->nickname->setText(QString::fromStdString(GetProfileUsername()));
        } else {
            ui->nickname->setText(QStringLiteral("yuzu"));
        }
    }

    // UI Buttons
    connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby);
    connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
    connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned);
    connect(ui->hide_empty, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterEmpty);
    connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull);
    connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
    connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);

    // Actions
    connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
            &Lobby::OnRefreshLobby);

    // Load persistent filters after events are connected to make sure they apply
    ui->search->setText(
        QString::fromStdString(UISettings::values.multiplayer_filter_text.GetValue()));
    ui->games_owned->setChecked(UISettings::values.multiplayer_filter_games_owned.GetValue());
    ui->hide_empty->setChecked(UISettings::values.multiplayer_filter_hide_empty.GetValue());
    ui->hide_full->setChecked(UISettings::values.multiplayer_filter_hide_full.GetValue());
}

Lobby::~Lobby() = default;

void Lobby::UpdateGameList(QStandardItemModel* list) {
    game_list->clear();
    for (int i = 0; i < list->rowCount(); i++) {
        auto parent = list->item(i, 0);
        for (int j = 0; j < parent->rowCount(); j++) {
            game_list->appendRow(parent->child(j)->clone());
        }
    }
    if (proxy)
        proxy->UpdateGameList(game_list);
    ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
}

void Lobby::RetranslateUi() {
    ui->retranslateUi(this);
}

QString Lobby::PasswordPrompt() {
    bool ok;
    const QString text =
        QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
                              QLineEdit::Password, QString(), &ok);
    return ok ? text : QString();
}

void Lobby::OnExpandRoom(const QModelIndex& index) {
    QModelIndex member_index = proxy->index(index.row(), Column::MEMBER);
    auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList();
}

void Lobby::OnJoinRoom(const QModelIndex& source) {
    if (!Network::GetSelectedNetworkInterface()) {
        LOG_INFO(WebService, "Automatically selected network interface for room network.");
        Network::SelectFirstNetworkInterface();
    }

    if (!Network::GetSelectedNetworkInterface()) {
        NetworkMessage::ErrorManager::ShowError(
            NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
        return;
    }

    if (system.IsPoweredOn()) {
        if (!NetworkMessage::WarnGameRunning()) {
            return;
        }
    }

    if (const auto member = room_network.GetRoomMember().lock()) {
        // Prevent the user from trying to join a room while they are already joining.
        if (member->GetState() == Network::RoomMember::State::Joining) {
            return;
        } else if (member->IsConnected()) {
            // And ask if they want to leave the room if they are already in one.
            if (!NetworkMessage::WarnDisconnect()) {
                return;
            }
        }
    }
    QModelIndex index = source;
    // If the user double clicks on a child row (aka the player list) then use the parent instead
    if (source.parent() != QModelIndex()) {
        index = source.parent();
    }
    if (!ui->nickname->hasAcceptableInput()) {
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
        return;
    }

    // Get a password to pass if the room is password protected
    QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME);
    bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool();
    const std::string password = has_password ? PasswordPrompt().toStdString() : "";
    if (has_password && password.empty()) {
        return;
    }

    QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
    const std::string nickname = ui->nickname->text().toStdString();
    const std::string ip =
        proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
    int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
    const std::string verify_uid =
        proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString();

    // attempt to connect in a different thread
    QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] {
        std::string token;
#ifdef ENABLE_WEB_SERVICE
        if (!Settings::values.yuzu_username.GetValue().empty() &&
            !Settings::values.yuzu_token.GetValue().empty()) {
            WebService::Client client(Settings::values.web_api_url.GetValue(),
                                      Settings::values.yuzu_username.GetValue(),
                                      Settings::values.yuzu_token.GetValue());
            token = client.GetExternalJWT(verify_uid).returned_data;
            if (token.empty()) {
                LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
            } else {
                LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
            }
        }
#endif
        if (auto room_member = room_network.GetRoomMember().lock()) {
            room_member->Join(nickname, ip.c_str(), port, 0, Network::NoPreferredIP, password,
                              token);
        }
    });
    watcher->setFuture(f);

    // TODO(jroweboy): disable widgets and display a connecting while we wait

    // Save settings
    UISettings::values.multiplayer_nickname = ui->nickname->text().toStdString();
    UISettings::values.multiplayer_filter_text = ui->search->text().toStdString();
    UISettings::values.multiplayer_filter_games_owned = ui->games_owned->isChecked();
    UISettings::values.multiplayer_filter_hide_empty = ui->hide_empty->isChecked();
    UISettings::values.multiplayer_filter_hide_full = ui->hide_full->isChecked();
    UISettings::values.multiplayer_ip =
        proxy->data(connection_index, LobbyItemHost::HostIPRole).value<QString>().toStdString();
    UISettings::values.multiplayer_port =
        proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
    emit SaveConfig();
}

void Lobby::ResetModel() {
    model->clear();
    model->insertColumns(0, Column::TOTAL);
    model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
    model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
    model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
    model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
}

void Lobby::RefreshLobby() {
    if (auto session = announce_multiplayer_session.lock()) {
        ResetModel();
        ui->refresh_list->setEnabled(false);
        ui->refresh_list->setText(tr("Refreshing"));
        room_list_watcher.setFuture(
            QtConcurrent::run([session]() { return session->GetRoomList(); }));
    } else {
        // TODO(jroweboy): Display an error box about announce couldn't be started
    }
}

void Lobby::OnRefreshLobby() {
    AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result();
    for (auto room : new_room_list) {
        // find the icon for the game if this person owns that game.
        QPixmap smdh_icon;
        for (int r = 0; r < game_list->rowCount(); ++r) {
            auto index = game_list->index(r, 0);
            auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();

            if (game_id != 0 && room.information.preferred_game.id == game_id) {
                smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
            }
        }

        QList<QVariant> members;
        for (auto member : room.members) {
            QVariant var;
            var.setValue(LobbyMember{QString::fromStdString(member.username),
                                     QString::fromStdString(member.nickname), member.game.id,
                                     QString::fromStdString(member.game.name)});
            members.append(var);
        }

        auto first_item = new LobbyItemGame(
            room.information.preferred_game.id,
            QString::fromStdString(room.information.preferred_game.name), smdh_icon);
        auto row = QList<QStandardItem*>({
            first_item,
            new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)),
            new LobbyItemMemberList(members, room.information.member_slots),
            new LobbyItemHost(QString::fromStdString(room.information.host_username),
                              QString::fromStdString(room.ip), room.information.port,
                              QString::fromStdString(room.verify_uid)),
        });
        model->appendRow(row);
        // To make the rows expandable, add the member data as a child of the first column of the
        // rows with people in them and have qt set them to colspan after the model is finished
        // resetting
        if (!room.information.description.empty()) {
            first_item->appendRow(
                new LobbyItemDescription(QString::fromStdString(room.information.description)));
        }
        if (!room.members.empty()) {
            first_item->appendRow(new LobbyItemExpandedMemberList(members));
        }
    }

    // Re-enable the refresh button and resize the columns
    ui->refresh_list->setEnabled(true);
    ui->refresh_list->setText(tr("Refresh List"));
    ui->room_list->header()->stretchLastSection();
    for (int i = 0; i < Column::TOTAL - 1; ++i) {
        ui->room_list->resizeColumnToContents(i);
    }

    // Set the member list child items to span all columns
    for (int i = 0; i < proxy->rowCount(); i++) {
        auto parent = model->item(i, 0);
        for (int j = 0; j < parent->rowCount(); j++) {
            ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true);
        }
    }

    ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
}

std::string Lobby::GetProfileUsername() {
    const auto& current_user =
        system.GetProfileManager().GetUser(Settings::values.current_user.GetValue());
    Service::Account::ProfileBase profile{};

    if (!current_user.has_value()) {
        return "";
    }

    if (!system.GetProfileManager().GetProfileBase(*current_user, profile)) {
        return "";
    }

    const auto text = Common::StringFromFixedZeroTerminatedBuffer(
        reinterpret_cast<const char*>(profile.username.data()), profile.username.size());

    return text;
}

LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
    : QSortFilterProxyModel(parent), game_list(list) {}

void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) {
    game_list = list;
}

bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
    // Prioritize filters by fastest to compute

    // pass over any child rows (aka row that shows the players in the room)
    if (sourceParent != QModelIndex()) {
        return true;
    }

    // filter by empty rooms
    if (filter_empty) {
        QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
        int player_count =
            sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
        if (player_count == 0) {
            return false;
        }
    }

    // filter by filled rooms
    if (filter_full) {
        QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
        int player_count =
            sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
        int max_players =
            sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
        if (player_count >= max_players) {
            return false;
        }
    }

    // filter by search parameters
    if (!filter_search.isEmpty()) {
        QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
        QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
        QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
        bool preferred_game_match = sourceModel()
                                        ->data(game_name, LobbyItemGame::GameNameRole)
                                        .toString()
                                        .contains(filter_search, filterCaseSensitivity());
        bool room_name_match = sourceModel()
                                   ->data(room_name, LobbyItemName::NameRole)
                                   .toString()
                                   .contains(filter_search, filterCaseSensitivity());
        bool username_match = sourceModel()
                                  ->data(host_name, LobbyItemHost::HostUsernameRole)
                                  .toString()
                                  .contains(filter_search, filterCaseSensitivity());
        if (!preferred_game_match && !room_name_match && !username_match) {
            return false;
        }
    }

    // filter by game owned
    if (filter_owned) {
        QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
        QList<QModelIndex> owned_games;
        for (int r = 0; r < game_list->rowCount(); ++r) {
            owned_games.append(QModelIndex(game_list->index(r, 0)));
        }
        auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
        if (current_id == 0) {
            // TODO(jroweboy): homebrew often doesn't have a game id and this hides them
            return false;
        }
        bool owned = false;
        for (const auto& game : owned_games) {
            auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
            if (current_id == game_id) {
                owned = true;
            }
        }
        if (!owned) {
            return false;
        }
    }

    return true;
}

void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
    sourceModel()->sort(column, order);
}

void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
    filter_owned = filter;
    invalidate();
}

void LobbyFilterProxyModel::SetFilterEmpty(bool filter) {
    filter_empty = filter;
    invalidate();
}

void LobbyFilterProxyModel::SetFilterFull(bool filter) {
    filter_full = filter;
    invalidate();
}

void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
    filter_search = filter;
    invalidate();
}


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <memory>
#include <QDialog>
#include <QFutureWatcher>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include "common/announce_multiplayer_room.h"
#include "network/announce_multiplayer_session.h"
#include "network/network.h"
#include "yuzu/multiplayer/validation.h"

namespace Ui {
class Lobby;
}

class LobbyModel;
class LobbyFilterProxyModel;

namespace Core {
class System;
}

/**
 * Listing of all public games pulled from services. The lobby should be simple enough for users to
 * find the game they want to play, and join it.
 */
class Lobby : public QDialog {
    Q_OBJECT

public:
    explicit Lobby(QWidget* parent, QStandardItemModel* list,
                   std::shared_ptr<Core::AnnounceMultiplayerSession> session,
                   Core::System& system_);
    ~Lobby() override;

    /**
     * Updates the lobby with a new game list model.
     * This model should be the original model of the game list.
     */
    void UpdateGameList(QStandardItemModel* list);
    void RetranslateUi();

public slots:
    /**
     * Begin the process to pull the latest room list from web services. After the listing is
     * returned from web services, `LobbyRefreshed` will be signalled
     */
    void RefreshLobby();

private slots:
    /**
     * Pulls the list of rooms from network and fills out the lobby model with the results
     */
    void OnRefreshLobby();

    /**
     * Handler for single clicking on a room in the list. Expands the treeitem to show player
     * information for the people in the room
     *
     * index - The row of the proxy model that the user wants to join.
     */
    void OnExpandRoom(const QModelIndex&);

    /**
     * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts
     * to connect. Will also prompt for a password in case one is required.
     *
     * index - The row of the proxy model that the user wants to join.
     */
    void OnJoinRoom(const QModelIndex&);

signals:
    void StateChanged(const Network::RoomMember::State&);
    void SaveConfig();

private:
    std::string GetProfileUsername();

    /**
     * Removes all entries in the Lobby before refreshing.
     */
    void ResetModel();

    /**
     * Prompts for a password. Returns an empty QString if the user either did not provide a
     * password or if the user closed the window.
     */
    QString PasswordPrompt();

    std::unique_ptr<Ui::Lobby> ui;

    QStandardItemModel* model{};
    QStandardItemModel* game_list{};
    LobbyFilterProxyModel* proxy{};

    QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher;
    std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
    QFutureWatcher<void>* watcher;
    Validation validation;
    Core::System& system;
    Network::RoomNetwork& room_network;
};

/**
 * Proxy Model for filtering the lobby
 */
class LobbyFilterProxyModel : public QSortFilterProxyModel {
    Q_OBJECT;

public:
    explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list);

    /**
     * Updates the filter with a new game list model.
     * This model should be the processed one created by the Lobby.
     */
    void UpdateGameList(QStandardItemModel* list);

    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
    void sort(int column, Qt::SortOrder order) override;

public slots:
    void SetFilterOwned(bool);
    void SetFilterEmpty(bool);
    void SetFilterFull(bool);
    void SetFilterSearch(const QString&);

private:
    QStandardItemModel* game_list;
    bool filter_owned = false;
    bool filter_empty = false;
    bool filter_full = false;
    QString filter_search;
};

repo_name: litucks/torzu

license: mit


code:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Lobby</class>
 <widget class="QWidget" name="Lobby">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>903</width>
    <height>487</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Public Room Browser</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <layout class="QVBoxLayout" name="verticalLayout_2">
     <property name="spacing">
      <number>3</number>
     </property>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout_3">
       <property name="spacing">
        <number>6</number>
       </property>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_5">
         <item>
          <widget class="QLabel" name="label">
           <property name="text">
            <string>Nickname</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="nickname">
           <property name="placeholderText">
            <string>Nickname</string>
           </property>
          </widget>
         </item>
         <item>
          <spacer name="horizontalSpacer_2">
           <property name="orientation">
            <enum>Qt::Horizontal</enum>
           </property>
           <property name="sizeHint" stdset="0">
            <size>
             <width>40</width>
             <height>20</height>
            </size>
           </property>
          </spacer>
         </item>
         <item>
          <widget class="QLabel" name="label_2">
           <property name="text">
            <string>Filters</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="search">
           <property name="placeholderText">
            <string>Search</string>
           </property>
           <property name="clearButtonEnabled">
            <bool>true</bool>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QCheckBox" name="games_owned">
           <property name="text">
            <string>Games I Own</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QCheckBox" name="hide_empty">
           <property name="text">
            <string>Hide Empty Rooms</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QCheckBox" name="hide_full">
           <property name="text">
            <string>Hide Full Rooms</string>
           </property>
          </widget>
         </item>
         <item>
          <spacer name="horizontalSpacer">
           <property name="orientation">
            <enum>Qt::Horizontal</enum>
           </property>
           <property name="sizeHint" stdset="0">
            <size>
             <width>40</width>
             <height>20</height>
            </size>
           </property>
          </spacer>
         </item>
         <item>
          <widget class="QPushButton" name="refresh_list">
           <property name="text">
            <string>Refresh Lobby</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
      </layout>
     </item>
     <item>
      <widget class="QTreeView" name="room_list"/>
     </item>
     <item>
      <widget class="QWidget" name="widget" native="true"/>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <utility>
#include <QPixmap>
#include <QStandardItem>
#include <QStandardItemModel>
#include "common/common_types.h"

namespace Column {
enum List {
    GAME_NAME,
    ROOM_NAME,
    MEMBER,
    HOST,
    TOTAL,
};
}

class LobbyItem : public QStandardItem {
public:
    LobbyItem() = default;
    explicit LobbyItem(const QString& string) : QStandardItem(string) {}
    virtual ~LobbyItem() override = default;
};

class LobbyItemName : public LobbyItem {
public:
    static const int NameRole = Qt::UserRole + 1;
    static const int PasswordRole = Qt::UserRole + 2;

    LobbyItemName() = default;
    explicit LobbyItemName(bool has_password, QString name) : LobbyItem() {
        setData(name, NameRole);
        setData(has_password, PasswordRole);
    }

    QVariant data(int role) const override {
        if (role == Qt::DecorationRole) {
            bool has_password = data(PasswordRole).toBool();
            return has_password ? QIcon::fromTheme(QStringLiteral("lock")).pixmap(16) : QIcon();
        }
        if (role != Qt::DisplayRole) {
            return LobbyItem::data(role);
        }
        return data(NameRole).toString();
    }

    bool operator<(const QStandardItem& other) const override {
        return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0;
    }
};

class 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(QStringLiteral("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;
    static const int GameNameRole = Qt::UserRole + 2;
    static const int GameIconRole = Qt::UserRole + 3;

    LobbyItemGame() = default;
    explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) {
        setData(static_cast<unsigned long long>(title_id), TitleIDRole);
        setData(game_name, GameNameRole);
        if (!smdh_icon.isNull()) {
            setData(smdh_icon, GameIconRole);
        } else {
            setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(32), GameIconRole);
        }
    }

    QVariant data(int role) const override {
        if (role == Qt::DecorationRole) {
            auto val = data(GameIconRole);
            if (val.isValid()) {
                val = val.value<QPixmap>().scaled(32, 32, Qt::KeepAspectRatio,
                                                  Qt::TransformationMode::SmoothTransformation);
            } else {
                auto blank_image = QPixmap(32, 32);
                blank_image.fill(Qt::black);
                val = blank_image;
            }
            return val;
        } else if (role != Qt::DisplayRole) {
            return LobbyItem::data(role);
        }
        return data(GameNameRole).toString();
    }

    bool operator<(const QStandardItem& other) const override {
        return data(GameNameRole)
                   .toString()
                   .localeAwareCompare(other.data(GameNameRole).toString()) < 0;
    }
};

class LobbyItemHost : public LobbyItem {
public:
    static const int HostUsernameRole = Qt::UserRole + 1;
    static const int HostIPRole = Qt::UserRole + 2;
    static const int HostPortRole = Qt::UserRole + 3;
    static const int HostVerifyUIDRole = Qt::UserRole + 4;

    LobbyItemHost() = default;
    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 {
        if (role != Qt::DisplayRole) {
            return LobbyItem::data(role);
        }
        return data(HostUsernameRole).toString();
    }

    bool operator<(const QStandardItem& other) const override {
        return data(HostUsernameRole)
                   .toString()
                   .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0;
    }
};

class LobbyMember {
public:
    LobbyMember() = default;
    LobbyMember(const LobbyMember& other) = default;
    explicit LobbyMember(QString username_, 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 GetName() const {
        if (username.isEmpty() || username == nickname) {
            return nickname;
        } else {
            return QStringLiteral("%1 (%2)").arg(nickname, username);
        }
    }
    u64 GetTitleId() const {
        return title_id;
    }
    QString GetGameName() const {
        return game_name;
    }

private:
    QString username;
    QString nickname;
    u64 title_id;
    QString game_name;
};

Q_DECLARE_METATYPE(LobbyMember);

class LobbyItemMemberList : public LobbyItem {
public:
    static const int MemberListRole = Qt::UserRole + 1;
    static const int MaxPlayerRole = Qt::UserRole + 2;

    LobbyItemMemberList() = default;
    explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) {
        setData(members, MemberListRole);
        setData(max_players, MaxPlayerRole);
    }

    QVariant data(int role) const override {
        switch (role) {
        case Qt::DisplayRole: {
            auto members = data(MemberListRole).toList();
            return QStringLiteral("%1 / %2 ")
                .arg(QString::number(members.size()), data(MaxPlayerRole).toString());
        }
        case Qt::ForegroundRole: {
            auto members = data(MemberListRole).toList();
            auto max_players = data(MaxPlayerRole).toInt();
            const QColor room_full_color(255, 48, 32);
            const QColor room_almost_full_color(255, 140, 32);
            const QColor room_has_players_color(32, 160, 32);
            const QColor room_empty_color(128, 128, 128);

            if (members.size() >= max_players) {
                return QBrush(room_full_color);
            } else if (members.size() == (max_players - 1)) {
                return QBrush(room_almost_full_color);
            } else if (members.size() == 0) {
                return QBrush(room_empty_color);
            } else if (members.size() > 0 && members.size() < (max_players - 1)) {
                return QBrush(room_has_players_color);
            }
            // FIXME: How to return a value that tells Qt not to modify the
            // text color from the default (as if Qt::ForegroundRole wasn't overridden)?
            return QBrush(nullptr);
        }
        default:
            return LobbyItem::data(role);
        }
    }

    bool operator<(const QStandardItem& other) const override {
        // sort by rooms that have the most players
        int left_members = data(MemberListRole).toList().size();
        int right_members = other.data(MemberListRole).toList().size();
        return left_members < right_members;
    }
};

/**
 * Member information for when a lobby is expanded in the UI
 */
class LobbyItemExpandedMemberList : public LobbyItem {
public:
    static const int MemberListRole = Qt::UserRole + 1;

    LobbyItemExpandedMemberList() = default;
    explicit LobbyItemExpandedMemberList(QList<QVariant> members) {
        setData(members, MemberListRole);
    }

    QVariant data(int role) const override {
        if (role != Qt::DisplayRole) {
            return LobbyItem::data(role);
        }
        auto members = data(MemberListRole).toList();
        QString out;
        bool first = true;
        for (const auto& member : members) {
            if (!first)
                out.append(QStringLiteral("\n"));
            const auto& m = member.value<LobbyMember>();
            if (m.GetGameName().isEmpty()) {
                out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName());
            } else {
                out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName());
            }
            first = false;
        }
        return out;
    }
};


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QMessageBox>
#include <QString>

#include "yuzu/multiplayer/message.h"

namespace NetworkMessage {
const ConnectionError ErrorManager::USERNAME_NOT_VALID(
    QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters."));
const ConnectionError ErrorManager::ROOMNAME_NOT_VALID(
    QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters."));
const ConnectionError ErrorManager::USERNAME_NOT_VALID_SERVER(
    QT_TR_NOOP("Username is already in use or not valid. Please choose another."));
const ConnectionError ErrorManager::IP_ADDRESS_NOT_VALID(
    QT_TR_NOOP("IP is not a valid IPv4 address."));
const ConnectionError ErrorManager::PORT_NOT_VALID(
    QT_TR_NOOP("Port must be a number between 0 to 65535."));
const ConnectionError ErrorManager::GAME_NOT_SELECTED(QT_TR_NOOP(
    "You must choose a Preferred Game to host a room. If you do not have any games in your game "
    "list yet, add a game folder by clicking on the plus icon in the game list."));
const ConnectionError ErrorManager::NO_INTERNET(
    QT_TR_NOOP("Unable to find an internet connection. Check your internet settings."));
const ConnectionError ErrorManager::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 ErrorManager::ROOM_IS_FULL(
    QT_TR_NOOP("Unable to connect to the room because it is already full."));
const ConnectionError ErrorManager::COULD_NOT_CREATE_ROOM(
    QT_TR_NOOP("Creating a room failed. Please retry. Restarting yuzu might be necessary."));
const ConnectionError ErrorManager::HOST_BANNED(
    QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you "
               "or try a different room."));
const ConnectionError ErrorManager::WRONG_VERSION(
    QT_TR_NOOP("Version mismatch! Please update to the latest version of yuzu. If the problem "
               "persists, contact the room host and ask them to update the server."));
const ConnectionError ErrorManager::WRONG_PASSWORD(QT_TR_NOOP("Incorrect password."));
const ConnectionError ErrorManager::GENERIC_ERROR(QT_TR_NOOP(
    "An unknown error occurred. If this error continues to occur, please open an issue"));
const ConnectionError ErrorManager::LOST_CONNECTION(
    QT_TR_NOOP("Connection to room lost. Try to reconnect."));
const ConnectionError ErrorManager::HOST_KICKED(
    QT_TR_NOOP("You have been kicked by the room host."));
const ConnectionError ErrorManager::IP_COLLISION(
    QT_TR_NOOP("IP address is already in use. Please choose another."));
const ConnectionError ErrorManager::PERMISSION_DENIED(
    QT_TR_NOOP("You do not have enough permission to perform this action."));
const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP(
    "The user you are trying to kick/ban could not be found.\nThey may have left the room."));
const ConnectionError ErrorManager::NO_INTERFACE_SELECTED(QT_TR_NOOP(
    "No valid network interface is selected.\nPlease go to Configure -> System -> Network and "
    "make a selection."));

static bool WarnMessage(const std::string& title, const std::string& text) {
    return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
                                                   QObject::tr(text.c_str()),
                                                   QMessageBox::Ok | QMessageBox::Cancel);
}

void ErrorManager::ShowError(const ConnectionError& e) {
    QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str()));
}

bool WarnGameRunning() {
    return WarnMessage(
        QT_TR_NOOP("Game already running"),
        QT_TR_NOOP("Joining a room when the game is already running is discouraged "
                   "and can cause the room feature not to work correctly.\nProceed anyway?"));
}

bool WarnCloseRoom() {
    return WarnMessage(
        QT_TR_NOOP("Leave Room"),
        QT_TR_NOOP("You are about to close the room. Any network connections will be closed."));
}

bool WarnDisconnect() {
    return WarnMessage(
        QT_TR_NOOP("Disconnect"),
        QT_TR_NOOP("You are about to leave the room. Any network connections will be closed."));
}

} // namespace NetworkMessage


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <utility>

namespace NetworkMessage {

class ConnectionError {

public:
    explicit ConnectionError(std::string str) : err(std::move(str)) {}
    const std::string& GetString() const {
        return err;
    }

private:
    std::string err;
};

class ErrorManager : QObject {
    Q_OBJECT
public:
    /// When the nickname is considered invalid by the client
    static const ConnectionError USERNAME_NOT_VALID;
    static const ConnectionError ROOMNAME_NOT_VALID;
    /// When the nickname is considered invalid by the room server
    static const ConnectionError USERNAME_NOT_VALID_SERVER;
    static const ConnectionError IP_ADDRESS_NOT_VALID;
    static const ConnectionError PORT_NOT_VALID;
    static const ConnectionError GAME_NOT_SELECTED;
    static const ConnectionError NO_INTERNET;
    static const ConnectionError UNABLE_TO_CONNECT;
    static const ConnectionError ROOM_IS_FULL;
    static const ConnectionError COULD_NOT_CREATE_ROOM;
    static const ConnectionError HOST_BANNED;
    static const ConnectionError WRONG_VERSION;
    static const ConnectionError WRONG_PASSWORD;
    static const ConnectionError GENERIC_ERROR;
    static const ConnectionError LOST_CONNECTION;
    static const ConnectionError HOST_KICKED;
    static const ConnectionError IP_COLLISION;
    static const ConnectionError PERMISSION_DENIED;
    static const ConnectionError NO_SUCH_USER;
    static const ConnectionError NO_INTERFACE_SELECTED;
    /**
     *  Shows a standard QMessageBox with a error message
     */
    static void ShowError(const ConnectionError& e);
};

/**
 * Show a standard QMessageBox with a warning message about joining a room when
 * the game is already running
 * return true if the user wants to close the network connection
 */
bool WarnGameRunning();

/**
 * Show a standard QMessageBox with a warning message about leaving the room
 * return true if the user wants to close the network connection
 */
bool WarnCloseRoom();

/**
 * Show a standard QMessageBox with a warning message about disconnecting from the room
 * return true if the user wants to disconnect
 */
bool WarnDisconnect();

} // namespace NetworkMessage


code:
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QStandardItem>
#include <QStandardItemModel>
#include "network/network.h"
#include "network/room_member.h"
#include "ui_moderation_dialog.h"
#include "yuzu/multiplayer/moderation_dialog.h"

namespace Column {
enum {
    SUBJECT,
    TYPE,
    COUNT,
};
}

ModerationDialog::ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent)
    : QDialog(parent), ui(std::make_unique<Ui::ModerationDialog>()), room_network{room_network_} {
    ui->setupUi(this);

    qRegisterMetaType<Network::Room::BanList>();

    if (auto member = room_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 = room_network.GetRoomMember().lock()) {
            room->Unbind(callback_handle_status_message);
        }
    }

    if (callback_handle_ban_list) {
        if (auto room = room_network.GetRoomMember().lock()) {
            room->Unbind(callback_handle_ban_list);
        }
    }
}

void ModerationDialog::LoadBanList() {
    if (auto room = 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 = 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();
}


code:
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#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(Network::RoomNetwork& room_network_, 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;

    Network::RoomNetwork& room_network;
};

Q_DECLARE_METATYPE(Network::Room::BanList);


code:
<?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>


code:
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QAction>
#include <QApplication>
#include <QIcon>
#include <QMessageBox>
#include <QStandardItemModel>
#include "common/announce_multiplayer_room.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "yuzu/game_list.h"
#include "yuzu/multiplayer/client_room.h"
#include "yuzu/multiplayer/direct_connect.h"
#include "yuzu/multiplayer/host_room.h"
#include "yuzu/multiplayer/lobby.h"
#include "yuzu/multiplayer/message.h"
#include "yuzu/multiplayer/state.h"
#include "yuzu/uisettings.h"
#include "yuzu/util/clickable_label.h"

MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_,
                                   QAction* leave_room_, QAction* show_room_, Core::System& system_)
    : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_),
      show_room(show_room_), system{system_}, room_network{system.GetRoomNetwork()} {
    if (auto member = room_network.GetRoomMember().lock()) {
        // register the network structs to use in slots and signals
        state_callback_handle = member->BindOnStateChanged(
            [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
        connect(this, &MultiplayerState::NetworkStateChanged, this,
                &MultiplayerState::OnNetworkStateChanged);
        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<WebService::WebResult>();
    announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(room_network);
    announce_multiplayer_session->BindErrorCallback(
        [this](const WebService::WebResult& result) { emit AnnounceFailed(result); });
    connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed);

    status_text = new ClickableLabel(this);
    status_icon = new ClickableLabel(this);

    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();
                }
            });

    retranslateUi();
}

MultiplayerState::~MultiplayerState() = default;

void MultiplayerState::Close() {
    if (state_callback_handle) {
        if (auto member = room_network.GetRoomMember().lock()) {
            member->Unbind(state_callback_handle);
        }
    }

    if (error_callback_handle) {
        if (auto member = room_network.GetRoomMember().lock()) {
            member->Unbind(error_callback_handle);
        }
    }
    if (host_room) {
        host_room->close();
    }
    if (direct_connect) {
        direct_connect->close();
    }
    if (client_room) {
        client_room->close();
    }
    if (lobby) {
        lobby->close();
    }
}

void MultiplayerState::retranslateUi() {
    status_text->setToolTip(tr("Current connection status"));

    UpdateNotificationStatus();

    if (lobby) {
        lobby->RetranslateUi();
    }
    if (host_room) {
        host_room->RetranslateUi();
    }
    if (client_room) {
        client_room->RetranslateUi();
    }
    if (direct_connect) {
        direct_connect->RetranslateUi();
    }
}

void MultiplayerState::SetNotificationStatus(NotificationStatus status) {
    notification_status = status;
    UpdateNotificationStatus();
}

void MultiplayerState::UpdateNotificationStatus() {
    switch (notification_status) {
    case NotificationStatus::Uninitialized:
        status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
        status_text->setText(tr("Not Connected. Click here to find a room!"));
        leave_room->setEnabled(false);
        show_room->setEnabled(false);
        break;
    case NotificationStatus::Disconnected:
        status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
        status_text->setText(tr("Not Connected"));
        leave_room->setEnabled(false);
        show_room->setEnabled(false);
        break;
    case NotificationStatus::Connected:
        status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
        status_text->setText(tr("Connected"));
        leave_room->setEnabled(true);
        show_room->setEnabled(true);
        break;
    case NotificationStatus::Notification:
        status_icon->setPixmap(
            QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
        status_text->setText(tr("New Messages Received"));
        leave_room->setEnabled(true);
        show_room->setEnabled(true);
        break;
    }

    // Clean up status bar if game is running
    if (system.IsPoweredOn()) {
        status_text->clear();
    }
}

void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
    LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
    if (state == Network::RoomMember::State::Joined ||
        state == Network::RoomMember::State::Moderator) {

        OnOpenNetworkRoom();
        SetNotificationStatus(NotificationStatus::Connected);
    } else {
        SetNotificationStatus(NotificationStatus::Disconnected);
    }

    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::ErrorManager::ShowError(NetworkMessage::ErrorManager::LOST_CONNECTION);
        break;
    case Network::RoomMember::Error::HostKicked:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_KICKED);
        break;
    case Network::RoomMember::Error::CouldNotConnect:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT);
        break;
    case Network::RoomMember::Error::NameCollision:
        NetworkMessage::ErrorManager::ShowError(
            NetworkMessage::ErrorManager::USERNAME_NOT_VALID_SERVER);
        break;
    case Network::RoomMember::Error::IpCollision:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::IP_COLLISION);
        break;
    case Network::RoomMember::Error::RoomIsFull:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOM_IS_FULL);
        break;
    case Network::RoomMember::Error::WrongPassword:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_PASSWORD);
        break;
    case Network::RoomMember::Error::WrongVersion:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_VERSION);
        break;
    case Network::RoomMember::Error::HostBanned:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_BANNED);
        break;
    case Network::RoomMember::Error::UnknownError:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT);
        break;
    case Network::RoomMember::Error::PermissionDenied:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PERMISSION_DENIED);
        break;
    case Network::RoomMember::Error::NoSuchUser:
        NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER);
        break;
    }
}

void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) {
    announce_multiplayer_session->Stop();
    QMessageBox::warning(this, tr("Error"),
                         tr("Failed to update the room information. Please check your Internet "
                            "connection and try hosting the room again.\nDebug Message: ") +
                             QString::fromStdString(result.result_string),
                         QMessageBox::Ok);
}

void MultiplayerState::OnSaveConfig() {
    emit SaveConfig();
}

void MultiplayerState::UpdateThemedIcons() {
    if (show_notification) {
        status_icon->setPixmap(
            QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
    } else if (current_state == Network::RoomMember::State::Joined ||
               current_state == Network::RoomMember::State::Moderator) {

        status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
    } else {
        status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
    }
    if (client_room)
        client_room->UpdateIconDisplay();
}

static void BringWidgetToFront(QWidget* widget) {
    widget->show();
    widget->activateWindow();
    widget->raise();
}

void MultiplayerState::OnViewLobby() {
    if (lobby == nullptr) {
        lobby = new Lobby(this, game_list_model, announce_multiplayer_session, system);
        connect(lobby, &Lobby::SaveConfig, this, &MultiplayerState::OnSaveConfig);
    }
    lobby->RefreshLobby();
    BringWidgetToFront(lobby);
}

void MultiplayerState::OnCreateRoom() {
    if (host_room == nullptr) {
        host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session, system);
        connect(host_room, &HostRoomWindow::SaveConfig, this, &MultiplayerState::OnSaveConfig);
    }
    BringWidgetToFront(host_room);
}

bool MultiplayerState::OnCloseRoom() {
    if (!NetworkMessage::WarnCloseRoom())
        return false;
    if (auto room = room_network.GetRoom().lock()) {
        // if you are in a room, leave it
        if (auto member = room_network.GetRoomMember().lock()) {
            member->Leave();
            LOG_DEBUG(Frontend, "Left the room (as a client)");
        }

        // if you are hosting a room, also stop hosting
        if (room->GetState() != Network::Room::State::Open) {
            return true;
        }
        // Save ban list
        UISettings::values.multiplayer_ban_list = room->GetBanList();

        room->Destroy();
        announce_multiplayer_session->Stop();
        LOG_DEBUG(Frontend, "Closed the room (as a server)");
    }
    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);
    QApplication::beep();
    SetNotificationStatus(NotificationStatus::Notification);
}

void MultiplayerState::HideNotification() {
    show_notification = false;
    SetNotificationStatus(NotificationStatus::Connected);
}

void MultiplayerState::OnOpenNetworkRoom() {
    if (auto member = room_network.GetRoomMember().lock()) {
        if (member->IsConnected()) {
            if (client_room == nullptr) {
                client_room = new ClientRoomWindow(this, room_network);
                connect(client_room, &ClientRoomWindow::ShowNotification, this,
                        &MultiplayerState::ShowNotification);
            }
            BringWidgetToFront(client_room);
            return;
        }
    }
    // If the user is not a member of a room, show the lobby instead.
    // This is currently only used on the clickable label in the status bar
    OnViewLobby();
}

void MultiplayerState::OnDirectConnectToRoom() {
    if (direct_connect == nullptr) {
        direct_connect = new DirectConnectWindow(system, this);
        connect(direct_connect, &DirectConnectWindow::SaveConfig, this,
                &MultiplayerState::OnSaveConfig);
    }
    BringWidgetToFront(direct_connect);
}

bool MultiplayerState::IsHostingPublicRoom() const {
    return announce_multiplayer_session->IsRunning();
}

void MultiplayerState::UpdateCredentials() {
    announce_multiplayer_session->UpdateCredentials();
}

void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) {
    game_list_model = game_list;
    if (lobby) {
        lobby->UpdateGameList(game_list);
    }
    if (host_room) {
        host_room->UpdateGameList(game_list);
    }
}


code:
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QWidget>
#include "network/announce_multiplayer_session.h"
#include "network/network.h"

class QStandardItemModel;
class Lobby;
class HostRoomWindow;
class ClientRoomWindow;
class DirectConnectWindow;
class ClickableLabel;

namespace Core {
class System;
}

class MultiplayerState : public QWidget {
    Q_OBJECT;

public:
    enum class NotificationStatus {
        Uninitialized,
        Disconnected,
        Connected,
        Notification,
    };

    explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
                              QAction* show_room, Core::System& system_);
    ~MultiplayerState();

    /**
     * Close all open multiplayer related dialogs
     */
    void Close();

    void SetNotificationStatus(NotificationStatus state);

    void UpdateNotificationStatus();

    ClickableLabel* GetStatusText() const {
        return status_text;
    }

    ClickableLabel* GetStatusIcon() const {
        return status_icon;
    }

    void retranslateUi();

    /**
     * Whether a public room is being hosted or not.
     * When this is true, Web Services configuration should be disabled.
     */
    bool IsHostingPublicRoom() const;

    void UpdateCredentials();

    /**
     * Updates the multiplayer dialogs with a new game list model.
     * This model should be the original model of the game list.
     */
    void UpdateGameList(QStandardItemModel* game_list);

public slots:
    void OnNetworkStateChanged(const Network::RoomMember::State& state);
    void OnNetworkError(const Network::RoomMember::Error& error);
    void OnViewLobby();
    void OnCreateRoom();
    bool OnCloseRoom();
    void OnOpenNetworkRoom();
    void OnDirectConnectToRoom();
    void OnAnnounceFailed(const WebService::WebResult&);
    void OnSaveConfig();
    void UpdateThemedIcons();
    void ShowNotification();
    void HideNotification();

signals:
    void NetworkStateChanged(const Network::RoomMember::State&);
    void NetworkError(const Network::RoomMember::Error&);
    void AnnounceFailed(const WebService::WebResult&);
    void SaveConfig();

private:
    Lobby* lobby = nullptr;
    HostRoomWindow* host_room = nullptr;
    ClientRoomWindow* client_room = nullptr;
    DirectConnectWindow* direct_connect = nullptr;
    ClickableLabel* status_icon = nullptr;
    ClickableLabel* status_text = nullptr;
    QStandardItemModel* game_list_model = nullptr;
    QAction* leave_room;
    QAction* show_room;
    std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
    Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized;
    NotificationStatus notification_status = NotificationStatus::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;
    Core::System& system;
    Network::RoomNetwork& room_network;
};

Q_DECLARE_METATYPE(WebService::WebResult);


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QRegularExpression>
#include <QString>
#include <QValidator>

class Validation {
public:
    Validation()
        : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, UINT16_MAX) {}

    ~Validation() = default;

    const QValidator* GetRoomName() const {
        return &room_name;
    }
    const QValidator* GetNickname() const {
        return &nickname;
    }
    const QValidator* GetIP() const {
        return &ip;
    }
    const QValidator* GetPort() const {
        return &port;
    }

private:
    /// room name can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20
    QRegularExpression room_name_regex =
        QRegularExpression(QStringLiteral("^[a-zA-Z0-9._ -]{4,20}"));
    QRegularExpressionValidator room_name;

    /// nickname can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20
    const QRegularExpression nickname_regex =
        QRegularExpression(QStringLiteral("^[a-zA-Z0-9._ -]{4,20}"));
    QRegularExpressionValidator nickname;

    /// ipv4 / ipv6 / hostnames
    QRegularExpression ip_regex = QRegularExpression(QStringLiteral(
        // IPv4 regex
        "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|"
        // IPv6 regex
        "^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|"
        "(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-"
        "5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|"
        "(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)"
        "(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|"
        "(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]"
        "\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|"
        "(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2["
        "0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|"
        "(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2["
        "0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|"
        "(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2["
        "0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|"
        "(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?"
        "\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$|"
        // Hostname regex
        "^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\\.)+[a-zA-Z]{2,}$"));
    QRegularExpressionValidator ip;

    /// port must be between 0 and 65535
    QIntValidator port;
};


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "common/alignment.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "common/thread.h"
#include "core/hle/service/acc/profile_manager.h"
#include "yuzu/play_time_manager.h"

namespace PlayTime {

namespace {

struct PlayTimeElement {
    ProgramId program_id;
    PlayTime play_time;
};

std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
    const Service::Account::ProfileManager& manager) {
    const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
    if (!uuid.has_value()) {
        return std::nullopt;
    }
    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) /
           uuid->RawString().append(".bin");
}

[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db,
                                    const Service::Account::ProfileManager& manager) {
    const auto filename = GetCurrentUserPlayTimePath(manager);

    if (!filename.has_value()) {
        LOG_ERROR(Frontend, "Failed to get current user path");
        return false;
    }

    out_play_time_db.clear();

    if (Common::FS::Exists(filename.value())) {
        Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read,
                                Common::FS::FileType::BinaryFile};
        if (!file.IsOpen()) {
            LOG_ERROR(Frontend, "Failed to open play time file: {}",
                      Common::FS::PathToUTF8String(filename.value()));
            return false;
        }

        const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement);
        std::vector<PlayTimeElement> elements(num_elements);

        if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) {
            return false;
        }

        for (const auto& [program_id, play_time] : elements) {
            if (program_id != 0) {
                out_play_time_db[program_id] = play_time;
            }
        }
    }

    return true;
}

[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db,
                                     const Service::Account::ProfileManager& manager) {
    const auto filename = GetCurrentUserPlayTimePath(manager);

    if (!filename.has_value()) {
        LOG_ERROR(Frontend, "Failed to get current user path");
        return false;
    }

    Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write,
                            Common::FS::FileType::BinaryFile};
    if (!file.IsOpen()) {
        LOG_ERROR(Frontend, "Failed to open play time file: {}",
                  Common::FS::PathToUTF8String(filename.value()));
        return false;
    }

    std::vector<PlayTimeElement> elements;
    elements.reserve(play_time_db.size());

    for (auto& [program_id, play_time] : play_time_db) {
        if (program_id != 0) {
            elements.push_back(PlayTimeElement{program_id, play_time});
        }
    }

    return file.WriteSpan<PlayTimeElement>(elements) == elements.size();
}

} // namespace

PlayTimeManager::PlayTimeManager(Service::Account::ProfileManager& profile_manager)
    : manager{profile_manager} {
    if (!ReadPlayTimeFile(database, manager)) {
        LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
    }
}

PlayTimeManager::~PlayTimeManager() {
    Save();
}

void PlayTimeManager::SetProgramId(u64 program_id) {
    running_program_id = program_id;
}

void PlayTimeManager::Start() {
    play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); });
}

void PlayTimeManager::Stop() {
    play_time_thread = {};
}

void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
    Common::SetCurrentThreadName("PlayTimeReport");

    using namespace std::literals::chrono_literals;
    using std::chrono::seconds;
    using std::chrono::steady_clock;

    auto timestamp = steady_clock::now();

    const auto GetDuration = [&]() -> u64 {
        const auto last_timestamp = std::exchange(timestamp, steady_clock::now());
        const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp);
        return static_cast<u64>(duration.count());
    };

    while (!stop_token.stop_requested()) {
        Common::StoppableTimedWait(stop_token, 30s);

        database[running_program_id] += GetDuration();
        Save();
    }
}

void PlayTimeManager::Save() {
    if (!WritePlayTimeFile(database, manager)) {
        LOG_ERROR(Frontend, "Failed to update play time database!");
    }
}

u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
    auto it = database.find(program_id);
    if (it != database.end()) {
        return it->second;
    } else {
        return 0;
    }
}

void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
    database.erase(program_id);
    Save();
}

QString ReadablePlayTime(qulonglong time_seconds) {
    if (time_seconds == 0) {
        return {};
    }
    const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0);
    const auto time_hours = static_cast<double>(time_seconds) / 3600;
    const bool is_minutes = time_minutes < 60;
    const char* unit = is_minutes ? "m" : "h";
    const auto value = is_minutes ? time_minutes : time_hours;

    return QStringLiteral("%L1 %2")
        .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0)
        .arg(QString::fromUtf8(unit));
}

} // namespace PlayTime


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QString>

#include <map>

#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/polyfill_thread.h"

namespace Service::Account {
class ProfileManager;
}

namespace PlayTime {

using ProgramId = u64;
using PlayTime = u64;
using PlayTimeDatabase = std::map<ProgramId, PlayTime>;

class PlayTimeManager {
public:
    explicit PlayTimeManager(Service::Account::ProfileManager& profile_manager);
    ~PlayTimeManager();

    YUZU_NON_COPYABLE(PlayTimeManager);
    YUZU_NON_MOVEABLE(PlayTimeManager);

    u64 GetPlayTime(u64 program_id) const;
    void ResetProgramPlayTime(u64 program_id);
    void SetProgramId(u64 program_id);
    void Start();
    void Stop();

private:
    void AutoTimestamp(std::stop_token stop_token);
    void Save();

    PlayTimeDatabase database;
    u64 running_program_id;
    std::jthread play_time_thread;
    Service::Account::ProfileManager& manager;
};

QString ReadablePlayTime(qulonglong time_seconds);

} // namespace PlayTime


code:
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "common/common_precompiled_headers.h"


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QGuiApplication>
#include <QStringLiteral>
#include <QWindow>
#include "common/logging/log.h"
#include "core/frontend/emu_window.h"
#include "yuzu/qt_common.h"

#if !defined(WIN32) && !defined(__APPLE__)
#include <qpa/qplatformnativeinterface.h>
#elif defined(__APPLE__)
#include <objc/message.h>
#endif

namespace QtCommon {
Core::Frontend::WindowSystemType GetWindowSystemType() {
    // Determine WSI type based on Qt platform.
    QString platform_name = QGuiApplication::platformName();
    if (platform_name == QStringLiteral("windows"))
        return Core::Frontend::WindowSystemType::Windows;
    else if (platform_name == QStringLiteral("xcb"))
        return Core::Frontend::WindowSystemType::X11;
    else if (platform_name == QStringLiteral("wayland"))
        return Core::Frontend::WindowSystemType::Wayland;
    else if (platform_name == QStringLiteral("wayland-egl"))
        return Core::Frontend::WindowSystemType::Wayland;
    else if (platform_name == QStringLiteral("cocoa"))
        return Core::Frontend::WindowSystemType::Cocoa;
    else if (platform_name == QStringLiteral("android"))
        return Core::Frontend::WindowSystemType::Android;

    LOG_CRITICAL(Frontend, "Unknown Qt platform {}!", platform_name.toStdString());
    return Core::Frontend::WindowSystemType::Windows;
} // namespace Core::Frontend::WindowSystemType

Core::Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window) {
    Core::Frontend::EmuWindow::WindowSystemInfo wsi;
    wsi.type = GetWindowSystemType();

#if defined(WIN32)
    // Our Win32 Qt external doesn't have the private API.
    wsi.render_surface = reinterpret_cast<void*>(window->winId());
#elif defined(__APPLE__)
    wsi.render_surface = reinterpret_cast<void* (*)(id, SEL)>(objc_msgSend)(
        reinterpret_cast<id>(window->winId()), sel_registerName("layer"));
#else
    QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface();
    wsi.display_connection = pni->nativeResourceForWindow("display", window);
    if (wsi.type == Core::Frontend::WindowSystemType::Wayland)
        wsi.render_surface = window ? pni->nativeResourceForWindow("surface", window) : nullptr;
    else
        wsi.render_surface = window ? reinterpret_cast<void*>(window->winId()) : nullptr;
#endif
    wsi.render_surface_scale = window ? static_cast<float>(window->devicePixelRatio()) : 1.0f;

    return wsi;
}
} // namespace QtCommon


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QWindow>
#include "core/frontend/emu_window.h"

namespace QtCommon {

Core::Frontend::WindowSystemType GetWindowSystemType();

Core::Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow* window);

} // namespace QtCommon


code:
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "video_core/vulkan_common/vulkan_wrapper.h"

#ifdef _WIN32
#include <cstring>
#include <processthreadsapi.h>
#include <windows.h>
#elif defined(YUZU_UNIX)
#include <cstring>
#include <errno.h>
#include <spawn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

#include <fmt/core.h>
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_library.h"
#include "yuzu/startup_checks.h"

void CheckVulkan() {
    // Just start the Vulkan loader, this will crash if something is wrong
    try {
        Vulkan::vk::InstanceDispatch dld;
        const auto library = Vulkan::OpenLibrary();
        const Vulkan::vk::Instance instance =
            Vulkan::CreateInstance(*library, dld, VK_API_VERSION_1_1);

    } catch (const Vulkan::vk::Exception& exception) {
        fmt::print(stderr, "Failed to initialize Vulkan: {}\n", exception.what());
    }
}

bool CheckEnvVars(bool* is_child) {
#ifdef _WIN32
    // Check environment variable to see if we are the child
    char variable_contents[8];
    const DWORD startup_check_var =
        GetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, variable_contents, 8);
    if (startup_check_var > 0 && std::strncmp(variable_contents, ENV_VAR_ENABLED_TEXT, 8) == 0) {
        CheckVulkan();
        return true;
    }

    // Don't perform startup checks if we are a child process
    char is_child_s[8];
    const DWORD is_child_len = GetEnvironmentVariableA(IS_CHILD_ENV_VAR, is_child_s, 8);
    if (is_child_len > 0 && std::strncmp(is_child_s, ENV_VAR_ENABLED_TEXT, 8) == 0) {
        *is_child = true;
        return false;
    } else if (!SetEnvironmentVariableA(IS_CHILD_ENV_VAR, ENV_VAR_ENABLED_TEXT)) {
        fmt::print(stderr, "SetEnvironmentVariableA failed to set {} with error {}\n",
                   IS_CHILD_ENV_VAR, GetLastError());
        return true;
    }
#elif defined(YUZU_UNIX)
    const char* startup_check_var = getenv(STARTUP_CHECK_ENV_VAR);
    if (startup_check_var != nullptr &&
        std::strncmp(startup_check_var, ENV_VAR_ENABLED_TEXT, 8) == 0) {
        CheckVulkan();
        return true;
    }
#endif
    return false;
}

bool StartupChecks(const char* arg0, bool* has_broken_vulkan, bool perform_vulkan_check) {
#ifdef _WIN32
    // Set the startup variable for child processes
    const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, ENV_VAR_ENABLED_TEXT);
    if (!env_var_set) {
        fmt::print(stderr, "SetEnvironmentVariableA failed to set {} with error {}\n",
                   STARTUP_CHECK_ENV_VAR, GetLastError());
        return false;
    }

    if (perform_vulkan_check) {
        // Spawn child process that performs Vulkan check
        PROCESS_INFORMATION process_info;
        std::memset(&process_info, '\0', sizeof(process_info));

        if (!SpawnChild(arg0, &process_info, 0)) {
            return false;
        }

        // Wait until the process exits and get exit code from it
        WaitForSingleObject(process_info.hProcess, INFINITE);
        DWORD exit_code = STILL_ACTIVE;
        const int err = GetExitCodeProcess(process_info.hProcess, &exit_code);
        if (err == 0) {
            fmt::print(stderr, "GetExitCodeProcess failed with error {}\n", GetLastError());
        }

        // Vulkan is broken if the child crashed (return value is not zero)
        *has_broken_vulkan = (exit_code != 0);

        if (CloseHandle(process_info.hProcess) == 0) {
            fmt::print(stderr, "CloseHandle failed with error {}\n", GetLastError());
        }
        if (CloseHandle(process_info.hThread) == 0) {
            fmt::print(stderr, "CloseHandle failed with error {}\n", GetLastError());
        }
    }

    if (!SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, nullptr)) {
        fmt::print(stderr, "SetEnvironmentVariableA failed to clear {} with error {}\n",
                   STARTUP_CHECK_ENV_VAR, GetLastError());
    }

#elif defined(YUZU_UNIX)
    const int env_var_set = setenv(STARTUP_CHECK_ENV_VAR, ENV_VAR_ENABLED_TEXT, 1);
    if (env_var_set == -1) {
        const int err = errno;
        fmt::print(stderr, "setenv failed to set {} with error {}\n", STARTUP_CHECK_ENV_VAR, err);
        return false;
    }

    if (perform_vulkan_check) {
        const pid_t pid = SpawnChild(arg0);
        if (pid == -1) {
            return false;
        }

        // Get exit code from child process
        int status;
        const int r_val = waitpid(pid, &status, 0);
        if (r_val == -1) {
            const int err = errno;
            fmt::print(stderr, "wait failed with error {}\n", err);
            return false;
        }
        // Vulkan is broken if the child crashed (return value is not zero)
        *has_broken_vulkan = (status != 0);
    }

    const int env_var_cleared = unsetenv(STARTUP_CHECK_ENV_VAR);
    if (env_var_cleared == -1) {
        const int err = errno;
        fmt::print(stderr, "unsetenv failed to clear {} with error {}\n", STARTUP_CHECK_ENV_VAR,
                   err);
    }
#endif
    return false;
}

#ifdef _WIN32
bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags) {
    STARTUPINFOA startup_info;

    std::memset(&startup_info, '\0', sizeof(startup_info));
    startup_info.cb = sizeof(startup_info);

    char p_name[255];
    std::strncpy(p_name, arg0, 254);
    p_name[254] = '\0';

    const bool process_created = CreateProcessA(nullptr,       // lpApplicationName
                                                p_name,        // lpCommandLine
                                                nullptr,       // lpProcessAttributes
                                                nullptr,       // lpThreadAttributes
                                                false,         // bInheritHandles
                                                flags,         // dwCreationFlags
                                                nullptr,       // lpEnvironment
                                                nullptr,       // lpCurrentDirectory
                                                &startup_info, // lpStartupInfo
                                                pi             // lpProcessInformation
    );
    if (!process_created) {
        fmt::print(stderr, "CreateProcessA failed with error {}\n", GetLastError());
        return false;
    }

    return true;
}
#elif defined(YUZU_UNIX)
pid_t SpawnChild(const char* arg0) {
    const pid_t pid = fork();

    if (pid == -1) {
        // error
        const int err = errno;
        fmt::print(stderr, "fork failed with error {}\n", err);
        return pid;
    } else if (pid == 0) {
        // child
        execlp(arg0, arg0, nullptr);
        const int err = errno;
        fmt::print(stderr, "execl failed with error {}\n", err);
        _exit(0);
    }

    return pid;
}
#endif


code:
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#ifdef _WIN32
#include <windows.h>
#elif defined(YUZU_UNIX)
#include <sys/types.h>
#endif

constexpr char IS_CHILD_ENV_VAR[] = "YUZU_IS_CHILD";
constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS";
constexpr char ENV_VAR_ENABLED_TEXT[] = "ON";

void CheckVulkan();
bool CheckEnvVars(bool* is_child);
bool StartupChecks(const char* arg0, bool* has_broken_vulkan, bool perform_vulkan_check);

#ifdef _WIN32
bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags);
#elif defined(YUZU_UNIX)
pid_t SpawnChild(const char* arg0);
#endif


code:
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <QSettings>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "yuzu/uisettings.h"

#ifndef CANNOT_EXPLICITLY_INSTANTIATE
namespace Settings {
template class Setting<bool>;
template class Setting<std::string>;
template class Setting<u16, true>;
template class Setting<u32>;
template class Setting<u8, true>;
template class Setting<u8>;
template class Setting<unsigned long long>;
} // namespace Settings
#endif

namespace FS = Common::FS;

namespace UISettings {

const Themes themes{{
    {"Default", "default"},
    {"Default Colorful", "colorful"},
    {"Dark", "qdarkstyle"},
    {"Dark Colorful", "colorful_dark"},
    {"Midnight Blue", "qdarkstyle_midnight_blue"},
    {"Midnight Blue Colorful", "colorful_midnight_blue"},
}};

bool IsDarkTheme() {
    const auto& theme = UISettings::values.theme;
    return theme == std::string("qdarkstyle") || theme == std::string("qdarkstyle_midnight_blue") ||
           theme == std::string("colorful_dark") || theme == std::string("colorful_midnight_blue");
}

Values values = {};

u32 CalculateWidth(u32 height, Settings::AspectRatio ratio) {
    switch (ratio) {
    case Settings::AspectRatio::R4_3:
        return height * 4 / 3;
    case Settings::AspectRatio::R21_9:
        return height * 21 / 9;
    case Settings::AspectRatio::R16_10:
        return height * 16 / 10;
    case Settings::AspectRatio::R16_9:
    case Settings::AspectRatio::Stretch:
        // TODO: Move this function wherever appropriate to implement Stretched aspect
        break;
    }
    return height * 16 / 9;
}

void SaveWindowState() {
    const auto window_state_config_loc =
        FS::PathToUTF8String(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "window_state.ini");

    void(FS::CreateParentDir(window_state_config_loc));
    QSettings config(QString::fromStdString(window_state_config_loc), QSettings::IniFormat);

    config.setValue(QStringLiteral("geometry"), values.geometry);
    config.setValue(QStringLiteral("state"), values.state);
    config.setValue(QStringLiteral("geometryRenderWindow"), values.renderwindow_geometry);
    config.setValue(QStringLiteral("gameListHeaderState"), values.gamelist_header_state);
    config.setValue(QStringLiteral("microProfileDialogGeometry"), values.microprofile_geometry);

    config.sync();
}

void RestoreWindowState(std::unique_ptr<QtConfig>& qtConfig) {
    const auto window_state_config_loc =
        FS::PathToUTF8String(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "window_state.ini");

    // Migrate window state from old location
    if (!FS::Exists(window_state_config_loc) && qtConfig->Exists("UI", "UILayout\\geometry")) {
        const auto config_loc =
            FS::PathToUTF8String(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "qt-config.ini");
        QSettings config(QString::fromStdString(config_loc), QSettings::IniFormat);

        config.beginGroup(QStringLiteral("UI"));
        config.beginGroup(QStringLiteral("UILayout"));
        values.geometry = config.value(QStringLiteral("geometry")).toByteArray();
        values.state = config.value(QStringLiteral("state")).toByteArray();
        values.renderwindow_geometry =
            config.value(QStringLiteral("geometryRenderWindow")).toByteArray();
        values.gamelist_header_state =
            config.value(QStringLiteral("gameListHeaderState")).toByteArray();
        values.microprofile_geometry =
            config.value(QStringLiteral("microProfileDialogGeometry")).toByteArray();
        config.endGroup();
        config.endGroup();
        return;
    }

    void(FS::CreateParentDir(window_state_config_loc));
    const QSettings config(QString::fromStdString(window_state_config_loc), QSettings::IniFormat);

    values.geometry = config.value(QStringLiteral("geometry")).toByteArray();
    values.state = config.value(QStringLiteral("state")).toByteArray();
    values.renderwindow_geometry =
        config.value(QStringLiteral("geometryRenderWindow")).toByteArray();
    values.gamelist_header_state =
        config.value(QStringLiteral("gameListHeaderState")).toByteArray();
    values.microprofile_geometry =
        config.value(QStringLiteral("microProfileDialogGeometry")).toByteArray();
}

} // namespace UISettings