Merge pull request #3463 from FearlessTobi/game-list-compat

citra-qt: Show Game Compatibility within Citra
This commit is contained in:
Weiyi Wang 2018-04-16 01:45:16 +03:00 committed by GitHub
commit a2ab91fa31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 153 additions and 11 deletions

View file

@ -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 export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH
mkdir build && cd build 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 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -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 export PATH=/citra/cmake/cmake-3.10.1-Linux-x86_64/bin:$PATH
mkdir build && cd build 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 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -7,7 +7,7 @@ export Qt5_DIR=$(brew --prefix)/opt/qt5
export PATH="/usr/local/opt/ccache/libexec:$PATH" export PATH="/usr/local/opt/ccache/libexec:$PATH"
mkdir build && cd build 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 make -j4
ctest -VV -C Release ctest -VV -C Release

View file

@ -40,6 +40,22 @@ function(check_submodules_present)
endfunction() endfunction()
check_submodules_present() 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 # Detect current compilation architecture and create standard definitions
# ======================================================================= # =======================================================================

View file

@ -43,9 +43,9 @@ before_build:
$COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING}
if ($env:BUILD_TYPE -eq 'msvc') { 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 # 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 { } 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 .. - cd ..

View file

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="compatibility_list">
<file>compatibility_list.json</file>
</qresource>
</RCC>

View file

@ -85,6 +85,9 @@ set(UIS
compatdb.ui 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 ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*)
file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*)
@ -125,6 +128,7 @@ endif()
target_sources(citra-qt target_sources(citra-qt
PRIVATE PRIVATE
${COMPAT_LIST}
${ICONS} ${ICONS}
${THEMES} ${THEMES}
${UI_HDRS} ${UI_HDRS}

View file

@ -2,11 +2,14 @@
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <cinttypes>
#include <QApplication> #include <QApplication>
#include <QFileInfo> #include <QFileInfo>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QJsonDocument>
#include <QJsonObject>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
@ -227,6 +230,7 @@ GameList::GameList(GMainWindow* parent) : QWidget{parent} {
item_model->insertColumns(0, COLUMN_COUNT); item_model->insertColumns(0, COLUMN_COUNT);
item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name"); 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_FILE_TYPE, Qt::Horizontal, "File type");
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); 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)); 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) { void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
if (!FileUtil::Exists(dir_path.toStdString()) || if (!FileUtil::Exists(dir_path.toStdString()) ||
!FileUtil::IsDirectory(dir_path.toStdString())) { !FileUtil::IsDirectory(dir_path.toStdString())) {
@ -351,7 +388,7 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
emit ShouldCancelWorker(); 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::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
@ -436,8 +473,21 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
return update_smdh; return update_smdh;
}(); }();
auto it = std::find_if(compatibility_list.begin(), compatibility_list.end(),
[program_id](const std::pair<std::string, QString>& 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({ emit EntryReady({
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
new GameListItemCompat(compatibility),
new GameListItem( new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)), new GameListItemSize(FileUtil::GetSize(physical_name)),

View file

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <unordered_map>
#include <QString> #include <QString>
#include <QWidget> #include <QWidget>
#include "common/common_types.h" #include "common/common_types.h"
@ -29,6 +30,7 @@ class GameList : public QWidget {
public: public:
enum { enum {
COLUMN_NAME, COLUMN_NAME,
COLUMN_COMPATIBILITY,
COLUMN_FILE_TYPE, COLUMN_FILE_TYPE,
COLUMN_SIZE, COLUMN_SIZE,
COLUMN_COUNT, // Number of columns COLUMN_COUNT, // Number of columns
@ -68,6 +70,7 @@ public:
void setFilterFocus(); void setFilterFocus();
void setFilterVisible(bool visibility); void setFilterVisible(bool visibility);
void LoadCompatibilityList();
void PopulateAsync(const QString& dir_path, bool deep_scan); void PopulateAsync(const QString& dir_path, bool deep_scan);
void SaveInterfaceLayout(); void SaveInterfaceLayout();
@ -100,6 +103,7 @@ private:
QStandardItemModel* item_model = nullptr; QStandardItemModel* item_model = nullptr;
GameListWorker* current_worker = nullptr; GameListWorker* current_worker = nullptr;
QFileSystemWatcher* watcher = nullptr; QFileSystemWatcher* watcher = nullptr;
std::unordered_map<std::string, QString> compatibility_list;
}; };
Q_DECLARE_METATYPE(GameListOpenTarget); Q_DECLARE_METATYPE(GameListOpenTarget);

View file

@ -5,11 +5,16 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <map>
#include <unordered_map>
#include <QImage> #include <QImage>
#include <QObject>
#include <QPainter>
#include <QRunnable> #include <QRunnable>
#include <QStandardItem> #include <QStandardItem>
#include <QString> #include <QString>
#include "citra_qt/util/util.h" #include "citra_qt/util/util.h"
#include "common/logging/log.h"
#include "common/string_util.h" #include "common/string_util.h"
#include "core/loader/smdh.h" #include "core/loader/smdh.h"
@ -39,6 +44,23 @@ static QPixmap GetDefaultIcon(bool large) {
return icon; 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. * Gets the short game title from SMDH data.
* @param smdh SMDH data * @param smdh SMDH data
@ -50,8 +72,25 @@ static QString GetQStringShortTitleFromSMDH(const Loader::SMDH& smdh,
return QString::fromUtf16(smdh.GetShortTitle(language).data()); 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<QString, CompatStatus> 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: public:
GameListItem() : QStandardItem() {} GameListItem() : QStandardItem() {}
GameListItem(const QString& string) : QStandardItem(string) {} 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. * If this class receives valid SMDH data, it will also display game icons and titles.
*/ */
class GameListItemPath : public GameListItem { class GameListItemPath : public GameListItem {
public: public:
static const int FullPathRole = Qt::UserRole + 1; static const int FullPathRole = Qt::UserRole + 1;
static const int TitleRole = Qt::UserRole + 2; 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. * A specialization of GameListItem for size values.
* This class ensures that for every numerical size value it holds (in bytes), a correct * 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. * human-readable string representation will be displayed to the user.
*/ */
class GameListItemSize : public GameListItem { class GameListItemSize : public GameListItem {
public: public:
static const int SizeRole = Qt::UserRole + 1; static const int SizeRole = Qt::UserRole + 1;
@ -152,8 +211,10 @@ class GameListWorker : public QObject, public QRunnable {
Q_OBJECT Q_OBJECT
public: public:
GameListWorker(QString dir_path, bool deep_scan) GameListWorker(QString dir_path, bool deep_scan,
: QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan) {} const std::unordered_map<std::string, QString>& compatibility_list)
: QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan),
compatibility_list(compatibility_list) {}
public slots: public slots:
/// Starts the processing of directory tree information. /// Starts the processing of directory tree information.
@ -179,6 +240,7 @@ private:
QStringList watch_list; QStringList watch_list;
QString dir_path; QString dir_path;
bool deep_scan; bool deep_scan;
const std::unordered_map<std::string, QString>& compatibility_list;
std::atomic_bool stop_processing; std::atomic_bool stop_processing;
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);

View file

@ -131,6 +131,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
show(); show();
game_list->LoadCompatibilityList();
game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
// Show one-time "callout" messages to the user // Show one-time "callout" messages to the user