diff options
Diffstat (limited to 'src/yuzu')
| -rw-r--r-- | src/yuzu/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 65 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 6 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 62 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 19 | ||||
| -rw-r--r-- | src/yuzu/main.h | 3 | ||||
| -rw-r--r-- | src/yuzu/util/util.cpp | 11 | ||||
| -rw-r--r-- | src/yuzu/util/util.h | 7 | 
8 files changed, 174 insertions, 3 deletions
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 46ed232d8..ea9ea69e4 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -70,6 +70,9 @@ set(UIS      main.ui  ) +file(GLOB COMPAT_LIST +     ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc +     ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)  file(GLOB_RECURSE ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*)  file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) @@ -77,6 +80,7 @@ qt5_wrap_ui(UI_HDRS ${UIS})  target_sources(yuzu      PRIVATE +        ${COMPAT_LIST}          ${ICONS}          ${THEMES}          ${UI_HDRS} diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 867a3c6f1..27525938a 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -7,10 +7,14 @@  #include <QDir>  #include <QFileInfo>  #include <QHeaderView> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject>  #include <QKeyEvent>  #include <QMenu>  #include <QThreadPool>  #include <boost/container/flat_map.hpp> +#include <fmt/format.h>  #include "common/common_paths.h"  #include "common/logging/log.h"  #include "common/string_util.h" @@ -224,6 +228,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, GMainWindow* parent)      item_model->insertColumns(0, COLUMN_COUNT);      item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name"); +    item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, "Compatibility");      item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");      item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); @@ -325,12 +330,62 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {      QMenu context_menu;      QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); +    QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); +      open_save_location->setEnabled(program_id != 0); +    auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); +    navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); +      connect(open_save_location, &QAction::triggered,              [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); +    connect(navigate_to_gamedb_entry, &QAction::triggered, +            [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); +      context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));  } +void GameList::LoadCompatibilityList() { +    QFile compat_list{":compatibility_list/compatibility_list.json"}; + +    if (!compat_list.open(QFile::ReadOnly | QFile::Text)) { +        LOG_ERROR(Frontend, "Unable to open game compatibility list"); +        return; +    } + +    if (compat_list.size() == 0) { +        LOG_WARNING(Frontend, "Game compatibility list is empty"); +        return; +    } + +    const QByteArray content = compat_list.readAll(); +    if (content.isEmpty()) { +        LOG_ERROR(Frontend, "Unable to completely read game compatibility list"); +        return; +    } + +    const QString string_content = content; +    QJsonDocument json = QJsonDocument::fromJson(string_content.toUtf8()); +    QJsonArray arr = json.array(); + +    for (const QJsonValue& value : arr) { +        QJsonObject game = value.toObject(); + +        if (game.contains("compatibility") && game["compatibility"].isDouble()) { +            int compatibility = game["compatibility"].toInt(); +            QString directory = game["directory"].toString(); +            QJsonArray ids = game["releases"].toArray(); + +            for (const QJsonValue& value : ids) { +                QJsonObject object = value.toObject(); +                QString id = object["id"].toString(); +                compatibility_list.emplace( +                    id.toUpper().toStdString(), +                    std::make_pair(QString::number(compatibility), directory)); +            } +        } +    } +} +  void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {      if (!FileUtil::Exists(dir_path.toStdString()) ||          !FileUtil::IsDirectory(dir_path.toStdString())) { @@ -345,7 +400,7 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {      emit ShouldCancelWorker(); -    GameListWorker* worker = new GameListWorker(vfs, dir_path, deep_scan); +    GameListWorker* worker = new GameListWorker(vfs, dir_path, deep_scan, compatibility_list);      connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);      connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, @@ -523,11 +578,19 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign                  }              } +            auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + +            // The game list uses this as compatibility number for untested games +            QString compatibility("99"); +            if (it != compatibility_list.end()) +                compatibility = it->second.first; +              emit EntryReady({                  new GameListItemPath(                      FormatGameName(physical_name), icon, QString::fromStdString(name),                      QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())),                      program_id), +                new GameListItemCompat(compatibility),                  new GameListItem(                      QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),                  new GameListItemSize(FileUtil::GetSize(physical_name)), diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 20252e778..c01351dc9 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -29,6 +29,7 @@ class GameList : public QWidget {  public:      enum {          COLUMN_NAME, +        COLUMN_COMPATIBILITY,          COLUMN_FILE_TYPE,          COLUMN_SIZE,          COLUMN_COUNT, // Number of columns @@ -68,6 +69,7 @@ public:      void setFilterFocus();      void setFilterVisible(bool visibility); +    void LoadCompatibilityList();      void PopulateAsync(const QString& dir_path, bool deep_scan);      void SaveInterfaceLayout(); @@ -79,6 +81,9 @@ signals:      void GameChosen(QString game_path);      void ShouldCancelWorker();      void OpenFolderRequested(u64 program_id, GameListOpenTarget target); +    void NavigateToGamedbEntryRequested( +        u64 program_id, +        std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);  private slots:      void onTextChanged(const QString& newText); @@ -100,6 +105,7 @@ private:      QStandardItemModel* item_model = nullptr;      GameListWorker* current_worker = nullptr;      QFileSystemWatcher* watcher = nullptr; +    std::unordered_map<std::string, std::pair<QString, QString>> compatibility_list;  };  Q_DECLARE_METATYPE(GameListOpenTarget); diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 1d6c85400..b9676d069 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -8,11 +8,15 @@  #include <atomic>  #include <map>  #include <memory> +#include <unordered_map>  #include <utility> +#include <QCoreApplication>  #include <QImage> +#include <QObject>  #include <QRunnable>  #include <QStandardItem>  #include <QString> +#include "common/logging/log.h"  #include "common/string_util.h"  #include "core/file_sys/content_archive.h"  #include "ui_settings.h" @@ -29,6 +33,17 @@ static QPixmap GetDefaultIcon(u32 size) {      return icon;  } +static auto FindMatchingCompatibilityEntry( +    const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list, +    u64 program_id) { +    return std::find_if( +        compatibility_list.begin(), compatibility_list.end(), +        [program_id](const std::pair<std::string, std::pair<QString, QString>>& element) { +            std::string pid = fmt::format("{:016X}", program_id); +            return element.first == pid; +        }); +} +  class GameListItem : public QStandardItem {  public: @@ -96,6 +111,45 @@ public:      }  }; +class GameListItemCompat : public GameListItem { +    Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) +public: +    static const int CompatNumberRole = Qt::UserRole + 1; +    GameListItemCompat() = default; +    explicit GameListItemCompat(const QString& compatiblity) { +        struct CompatStatus { +            QString color; +            const char* text; +            const char* tooltip; +        }; +        // clang-format off +        static const std::map<QString, CompatStatus> status_data = { +        {"0",  {"#5c93ed", QT_TR_NOOP("Perfect"),    QT_TR_NOOP("Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without\nany workarounds needed.")}}, +        {"1",  {"#47d35c", QT_TR_NOOP("Great"),      QT_TR_NOOP("Game functions with minor graphical or audio glitches and is playable from start to finish. May require some\nworkarounds.")}}, +        {"2",  {"#94b242", QT_TR_NOOP("Okay"),       QT_TR_NOOP("Game functions with major graphical or audio glitches, but game is playable from start to finish with\nworkarounds.")}}, +        {"3",  {"#f2d624", QT_TR_NOOP("Bad"),        QT_TR_NOOP("Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches\neven with workarounds.")}}, +        {"4",  {"#FF0000", QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start\nScreen.")}}, +        {"5",  {"#828282", QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}}, +        {"99", {"#000000", QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}}}; +        // clang-format on + +        auto iterator = status_data.find(compatiblity); +        if (iterator == status_data.end()) { +            LOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString()); +            return; +        } +        CompatStatus status = iterator->second; +        setData(compatiblity, CompatNumberRole); +        setText(QObject::tr(status.text)); +        setToolTip(QObject::tr(status.tooltip)); +        setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); +    } + +    bool operator<(const QStandardItem& other) const override { +        return data(CompatNumberRole) < other.data(CompatNumberRole); +    } +}; +  /**   * A specialization of GameListItem for size values.   * This class ensures that for every numerical size value it holds (in bytes), a correct @@ -141,8 +195,11 @@ class GameListWorker : public QObject, public QRunnable {      Q_OBJECT  public: -    GameListWorker(FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan) -        : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan) {} +    GameListWorker( +        FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan, +        const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list) +        : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan), +          compatibility_list(compatibility_list) {}  public slots:      /// Starts the processing of directory tree information. @@ -170,6 +227,7 @@ private:      QStringList watch_list;      QString dir_path;      bool deep_scan; +    const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;      std::atomic_bool stop_processing;      void AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ffa9f72aa..1501aedc4 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -16,6 +16,7 @@  #include <QMessageBox>  #include <QtGui>  #include <QtWidgets> +#include <fmt/format.h>  #include "common/common_paths.h"  #include "common/logging/backend.h"  #include "common/logging/filter.h" @@ -35,6 +36,7 @@  #include "core/gdbstub/gdbstub.h"  #include "core/loader/loader.h"  #include "core/settings.h" +#include "game_list_p.h"  #include "video_core/debug_utils/debug_utils.h"  #include "yuzu/about_dialog.h"  #include "yuzu/bootmanager.h" @@ -134,6 +136,7 @@ GMainWindow::GMainWindow()      // Necessary to load titles from nand in gamelist.      Service::FileSystem::CreateFactories(vfs); +    game_list->LoadCompatibilityList();      game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);      // Show one-time "callout" messages to the user @@ -349,6 +352,8 @@ void GMainWindow::RestoreUIState() {  void GMainWindow::ConnectWidgetEvents() {      connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);      connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); +    connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, +            &GMainWindow::OnGameListNavigateToGamedbEntry);      connect(this, &GMainWindow::EmulationStarting, render_window,              &GRenderWindow::OnEmulationStarting); @@ -678,6 +683,20 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target      QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));  } +void GMainWindow::OnGameListNavigateToGamedbEntry( +    u64 program_id, +    std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list) { + +    auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + +    QString directory; + +    if (it != compatibility_list.end()) +        directory = it->second.second; + +    QDesktopServices::openUrl(QUrl("https://yuzu-emu.org/game/" + directory)); +} +  void GMainWindow::OnMenuLoadFile() {      QString extensions;      for (const auto& piece : game_list->supported_file_extensions) diff --git a/src/yuzu/main.h b/src/yuzu/main.h index d1d34552b..fd2436f4d 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -124,6 +124,9 @@ private slots:      /// Called whenever a user selects a game in the game list widget.      void OnGameListLoadFile(QString game_path);      void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); +    void OnGameListNavigateToGamedbEntry( +        u64 program_id, +        std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);      void OnMenuLoadFile();      void OnMenuLoadFolder();      void OnMenuInstallToNAND(); diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 91d3f7def..e99042a23 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -4,6 +4,7 @@  #include <array>  #include <cmath> +#include <QPainter>  #include "yuzu/util/util.h"  QFont GetMonospaceFont() { @@ -24,3 +25,13 @@ QString ReadableByteSize(qulonglong size) {          .arg(size / std::pow(1024, digit_groups), 0, 'f', 1)          .arg(units[digit_groups]);  } + +QPixmap CreateCirclePixmapFromColor(const QColor& color) { +    QPixmap circle_pixmap(16, 16); +    circle_pixmap.fill(Qt::transparent); +    QPainter painter(&circle_pixmap); +    painter.setPen(color); +    painter.setBrush(color); +    painter.drawEllipse(0, 0, 15, 15); +    return circle_pixmap; +} diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index ab443ef9b..e6790f260 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -12,3 +12,10 @@ QFont GetMonospaceFont();  /// Convert a size in bytes into a readable format (KiB, MiB, etc.)  QString ReadableByteSize(qulonglong size); + +/** + * Creates a circle pixmap from a specified color + * @param color The color the pixmap shall have + * @return QPixmap circle pixmap + */ +QPixmap CreateCirclePixmapFromColor(const QColor& color);  | 
