diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 376ad28dd..d13ca50d8 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh @@ -10,7 +10,7 @@ ln -sf /usr/bin/ccache /usr/lib/ccache/cc ln -sf /usr/bin/ccache /usr/lib/ccache/c++ mkdir build && cd build ccache --show-stats > ccache_before -cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -G Ninja +cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -G Ninja ninja ccache --show-stats > ccache_after diff -U100 ccache_before ccache_after || true diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh index 5816b1d6e..d32340b7c 100755 --- a/.travis/macos/build.sh +++ b/.travis/macos/build.sh @@ -10,7 +10,7 @@ mkdir build && cd build export PATH=/usr/local/opt/ccache/libexec:$PATH ccache --show-stats > ccache_before cmake --version -cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release +cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON make -j4 ccache --show-stats > ccache_after diff -U100 ccache_before ccache_after || true diff --git a/CMakeLists.txt b/CMakeLists.txt index 59c610732..0f32ecfba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,19 @@ function(check_submodules_present) endfunction() check_submodules_present() +configure_file(${CMAKE_SOURCE_DIR}/dist/compatibility_list/compatibility_list.qrc + ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc + COPYONLY) +if (ENABLE_COMPATIBILITY_LIST_DOWNLOAD AND NOT EXISTS ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) + message(STATUS "Downloading compatibility list for yuzu...") + file(DOWNLOAD + https://api.yuzu-emu.org/gamedb/ + "${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" SHOW_PROGRESS) +endif() +if (NOT EXISTS ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) + file(WRITE ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json "") +endif() + # Detect current compilation architecture and create standard definitions # ======================================================================= diff --git a/appveyor.yml b/appveyor.yml index a6f12b267..d68ae87b1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,9 +41,9 @@ before_build: - ps: | if ($env:BUILD_TYPE -eq 'msvc') { # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning - cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 .. 2>&1 && exit 0' + cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0' } else { - C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release .. 2>&1" + C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1" } - cd .. diff --git a/dist/compatibility_list/compatibility_list.qrc b/dist/compatibility_list/compatibility_list.qrc new file mode 100644 index 000000000..a29b73598 --- /dev/null +++ b/dist/compatibility_list/compatibility_list.qrc @@ -0,0 +1,5 @@ + + + compatibility_list.json + + 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 #include #include +#include +#include +#include #include #include #include #include +#include #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>& 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> 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 #include #include +#include #include +#include #include +#include #include #include #include +#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>& compatibility_list, + u64 program_id) { + return std::find_if( + compatibility_list.begin(), compatibility_list.end(), + [program_id](const std::pair>& 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 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>& 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>& compatibility_list; std::atomic_bool stop_processing; void AddInstalledTitlesToGameList(std::shared_ptr 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 #include #include +#include #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>& 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>& 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 #include +#include #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);