mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	Show game compatibility within Citra
This commit is contained in:
		
							parent
							
								
									17e14d5615
								
							
						
					
					
						commit
						fbc05fac19
					
				
					 11 changed files with 153 additions and 11 deletions
				
			
		|  | @ -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
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue