diff --git a/.travis/linux-frozen/docker.sh b/.travis/linux-frozen/docker.sh index d5031c651a..eef05d0b24 100755 --- a/.travis/linux-frozen/docker.sh +++ b/.travis/linux-frozen/docker.sh @@ -20,7 +20,7 @@ echo y | sh cmake-3.10.1-Linux-x86_64.sh --prefix=cmake export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH mkdir build && cd build -cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON make -j4 ctest -VV -C Release diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 8d03946cf9..703817d9f1 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh @@ -11,7 +11,7 @@ echo y | sh cmake-3.10.1-Linux-x86_64.sh --prefix=cmake export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH mkdir build && cd build -cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON make -j4 ctest -VV -C Release diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh index ce6445fab1..281f6b5be5 100755 --- a/.travis/macos/build.sh +++ b/.travis/macos/build.sh @@ -7,7 +7,7 @@ export Qt5_DIR=$(brew --prefix)/opt/qt5 export PATH="/usr/local/opt/ccache/libexec:$PATH" mkdir build && cd build -cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} +cmake .. -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON make -j4 ctest -VV -C Release diff --git a/CMakeLists.txt b/CMakeLists.txt index d587e9c1bd..9c78b70aac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,22 @@ 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 citra...") + file(DOWNLOAD + https://api.citra-emu.org/gamedb/titleid/ + "${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 aaaaaa7691..6a216a3809 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,9 +43,9 @@ before_build: $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} 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" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1 && exit 0' + cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DCITRA_USE_BUNDLED_QT=1 -DCITRA_USE_BUNDLED_SDL2=1 -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0' } else { - C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} .. 2>&1" + C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -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 0000000000..a29b735981 --- /dev/null +++ b/dist/compatibility_list/compatibility_list.qrc @@ -0,0 +1,5 @@ + + + compatibility_list.json + + diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index b0fc1acca0..4d08a622eb 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -85,6 +85,9 @@ set(UIS compatdb.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/*) @@ -125,6 +128,7 @@ endif() target_sources(citra-qt PRIVATE + ${COMPAT_LIST} ${ICONS} ${THEMES} ${UI_HDRS} diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index cc87f24151..401cd9d983 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -2,11 +2,14 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include #include #include +#include +#include #include #include #include @@ -227,6 +230,7 @@ GameList::GameList(GMainWindow* parent) : QWidget{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"); @@ -337,6 +341,39 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { 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)) { + NGLOG_ERROR(Frontend, "Unable to open game compatibility list"); + return; + } + + if (compat_list.size() == 0) { + NGLOG_ERROR(Frontend, "Game compatibility list is empty"); + return; + } + + const QByteArray content = compat_list.readAll(); + if (content.isEmpty()) { + NGLOG_ERROR(Frontend, "Unable to completely read game compatibility list"); + return; + } + + const QString string_content = content; + QJsonDocument json = QJsonDocument::fromJson(string_content.toUtf8()); + QJsonObject list = json.object(); + QStringList game_ids = list.keys(); + for (QString id : game_ids) { + QJsonObject game = list[id].toObject(); + + if (game.contains("compatibility") && game["compatibility"].isString()) { + QString compatibility = game["compatibility"].toString(); + compatibility_list.insert(std::make_pair(id.toUpper().toStdString(), compatibility)); + } + } +} + void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { if (!FileUtil::Exists(dir_path.toStdString()) || !FileUtil::IsDirectory(dir_path.toStdString())) { @@ -351,7 +388,7 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { emit ShouldCancelWorker(); - GameListWorker* worker = new GameListWorker(dir_path, deep_scan); + GameListWorker* worker = new GameListWorker(dir_path, deep_scan, compatibility_list); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, @@ -436,8 +473,21 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign return update_smdh; }(); + auto it = std::find_if(compatibility_list.begin(), compatibility_list.end(), + [program_id](const std::pair& element) { + std::string pid = + Common::StringFromFormat("%016" PRIX64, program_id); + return element.first == pid; + }); + + // The game list uses this as compatibility number for untested games + QString compatibility("99"); + if (it != compatibility_list.end()) + compatibility = it->second; + emit EntryReady({ new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), + new GameListItemCompat(compatibility), new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(FileUtil::GetSize(physical_name)), diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index 74773d3d9c..c01dc1d21a 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include "common/common_types.h" @@ -29,6 +30,7 @@ class GameList : public QWidget { public: enum { COLUMN_NAME, + COLUMN_COMPATIBILITY, COLUMN_FILE_TYPE, COLUMN_SIZE, COLUMN_COUNT, // Number of columns @@ -68,6 +70,7 @@ public: void setFilterFocus(); void setFilterVisible(bool visibility); + void LoadCompatibilityList(); void PopulateAsync(const QString& dir_path, bool deep_scan); void SaveInterfaceLayout(); @@ -100,6 +103,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/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 6cf29be9e2..170c77b731 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -5,11 +5,16 @@ #pragma once #include +#include +#include #include +#include +#include #include #include #include #include "citra_qt/util/util.h" +#include "common/logging/log.h" #include "common/string_util.h" #include "core/loader/smdh.h" @@ -39,6 +44,23 @@ static QPixmap GetDefaultIcon(bool large) { return icon; } +/** + * Creates a circle pixmap from a specified color + * @param color The color the pixmap shall have + * @return QPixmap circle pixmap + */ +static 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; +} + /** * Gets the short game title from SMDH data. * @param smdh SMDH data @@ -50,8 +72,25 @@ static QString GetQStringShortTitleFromSMDH(const Loader::SMDH& smdh, return QString::fromUtf16(smdh.GetShortTitle(language).data()); } -class GameListItem : public QStandardItem { +struct CompatStatus { + QString color; + QString text; + QString tooltip; +}; +// When this is put in a class, MSVS builds crash when closing Citra +// clang-format off +const static inline std::map status_data = { +{ "0", { "#5c93ed", GameList::tr("Perfect"), GameList::tr("Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without\nany workarounds needed.") } }, +{ "1", { "#47d35c", GameList::tr("Great"), GameList::tr("Game functions with minor graphical or audio glitches and is playable from start to finish. May require some\nworkarounds.") } }, +{ "2", { "#94b242", GameList::tr("Okay"), GameList::tr("Game functions with major graphical or audio glitches, but game is playable from start to finish with\nworkarounds.") } }, +{ "3", { "#f2d624", GameList::tr("Bad"), GameList::tr("Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches\neven with workarounds.") } }, +{ "4", { "#FF0000", GameList::tr("Intro/Menu"), GameList::tr("Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start\nScreen.") } }, +{ "5", { "#828282", GameList::tr("Won't Boot"), GameList::tr("The game crashes when attempting to startup.") } }, +{ "99",{ "#000000", GameList::tr("Not Tested"), GameList::tr("The game has not yet been tested.") } }, }; +// clang-format on + +class GameListItem : public QStandardItem { public: GameListItem() : QStandardItem() {} GameListItem(const QString& string) : QStandardItem(string) {} @@ -65,7 +104,6 @@ public: * If this class receives valid SMDH data, it will also display game icons and titles. */ class GameListItemPath : public GameListItem { - public: static const int FullPathRole = Qt::UserRole + 1; static const int TitleRole = Qt::UserRole + 2; @@ -107,13 +145,34 @@ public: } }; +class GameListItemCompat : public GameListItem { +public: + static const int CompatNumberRole = Qt::UserRole + 1; + GameListItemCompat() = default; + explicit GameListItemCompat(const QString compatiblity) { + auto iterator = status_data.find(compatiblity); + if (iterator == status_data.end()) { + NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString()); + return; + } + CompatStatus status = iterator->second; + setData(compatiblity, CompatNumberRole); + setText(status.text); + setToolTip(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 * human-readable string representation will be displayed to the user. */ class GameListItemSize : public GameListItem { - public: static const int SizeRole = Qt::UserRole + 1; @@ -152,8 +211,10 @@ class GameListWorker : public QObject, public QRunnable { Q_OBJECT public: - GameListWorker(QString dir_path, bool deep_scan) - : QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan) {} + GameListWorker(QString dir_path, bool deep_scan, + const std::unordered_map& compatibility_list) + : QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan), + compatibility_list(compatibility_list) {} public slots: /// Starts the processing of directory tree information. @@ -179,6 +240,7 @@ private: QStringList watch_list; QString dir_path; bool deep_scan; + const std::unordered_map& compatibility_list; std::atomic_bool stop_processing; void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index ec9b7ba588..87d1a58ba4 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -131,6 +131,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { show(); + game_list->LoadCompatibilityList(); game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); // Show one-time "callout" messages to the user