From 0ec2c87a0e14ea060536d9a087263a7987beb55d Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 27 Feb 2021 00:44:53 +1000 Subject: [PATCH] Implement RetroAchivements --- dep/CMakeLists.txt | 2 +- src/core/system.cpp | 7 +- src/core/system.h | 3 + .../duckstation-nogui.vcxproj | 24 +- src/duckstation-qt/CMakeLists.txt | 11 + src/duckstation-qt/achievementlogindialog.cpp | 63 + src/duckstation-qt/achievementlogindialog.h | 22 + src/duckstation-qt/achievementlogindialog.ui | 151 +++ .../achievementsettingswidget.cpp | 113 ++ .../achievementsettingswidget.h | 27 + .../achievementsettingswidget.ui | 183 +++ src/duckstation-qt/duckstation-qt.vcxproj | 36 +- .../duckstation-qt.vcxproj.filters | 8 + src/duckstation-qt/mainwindow.cpp | 2 + src/duckstation-qt/mainwindow.ui | 10 + src/duckstation-qt/qthostinterface.cpp | 38 + src/duckstation-qt/qthostinterface.h | 2 + .../resources/icons/emblem-person-blue.png | Bin 0 -> 1521 bytes .../resources/icons/emblem-person-blue@2x.png | Bin 0 -> 3939 bytes src/duckstation-qt/resources/icons/trophy.png | Bin 0 -> 1364 bytes .../resources/icons/trophy@2x.png | Bin 0 -> 2757 bytes src/duckstation-qt/resources/resources.qrc | 4 + src/duckstation-qt/settingsdialog.cpp | 18 + src/duckstation-qt/settingsdialog.h | 4 + src/duckstation-qt/settingsdialog.ui | 9 + src/frontend-common/CMakeLists.txt | 9 +- src/frontend-common/cheevos.cpp | 1114 +++++++++++++++++ src/frontend-common/cheevos.h | 76 ++ src/frontend-common/common_host_interface.cpp | 103 +- src/frontend-common/common_host_interface.h | 20 +- src/frontend-common/frontend-common.vcxproj | 57 +- .../frontend-common.vcxproj.filters | 6 +- src/frontend-common/fullscreen_ui.cpp | 359 +++++- src/frontend-common/fullscreen_ui.h | 3 +- 34 files changed, 2419 insertions(+), 65 deletions(-) create mode 100644 src/duckstation-qt/achievementlogindialog.cpp create mode 100644 src/duckstation-qt/achievementlogindialog.h create mode 100644 src/duckstation-qt/achievementlogindialog.ui create mode 100644 src/duckstation-qt/achievementsettingswidget.cpp create mode 100644 src/duckstation-qt/achievementsettingswidget.h create mode 100644 src/duckstation-qt/achievementsettingswidget.ui create mode 100644 src/duckstation-qt/resources/icons/emblem-person-blue.png create mode 100644 src/duckstation-qt/resources/icons/emblem-person-blue@2x.png create mode 100644 src/duckstation-qt/resources/icons/trophy.png create mode 100644 src/duckstation-qt/resources/icons/trophy@2x.png create mode 100644 src/frontend-common/cheevos.cpp create mode 100644 src/frontend-common/cheevos.h diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index b5d2b6aaa..374d934ab 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -23,7 +23,7 @@ if(ENABLE_DISCORD_PRESENCE) add_subdirectory(discord-rpc) endif() -if(ENABLE_RETROACHIEVEMENTS) +if(ENABLE_CHEEVOS) add_subdirectory(rcheevos) endif() diff --git a/src/core/system.cpp b/src/core/system.cpp index e4debf47c..a0c7538e9 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1979,9 +1979,14 @@ u32 GetMediaPlaylistIndex() return std::numeric_limits::max(); const std::string& media_path = g_cdrom.GetMediaFileName(); + return GetMediaPlaylistIndexForPath(media_path); +} + +u32 GetMediaPlaylistIndexForPath(const std::string& path) +{ for (u32 i = 0; i < static_cast(s_media_playlist.size()); i++) { - if (s_media_playlist[i] == media_path) + if (s_media_playlist[i] == path) return i; } diff --git a/src/core/system.h b/src/core/system.h index 1408839e2..9fd140bec 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -211,6 +211,9 @@ u32 GetMediaPlaylistCount(); /// Returns the current image from the media/disc playlist. u32 GetMediaPlaylistIndex(); +/// Returns the index of the specified path in the playlist, or UINT32_MAX if it does not exist. +u32 GetMediaPlaylistIndexForPath(const std::string& path); + /// Returns the path to the specified playlist index. const std::string& GetMediaPlaylistPath(u32 index); diff --git a/src/duckstation-nogui/duckstation-nogui.vcxproj b/src/duckstation-nogui/duckstation-nogui.vcxproj index 78033233b..5089edb62 100644 --- a/src/duckstation-nogui/duckstation-nogui.vcxproj +++ b/src/duckstation-nogui/duckstation-nogui.vcxproj @@ -342,7 +342,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -364,7 +364,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -386,7 +386,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -408,7 +408,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -433,7 +433,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -458,7 +458,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) @@ -484,7 +484,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -508,7 +508,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true @@ -533,7 +533,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -557,7 +557,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true false @@ -581,7 +581,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true @@ -606,7 +606,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index d75e7af07..9901cf60e 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -103,6 +103,17 @@ set(SRCS settingsdialog.ui ) +if(WITH_CHEEVOS) + set(SRCS ${SRCS} + achievementlogindialog.cpp + achievementlogindialog.h + achievementlogindialog.ui + achievementsettingswidget.cpp + achievementsettingswidget.h + achievementsettingswidget.ui + ) +endif() + set(TS_FILES translations/duckstation-qt_de.ts translations/duckstation-qt_es.ts diff --git a/src/duckstation-qt/achievementlogindialog.cpp b/src/duckstation-qt/achievementlogindialog.cpp new file mode 100644 index 000000000..cb65c8fe6 --- /dev/null +++ b/src/duckstation-qt/achievementlogindialog.cpp @@ -0,0 +1,63 @@ +#include "achievementlogindialog.h" +#include "frontend-common/cheevos.h" +#include "qthostinterface.h" +#include + +AchievementLoginDialog::AchievementLoginDialog(QWidget* parent) : QDialog(parent) +{ + m_ui.setupUi(this); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + connectUi(); +} + +AchievementLoginDialog::~AchievementLoginDialog() = default; + +void AchievementLoginDialog::loginClicked() +{ + const std::string username(m_ui.userName->text().toStdString()); + const std::string password(m_ui.password->text().toStdString()); + if (username.empty() || password.empty()) + { + QMessageBox::critical(this, tr("Login Error"), tr("A user name and password must be provided.")); + return; + } + + // TODO: Make cancellable. + m_ui.status->setText(tr("Logging in...")); + enableUI(false); + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + + bool result; + QtHostInterface::GetInstance()->executeOnEmulationThread( + [&username, &password, &result]() { result = Cheevos::Login(username.c_str(), password.c_str()); }, true); + + if (!result) + { + QMessageBox::critical(this, tr("Login Error"), + tr("Login failed. Please check your username and password, and try again.")); + m_ui.status->setText(tr("Login failed.")); + enableUI(true); + return; + } + + done(0); +} + +void AchievementLoginDialog::cancelClicked() +{ + done(1); +} + +void AchievementLoginDialog::connectUi() +{ + connect(m_ui.login, &QPushButton::clicked, this, &AchievementLoginDialog::loginClicked); + connect(m_ui.cancel, &QPushButton::clicked, this, &AchievementLoginDialog::cancelClicked); +} + +void AchievementLoginDialog::enableUI(bool enabled) +{ + m_ui.userName->setEnabled(enabled); + m_ui.password->setEnabled(enabled); + m_ui.cancel->setEnabled(enabled); + m_ui.login->setEnabled(enabled); +} diff --git a/src/duckstation-qt/achievementlogindialog.h b/src/duckstation-qt/achievementlogindialog.h new file mode 100644 index 000000000..c40c146f5 --- /dev/null +++ b/src/duckstation-qt/achievementlogindialog.h @@ -0,0 +1,22 @@ +#pragma once +#include "ui_achievementlogindialog.h" +#include + +class AchievementLoginDialog : public QDialog +{ + Q_OBJECT + +public: + AchievementLoginDialog(QWidget* parent); + ~AchievementLoginDialog(); + +private Q_SLOTS: + void loginClicked(); + void cancelClicked(); + +private: + void connectUi(); + void enableUI(bool enabled); + + Ui::AchievementLoginDialog m_ui; +}; diff --git a/src/duckstation-qt/achievementlogindialog.ui b/src/duckstation-qt/achievementlogindialog.ui new file mode 100644 index 000000000..555bf393c --- /dev/null +++ b/src/duckstation-qt/achievementlogindialog.ui @@ -0,0 +1,151 @@ + + + AchievementLoginDialog + + + Qt::WindowModal + + + + 0 + 0 + 410 + 190 + + + + + 410 + 190 + + + + + 410 + 190 + + + + RetroAchievements Login + + + true + + + + + + + + + + + :/icons/emblem-person-blue.png + + + + + + + + 14 + 50 + false + + + + RetroAchievements Login + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + Please enter user name and password for retroachievements.org below. Your password will not be saved in DuckStation, instead an access token will be generated and used instead. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + User Name: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + + Ready... + + + + + + + &Login + + + true + + + + + + + &Cancel + + + + + + + + + + + + diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp new file mode 100644 index 000000000..170eacf80 --- /dev/null +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -0,0 +1,113 @@ +#include "achievementsettingswidget.h" +#include "achievementlogindialog.h" +#include "common/string_util.h" +#include "core/system.h" +#include "frontend-common/cheevos.h" +#include "qtutils.h" +#include "settingsdialog.h" +#include "settingwidgetbinder.h" +#include +#include + +AchievementSettingsWidget::AchievementSettingsWidget(QtHostInterface* host_interface, QWidget* parent, + SettingsDialog* dialog) + : QWidget(parent), m_host_interface(host_interface) +{ + m_ui.setupUi(this); + + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.enable, "Cheevos", "Enabled", false); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.richPresence, "Cheevos", "RichPresence", true); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.testMode, "Cheevos", "TestMode", false); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.useFirstDiscFromPlaylist, "Cheevos", + "UseFirstDiscFromPlaylist", true); + + dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"), + tr("When enabled and logged in, DuckStation will scan for achievements on startup.")); + dialog->registerWidgetHelp(m_ui.testMode, tr("Enable Test Mode"), tr("Unchecked"), + tr("When enabled, DuckStation will assume all achievements are locked and not send any " + "unlock notifications to the server.")); + dialog->registerWidgetHelp( + m_ui.richPresence, tr("Enable Rich Presence"), tr("Unchecked"), + tr("When enabled, rich presence information will be collected and sent to the server where supported.")); + dialog->registerWidgetHelp( + m_ui.useFirstDiscFromPlaylist, tr("Use First Disc From Playlist"), tr("Unchecked"), + tr( + "When enabled, the first disc in a playlist will be used for achievements, regardless of which disc is active.")); + + connect(m_ui.enable, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); + connect(m_ui.loginButton, &QPushButton::clicked, this, &AchievementSettingsWidget::onLoginLogoutPressed); + connect(m_ui.viewProfile, &QPushButton::clicked, this, &AchievementSettingsWidget::onViewProfilePressed); + connect(host_interface, &QtHostInterface::achievementsLoaded, this, &AchievementSettingsWidget::onAchievementsLoaded); + + updateEnableState(); + updateLoginState(); + + // force a refresh of game info + host_interface->OnAchievementsRefreshed(); +} + +AchievementSettingsWidget::~AchievementSettingsWidget() = default; + +void AchievementSettingsWidget::updateEnableState() +{ + const bool enabled = m_host_interface->GetBoolSettingValue("Cheevos", "Enabled", false); + m_ui.testMode->setEnabled(enabled); + m_ui.useFirstDiscFromPlaylist->setEnabled(enabled); +} + +void AchievementSettingsWidget::updateLoginState() +{ + const std::string username(m_host_interface->GetStringSettingValue("Cheevos", "Username")); + const bool logged_in = !username.empty(); + + if (logged_in) + { + const u64 login_unix_timestamp = + StringUtil::FromChars(m_host_interface->GetStringSettingValue("Cheevos", "LoginTimestamp", "0")).value_or(0); + const QDateTime login_timestamp(QDateTime::fromSecsSinceEpoch(static_cast(login_unix_timestamp))); + m_ui.loginStatus->setText(tr("Username: %1\nLogin token generated on %2.") + .arg(QString::fromStdString(username)) + .arg(login_timestamp.toString(Qt::TextDate))); + m_ui.loginButton->setText(tr("Logout")); + } + else + { + m_ui.loginStatus->setText(tr("Not Logged In.")); + m_ui.loginButton->setText(tr("Login...")); + m_ui.viewProfile->setEnabled(false); + } +} + +void AchievementSettingsWidget::onLoginLogoutPressed() +{ + if (Cheevos::IsLoggedIn()) + { + m_host_interface->executeOnEmulationThread([]() { Cheevos::Logout(); }, true); + updateLoginState(); + return; + } + + AchievementLoginDialog login(this); + int res = login.exec(); + if (res != 0) + return; + + updateLoginState(); +} + +void AchievementSettingsWidget::onViewProfilePressed() +{ + if (!Cheevos::IsLoggedIn()) + return; + + const QByteArray encoded_username(QUrl::toPercentEncoding(QString::fromStdString(Cheevos::GetUsername()))); + QtUtils::OpenURL( + QtUtils::GetRootWidget(this), + QUrl(QStringLiteral("https://retroachievements.org/user/%1").arg(QString::fromUtf8(encoded_username)))); +} + +void AchievementSettingsWidget::onAchievementsLoaded(quint32 id, const QString& game_info_string, quint32 total, + quint32 points) +{ + m_ui.gameInfo->setText(game_info_string); +} diff --git a/src/duckstation-qt/achievementsettingswidget.h b/src/duckstation-qt/achievementsettingswidget.h new file mode 100644 index 000000000..a97f7708f --- /dev/null +++ b/src/duckstation-qt/achievementsettingswidget.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "ui_achievementsettingswidget.h" + +class QtHostInterface; +class SettingsDialog; + +class AchievementSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit AchievementSettingsWidget(QtHostInterface* host_interface, QWidget* parent, SettingsDialog* dialog); + ~AchievementSettingsWidget(); + +private Q_SLOTS: + void updateEnableState(); + void updateLoginState(); + void onLoginLogoutPressed(); + void onViewProfilePressed(); + void onAchievementsLoaded(quint32 id, const QString& game_info_string, quint32 total, quint32 points); + +private: + Ui::AchievementSettingsWidget m_ui; + + QtHostInterface* m_host_interface; +}; diff --git a/src/duckstation-qt/achievementsettingswidget.ui b/src/duckstation-qt/achievementsettingswidget.ui new file mode 100644 index 000000000..f40200a4c --- /dev/null +++ b/src/duckstation-qt/achievementsettingswidget.ui @@ -0,0 +1,183 @@ + + + AchievementSettingsWidget + + + + 0 + 0 + 648 + 456 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Global Settings + + + + + + Enable Achievements + + + + + + + Enable Rich Presence + + + + + + + Enable Test Mode + + + + + + + Use First Disc From Playlist + + + + + + + + + + Account + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + Login... + + + + + + + View Profile... + + + + + + + + + + + + Account Settings + + + + + + false + + + Enable Hardcode Mode + + + + + + + Enabling hardcore mode will disable cheats, save sates, and debugging features. + + + + + + + + + + + 0 + 160 + + + + Game Info + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + <html><head/><body><p>DuckStation uses RetroAchievements as an achievement database and for tracking progress. To use achievements, please sign up for an account at <a href="https://retroachievements.org/"><span style=" text-decoration: underline; color:#0000ff;">retroachievements.org</span></a>.</p></body></html> + + + Qt::RichText + + + true + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index b90bcd023..6b20ee4e3 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -52,6 +52,8 @@ + + @@ -114,6 +116,8 @@ + + @@ -216,6 +220,8 @@ + + @@ -316,6 +322,12 @@ + + Document + + + Document + @@ -532,7 +544,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) @@ -554,7 +566,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -576,7 +588,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -598,7 +610,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) @@ -622,7 +634,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -646,7 +658,7 @@ Level4 Disabled - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) true ProgramDatabase $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) @@ -671,7 +683,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) true false @@ -695,7 +707,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories) true true @@ -720,7 +732,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true false @@ -744,7 +756,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true false @@ -768,7 +780,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true true @@ -793,7 +805,7 @@ MaxSpeed true - WITH_DISCORD_PRESENCE=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WITH_SDL2=1;WITH_IMGUI=1;WITH_RECOMPILER=1;WITH_MMAP_FASTMEM=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\Include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories) true true diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 2d6acace6..fc250c6e7 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -77,6 +77,10 @@ + + + + @@ -129,6 +133,8 @@ + + @@ -151,6 +157,8 @@ + + diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 49299e4f7..0efdb316f 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1006,6 +1006,8 @@ void MainWindow::connectSignals() [this]() { doSettings(SettingsDialog::Category::PostProcessingSettings); }); connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::AudioSettings); }); + connect(m_ui.actionAchievementSettings, &QAction::triggered, + [this]() { doSettings(SettingsDialog::Category::AchievementSettings); }); connect(m_ui.actionAdvancedSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::AdvancedSettings); }); connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 70d2a468e..57ff5ea3b 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -128,6 +128,7 @@ + @@ -503,6 +504,15 @@ Audio Settings... + + + + :/icons/trophy.png:/icons/trophy.png + + + Achievement Settings... + + diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index a9cc2e822..c4979b480 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -43,6 +43,10 @@ Log_SetChannel(QtHostInterface); #include #endif +#ifdef WITH_CHEEVOS +#include "frontend-common/cheevos.h" +#endif + QtHostInterface::QtHostInterface(QObject* parent) : QObject(parent), CommonHostInterface() { qRegisterMetaType>(); @@ -1352,6 +1356,40 @@ void QtHostInterface::saveScreenshot() SaveScreenshot(nullptr, true, true); } +void QtHostInterface::OnAchievementsRefreshed() +{ +#ifdef WITH_CHEEVOS + QString game_info; + + if (Cheevos::HasActiveGame()) + { + game_info = tr("Game ID: %1\n" + "Game Title: %2\n" + "Game Developer: %3\n" + "Game Publisher: %4\n" + "Achievements: %5 (%6 points)\n\n") + .arg(Cheevos::GetGameID()) + .arg(QString::fromStdString(Cheevos::GetGameTitle())) + .arg(QString::fromStdString(Cheevos::GetGameDeveloper())) + .arg(QString::fromStdString(Cheevos::GetGamePublisher())) + .arg(Cheevos::GetAchievementCount()) + .arg(Cheevos::GetMaximumPointsForGame()); + + const std::string& rich_presence_string = Cheevos::GetRichPresenceString(); + if (!rich_presence_string.empty()) + game_info.append(QString::fromStdString(rich_presence_string)); + else + game_info.append(tr("Rich presence inactive or unsupported.")); + } + else + { + game_info = tr("Game not loaded or no RetroAchievements available."); + } + + emit achievementsLoaded(Cheevos::GetGameID(), game_info, Cheevos::GetAchievementCount(), + Cheevos::GetMaximumPointsForGame()); +#endif +} void QtHostInterface::doBackgroundControllerPoll() { PollAndUpdate(); diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index 9b757f207..878279e4b 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -137,6 +137,7 @@ Q_SIGNALS: void exitRequested(); void inputProfileLoaded(); void mouseModeRequested(bool relative, bool hide_cursor); + void achievementsLoaded(quint32 id, const QString& game_info_string, quint32 total, quint32 points); public Q_SLOTS: void setDefaultSettings(); @@ -173,6 +174,7 @@ public Q_SLOTS: void reloadPostProcessingShaders(); void requestRenderWindowScale(qreal scale); void executeOnEmulationThread(std::function callback, bool wait = false); + void OnAchievementsRefreshed() override; private Q_SLOTS: void doStopThread(); diff --git a/src/duckstation-qt/resources/icons/emblem-person-blue.png b/src/duckstation-qt/resources/icons/emblem-person-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6a9e2d98125a3c062017f3c898960c0323a087 GIT binary patch literal 1521 zcmVS;JIdK2B>(^glSxEDR9J=OmS2cmR~^Sc=bU@*&cFRP zJNq};O#VS@3??WTA2!f}mc-D9ib&gs79V{meJv=m2*p+qe3XKMqLc(d@IjI0A=*#{ zrL9(N+@{%dcQZS?Gdnx8Gk5OXKj$1DX0kEa+3v2XPyNDuxSZdg&pE%}{hbS+c8P;J z{Bs^}(~LcX63=7pF$+qdZvfZlN9Uv%czt;4WPW(Om{E=gHX`n{ zXw~k-waQA}61{Z(yS1Nw;s9@cXY9*vZs>CH^jGpiNo>*qlVIWi8?~?|2I;bP=Vl{l z*1v!LdmFEQVgPT=59NhB^xo04mu4g&+%!UYSgWy-kM3-ugDSdHL!}CoR~D*aP`@}g z-}viZvE_aMjPQOib^HsH0*&yps7wKso5smZAXB5rfkC93MMw|bT47@5L_y;Ict5fC z1hDf+Db>aF(1gNTgp)>kLpa$^cOZ?m;|~pxD6yf#jzLEl9fDL?qcO?@t1*#}3AeDZkB!@1C+VWYxNU@8 z+bjN_C3tMU{!th-B5O3dQ%46IXn&2wFO#&&=%9iQYUr?m)nPAxmR7UYveNz2Ljwq~ zozVQIy;V)D4$=NPiNA(!t)l%kw7-rHs@SO6wO1Y*K__V4dVIc8erN!I*WNkv!<$R1 zMtH+?A)vy0ILBG4 zgk-ZRH9C|TU$2B`HW%Kwd|-f)Q%MKw6h8CYNHwLj0#90D2!zAq-MFaRW85#%NKQfvj*HZ75f!11hy*gTz+1v zXMQz%cxtvdF`5!mVr}=+`Zx)|Q4*~+)oLwVE;Tnc7q2}VRz6(s#ol4Rn{uht@WklZ zXP@>)Cco}EsUyl2jx{#f_Ord=BE&Y8MW7RHwTai_M)~#X!tbwy)mtT?wHxrqNdd#a zAdmyHKnfTDT<-Od0b(Em0>JMjYyu4)IQD}8{I^m)fC0MQ>^uOF^Z>#hK<5ClKgIeF X($1bzr4S3V00000NkvXXu0mjfvMBAO literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/emblem-person-blue@2x.png b/src/duckstation-qt/resources/icons/emblem-person-blue@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..822004b1bfd69275550247716f31536b74f272c1 GIT binary patch literal 3939 zcmV-p51jCcP)AK~#90?VD?iT~~F-e`}v}?&I8hXYM@h*v@1Ev7HwM%0mQ!NoWgA zP`&`^2ii)crJ~{k6%s9?Qmbj9P(dqbE43h1f-hA+R6#AM1WJl&6k72}ngl0K9LJB? zGqz_uGk5O$JoaAwaPG`p+mrat_&Q&jzqEDdvG+b}{r6gHueJBN@ETsjYxw^Rc{LxV zbC^ey@poW&C*pRi$}T~RAm>DN5s??Hr{BA0u2y@sWO`KyI6ODL&shB|@BlEi@o*6p z5p~X)zV4}@KeB56`kv3PeD8Y8c3lZLJU3hL?Ao8H@JW7(YN% zu}OzCZV@*xQm-x2Zq!_ll*7*BllRQ69=TpJUN-_Bo15H|+Vo!qd0VkOs?)P?kz#cZ zh=4lO##wVIf`Td_sEe`PIxCB(s4Xw4pdD5H?e~1X_O0t7=XD|A(Yf*4)$&7u>0LL! zNveDAM7#h^I=G~T+8ESjM7ShtX$0yJFF=A2REaxloI8C~lQ=?@&m8*v>X(Nt>(B}K z@mzV@+T2gYGdJFN>md_P%z`>x+{ly`(|fFlT@JIIIY|5Dzp3pRox`N*I?$6bp8kWrF>RkqhXU9K^a@W+p+o??N&rCaN zAVCocLQGJ?1Z7OFj0EN0?;*x7B7PC^^N5#6yd2`?aJEZYKZEfC_RQWPV#qs@uMJh^ zVNLY?Pwy)ELHcjGTq)dp+aVD#xVVn-3rJAJ_(e<*B7Onm=Mm2b@yLuYbBQc2Sa3G` z)D%rx0QkjmaH(`!jeWm!D17D{k9M9NhTKE5fP9#I1Z3CbO}88I3)rZE7{9lg`AsXh zZ1u%2^j1EHm;f;W7$1y}h!0}0?Te_3sZP$|dA?KQ{%}}w4~+m({jl*pS00~5Z3HgP z6zC}>UOrp>pwuIwv}x|^%9_k7#CTb(SZ^q{y@dEVD%A;t%G(~B8=D!1+(RMYu|Jxu zh;VmhWZH=5v7seb-+;?nE;S9)=X;u_ zcJDA`9|{48?gsGv09=yo?=D5wvpgH)#KS;F2$Cskqby!*5Rq=|0P1=~sM~mqx�( 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 LNkvXXu0mjf3*H-S literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/resources.qrc b/src/duckstation-qt/resources/resources.qrc index 1d9ba5554..ee73cad70 100644 --- a/src/duckstation-qt/resources/resources.qrc +++ b/src/duckstation-qt/resources/resources.qrc @@ -52,6 +52,8 @@ icons/edit-clear-16.png icons/edit-clear-16@2x.png icons/edit-find.png + icons/emblem-person-blue.png + icons/emblem-person-blue@2x.png icons/flag-eu.png icons/flag-eu@2x.png icons/flag-jp.png @@ -112,6 +114,8 @@ icons/system-search@2x.png icons/system-shutdown.png icons/system-shutdown@2x.png + icons/trophy.png + icons/trophy@2x.png icons/utilities-system-monitor.png icons/utilities-system-monitor@2x.png icons/video-display.png diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index 6a6028e95..e26b614c0 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -15,6 +15,10 @@ #include "qthostinterface.h" #include +#ifdef WITH_CHEEVOS +#include "achievementsettingswidget.h" +#endif + static constexpr char DEFAULT_SETTING_HELP_TEXT[] = ""; SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) @@ -39,6 +43,10 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent m_audio_settings = new AudioSettingsWidget(host_interface, m_ui.settingsContainer, this); m_advanced_settings = new AdvancedSettingsWidget(host_interface, m_ui.settingsContainer, this); +#ifdef WITH_CHEEVOS + m_achievement_settings = new AchievementSettingsWidget(host_interface, m_ui.settingsContainer, this); +#endif + m_ui.settingsContainer->insertWidget(static_cast(Category::GeneralSettings), m_general_settings); m_ui.settingsContainer->insertWidget(static_cast(Category::BIOSSettings), m_bios_settings); m_ui.settingsContainer->insertWidget(static_cast(Category::ConsoleSettings), m_console_settings); @@ -51,6 +59,16 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent m_ui.settingsContainer->insertWidget(static_cast(Category::EnhancementSettings), m_enhancement_settings); m_ui.settingsContainer->insertWidget(static_cast(Category::PostProcessingSettings), m_post_processing_settings); m_ui.settingsContainer->insertWidget(static_cast(Category::AudioSettings), m_audio_settings); + +#ifdef WITH_CHEEVOS + m_ui.settingsContainer->insertWidget(static_cast(Category::AchievementSettings), m_achievement_settings); +#else + QLabel* placeholder_label = + new QLabel(tr("This DuckStation build was not compiled with RetroAchievements support."), m_ui.settingsContainer); + placeholder_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_ui.settingsContainer->insertWidget(static_cast(Category::AchievementSettings), placeholder_label); +#endif + m_ui.settingsContainer->insertWidget(static_cast(Category::AdvancedSettings), m_advanced_settings); m_ui.settingsCategory->setCurrentRow(0); diff --git a/src/duckstation-qt/settingsdialog.h b/src/duckstation-qt/settingsdialog.h index 5ff28b0d3..fc5294185 100644 --- a/src/duckstation-qt/settingsdialog.h +++ b/src/duckstation-qt/settingsdialog.h @@ -19,6 +19,7 @@ class DisplaySettingsWidget; class EnhancementSettingsWidget; class PostProcessingSettingsWidget; class AudioSettingsWidget; +class AchievementSettingsWidget; class AdvancedSettingsWidget; class SettingsDialog final : public QDialog @@ -40,6 +41,7 @@ public: EnhancementSettings, PostProcessingSettings, AudioSettings, + AchievementSettings, AdvancedSettings, Count }; @@ -58,6 +60,7 @@ public: DisplaySettingsWidget* getDisplaySettingsWidget() const { return m_display_settings; } EnhancementSettingsWidget* getEnhancementSettingsWidget() const { return m_enhancement_settings; } AudioSettingsWidget* getAudioSettingsWidget() const { return m_audio_settings; } + AchievementSettingsWidget* getAchievementSettingsWidget() const { return m_achievement_settings; } AdvancedSettingsWidget* getAdvancedSettingsWidget() const { return m_advanced_settings; } PostProcessingSettingsWidget* getPostProcessingSettingsWidget() { return m_post_processing_settings; } @@ -89,6 +92,7 @@ private: EnhancementSettingsWidget* m_enhancement_settings = nullptr; PostProcessingSettingsWidget* m_post_processing_settings = nullptr; AudioSettingsWidget* m_audio_settings = nullptr; + AchievementSettingsWidget* m_achievement_settings = nullptr; AdvancedSettingsWidget* m_advanced_settings = nullptr; std::array(Category::Count)> m_category_help_text; diff --git a/src/duckstation-qt/settingsdialog.ui b/src/duckstation-qt/settingsdialog.ui index e279d53c0..86ae0532c 100644 --- a/src/duckstation-qt/settingsdialog.ui +++ b/src/duckstation-qt/settingsdialog.ui @@ -161,6 +161,15 @@ :/icons/audio-card.png:/icons/audio-card.png + + + Achievement Settings + + + + :/icons/trophy.png:/icons/trophy.png + + Advanced Settings diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 177000a85..5f1dac5ed 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -91,7 +91,7 @@ if(ENABLE_DISCORD_PRESENCE) target_link_libraries(frontend-common PRIVATE discord-rpc) endif() -if(ENABLE_RETROACHIEVEMENTS) +if(ENABLE_CHEEVOS) target_sources(frontend-common PRIVATE http_downloader.cpp http_downloader.h @@ -102,6 +102,13 @@ if(ENABLE_RETROACHIEVEMENTS) http_downloader_winhttp.h ) endif() + + target_sources(frontend-common PRIVATE + cheevos.cpp + cheevos.h + ) + target_compile_definitions(frontend-common PUBLIC -DWITH_CHEEVOS=1) + target_link_libraries(frontend-common PRIVATE rcheevos rapidjson) endif() # Copy the provided data directory to the output directory. diff --git a/src/frontend-common/cheevos.cpp b/src/frontend-common/cheevos.cpp new file mode 100644 index 000000000..26640527c --- /dev/null +++ b/src/frontend-common/cheevos.cpp @@ -0,0 +1,1114 @@ +#include "cheevos.h" +#include "common/cd_image.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/md5_digest.h" +#include "common/string.h" +#include "common/string_util.h" +#include "common/timestamp.h" +#include "common_host_interface.h" +#include "core/bios.h" +#include "core/bus.h" +#include "core/cpu_core.h" +#include "core/host_display.h" +#include "core/system.h" +#include "fullscreen_ui.h" +#include "http_downloader.h" +#include "imgui_fullscreen.h" +#include "rapidjson/document.h" +#include "rc_url.h" +#include "rcheevos.h" +#include "scmversion/scmversion.h" +#include +#include +#include +#include +#include +Log_SetChannel(Cheevos); + +namespace Cheevos { + +enum : s32 +{ + HTTP_OK = FrontendCommon::HTTPDownloader::HTTP_OK, + + // Number of seconds between rich presence pings. RAIntegration uses 2 minutes. + RICH_PRESENCE_PING_FREQUENCY = 2 * 60, + NO_RICH_PRESENCE_PING_FREQUENCY = RICH_PRESENCE_PING_FREQUENCY * 2 +}; + +static void CheevosEventHandler(const rc_runtime_event_t* runtime_event); +static unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud); +static void ActivateLockedAchievements(); +static bool ActivateAchievement(Achievement* achievement); +static void DeactivateAchievement(Achievement* achievement); +static void SendPing(); +static void SendPlaying(); + +/// Uses a temporarily (second) CD image to resolve the hash. +static void GameChanged(); + +bool g_active = false; +u32 g_game_id = 0; + +static bool s_logged_in = false; +static bool s_test_mode = false; +static bool s_use_first_disc_from_playlist = true; +static bool s_hardcode_mode = false; +static bool s_rich_presence_enabled = false; + +static CommonHostInterface* s_host_interface; +static rc_runtime_t s_rcheevos_runtime; +static std::unique_ptr s_http_downloader; + +static std::string s_username; +static std::string s_login_token; + +static std::string s_game_path; +static std::string s_game_hash; +static std::string s_game_title; +static std::string s_game_developer; +static std::string s_game_publisher; +static std::string s_game_release_date; +static std::string s_game_icon; +static std::vector s_achievements; + +static bool s_has_rich_presence = false; +static std::string s_rich_presence_string; +static Common::Timer s_last_ping_time; + +static u32 s_total_image_downloads; +static u32 s_completed_image_downloads; +static bool s_image_download_progress_active; + +static void FormattedError(const char* format, ...) +{ + if (!s_host_interface) + return; + + std::va_list ap; + va_start(ap, format); + + SmallString str; + str.AppendString("Cheevos Error: "); + str.AppendFormattedStringVA(format, ap); + + va_end(ap); + + s_host_interface->AddOSDMessage(str.GetCharArray(), 10.0f); + Log_ErrorPrint(str.GetCharArray()); +} + +static std::string GetErrorFromResponseJSON(const rapidjson::Document& doc) +{ + if (doc.HasMember("Error") && doc["Error"].IsString()) + return doc["Error"].GetString(); + + return ""; +} + +static void LogFailedResponseJSON(const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + const std::string str_data(reinterpret_cast(data.data()), data.size()); + Log_ErrorPrintf("API call failed. Response JSON was:\n%s", str_data.c_str()); +} + +static bool ParseResponseJSON(const char* request_type, s32 status_code, + const FrontendCommon::HTTPDownloader::Request::Data& data, rapidjson::Document& doc, + const char* success_field = "Success") +{ + if (status_code != HTTP_OK || data.empty()) + { + FormattedError("%s failed: empty response", request_type); + LogFailedResponseJSON(data); + return false; + } + + doc.Parse(reinterpret_cast(data.data()), data.size()); + if (doc.HasParseError()) + { + FormattedError("%s failed: parse error at offset %zu: %u", request_type, doc.GetErrorOffset(), + static_cast(doc.GetParseError())); + LogFailedResponseJSON(data); + return false; + } + + if (success_field && (!doc.HasMember(success_field) || !doc[success_field].GetBool())) + { + const std::string error(GetErrorFromResponseJSON(doc)); + FormattedError("%s failed: Server returned an error: %s", request_type, error.c_str()); + LogFailedResponseJSON(data); + return false; + } + + return true; +} + +template +static std::string GetOptionalString(const T& value, const char* key) +{ + if (!value.HasMember(key) || !value[key].IsString()) + return std::string(); + + return value[key].GetString(); +} + +template +static u32 GetOptionalUInt(const T& value, const char* key) +{ + if (!value.HasMember(key) || !value[key].IsUint()) + return 0; + + return value[key].GetUint(); +} + +static Achievement* GetAchievementByID(u32 id) +{ + for (Achievement& ach : s_achievements) + { + if (ach.id == id) + return &ach; + } + + return nullptr; +} + +static void ClearGameInfo() +{ + const bool had_game = (g_game_id != 0); + + s_has_rich_presence = false; + + while (!s_achievements.empty()) + { + Achievement& ach = s_achievements.back(); + DeactivateAchievement(&ach); + s_achievements.pop_back(); + } + + std::string().swap(s_game_title); + std::string().swap(s_game_developer); + std::string().swap(s_game_publisher); + std::string().swap(s_game_release_date); + std::string().swap(s_game_icon); + s_rich_presence_string.clear(); + g_game_id = 0; + + if (had_game) + s_host_interface->OnAchievementsRefreshed(); +} + +static void ClearGamePath() +{ + std::string().swap(s_game_path); + std::string().swap(s_game_hash); +} + +bool Initialize(CommonHostInterface* hi, bool test_mode, bool use_first_disc_from_playlist, bool enable_rich_presence) +{ + s_http_downloader = FrontendCommon::HTTPDownloader::Create(); + if (!s_http_downloader) + { + Log_ErrorPrint("Failed to create HTTP downloader, cannot use cheevos"); + return false; + } + + s_http_downloader->SetUserAgent(StringUtil::StdStringFromFormat("DuckStation %s", g_scm_tag_str)); + s_host_interface = hi; + g_active = true; + s_test_mode = test_mode; + s_use_first_disc_from_playlist = use_first_disc_from_playlist; + s_rich_presence_enabled = enable_rich_presence; + rc_runtime_init(&s_rcheevos_runtime); + + s_last_ping_time.Reset(); + s_username = hi->GetStringSettingValue("Cheevos", "Username"); + s_login_token = hi->GetStringSettingValue("Cheevos", "Token"); + s_logged_in = (!s_username.empty() && !s_login_token.empty()); + + if (IsLoggedIn() && System::IsValid()) + GameChanged(); + + return true; +} + +void Reset() +{ + if (!g_active) + return; + + Log_DevPrint("Resetting rcheevos state..."); + rc_runtime_reset(&s_rcheevos_runtime); +} + +void Shutdown() +{ + if (!g_active) + return; + + Assert(!s_image_download_progress_active); + + s_http_downloader->WaitForAllRequests(); + + ClearGameInfo(); + ClearGamePath(); + std::string().swap(s_username); + std::string().swap(s_login_token); + s_logged_in = false; + s_host_interface->OnAchievementsRefreshed(); + + s_host_interface = nullptr; + g_active = false; + rc_runtime_destroy(&s_rcheevos_runtime); + + s_http_downloader.reset(); +} + +void Update() +{ + s_http_downloader->PollRequests(); + + if (HasActiveGame()) + { + rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &CheevosPeek, nullptr, nullptr); + + if (!s_test_mode) + { + const s32 ping_frequency = + s_rich_presence_enabled ? RICH_PRESENCE_PING_FREQUENCY : NO_RICH_PRESENCE_PING_FREQUENCY; + if (static_cast(s_last_ping_time.GetTimeSeconds()) >= ping_frequency) + SendPing(); + } + } +} + +bool IsLoggedIn() +{ + return s_logged_in; +} + +bool IsTestModeActive() +{ + return s_test_mode; +} + +bool IsUsingFirstDiscFromPlaylist() +{ + return s_use_first_disc_from_playlist; +} + +bool IsRichPresenceEnabled() +{ + return s_rich_presence_enabled; +} + +const std::string& GetUsername() +{ + return s_username; +} + +const std::string& GetRichPresenceString() +{ + return s_rich_presence_string; +} + +static void LoginASyncCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Login", status_code, data, doc)) + return; + + if (!doc["User"].IsString() || !doc["Token"].IsString()) + { + FormattedError("Login failed. Please check your user name and password, and try again."); + return; + } + + s_username = doc["User"].GetString(); + s_login_token = doc["Token"].GetString(); + s_logged_in = true; + + s_host_interface->ReportFormattedMessage("Logged into RetroAchievements using username '%s'.", s_username.c_str()); + + // save to config + std::lock_guard guard(s_host_interface->GetSettingsLock()); + { + s_host_interface->GetSettingsInterface()->SetStringValue("Cheevos", "Username", s_username.c_str()); + s_host_interface->GetSettingsInterface()->SetStringValue("Cheevos", "Token", s_login_token.c_str()); + s_host_interface->GetSettingsInterface()->SetStringValue( + "Cheevos", "LoginTimestamp", TinyString::FromFormat("%" PRIu64, Timestamp::Now().AsUnixTimestamp())); + s_host_interface->GetSettingsInterface()->Save(); + } + + // If we have a game running, set it up. + if (System::IsValid()) + GameChanged(); +} + +bool LoginAsync(const char* username, const char* password) +{ + s_http_downloader->WaitForAllRequests(); + + if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0) + return false; + + char url[256] = {}; + int res = rc_url_login_with_password(url, sizeof(url), username, password); + Assert(res == 0); + + s_http_downloader->CreateRequest(url, LoginASyncCallback); + return true; +} + +bool Login(const char* username, const char* password) +{ + if (!LoginAsync(username, password)) + return false; + + s_http_downloader->WaitForAllRequests(); + return IsLoggedIn(); +} + +void Logout() +{ + s_http_downloader->WaitForAllRequests(); + if (!s_logged_in) + return; + + ClearGameInfo(); + std::string().swap(s_username); + std::string().swap(s_login_token); + s_logged_in = false; + s_host_interface->OnAchievementsRefreshed(); + + // remove from config + std::lock_guard guard(s_host_interface->GetSettingsLock()); + { + s_host_interface->GetSettingsInterface()->DeleteValue("Cheevos", "Username"); + s_host_interface->GetSettingsInterface()->DeleteValue("Cheevos", "Token"); + s_host_interface->GetSettingsInterface()->Save(); + } +} + +static void UpdateImageDownloadProgress() +{ + static const char* str_id = "cheevo_image_download"; + + if (s_completed_image_downloads >= s_total_image_downloads) + { + s_completed_image_downloads = 0; + s_total_image_downloads = 0; + + if (s_image_download_progress_active) + { + ImGuiFullscreen::CloseBackgroundProgressDialog(str_id); + s_image_download_progress_active = false; + } + + return; + } + + if (!s_host_interface->IsFullscreenUIEnabled()) + return; + + std::string message("Downloading achievement resources..."); + if (!s_image_download_progress_active) + { + ImGuiFullscreen::OpenBackgroundProgressDialog(str_id, std::move(message), 0, + static_cast(s_total_image_downloads), + static_cast(s_completed_image_downloads)); + s_image_download_progress_active = true; + } + else + { + ImGuiFullscreen::UpdateBackgroundProgressDialog(str_id, std::move(message), 0, + static_cast(s_total_image_downloads), + static_cast(s_completed_image_downloads)); + } +} + +static void DownloadImage(std::string url, std::string cache_filename) +{ + auto callback = [cache_filename](s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { + s_completed_image_downloads++; + UpdateImageDownloadProgress(); + + if (status_code != HTTP_OK) + return; + + if (!FileSystem::WriteBinaryFile(cache_filename.c_str(), data.data(), data.size())) + { + Log_ErrorPrintf("Failed to write badge image to '%s'", cache_filename.c_str()); + return; + } + + FullscreenUI::InvalidateCachedTexture(cache_filename); + UpdateImageDownloadProgress(); + }; + + s_total_image_downloads++; + UpdateImageDownloadProgress(); + + s_http_downloader->CreateRequest(std::move(url), std::move(callback)); +} + +static std::string GetBadgeImageFilename(const char* badge_name, bool locked, bool cache_path) +{ + if (!cache_path) + { + return StringUtil::StdStringFromFormat("%s%s.png", badge_name, locked ? "_lock" : ""); + } + else + { + // well, this comes from the internet.... :) + SmallString clean_name(badge_name); + FileSystem::SanitizeFileName(clean_name); + return s_host_interface->GetUserDirectoryRelativePath("cache" FS_OSPATH_SEPARATOR_STR + "achievement_badge" FS_OSPATH_SEPARATOR_STR "%s%s.png", + clean_name.GetCharArray(), locked ? "_lock" : ""); + } +} + +static std::string ResolveBadgePath(const char* badge_name, bool locked) +{ + char url[256]; + + // unlocked image + std::string cache_path(GetBadgeImageFilename(badge_name, locked, true)); + if (FileSystem::FileExists(cache_path.c_str())) + return cache_path; + + std::string badge_name_with_extension(GetBadgeImageFilename(badge_name, locked, false)); + int res = rc_url_get_badge_image(url, sizeof(url), badge_name_with_extension.c_str()); + Assert(res == 0); + DownloadImage(url, cache_path); + return cache_path; +} + +static void DisplayAchievementSummary() +{ + std::string title( + StringUtil::StdStringFromFormat("%s%s", s_game_title.c_str(), s_hardcode_mode ? " (Hardcore Mode)" : "")); + std::string summary; + + if (GetAchievementCount() > 0) + { + summary = StringUtil::StdStringFromFormat("You have earned %u of %u achievements, and %u of %u points.", + GetUnlockedAchiementCount(), GetAchievementCount(), + GetCurrentPointsForGame(), GetMaximumPointsForGame()); + } + else + { + summary = "This game has no achievements."; + } + + ImGuiFullscreen::AddNotification(10.0f, std::move(title), std::move(summary), s_game_icon); +} + +static void GetUserUnlocksCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Get User Unlocks", status_code, data, doc)) + { + ClearGameInfo(); + return; + } + + // verify game id for sanity + const u32 game_id = GetOptionalUInt(doc, "GameID"); + if (game_id != g_game_id) + { + FormattedError("GameID from user unlocks doesn't match (got %u expected %u)", game_id, g_game_id); + ClearGameInfo(); + return; + } + + // flag achievements as unlocked + if (doc.HasMember("UserUnlocks") && doc["UserUnlocks"].IsArray()) + { + for (const auto& value : doc["UserUnlocks"].GetArray()) + { + if (!value.IsUint()) + continue; + + const u32 achievement_id = value.GetUint(); + Achievement* cheevo = GetAchievementByID(achievement_id); + if (!cheevo) + { + Log_ErrorPrintf("Server returned unknown achievement %u", achievement_id); + continue; + } + + cheevo->locked = false; + } + } + + // start scanning for locked achievements + ActivateLockedAchievements(); + DisplayAchievementSummary(); + SendPlaying(); + SendPing(); + s_host_interface->OnAchievementsRefreshed(); +} + +static void GetUserUnlocks() +{ + char url[256]; + int res = rc_url_get_unlock_list(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), g_game_id, + static_cast(s_hardcode_mode)); + Assert(res == 0); + + s_http_downloader->CreateRequest(url, GetUserUnlocksCallback); +} + +static void GetPatchesCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + ClearGameInfo(); + + rapidjson::Document doc; + if (!ParseResponseJSON("Get Patches", status_code, data, doc)) + return; + + if (!doc.HasMember("PatchData") || !doc["PatchData"].IsObject()) + { + FormattedError("No patch data returned from server."); + return; + } + + // parse info + const auto patch_data(doc["PatchData"].GetObject()); + if (!patch_data["ID"].IsUint()) + { + FormattedError("Patch data is missing game ID"); + return; + } + + g_game_id = GetOptionalUInt(patch_data, "ID"); + s_game_title = GetOptionalString(patch_data, "Title"); + s_game_developer = GetOptionalString(patch_data, "Developer"); + s_game_publisher = GetOptionalString(patch_data, "Publisher"); + s_game_release_date = GetOptionalString(patch_data, "Released"); + + // try for a icon + std::string icon_name(GetOptionalString(patch_data, "ImageIcon")); + if (!icon_name.empty()) + { + s_game_icon = s_host_interface->GetUserDirectoryRelativePath( + "cache" FS_OSPATH_SEPARATOR_STR "achievement_gameicon" FS_OSPATH_SEPARATOR_STR "%u.png", g_game_id); + if (!FileSystem::FileExists(s_game_icon.c_str())) + { + // for some reason rurl doesn't have this :( + std::string icon_url(StringUtil::StdStringFromFormat("http://i.retroachievements.org%s", icon_name.c_str())); + DownloadImage(std::move(icon_url), s_game_icon); + } + } + + // parse achievements + if (patch_data.HasMember("Achievements") && patch_data["Achievements"].IsArray()) + { + const auto achievements(patch_data["Achievements"].GetArray()); + for (const auto& achievement : achievements) + { + if (!achievement.HasMember("ID") || !achievement["ID"].IsNumber() || !achievement.HasMember("MemAddr") || + !achievement["MemAddr"].IsString() || !achievement.HasMember("Title") || !achievement["Title"].IsString()) + { + continue; + } + + const u32 id = achievement["ID"].GetUint(); + const char* memaddr = achievement["MemAddr"].GetString(); + std::string title = achievement["Title"].GetString(); + std::string description = GetOptionalString(achievement, "Description"); + std::string badge_name = GetOptionalString(achievement, "BadgeName"); + const u32 points = GetOptionalUInt(achievement, "Points"); + + if (GetAchievementByID(id)) + { + Log_ErrorPrintf("Achievement %u already exists", id); + continue; + } + + Achievement achievement; + achievement.id = id; + achievement.memaddr = memaddr; + achievement.title = std::move(title); + achievement.description = std::move(description); + achievement.locked = true; + achievement.active = false; + achievement.points = points; + + if (!badge_name.empty()) + { + achievement.locked_badge_path = ResolveBadgePath(badge_name.c_str(), true); + achievement.unlocked_badge_path = ResolveBadgePath(badge_name.c_str(), false); + } + + s_achievements.push_back(std::move(achievement)); + } + } + + // parse rich presence + if (s_rich_presence_enabled && patch_data.HasMember("RichPresencePatch") && patch_data["RichPresencePatch"].IsString()) + { + const char* patch = patch_data["RichPresencePatch"].GetString(); + int res = rc_runtime_activate_richpresence(&s_rcheevos_runtime, patch, nullptr, 0); + if (res == RC_OK) + s_has_rich_presence = true; + else + Log_WarningPrintf("Failed to activate rich presence: %s", rc_error_str(res)); + } + + Log_InfoPrintf("Game Title: %s", s_game_title.c_str()); + Log_InfoPrintf("Game Developer: %s", s_game_developer.c_str()); + Log_InfoPrintf("Game Publisher: %s", s_game_publisher.c_str()); + Log_InfoPrintf("Achievements: %u", s_achievements.size()); + + if (!s_achievements.empty() || s_has_rich_presence) + { + if (!s_test_mode) + { + GetUserUnlocks(); + } + else + { + ActivateLockedAchievements(); + DisplayAchievementSummary(); + s_host_interface->OnAchievementsRefreshed(); + } + } + else + { + DisplayAchievementSummary(); + ClearGameInfo(); + return; + } +} + +static void GetPatches(u32 game_id) +{ +#if 1 + char url[256] = {}; + int res = rc_url_get_patch(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), game_id); + Assert(res == 0); + + s_http_downloader->CreateRequest(url, GetPatchesCallback); +#else + std::optional> f = FileSystem::ReadBinaryFile("D:\\10434.txt"); + if (!f) + return; + + GetPatchesCallback(200, *f); +#endif +} + +static std::string GetGameHash(CDImage* cdi) +{ + std::string executable_name; + std::vector executable_data; + if (!System::ReadExecutableFromImage(cdi, &executable_name, &executable_data)) + return {}; + + BIOS::PSEXEHeader header; + if (executable_data.size() >= sizeof(header)) + std::memcpy(&header, executable_data.data(), sizeof(header)); + if (!BIOS::IsValidPSExeHeader(header, static_cast(executable_data.size()))) + { + Log_ErrorPrintf("PS-EXE header is invalid in '%s' (%zu bytes)", executable_name.c_str(), executable_data.size()); + return {}; + } + + // See rcheevos hash.c - rc_hash_psx(). + const u32 MAX_HASH_SIZE = 64 * 1024 * 1024; + const u32 hash_size = std::min(sizeof(header) + header.file_size, MAX_HASH_SIZE); + Assert(hash_size <= executable_data.size()); + + MD5Digest digest; + digest.Update(executable_name.c_str(), static_cast(executable_name.size())); + if (hash_size > 0) + digest.Update(executable_data.data(), hash_size); + + u8 hash[16]; + digest.Final(hash); + + std::string hash_str(StringUtil::StdStringFromFormat( + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", hash[0], hash[1], hash[2], hash[3], hash[4], + hash[5], hash[6], hash[7], hash[8], hash[9], hash[10], hash[11], hash[12], hash[13], hash[14], hash[15])); + + Log_InfoPrintf("Hash for '%s' (%zu bytes, %u bytes hashed): %s", executable_name.c_str(), executable_data.size(), + hash_size, hash_str.c_str()); + return hash_str; +} + +static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Get Game ID", status_code, data, doc)) + return; + + const u32 game_id = (doc.HasMember("GameID") && doc["GameID"].IsUint()) ? doc["GameID"].GetUint() : 0; + Log_InfoPrintf("Server returned GameID %u", game_id); + if (game_id != 0) + GetPatches(game_id); +} + +void GameChanged() +{ + Assert(System::IsValid()); + + const std::string& path = System::GetRunningPath(); + if (path.empty() || s_game_path == path) + return; + + std::unique_ptr cdi = CDImage::Open(path.c_str()); + if (!cdi) + { + Log_ErrorPrintf("Failed to open temporary CD image '%s'", path.c_str()); + ClearGameInfo(); + return; + } + + GameChanged(path, cdi.get()); +} + +void GameChanged(const std::string& path, CDImage* image) +{ + if (s_game_path == path) + return; + + s_http_downloader->WaitForAllRequests(); + + const u32 playlist_count = System::GetMediaPlaylistCount(); + if (playlist_count > 1 && s_use_first_disc_from_playlist) + { + // have to pass the path in, because the image isn't owned by the system yet + const u32 playlist_index = System::GetMediaPlaylistIndexForPath(path); + if (playlist_index > 0 && playlist_index < playlist_count) + { + const std::string& first_disc_path(System::GetMediaPlaylistPath(0)); + std::unique_ptr first_disc_image(CDImage::Open(first_disc_path.c_str())); + if (first_disc_image) + { + Log_InfoPrintf("Using first disc '%s' from playlist (currently '%s')", first_disc_path.c_str(), path.c_str()); + GameChanged(first_disc_path, first_disc_image.get()); + return; + } + else + { + Log_ErrorPrintf("Failed to open first disc '%s' from playlist", first_disc_path.c_str()); + return; + } + } + } + + ClearGameInfo(); + ClearGamePath(); + s_game_path = path; + if (!image) + return; + +#if 1 + s_game_hash = GetGameHash(image); + if (s_game_hash.empty()) + { + s_host_interface->AddOSDMessage( + s_host_interface->TranslateStdString("OSDMessage", "Failed to read executable from disc. Achievements disabled."), + 10.0f); + return; + } + + char url[256]; + int res = rc_url_get_gameid(url, sizeof(url), s_game_hash.c_str()); + Assert(res == 0); + + s_http_downloader->CreateRequest(url, GetGameIdCallback); +#else + g_game_id = 10434; + GetPatches(); +#endif +} + +static void SendPlayingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Post Activity", status_code, data, doc)) + return; + + Log_InfoPrintf("Playing game updated to %u (%s)", g_game_id, s_game_title.c_str()); +} + +void SendPlaying() +{ + if (!HasActiveGame()) + return; + + char url[256]; + int res = rc_url_post_playing(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), g_game_id); + Assert(res == 0); + + s_http_downloader->CreateRequest(url, SendPlayingCallback); +} + +static void UpdateRichPresence() +{ + char buffer[512]; + int res = rc_runtime_get_richpresence(&s_rcheevos_runtime, buffer, sizeof(buffer), CheevosPeek, nullptr, nullptr); + if (res <= 0) + { + const bool had_rich_presence = !s_rich_presence_string.empty(); + s_rich_presence_string.clear(); + if (had_rich_presence) + s_host_interface->OnAchievementsRefreshed(); + + return; + } + + if (s_rich_presence_string == buffer) + return; + + s_rich_presence_string.assign(buffer); + s_host_interface->OnAchievementsRefreshed(); +} + +static void SendPingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Ping", status_code, data, doc)) + return; +} + +void SendPing() +{ + if (!HasActiveGame()) + return; + + if (s_has_rich_presence) + UpdateRichPresence(); + + char url[256]; + char post_data[512]; + int res = rc_url_ping(url, sizeof(url), post_data, sizeof(post_data), s_username.c_str(), s_login_token.c_str(), + g_game_id, s_rich_presence_string.c_str()); + Assert(res == 0); + + s_http_downloader->CreatePostRequest(url, post_data, SendPingCallback); + s_last_ping_time.Reset(); +} + +const std::string& GetGameTitle() +{ + return s_game_title; +} + +const std::string& GetGameDeveloper() +{ + return s_game_developer; +} + +const std::string& GetGamePublisher() +{ + return s_game_publisher; +} + +const std::string& GetGameReleaseDate() +{ + return s_game_release_date; +} + +const std::string& GetGameIcon() +{ + return s_game_icon; +} + +bool EnumerateAchievements(std::function callback) +{ + for (const Achievement& cheevo : s_achievements) + { + if (!callback(cheevo)) + return false; + } + + return true; +} + +u32 GetUnlockedAchiementCount() +{ + u32 count = 0; + for (const Achievement& cheevo : s_achievements) + { + if (!cheevo.locked) + count++; + } + + return count; +} + +u32 GetAchievementCount() +{ + return static_cast(s_achievements.size()); +} + +u32 GetMaximumPointsForGame() +{ + u32 points = 0; + for (const Achievement& cheevo : s_achievements) + points += cheevo.points; + + return points; +} + +u32 GetCurrentPointsForGame() +{ + u32 points = 0; + for (const Achievement& cheevo : s_achievements) + { + if (!cheevo.locked) + points += cheevo.points; + } + + return points; +} + +void ActivateLockedAchievements() +{ + for (Achievement& cheevo : s_achievements) + { + if (cheevo.locked) + ActivateAchievement(&cheevo); + } +} + +bool ActivateAchievement(Achievement* achievement) +{ + if (achievement->active) + return true; + + const int err = + rc_runtime_activate_achievement(&s_rcheevos_runtime, achievement->id, achievement->memaddr.c_str(), nullptr, 0); + if (err != RC_OK) + { + Log_ErrorPrintf("Achievement %u memaddr parse error: %s", achievement->id, rc_error_str(err)); + return false; + } + + achievement->active = true; + + Log_DevPrintf("Activated achievement %s (%u)", achievement->title.c_str(), achievement->id); + return true; +} + +void DeactivateAchievement(Achievement* achievement) +{ + if (!achievement->active) + return; + + rc_runtime_deactivate_achievement(&s_rcheevos_runtime, achievement->id); + achievement->active = false; + + Log_DevPrintf("Deactivated achievement %s (%u)", achievement->title.c_str(), achievement->id); +} + +static void UnlockAchievementCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +{ + rapidjson::Document doc; + if (!ParseResponseJSON("Award Cheevo", status_code, data, doc)) + return; + + // we don't really need to do anything here +} + +void UnlockAchievement(u32 achievement_id, bool add_notification /* = true*/) +{ + Achievement* achievement = GetAchievementByID(achievement_id); + if (!achievement) + { + Log_ErrorPrintf("Attempting to unlock unknown achievement %u", achievement_id); + return; + } + else if (!achievement->locked) + { + Log_WarningPrintf("Achievement %u for game %u is already unlocked", achievement_id, g_game_id); + return; + } + + achievement->locked = false; + DeactivateAchievement(achievement); + + Log_InfoPrintf("Achievement %s (%u) for game %u unlocked", achievement->title.c_str(), achievement_id, g_game_id); + ImGuiFullscreen::AddNotification(15.0f, achievement->title, achievement->description, + achievement->unlocked_badge_path); + + if (s_test_mode) + { + Log_WarningPrintf("Skipping sending achievement %u unlock to server because of test mode.", achievement_id); + return; + } + + char url[256]; + rc_url_award_cheevo(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), achievement_id, + static_cast(s_hardcode_mode), s_game_hash.c_str()); + s_http_downloader->CreateRequest(url, UnlockAchievementCallback); +} + +void CheevosEventHandler(const rc_runtime_event_t* runtime_event) +{ + static const char* events[] = {"RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED", "RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED", + "RC_RUNTIME_EVENT_ACHIEVEMENT_RESET", "RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED", + "RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED", "RC_RUNTIME_EVENT_LBOARD_STARTED", + "RC_RUNTIME_EVENT_LBOARD_CANCELED", "RC_RUNTIME_EVENT_LBOARD_UPDATED", + "RC_RUNTIME_EVENT_LBOARD_TRIGGERED", "RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED", + "RC_RUNTIME_EVENT_LBOARD_DISABLED"}; + const char* event_text = + ((unsigned)runtime_event->type >= countof(events)) ? "unknown" : events[(unsigned)runtime_event->type]; + Log_DevPrintf("Cheevos Event %s for %u", event_text, runtime_event->id); + + if (runtime_event->type == RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED) + UnlockAchievement(runtime_event->id); +} + +// from cheats.cpp - do we want to move this somewhere else? +template +static T DoMemoryRead(PhysicalMemoryAddress address) +{ + T result; + + if ((address & CPU::DCACHE_LOCATION_MASK) == CPU::DCACHE_LOCATION && + (address & CPU::DCACHE_OFFSET_MASK) < CPU::DCACHE_SIZE) + { + std::memcpy(&result, &CPU::g_state.dcache[address & CPU::DCACHE_OFFSET_MASK], sizeof(result)); + return result; + } + + address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + + if (address < Bus::RAM_MIRROR_END) + { + std::memcpy(&result, &Bus::g_ram[address & Bus::RAM_MASK], sizeof(result)); + return result; + } + + if (address >= Bus::BIOS_BASE && address < (Bus::BIOS_BASE + Bus::BIOS_SIZE)) + { + std::memcpy(&result, &Bus::g_bios[address & Bus::BIOS_MASK], sizeof(result)); + return result; + } + + result = static_cast(0); + return result; +} + +unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud) +{ + switch (num_bytes) + { + case 1: + return ZeroExtend32(DoMemoryRead(address)); + case 2: + return ZeroExtend32(DoMemoryRead(address)); + case 4: + return ZeroExtend32(DoMemoryRead(address)); + default: + return 0; + } +} + +} // namespace Cheevos \ No newline at end of file diff --git a/src/frontend-common/cheevos.h b/src/frontend-common/cheevos.h new file mode 100644 index 000000000..67a971e0c --- /dev/null +++ b/src/frontend-common/cheevos.h @@ -0,0 +1,76 @@ +#pragma once +#include "core/types.h" +#include +#include + +class CDImage; +class CommonHostInterface; +class SettingsInterface; + +namespace Cheevos { + +struct Achievement +{ + u32 id; + std::string title; + std::string description; + std::string memaddr; + std::string locked_badge_path; + std::string unlocked_badge_path; + u32 points; + bool locked; + bool active; +}; + +extern bool g_active; +extern u32 g_game_id; + +ALWAYS_INLINE bool IsActive() +{ + return g_active; +} + +ALWAYS_INLINE bool HasActiveGame() +{ + return g_game_id != 0; +} + +ALWAYS_INLINE u32 GetGameID() +{ + return g_game_id; +} + +bool Initialize(CommonHostInterface* hi, bool test_mode, bool use_first_disc_from_playlist, bool enable_rich_presence); +void Reset(); +void Shutdown(); +void Update(); + +bool IsLoggedIn(); +bool IsTestModeActive(); +bool IsUsingFirstDiscFromPlaylist(); +bool IsRichPresenceEnabled(); +const std::string& GetUsername(); +const std::string& GetRichPresenceString(); + +bool LoginAsync(const char* username, const char* password); +bool Login(const char* username, const char* password); +void Logout(); + +bool HasActiveGame(); +void GameChanged(const std::string& path, CDImage* image); + +const std::string& GetGameTitle(); +const std::string& GetGameDeveloper(); +const std::string& GetGamePublisher(); +const std::string& GetGameReleaseDate(); +const std::string& GetGameIcon(); + +bool EnumerateAchievements(std::function callback); +u32 GetUnlockedAchiementCount(); +u32 GetAchievementCount(); +u32 GetMaximumPointsForGame(); +u32 GetCurrentPointsForGame(); + +void UnlockAchievement(u32 achievement_id, bool add_notification = true); + +} // namespace Cheevos diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 336a64230..624f49995 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -43,6 +43,10 @@ #include "discord_rpc.h" #endif +#ifdef WITH_CHEEVOS +#include "cheevos.h" +#endif + #ifdef WIN32 #include "common/windows_headers.h" #include @@ -89,6 +93,10 @@ bool CommonHostInterface::Initialize() CreateImGuiContext(); +#ifdef WITH_CHEEVOS + UpdateCheevosActive(); +#endif + return true; } @@ -102,6 +110,10 @@ void CommonHostInterface::Shutdown() ShutdownDiscordPresence(); #endif +#ifdef WITH_CHEEVOS + Cheevos::Shutdown(); +#endif + if (m_controller_interface) { m_controller_interface->Shutdown(); @@ -127,11 +139,17 @@ void CommonHostInterface::InitializeUserDirectory() result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("bios").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("cache").c_str(), false); + result &= FileSystem::CreateDirectory( + GetUserDirectoryRelativePath("cache" FS_OSPATH_SEPARATOR_STR "achievement_badge").c_str(), false); + result &= FileSystem::CreateDirectory( + GetUserDirectoryRelativePath("cache" FS_OSPATH_SEPARATOR_STR "achievement_gameicon").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("cheats").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("covers").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("dump").c_str(), false); - result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("dump/audio").c_str(), false); - result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("dump/textures").c_str(), false); + result &= + FileSystem::CreateDirectory(GetUserDirectoryRelativePath("dump" FS_OSPATH_SEPARATOR_STR "audio").c_str(), false); + result &= + FileSystem::CreateDirectory(GetUserDirectoryRelativePath("dump" FS_OSPATH_SEPARATOR_STR "textures").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("inputprofiles").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("memcards").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("savestates").c_str(), false); @@ -192,6 +210,15 @@ void CommonHostInterface::PowerOffSystem() RequestExit(); } +void CommonHostInterface::ResetSystem() +{ + HostInterface::ResetSystem(); + +#ifdef WITH_CHEEVOS + Cheevos::Reset(); +#endif +} + static void PrintCommandLineVersion(const char* frontend_name) { const bool was_console_enabled = Log::IsConsoleOutputEnabled(); @@ -434,11 +461,23 @@ bool CommonHostInterface::ParseCommandLineParameters(int argc, char* argv[], return true; } +void CommonHostInterface::OnAchievementsRefreshed() +{ +#ifdef WITH_CHEEVOS + // noop +#endif +} + void CommonHostInterface::PollAndUpdate() { #ifdef WITH_DISCORD_PRESENCE PollDiscordPresence(); #endif + +#ifdef WITH_CHEEVOS + if (Cheevos::IsActive()) + Cheevos::Update(); +#endif } bool CommonHostInterface::IsFullscreen() const @@ -612,6 +651,20 @@ void CommonHostInterface::UpdateControllerInterface() } } +bool CommonHostInterface::LoadState(const char* filename) +{ + const bool system_was_valid = System::IsValid(); + const bool result = HostInterface::LoadState(filename); + if (system_was_valid || !result) + { +#ifdef WITH_CHEEVOS + Cheevos::Reset(); +#endif + } + + return result; +} + bool CommonHostInterface::LoadState(bool global, s32 slot) { if (!global && (System::IsShutdown() || System::GetRunningCode().empty())) @@ -924,6 +977,11 @@ void CommonHostInterface::OnRunningGameChanged(const std::string& path, CDImage* #ifdef WITH_DISCORD_PRESENCE UpdateDiscordPresence(); #endif + +#ifdef WITH_CHEEVOS + if (Cheevos::IsLoggedIn()) + Cheevos::GameChanged(path, image); +#endif } void CommonHostInterface::OnControllerTypeChanged(u32 slot) @@ -2448,6 +2506,14 @@ void CommonHostInterface::SetDefaultSettings(SettingsInterface& si) #ifdef WITH_DISCORD_PRESENCE si.SetBoolValue("Main", "EnableDiscordPresence", false); #endif + +#ifdef WITH_CHEEVOS + si.SetBoolValue("Cheevos", "Enabled", false); + si.SetBoolValue("Cheevos", "TestMode", false); + si.SetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", true); + si.DeleteValue("Cheevos", "Username"); + si.DeleteValue("Cheevos", "Token"); +#endif } void CommonHostInterface::LoadSettings() @@ -2482,8 +2548,15 @@ void CommonHostInterface::LoadSettings(SettingsInterface& si) SetDiscordPresenceEnabled(si.GetBoolValue("Main", "EnableDiscordPresence", false)); #endif +#ifdef WITH_CHEEVOS + UpdateCheevosActive(); + const bool cheevos_active = Cheevos::IsActive(); +#else + const bool cheevos_active = false; +#endif + const bool fullscreen_ui_enabled = - si.GetBoolValue("Main", "EnableFullscreenUI", false) || m_flags.force_fullscreen_ui; + si.GetBoolValue("Main", "EnableFullscreenUI", false) || cheevos_active || m_flags.force_fullscreen_ui; if (fullscreen_ui_enabled != m_fullscreen_ui_enabled) { m_fullscreen_ui_enabled = fullscreen_ui_enabled; @@ -3211,3 +3284,27 @@ void CommonHostInterface::PollDiscordPresence() } #endif + +#ifdef WITH_CHEEVOS + +void CommonHostInterface::UpdateCheevosActive() +{ + const bool cheevos_enabled = GetBoolSettingValue("Cheevos", "Enabled", false); + const bool cheevos_test_mode = GetBoolSettingValue("Cheevos", "TestMode", false); + const bool cheevos_use_first_disc_from_playlist = GetBoolSettingValue("Cheevos", "UseFirstDiscFromPlaylist", true); + const bool cheevos_rich_presence = GetBoolSettingValue("Cheevos", "RichPresence", true); + + if (cheevos_enabled != Cheevos::IsActive() || cheevos_test_mode != Cheevos::IsTestModeActive() || + cheevos_use_first_disc_from_playlist != Cheevos::IsUsingFirstDiscFromPlaylist() || + cheevos_rich_presence != Cheevos::IsRichPresenceEnabled()) + { + Cheevos::Shutdown(); + if (cheevos_enabled) + { + if (!Cheevos::Initialize(this, cheevos_test_mode, cheevos_use_first_disc_from_playlist, cheevos_rich_presence)) + ReportError("Failed to initialize cheevos after settings change."); + } + } +} + +#endif \ No newline at end of file diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 0e886aeb8..3390f8956 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -93,7 +93,6 @@ public: std::vector screenshot_data; }; - using HostInterface::LoadState; using HostInterface::SaveState; /// Returns the name of the frontend. @@ -127,10 +126,15 @@ public: virtual bool BootSystem(const SystemBootParameters& parameters) override; virtual void PowerOffSystem() override; + virtual void ResetSystem() override; virtual void DestroySystem() override; /// Returns the settings interface. ALWAYS_INLINE SettingsInterface* GetSettingsInterface() const { return m_settings_interface.get(); } + ALWAYS_INLINE std::lock_guard GetSettingsLock() + { + return std::lock_guard(m_settings_mutex); + } /// Returns the game list. ALWAYS_INLINE GameList* GetGameList() const { return m_game_list.get(); } @@ -165,6 +169,9 @@ public: /// Saves the current input configuration to the specified profile name. bool SaveInputProfile(const char* profile_path); + /// Loads state from the specified filename. + bool LoadState(const char* filename); + /// Loads the current emulation state from file. Specifying a slot of -1 loads the "resume" game state. bool LoadState(bool global, s32 slot); @@ -273,6 +280,9 @@ public: /// Returns a pointer to the top-level window, needed by some controller interfaces. virtual void* GetTopLevelWindowHandle() const; + /// Called when achievements data is loaded. + virtual void OnAchievementsRefreshed(); + /// Opens a file in the DuckStation "package". /// This is the APK for Android builds, or the program directory for standalone builds. virtual std::unique_ptr OpenPackageFile(const char* path, u32 flags) override; @@ -408,8 +418,8 @@ protected: std::unique_ptr m_logo_texture; - std::deque m_osd_active_messages; // accessed only by GUI/OSD thread (no lock reqs) - std::deque m_osd_posted_messages; // written to by multiple threads. + std::deque m_osd_active_messages; // accessed only by GUI/OSD thread (no lock reqs) + std::deque m_osd_posted_messages; // written to by multiple threads. std::mutex m_osd_messages_lock; bool m_fullscreen_ui_enabled = false; @@ -459,6 +469,10 @@ private: void PollDiscordPresence(); #endif +#ifdef WITH_CHEEVOS + void UpdateCheevosActive(); +#endif + HotkeyInfoList m_hotkeys; std::unique_ptr m_save_state_selector_ui; diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 3a0d7ddd5..5ea5b5e2a 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -66,6 +66,9 @@ {6a4208ed-e3dc-41e1-81cd-f61025fc285a} + + {4ba0a6d4-3ae1-42b2-9347-096fd023ff64} + {3773f4cc-614e-4028-8595-22e08ca649e3} @@ -93,7 +96,7 @@ - + @@ -105,6 +108,7 @@ + @@ -123,7 +127,7 @@ - + @@ -135,6 +139,7 @@ + @@ -347,10 +352,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -374,10 +379,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -404,10 +409,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -431,10 +436,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -458,10 +463,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -488,10 +493,10 @@ Level3 Disabled - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;_ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -520,9 +525,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -551,9 +556,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -582,9 +587,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -613,9 +618,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -644,9 +649,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -675,9 +680,9 @@ MaxSpeed true true - WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WITH_IMGUI=1;WITH_SDL2=1;WITH_DISCORD_PRESENCE=1;WITH_CHEEVOS=1;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\msvc\include\SDL;$(SolutionDir)dep\cubeb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\discord-rpc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 22e30be64..08b2c5c4e 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -27,8 +27,9 @@ - + + @@ -57,8 +58,9 @@ - + + diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 4775aeb6a..c7f651a77 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -2,6 +2,7 @@ #include "fullscreen_ui.h" #include "IconsFontAwesome5.h" +#include "cheevos.h" #include "common/byte_stream.h" #include "common/file_system.h" #include "common/log.h" @@ -81,6 +82,7 @@ static void ClearImGuiFocus(); static void ReturnToMainWindow(); static void DrawLandingWindow(); static void DrawQuickMenu(MainWindowType type); +static void DrawAchievementWindow(); static void DrawDebugMenu(); static void DrawStatsOverlay(); static void DrawOSDMessages(); @@ -304,9 +306,11 @@ void Render() DrawSettingsWindow(); break; case MainWindowType::QuickMenu: - case MainWindowType::MoreQuickMenu: DrawQuickMenu(s_current_main_window); break; + case MainWindowType::Achievements: + DrawAchievementWindow(); + break; default: break; } @@ -1073,7 +1077,7 @@ void DrawSettingsWindow() ICON_FA_GAMEPAD " Controller Settings", ICON_FA_KEYBOARD " Hotkey Settings", ICON_FA_SD_CARD " Memory Card Settings", ICON_FA_TV " Display Settings", ICON_FA_MAGIC " Enhancement Settings", ICON_FA_HEADPHONES " Audio Settings", - ICON_FA_EXCLAMATION_TRIANGLE " Advanced Settings"}}; + ICON_FA_TROPHY " Achievements Settings", ICON_FA_EXCLAMATION_TRIANGLE " Advanced Settings"}}; BeginMenuButtons(); for (u32 i = 0; i < static_cast(titles.size()); i++) @@ -1932,6 +1936,108 @@ void DrawSettingsWindow() } break; + case SettingsPage::AchievementsSetings: + { +#ifdef WITH_CHEEVOS + BeginMenuButtons(); + + MenuHeading("Settings"); + settings_changed |= ToggleButtonForNonSetting( + "Enable RetroAchievements", "When enabled and logged in, DuckStation will scan for achievements on startup.", + "Cheevos", "Enabled", false); + settings_changed |= ToggleButtonForNonSetting( + "Rich Presence", + "When enabled, rich presence information will be collected and sent to the server where supported.", + "Cheevos", "RichPresence", true); + settings_changed |= + ToggleButtonForNonSetting("Test Mode", + "When enabled, DuckStation will assume all achievements are locked and not " + "send any unlock notifications to the server.", + "Cheevos", "TestMode", false); + settings_changed |= ToggleButtonForNonSetting("Use First Disc From Playlist", + "When enabled, the first disc in a playlist will be used for " + "achievements, regardless of which disc is active.", + "Cheevos", "UseFirstDiscFromPlaylist", true); + + MenuHeading("Account"); + if (Cheevos::IsLoggedIn()) + { + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); + ActiveButton(SmallString::FromFormat(ICON_FA_USER " Username: %s", Cheevos::GetUsername().c_str()), false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + Timestamp ts; + TinyString ts_string; + ts.SetUnixTimestamp(StringUtil::FromChars(s_host_interface->GetSettingsInterface()->GetStringValue( + "Cheevos", "LoginTimestamp", "0")) + .value_or(0)); + ts.ToString(ts_string, "%Y-%m-%d %H:%M:%S"); + ActiveButton(SmallString::FromFormat(ICON_FA_CLOCK " Login token generated on %s", ts_string.GetCharArray()), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ImGui::PopStyleColor(); + + if (MenuButton(ICON_FA_KEY " Logout", "Logs out of RetroAchievements.")) + Cheevos::Logout(); + } + else + { + ActiveButton(SmallString::FromFormat(ICON_FA_USER " Not Logged In", Cheevos::GetUsername().c_str()), false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + if (MenuButton(ICON_FA_KEY " Login", "Logs in to RetroAchievements.")) + Cheevos::LoginAsync("", ""); + } + + MenuHeading("Current Game"); + if (Cheevos::HasActiveGame()) + { + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]); + ActiveButton(TinyString::FromFormat(ICON_FA_BOOKMARK " Game ID: %u", Cheevos::GetGameID()), false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ActiveButton(TinyString::FromFormat(ICON_FA_BOOK " Game Title: %s", Cheevos::GetGameTitle().c_str()), false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ActiveButton( + TinyString::FromFormat(ICON_FA_DESKTOP " Game Developer: %s", Cheevos::GetGameDeveloper().c_str()), false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ActiveButton( + TinyString::FromFormat(ICON_FA_DESKTOP " Game Publisher: %s", Cheevos::GetGamePublisher().c_str()), false, + false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + ActiveButton(TinyString::FromFormat(ICON_FA_TROPHY " Achievements: %u (%u points)", + Cheevos::GetAchievementCount(), Cheevos::GetMaximumPointsForGame()), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + const std::string& rich_presence_string = Cheevos::GetRichPresenceString(); + if (!rich_presence_string.empty()) + { + ActiveButton(SmallString::FromFormat(ICON_FA_MAP " %s", rich_presence_string.c_str()), false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + else + { + ActiveButton(ICON_FA_MAP " Rich presence inactive or unsupported.", false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + + ImGui::PopStyleColor(); + } + else + { + ActiveButton(ICON_FA_BAN " Game not loaded or no RetroAchievements available.", false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + + EndMenuButtons(); +#else + BeginMenuButtons(); + ActiveButton(ICON_FA_BAN " This build was not compiled with RetroAchivements support.", false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + EndMenuButtons(); +#endif + // ImGuiFullscreen::moda + // if (ImGui::BeginPopup(")) + } + break; + case SettingsPage::AdvancedSettings: { BeginMenuButtons(); @@ -2073,7 +2179,7 @@ void DrawQuickMenu(MainWindowType type) if (BeginFullscreenWindow(window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, 10.0f, ImGuiWindowFlags_NoBackground)) { - BeginMenuButtons(11, 1.0f, ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + BeginMenuButtons(12, 1.0f, ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); @@ -2087,6 +2193,17 @@ void DrawQuickMenu(MainWindowType type) CloseQuickMenu(); } +#ifdef WITH_CHEEVOS + const bool achievements_enabled = Cheevos::HasActiveGame() && (Cheevos::GetAchievementCount() > 0); + if (ActiveButton(ICON_FA_TROPHY " Achievements", false, achievements_enabled)) + { + CloseQuickMenu(); + s_current_main_window = MainWindowType::Achievements; + } +#else + ActiveButton(ICON_FA_TROPHY " Achievements", false, false); +#endif + if (ActiveButton(ICON_FA_CAMERA " Save Screenshot", false)) { CloseQuickMenu(); @@ -3578,6 +3695,242 @@ void DrawDebugDebugMenu() } } +#ifdef WITH_CHEEVOS + +static void DrawAchievement(const Cheevos::Achievement& cheevo) +{ + static constexpr float alpha = 0.8f; + + TinyString id_str; + id_str.Format("%u", cheevo.id); + + ImRect bb; + bool visible, hovered; + bool pressed = + MenuButtonFrame(id_str, true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max, 0, alpha); + if (!visible) + return; + + const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); + const std::string& badge_path = cheevo.locked ? cheevo.locked_badge_path : cheevo.unlocked_badge_path; + if (!badge_path.empty()) + { + HostDisplayTexture* badge = GetCachedTexture(badge_path); + if (badge) + { + ImGui::GetWindowDrawList()->AddImage(badge->GetHandle(), bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255)); + } + } + + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); + SmallString text; + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo.title.c_str(), cheevo.title.c_str() + cheevo.title.size(), + nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (!cheevo.description.empty()) + { + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, cheevo.description.c_str(), + cheevo.description.c_str() + cheevo.description.size(), nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + +#if 0 + // The API doesn't seem to send us this :( + if (!cheevo.locked) + { + ImGui::PushFont(g_medium_font); + + const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), + ImVec2(bb.Max.x, bb.Min.y + g_medium_font->FontSize + LayoutScale(4.0f))); + text.Format("Unlocked 21 Feb, 2019 @ 3:14am"); + ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + nullptr, ImVec2(1.0f, 0.0f), &time_bb); + ImGui::PopFont(); + } +#endif + + if (pressed) + { + // TODO: What should we do here? + // Display information or something.. + } +} + +void DrawAchievementWindow() +{ + static constexpr float alpha = 0.8f; + static constexpr float heading_height_unscaled = 110.0f; + + ImGui::SetNextWindowBgAlpha(alpha); + + const ImVec4 background(0.13f, 0.13f, 0.13f, alpha); + const ImVec2 display_size(ImGui::GetIO().DisplaySize); + const float window_width = LayoutScale(LAYOUT_SCREEN_WIDTH); + const float heading_height = LayoutScale(heading_height_unscaled); + + if (BeginFullscreenWindow( + ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "achievements_heading", background, 0.0f, 0.0f, + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse)) + { + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered, &bb.Min, + &bb.Max, 0, alpha); + if (visible) + { + const float padding = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float image_height = LayoutScale(85.0f); + + const ImVec2 icon_min(bb.Min + ImVec2(padding, padding)); + const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height)); + + const std::string& icon_path = Cheevos::GetGameIcon(); + if (!icon_path.empty()) + { + HostDisplayTexture* badge = GetCachedTexture(icon_path); + if (badge) + { + ImGui::GetWindowDrawList()->AddImage(badge->GetHandle(), icon_min, icon_max, ImVec2(0.0f, 0.0f), + ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255)); + } + } + + float left = bb.Min.x + padding + image_height + spacing; + float right = bb.Max.x - padding; + float top = bb.Min.y + padding; + ImDrawList* dl = ImGui::GetWindowDrawList(); + SmallString text; + ImVec2 text_size; + + const u32 unlocked_count = Cheevos::GetUnlockedAchiementCount(); + const u32 achievement_count = Cheevos::GetAchievementCount(); + const u32 current_points = Cheevos::GetCurrentPointsForGame(); + const u32 total_points = Cheevos::GetMaximumPointsForGame(); + + text.Format(ICON_FA_TIMES); + text_size = g_large_font->CalcTextSizeA(g_large_font->FontSize, right, -1.0f, text.GetCharArray(), + text.GetCharArray() + text.GetLength()); + const ImRect close_button_bb(ImVec2(right - padding - text_size.x, top), ImVec2(right, top + text_size.y)); + + bool close_held, close_hovered; + bool close_clicked = ImGui::ButtonBehavior(close_button_bb, ImGui::GetCurrentWindow()->GetID("close_button"), + &close_hovered, &close_held); + if (close_clicked) + { + ReturnToMainWindow(); + } + else if (close_hovered || close_held) + { + const ImU32 col = ImGui::GetColorU32(close_held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, alpha); + ImGui::RenderFrame(close_button_bb.Min, close_button_bb.Max, col, true, 0.0f); + } + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(close_button_bb.Min, close_button_bb.Max, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &close_button_bb); + ImGui::PopFont(); + + const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize)); + text.Assign(Cheevos::GetGameTitle()); + + const std::string& developer = Cheevos::GetGameDeveloper(); + if (!developer.empty()) + text.AppendFormattedString(" (%s)", developer.c_str()); + + top += g_large_font->FontSize + spacing; + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(), + nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize)); + if (unlocked_count == achievement_count) + { + text.Format("You have unlocked all achievements and earned %u points!", total_points); + } + else + { + text.Format("You have unlocked %u of %u achievements, earning %u of %u possible points.", unlocked_count, + achievement_count, current_points, total_points); + } + + top += g_medium_font->FontSize + spacing; + + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + + const float progress_height = LayoutScale(20.0f); + const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height)); + const float fraction = static_cast(unlocked_count) / static_cast(achievement_count); + dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor())); + dl->AddRectFilled(progress_bb.Min, + ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y), + ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor())); + + text.Format("%d%%", static_cast(std::round(fraction * 100.0f))); + text_size = ImGui::CalcTextSize(text); + const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), + progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - + (text_size.y / 2.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, + ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor()), text.GetCharArray(), + text.GetCharArray() + text.GetLength()); + top += progress_height + spacing; + } + } + EndFullscreenWindow(); + + ImGui::SetNextWindowBgAlpha(alpha); + + if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.x - heading_height), + "achievements", background, 0.0f, 0.0f, 0)) + { + + BeginMenuButtons(); + + MenuHeading("Unlocked Achievements"); + Cheevos::EnumerateAchievements([](const Cheevos::Achievement& cheevo) -> bool { + if (!cheevo.locked) + DrawAchievement(cheevo); + + return true; + }); + + if (Cheevos::GetUnlockedAchiementCount() != Cheevos::GetAchievementCount()) + { + MenuHeading("Locked Achievements"); + Cheevos::EnumerateAchievements([](const Cheevos::Achievement& cheevo) -> bool { + if (cheevo.locked) + DrawAchievement(cheevo); + + return true; + }); + } + + EndMenuButtons(); + } + EndFullscreenWindow(); +} + +#else + +void DrawAchievementWindow() {} + +#endif + bool SetControllerNavInput(FrontendCommon::ControllerNavigationButton button, bool value) { s_nav_input_values[static_cast(button)] = value; diff --git a/src/frontend-common/fullscreen_ui.h b/src/frontend-common/fullscreen_ui.h index 62052d3e8..6428e9042 100644 --- a/src/frontend-common/fullscreen_ui.h +++ b/src/frontend-common/fullscreen_ui.h @@ -18,7 +18,7 @@ enum class MainWindowType GameList, Settings, QuickMenu, - MoreQuickMenu + Achievements, }; enum class SettingsPage @@ -34,6 +34,7 @@ enum class SettingsPage DisplaySettings, EnhancementSettings, AudioSettings, + AchievementsSetings, AdvancedSettings, Count };