Qt: Add RetroAchievements implementation

This commit is contained in:
Connor McLaughlin
2022-10-01 00:00:02 +01:00
committed by refractionpcsx2
parent 843b0b3eb1
commit 0419de4baf
44 changed files with 4745 additions and 32 deletions
@@ -0,0 +1,2 @@
lbsubmit.wav: https://freesound.org/people/Eponn/sounds/636656/
unlock.wav and message.wav are from https://github.com/RetroAchievements/RAInterface
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -19,6 +19,7 @@
#include "CocoaTools.h"
#include "Console.h"
#include "General.h"
#include "WindowInfo.h"
#include <vector>
#include <Cocoa/Cocoa.h>
@@ -125,3 +126,12 @@ void CocoaTools::RemoveThemeChangeHandler(void* ctx)
assert([NSThread isMainThread]);
[s_themeChangeHandler removeCallback:ctx];
}
// MARK: - Sound playback
bool Common::PlaySoundAsync(const char* path)
{
NSString* nspath = [[NSString alloc] initWithUTF8String:path];
NSSound* sound = [[NSSound alloc] initWithContentsOfFile:nspath byReference:YES];
return [sound play];
}
+7
View File
@@ -166,3 +166,10 @@ extern const u32 SPIN_TIME_NS;
extern std::string GetOSVersionString();
void ScreensaverAllow(bool allow);
namespace Common
{
/// Abstracts platform-specific code for asynchronously playing a sound.
/// On Windows, this will use PlaySound(). On Linux, it will shell out to aplay. On MacOS, it uses NSSound.
bool PlaySoundAsync(const char* path);
} // namespace Common
+19
View File
@@ -17,7 +17,9 @@
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <spawn.h>
#include <sys/time.h>
#include <sys/wait.h>
#include "common/Pcsx2Types.h"
#include "common/General.h"
@@ -64,4 +66,21 @@ void ScreensaverAllow(bool allow)
{
// no-op
}
bool Common::PlaySoundAsync(const char* path)
{
#ifdef __linux__
// This is... pretty awful. But I can't think of a better way without linking to e.g. gstreamer.
const char* cmdname = "aplay";
const char* argv[] = {cmdname, path, nullptr};
pid_t pid;
// Since we set SA_NOCLDWAIT in Qt, we don't need to wait here.
int res = posix_spawnp(&pid, cmdname, nullptr, nullptr, const_cast<char**>(argv), environ);
return (res == 0);
#else
return false;
#endif
}
#endif
+10
View File
@@ -19,9 +19,12 @@
#include "common/RedtapeWindows.h"
#include "common/Exceptions.h"
#include "common/StringUtil.h"
#include "common/General.h"
#include "fmt/core.h"
#include <mmsystem.h>
#pragma comment(lib, "User32.lib")
alignas(16) static LARGE_INTEGER lfreq;
@@ -116,4 +119,11 @@ void ScreensaverAllow(bool allow)
flags |= ES_DISPLAY_REQUIRED;
SetThreadExecutionState(flags);
}
bool Common::PlaySoundAsync(const char* path)
{
const std::wstring wpath(StringUtil::UTF8StringToWideString(path));
return PlaySoundW(wpath.c_str(), NULL, SND_ASYNC | SND_NODEFAULT);
}
#endif
+39
View File
@@ -104,12 +104,51 @@ static inline std::optional<T> ReadFileInZipToContainer(zip_t* zip, const char*
return ret;
}
template <typename T>
static inline std::optional<T> ReadFileInZipToContainer(zip_file_t* file, u32 chunk_size = 4096)
{
std::optional<T> ret = T();
for (;;)
{
const size_t pos = ret->size();
ret->resize(pos + chunk_size);
const s64 read = zip_fread(file, ret->data() + pos, chunk_size);
if (read < 0)
{
// read error
ret.reset();
break;
}
// if less than chunk size, we're EOF
if (read != static_cast<s64>(chunk_size))
{
ret->resize(pos + static_cast<size_t>(read));
break;
}
}
return ret;
}
static inline std::optional<std::string> ReadFileInZipToString(zip_t* zip, const char* name)
{
return ReadFileInZipToContainer<std::string>(zip, name);
}
static inline std::optional<std::string> ReadFileInZipToString(zip_file_t* file, u32 chunk_size = 4096)
{
return ReadFileInZipToContainer<std::string>(file, chunk_size);
}
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_t* zip, const char* name)
{
return ReadFileInZipToContainer<std::vector<u8>>(zip, name);
}
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size = 4096)
{
return ReadFileInZipToContainer<std::vector<u8>>(file, chunk_size);
}
+11
View File
@@ -121,6 +121,17 @@ target_sources(pcsx2-qt PRIVATE
resources/resources.qrc
)
if(USE_ACHIEVEMENTS)
target_sources(pcsx2-qt PRIVATE
Settings/AchievementLoginDialog.cpp
Settings/AchievementLoginDialog.h
Settings/AchievementLoginDialog.ui
Settings/AchievementSettingsWidget.cpp
Settings/AchievementSettingsWidget.h
Settings/AchievementSettingsWidget.ui
)
endif()
target_precompile_headers(pcsx2-qt PRIVATE PrecompiledHeader.h)
target_include_directories(pcsx2-qt PRIVATE
+44 -2
View File
@@ -54,6 +54,10 @@
#include "svnrev.h"
#include "Tools/InputRecording/NewInputRecordingDlg.h"
#ifdef ENABLE_RAINTEGRATION
#include "pcsx2/Frontend/Achievements.h"
#endif
static constexpr char OPEN_FILE_FILTER[] =
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.elf *.irx *.m3u *.gs *.gs.xz *.gs.zst *.dump);;"
@@ -128,8 +132,8 @@ void MainWindow::initialize()
CocoaTools::AddThemeChangeHandler(this, [](void* ctx) {
// This handler is called *before* the style change has propagated far enough for Qt to see it
// Use RunOnUIThread to delay until it has
QtHost::RunOnUIThread([ctx = static_cast<MainWindow*>(ctx)]{
ctx->updateTheme();// Qt won't notice the style change without us touching the palette in some way
QtHost::RunOnUIThread([ctx = static_cast<MainWindow*>(ctx)] {
ctx->updateTheme(); // Qt won't notice the style change without us touching the palette in some way
});
});
#endif
@@ -238,6 +242,38 @@ void MainWindow::setupAdditionalUi()
updateEmulationActions(false, false);
updateDisplayRelatedActions(false, false, false);
#ifdef ENABLE_RAINTEGRATION
if (Achievements::IsUsingRAIntegration())
{
QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menuDebug);
connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() {
raMenu->clear();
const auto items = Achievements::RAIntegration::GetMenuItems();
for (const auto& [id, title, checked] : items)
{
if (id == 0)
{
raMenu->addSeparator();
continue;
}
QAction* raAction = raMenu->addAction(QString::fromUtf8(title));
if (checked)
{
raAction->setCheckable(true);
raAction->setChecked(checked);
}
connect(raAction, &QAction::triggered, this, [id = id]() {
Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }, false);
});
}
});
m_ui.menuDebug->insertMenu(m_ui.actionToggleSoftwareRendering, raMenu);
}
#endif
}
void MainWindow::connectSignals()
@@ -271,6 +307,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); });
connect(m_ui.actionDEV9Settings, &QAction::triggered, [this]() { doSettings("Network & HDD"); });
connect(m_ui.actionFolderSettings, &QAction::triggered, [this]() { doSettings("Folders"); });
connect(m_ui.actionAchievementSettings, &QAction::triggered, [this]() { doSettings("Achievements"); });
connect(
m_ui.actionControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::GlobalSettings); });
connect(m_ui.actionHotkeySettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::HotkeySettings); });
@@ -1690,6 +1727,11 @@ void MainWindow::showEvent(QShowEvent* event)
// the rest of the window... so, instead, let's just force it to be resized on show.
if (isShowingGameList())
m_game_list_widget->resizeTableViewColumnsToFit();
#ifdef ENABLE_RAINTEGRATION
if (Achievements::IsUsingRAIntegration())
Achievements::RAIntegration::MainWindowChanged((void*)winId());
#endif
}
void MainWindow::closeEvent(QCloseEvent* event)
+10
View File
@@ -98,6 +98,7 @@
<addaction name="actionFolderSettings"/>
<addaction name="actionControllerSettings"/>
<addaction name="actionHotkeySettings"/>
<addaction name="actionAchievementSettings"/>
<addaction name="separator"/>
<addaction name="actionAddGameDirectory"/>
<addaction name="actionScanForNewGames"/>
@@ -409,6 +410,15 @@
<string>&amp;Graphics</string>
</property>
</action>
<action name="actionAchievementSettings">
<property name="icon">
<iconset theme="trophy-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>A&amp;chievements</string>
</property>
</action>
<action name="actionPostProcessingSettings">
<property name="text">
<string>&amp;Post-Processing Settings...</string>
+61
View File
@@ -63,6 +63,10 @@
#include "QtUtils.h"
#include "svnrev.h"
#ifdef ENABLE_ACHIEVEMENTS
#include "Frontend/Achievements.h"
#endif
static constexpr u32 SETTINGS_SAVE_DELAY = 1000;
EmuThread* g_emu_thread = nullptr;
@@ -1111,6 +1115,45 @@ void Host::OnSaveStateSaved(const std::string_view& filename)
emit g_emu_thread->onSaveStateSaved(QtUtils::StringViewToQString(filename));
}
#ifdef ENABLE_ACHIEVEMENTS
void Host::OnAchievementsRefreshed()
{
u32 game_id = 0;
u32 achievement_count = 0;
u32 max_points = 0;
QString game_info;
if (Achievements::HasActiveGame())
{
game_id = Achievements::GetGameID();
achievement_count = Achievements::GetAchievementCount();
max_points = Achievements::GetMaximumPointsForGame();
game_info = qApp->translate("EmuThread",
"Game ID: %1\n"
"Game Title: %2\n"
"Achievements: %5 (%6)\n\n")
.arg(game_id)
.arg(QString::fromStdString(Achievements::GetGameTitle()))
.arg(achievement_count)
.arg(qApp->translate("EmuThread", "%n points", "", max_points));
const std::string rich_presence_string(Achievements::GetRichPresenceString());
if (!rich_presence_string.empty())
game_info.append(QString::fromStdString(rich_presence_string));
else
game_info.append(qApp->translate("EmuThread", "Rich presence inactive or unsupported."));
}
else
{
game_info = qApp->translate("EmuThread", "Game not loaded or no RetroAchievements available.");
}
emit g_emu_thread->onAchievementsRefreshed(game_id, game_info, achievement_count, max_points);
}
#endif
void Host::CPUThreadVSync()
{
g_emu_thread->getEventLoop()->processEvents(QEventLoop::AllEvents);
@@ -1475,6 +1518,14 @@ void QtHost::HookSignals()
{
std::signal(SIGINT, SignalHandler);
std::signal(SIGTERM, SignalHandler);
#ifdef __linux__
// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.
struct sigaction sa_chld = {};
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_SIGINFO | SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa_chld, nullptr);
#endif
}
void QtHost::PrintCommandLineVersion()
@@ -1504,6 +1555,9 @@ void QtHost::PrintCommandLineHelp(const std::string_view& progname)
std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n");
std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");
std::fprintf(stderr, " -earlyconsolelog: Forces logging of early console messages to console.\n");
#ifdef ENABLE_RAINTEGRATION
std::fprintf(stderr, " -raintegration: Use RAIntegration instead of built-in achievement support.\n");
#endif
std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"
" parameters make up the filename. Use when the filename contains\n"
" spaces or starts with a dash.\n");
@@ -1613,6 +1667,13 @@ bool QtHost::ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VM
s_start_fullscreen_ui = true;
continue;
}
#ifdef ENABLE_RAINTEGRATION
else if (CHECK_ARG(QStringLiteral("-raintegration")))
{
Achievements::SwitchToRAIntegration();
continue;
}
#endif
else if (CHECK_ARG(QStringLiteral("--")))
{
no_more_args = true;
+3
View File
@@ -151,6 +151,9 @@ Q_SIGNALS:
/// just signifies that the save has started, not necessarily completed.
void onSaveStateSaved(const QString& path);
/// Called when achievements are reloaded/refreshed (e.g. game change, login, option change).
void onAchievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points);
protected:
void run();
@@ -0,0 +1,91 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "AchievementLoginDialog.h"
#include "pcsx2/Frontend/Achievements.h"
#include "QtHost.h"
#include <QtWidgets/QMessageBox>
AchievementLoginDialog::AchievementLoginDialog(QWidget* parent)
: QDialog(parent)
{
m_ui.setupUi(this);
m_ui.loginIcon->setPixmap(QIcon::fromTheme("login-box-line").pixmap(32));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_login = m_ui.buttonBox->addButton(tr("&Login"), QDialogButtonBox::AcceptRole);
m_login->setEnabled(false);
connectUi();
}
AchievementLoginDialog::~AchievementLoginDialog() = default;
void AchievementLoginDialog::loginClicked()
{
std::string username(m_ui.userName->text().toStdString());
std::string password(m_ui.password->text().toStdString());
// TODO: Make cancellable.
m_ui.status->setText(tr("Logging in..."));
enableUI(false);
Host::RunOnCPUThread([this, username = std::move(username), password = std::move(password)]() {
const bool result = Achievements::Login(username.c_str(), password.c_str());
QMetaObject::invokeMethod(this, "processLoginResult", Qt::QueuedConnection, Q_ARG(bool, result));
});
}
void AchievementLoginDialog::cancelClicked()
{
done(1);
}
void AchievementLoginDialog::processLoginResult(bool result)
{
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::connectUi()
{
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &AchievementLoginDialog::loginClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &AchievementLoginDialog::cancelClicked);
auto enableLoginButton = [this](const QString&) { m_login->setEnabled(canEnableLoginButton()); };
connect(m_ui.userName, &QLineEdit::textChanged, enableLoginButton);
connect(m_ui.password, &QLineEdit::textChanged, enableLoginButton);
}
void AchievementLoginDialog::enableUI(bool enabled)
{
m_ui.userName->setEnabled(enabled);
m_ui.password->setEnabled(enabled);
m_ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(enabled);
m_login->setEnabled(enabled && canEnableLoginButton());
}
bool AchievementLoginDialog::canEnableLoginButton() const
{
return !m_ui.userName->text().isEmpty() && !m_ui.password->text().isEmpty();
}
@@ -0,0 +1,41 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "ui_AchievementLoginDialog.h"
#include <QtWidgets/QDialog>
#include <QtWidgets/QPushButton>
class AchievementLoginDialog : public QDialog
{
Q_OBJECT
public:
AchievementLoginDialog(QWidget* parent);
~AchievementLoginDialog();
private Q_SLOTS:
void loginClicked();
void cancelClicked();
void processLoginResult(bool result);
private:
void connectUi();
void enableUI(bool enabled);
bool canEnableLoginButton() const;
Ui::AchievementLoginDialog m_ui;
QPushButton* m_login;
};
+140
View File
@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AchievementLoginDialog</class>
<widget class="QDialog" name="AchievementLoginDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>410</width>
<height>190</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>410</width>
<height>190</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>410</width>
<height>190</height>
</size>
</property>
<property name="windowTitle">
<string comment="Window title">RetroAchievements Login</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="loginIcon">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>:/icons/black/48/login-box-line.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string comment="Header text">RetroAchievements Login</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Please enter user name and password for retroachievements.org below. Your password will not be saved in PCSX2, an access token will be generated and used instead.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>User Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="userName"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Ready...</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,194 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "AchievementSettingsWidget.h"
#include "AchievementLoginDialog.h"
#include "MainWindow.h"
#include "SettingsDialog.h"
#include "SettingWidgetBinder.h"
#include "QtUtils.h"
#include "pcsx2/Frontend/Achievements.h"
#include "pcsx2/HostSettings.h"
#include "common/StringUtil.h"
#include <QtCore/QDateTime>
#include <QtWidgets/QMessageBox>
AchievementSettingsWidget::AchievementSettingsWidget(SettingsDialog* dialog, QWidget* parent)
: QWidget(parent)
, m_dialog(dialog)
{
m_ui.setupUi(this);
SettingsInterface* sif = dialog->getSettingsInterface();
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enable, "Achievements", "Enabled", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.richPresence, "Achievements", "RichPresence", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.challengeMode, "Achievements", "ChallengeMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboards, "Achievements", "Leaderboards", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.testMode, "Achievements", "TestMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialTestMode, "Achievements",
"UnofficialTestMode", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Achievements",
"SoundEffects", true);
dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"),
tr("When enabled and logged in, PCSX2 will scan for achievements on game load."));
dialog->registerWidgetHelp(m_ui.testMode, tr("Enable Test Mode"), tr("Unchecked"),
tr("When enabled, PCSX2 will assume all achievements are locked and not send any "
"unlock notifications to the server."));
dialog->registerWidgetHelp(
m_ui.unofficialTestMode, tr("Test Unofficial Achievements"), tr("Unchecked"),
tr("When enabled, PCSX2 will list achievements from unofficial sets. Please note that these achievements are "
"not tracked by RetroAchievements, so they unlock every time."));
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.challengeMode, tr("Enable Hardcore Mode"), tr("Unchecked"),
tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save state, cheats, and slowdown functions."));
dialog->registerWidgetHelp(m_ui.leaderboards, tr("Enable Leaderboards"), tr("Checked"),
tr("Enables tracking and submission of leaderboards in supported games. If leaderboards are disabled, you will still "
"be able to view the leaderboard and scores, but no scores will be uploaded."));
dialog->registerWidgetHelp(
m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),
tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
connect(m_ui.enable, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState);
connect(m_ui.challengeMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState);
connect(m_ui.challengeMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::onChallengeModeStateChanged);
if (!m_dialog->isPerGameSettings())
{
connect(m_ui.loginButton, &QPushButton::clicked, this, &AchievementSettingsWidget::onLoginLogoutPressed);
connect(m_ui.viewProfile, &QPushButton::clicked, this, &AchievementSettingsWidget::onViewProfilePressed);
connect(g_emu_thread, &EmuThread::onAchievementsRefreshed, this, &AchievementSettingsWidget::onAchievementsRefreshed);
updateLoginState();
// force a refresh of game info
Host::RunOnCPUThread(Host::OnAchievementsRefreshed);
}
else
{
// remove login and game info, not relevant for per-game
m_ui.verticalLayout->removeWidget(m_ui.gameInfoBox);
m_ui.gameInfoBox->deleteLater();
m_ui.gameInfoBox = nullptr;
m_ui.verticalLayout->removeWidget(m_ui.loginBox);
m_ui.loginBox->deleteLater();
m_ui.loginBox = nullptr;
}
updateEnableState();
}
AchievementSettingsWidget::~AchievementSettingsWidget() = default;
void AchievementSettingsWidget::updateEnableState()
{
const bool enabled = m_dialog->getEffectiveBoolValue("Achievements", "Enabled", false);
const bool challenge = m_dialog->getEffectiveBoolValue("Achievements", "ChallengeMode", false);
m_ui.testMode->setEnabled(enabled);
m_ui.unofficialTestMode->setEnabled(enabled);
m_ui.richPresence->setEnabled(enabled);
m_ui.challengeMode->setEnabled(enabled);
m_ui.leaderboards->setEnabled(enabled && challenge);
m_ui.soundEffects->setEnabled(enabled);
}
void AchievementSettingsWidget::onChallengeModeStateChanged()
{
if (!QtHost::IsVMValid())
return;
const bool enabled = m_dialog->getEffectiveBoolValue("Achievements", "Enabled", false);
const bool challenge = m_dialog->getEffectiveBoolValue("Achievements", "ChallengeMode", false);
if (!enabled || !challenge)
return;
// don't bother prompting if the game doesn't have achievements
auto lock = Achievements::GetLock();
if (!Achievements::HasActiveGame())
return;
if (QMessageBox::question(QtUtils::GetRootWidget(this), tr("Reset System"),
tr("Hardcore mode will not be enabled until the system is reset. Do you want to reset the system now?")) != QMessageBox::Yes)
{
return;
}
g_emu_thread->resetVM();
}
void AchievementSettingsWidget::updateLoginState()
{
const std::string username(Host::GetBaseStringSettingValue("Achievements", "Username"));
const bool logged_in = !username.empty();
if (logged_in)
{
const u64 login_unix_timestamp = StringUtil::FromChars<u64>(Host::GetBaseStringSettingValue("Achievements", "LoginTimestamp", "0")).value_or(0);
const QDateTime login_timestamp(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(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(logged_in);
}
void AchievementSettingsWidget::onLoginLogoutPressed()
{
if (!Host::GetBaseStringSettingValue("Achievements", "Username").empty())
{
Host::RunOnCPUThread(Achievements::Logout, true);
updateLoginState();
return;
}
AchievementLoginDialog login(this);
int res = login.exec();
if (res != 0)
return;
updateLoginState();
}
void AchievementSettingsWidget::onViewProfilePressed()
{
const std::string username(Host::GetBaseStringSettingValue("Achievements", "Username"));
if (username.empty())
return;
const QByteArray encoded_username(QUrl::toPercentEncoding(QString::fromStdString(username)));
QtUtils::OpenURL(
QtUtils::GetRootWidget(this),
QUrl(QStringLiteral("https://retroachievements.org/user/%1").arg(QString::fromUtf8(encoded_username))));
}
void AchievementSettingsWidget::onAchievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total,
quint32 points)
{
m_ui.gameInfo->setText(game_info_string);
}
@@ -0,0 +1,42 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QtWidgets/QWidget>
#include "ui_AchievementSettingsWidget.h"
class SettingsDialog;
class AchievementSettingsWidget : public QWidget
{
Q_OBJECT
public:
explicit AchievementSettingsWidget(SettingsDialog* dialog, QWidget* parent);
~AchievementSettingsWidget();
private Q_SLOTS:
void updateEnableState();
void onChallengeModeStateChanged();
void onLoginLogoutPressed();
void onViewProfilePressed();
void onAchievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points);
private:
void updateLoginState();
Ui::AchievementSettingsWidget m_ui;
SettingsDialog* m_dialog;
};
@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AchievementSettingsWidget</class>
<widget class="QWidget" name="AchievementSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>648</width>
<height>456</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Global Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QCheckBox" name="leaderboards">
<property name="text">
<string>Enable Leaderboards</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="enable">
<property name="text">
<string>Enable Achievements</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="richPresence">
<property name="text">
<string>Enable Rich Presence</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="challengeMode">
<property name="text">
<string>Enable Hardcore Mode</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="testMode">
<property name="text">
<string>Enable Test Mode</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="soundEffects">
<property name="text">
<string>Enable Sound Effects</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="unofficialTestMode">
<property name="text">
<string>Test Unofficial Achievements</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="loginBox">
<property name="title">
<string>Account</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
<item>
<widget class="QLabel" name="loginStatus">
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="loginButton">
<property name="text">
<string>Login...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="viewProfile">
<property name="text">
<string>View Profile...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameInfoBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>160</height>
</size>
</property>
<property name="title">
<string>Game Info</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="gameInfo">
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;justify&quot;&gt;PCSX2 uses RetroAchievements as an achievement database and for tracking progress. To use achievements, please sign up for an account at &lt;a href=&quot;https://retroachievements.org/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;retroachievements.org&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p align=&quot;justify&quot;&gt;To view the achievement list in-game, press the hotkey for &lt;span style=&quot; font-weight:600;&quot;&gt;Open Pause Menu&lt;/span&gt; and select &lt;span style=&quot; font-weight:600;&quot;&gt;Achievements&lt;/span&gt; from the menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>8</number>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

Some files were not shown because too many files have changed in this diff Show More