summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-x.travis/linux/docker.sh2
-rwxr-xr-x.travis/macos/build.sh2
-rw-r--r--CMakeLists.txt13
-rw-r--r--appveyor.yml4
-rw-r--r--dist/compatibility_list/compatibility_list.qrc5
-rw-r--r--src/yuzu/CMakeLists.txt4
-rw-r--r--src/yuzu/game_list.cpp65
-rw-r--r--src/yuzu/game_list.h6
-rw-r--r--src/yuzu/game_list_p.h62
-rw-r--r--src/yuzu/main.cpp19
-rw-r--r--src/yuzu/main.h3
-rw-r--r--src/yuzu/util/util.cpp11
-rw-r--r--src/yuzu/util/util.h7
13 files changed, 196 insertions, 7 deletions
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 @@
+<RCC>
+ <qresource prefix="compatibility_list">
+ <file>compatibility_list.json</file>
+ </qresource>
+</RCC>
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 <QDir>
#include <QFileInfo>
#include <QHeaderView>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
#include <QKeyEvent>
#include <QMenu>
#include <QThreadPool>
#include <boost/container/flat_map.hpp>
+#include <fmt/format.h>
#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<std::string, std::pair<QString, QString>>& 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<std::string, std::pair<QString, QString>> 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 <atomic>
#include <map>
#include <memory>
+#include <unordered_map>
#include <utility>
+#include <QCoreApplication>
#include <QImage>
+#include <QObject>
#include <QRunnable>
#include <QStandardItem>
#include <QString>
+#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<std::string, std::pair<QString, QString>>& compatibility_list,
+ u64 program_id) {
+ return std::find_if(
+ compatibility_list.begin(), compatibility_list.end(),
+ [program_id](const std::pair<std::string, std::pair<QString, QString>>& 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<QString, CompatStatus> 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<std::string, std::pair<QString, QString>>& 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<std::string, std::pair<QString, QString>>& compatibility_list;
std::atomic_bool stop_processing;
void AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> 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 <QMessageBox>
#include <QtGui>
#include <QtWidgets>
+#include <fmt/format.h>
#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<std::string, std::pair<QString, QString>>& 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<std::string, std::pair<QString, QString>>& 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 <array>
#include <cmath>
+#include <QPainter>
#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);