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

citra-qt: Show Game Compatibility within Citra
master
Weiyi Wang 2018-04-16 01:45:16 +07: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

@ -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

@ -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

@ -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

@ -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
# =======================================================================

@ -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 ..

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

@ -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}

@ -2,11 +2,14 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cinttypes>
#include <QApplication>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QJsonDocument>
#include <QJsonObject>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
@ -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<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({
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)),

@ -4,6 +4,7 @@
#pragma once
#include <unordered_map>
#include <QString>
#include <QWidget>
#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<std::string, QString> compatibility_list;
};
Q_DECLARE_METATYPE(GameListOpenTarget);

@ -5,11 +5,16 @@
#pragma once
#include <atomic>
#include <map>
#include <unordered_map>
#include <QImage>
#include <QObject>
#include <QPainter>
#include <QRunnable>
#include <QStandardItem>
#include <QString>
#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<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:
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<std::string, QString>& 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<std::string, QString>& compatibility_list;
std::atomic_bool stop_processing;
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);

@ -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