Cheevos: Add RAIntergration support

This commit is contained in:
Connor McLaughlin
2022-04-18 15:09:21 +10:00
parent 8b61fb8b58
commit 9a5ef2d0a2
17 changed files with 1997 additions and 22 deletions

View File

@ -22,11 +22,16 @@
#include "scmversion/scmversion.h"
#include <algorithm>
#include <cstdarg>
#include <cstdlib>
#include <functional>
#include <string>
#include <vector>
Log_SetChannel(Cheevos);
#ifdef WITH_RAINTEGRATION
#include "RA_Interface.h"
#endif
namespace Cheevos {
enum : s32
@ -60,6 +65,11 @@ static bool s_unofficial_test_mode = false;
static bool s_use_first_disc_from_playlist = true;
static bool s_rich_presence_enabled = false;
#ifdef WITH_RAINTEGRATION
bool g_using_raintegration = false;
bool g_raintegration_initialized = false;
#endif
static rc_runtime_t s_rcheevos_runtime;
static std::unique_ptr<FrontendCommon::HTTPDownloader> s_http_downloader;
@ -244,6 +254,10 @@ bool Initialize(bool test_mode, bool use_first_disc_from_playlist, bool enable_r
g_active = true;
g_challenge_mode = challenge_mode;
#ifdef WITH_RAINTEGRATION
g_using_raintegration = false;
#endif
s_test_mode = test_mode;
s_unofficial_test_mode = include_unofficial;
s_use_first_disc_from_playlist = use_first_disc_from_playlist;
@ -266,6 +280,14 @@ void Reset()
if (!g_active)
return;
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration())
{
RA_OnReset();
return;
}
#endif
Log_DevPrint("Resetting rcheevos state...");
rc_runtime_reset(&s_rcheevos_runtime);
}
@ -298,6 +320,16 @@ void Update()
if (HasActiveGame())
{
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration())
{
if (g_raintegration_initialized)
RA_DoAchievementsFrame();
return;
}
#endif
rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &CheevosPeek, nullptr, nullptr);
UpdateRichPresence();
@ -332,7 +364,20 @@ bool DoState(StateWrapper& sw)
{
// reset runtime, no data (state might've been created without cheevos)
Log_DevPrintf("State is missing cheevos data, resetting runtime");
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration())
{
if (g_raintegration_initialized)
RA_OnReset();
}
else
{
rc_runtime_reset(&s_rcheevos_runtime);
}
#else
rc_runtime_reset(&s_rcheevos_runtime);
#endif
return !sw.HasError();
}
@ -341,30 +386,64 @@ bool DoState(StateWrapper& sw)
if (sw.HasError())
return false;
const int result = rc_runtime_deserialize_progress(&s_rcheevos_runtime, data.get(), nullptr);
if (result != RC_OK)
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration() && g_raintegration_initialized)
{
Log_WarningPrintf("Failed to deserialize cheevos state (%d), resetting", result);
rc_runtime_reset(&s_rcheevos_runtime);
RA_RestoreState(reinterpret_cast<const char*>(data.get()));
}
else
{
const int result = rc_runtime_deserialize_progress(&s_rcheevos_runtime, data.get(), nullptr);
if (result != RC_OK)
{
Log_WarningPrintf("Failed to deserialize cheevos state (%d), resetting", result);
rc_runtime_reset(&s_rcheevos_runtime);
}
}
#endif
return true;
}
else
{
// internally this happens twice.. not great.
const int size = rc_runtime_progress_size(&s_rcheevos_runtime, nullptr);
u32 data_size;
std::unique_ptr<u8[]> data;
u32 data_size = (size >= 0) ? static_cast<u32>(size) : 0;
std::unique_ptr<u8[]> data(new u8[data_size]);
const int result = rc_runtime_serialize_progress(data.get(), &s_rcheevos_runtime, nullptr);
if (result != RC_OK)
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration())
{
// set data to zero, effectively serializing nothing
Log_WarningPrintf("Failed to serialize cheevos state (%d)", result);
data_size = 0;
if (g_raintegration_initialized)
{
const int size = RA_CaptureState(nullptr, 0);
data_size = (size >= 0) ? static_cast<u32>(size) : 0;
data = std::unique_ptr<u8[]>(new u8[data_size]);
const int result = RA_CaptureState(reinterpret_cast<char*>(data.get()), static_cast<int>(data_size));
if (result != static_cast<int>(data_size))
{
Log_WarningPrint("Failed to serialize cheevos state from RAIntegration.");
data_size = 0;
}
}
}
else
{
// internally this happens twice.. not great.
const int size = rc_runtime_progress_size(&s_rcheevos_runtime, nullptr);
data_size = (size >= 0) ? static_cast<u32>(size) : 0;
data = std::unique_ptr<u8[]>(new u8[data_size]);
const int result = rc_runtime_serialize_progress(data.get(), &s_rcheevos_runtime, nullptr);
if (result != RC_OK)
{
// set data to zero, effectively serializing nothing
Log_WarningPrintf("Failed to serialize cheevos state (%d)", result);
data_size = 0;
}
}
#endif
sw.Do(&data_size);
if (data_size > 0)
@ -468,7 +547,7 @@ bool LoginAsync(const char* username, const char* password)
{
s_http_downloader->WaitForAllRequests();
if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0)
if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration())
return false;
if (ImGuiFullscreen::IsInitialized())
@ -487,7 +566,7 @@ bool Login(const char* username, const char* password)
if (g_active)
s_http_downloader->WaitForAllRequests();
if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0)
if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration())
return false;
if (g_active)
@ -1016,8 +1095,10 @@ static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloa
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);
if (game_id == 0)
return;
GetPatches(game_id);
}
void GameChanged()
@ -1089,6 +1170,14 @@ void GameChanged(const std::string& path, CDImage* image)
return;
}
#ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration())
{
RAIntegration::GameChanged();
return;
}
#endif
char url[256];
int res = rc_url_get_gameid(url, sizeof(url), s_game_hash.c_str());
Assert(res == 0);
@ -1492,4 +1581,184 @@ unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud)
}
}
#ifdef WITH_RAINTEGRATION
#include "RA_Consoles.h"
static int RACallbackIsActive();
static void RACallbackCauseUnpause();
static void RACallbackCausePause();
static void RACallbackRebuildMenu();
static void RACallbackEstimateTitle(char* buf);
static void RACallbackResetEmulator();
static void RACallbackLoadROM(const char* unused);
static unsigned char RACallbackReadMemory(unsigned int address);
static void RACallbackWriteMemory(unsigned int address, unsigned char value);
void SwitchToRAIntegration()
{
g_using_raintegration = true;
g_raintegration_initialized = false;
g_active = true;
s_logged_in = true;
}
static void InitializeRAIntegration(void* main_window_handle)
{
RA_InitClient((HWND)main_window_handle, "DuckStation", g_scm_tag_str);
RA_SetUserAgentDetail(Cheevos::GetUserAgent().c_str());
RA_InstallSharedFunctions(RACallbackIsActive, RACallbackCauseUnpause, RACallbackCausePause, RACallbackRebuildMenu,
RACallbackEstimateTitle, RACallbackResetEmulator, RACallbackLoadROM);
RA_SetConsoleID(PlayStation);
// Apparently this has to be done early, or the memory inspector doesn't work.
// That's a bit unfortunate, because the RAM size can vary between games, and depending on the option.
RA_InstallMemoryBank(0, RACallbackReadMemory, RACallbackWriteMemory, Bus::RAM_2MB_SIZE);
// Fire off a login anyway. Saves going into the menu and doing it.
RA_AttemptLogin(0);
g_challenge_mode = RA_HardcoreModeIsActive() != 0;
g_raintegration_initialized = true;
// this is pretty lame, but we may as well persist until we exit anyway
std::atexit(RA_Shutdown);
}
void RAIntegration::MainWindowChanged(void* new_handle)
{
if (g_raintegration_initialized)
{
RA_UpdateHWnd((HWND)new_handle);
return;
}
InitializeRAIntegration(new_handle);
}
void RAIntegration::GameChanged()
{
g_game_id = RA_IdentifyHash(s_game_hash.c_str());
RA_ActivateGame(g_game_id);
}
std::vector<std::pair<int, const char*>> RAIntegration::GetMenuItems()
{
// NOTE: I *really* don't like doing this. But sadly it's the only way we can integrate with Qt.
static constexpr int IDM_RA_RETROACHIEVEMENTS = 1700;
static constexpr int IDM_RA_OVERLAYSETTINGS = 1701;
static constexpr int IDM_RA_FILES_MEMORYBOOKMARKS = 1703;
static constexpr int IDM_RA_FILES_ACHIEVEMENTS = 1704;
static constexpr int IDM_RA_FILES_MEMORYFINDER = 1705;
static constexpr int IDM_RA_FILES_LOGIN = 1706;
static constexpr int IDM_RA_FILES_LOGOUT = 1707;
static constexpr int IDM_RA_FILES_ACHIEVEMENTEDITOR = 1708;
static constexpr int IDM_RA_HARDCORE_MODE = 1710;
static constexpr int IDM_RA_REPORTBROKENACHIEVEMENTS = 1711;
static constexpr int IDM_RA_GETROMCHECKSUM = 1712;
static constexpr int IDM_RA_OPENUSERPAGE = 1713;
static constexpr int IDM_RA_OPENGAMEPAGE = 1714;
static constexpr int IDM_RA_PARSERICHPRESENCE = 1716;
static constexpr int IDM_RA_TOGGLELEADERBOARDS = 1717;
static constexpr int IDM_RA_NON_HARDCORE_WARNING = 1718;
std::vector<std::pair<int, const char*>> ret;
const char* username = RA_UserName();
if (!username || std::strlen(username) == 0)
{
ret.emplace_back(IDM_RA_FILES_LOGIN, "&Login");
}
else
{
ret.emplace_back(IDM_RA_FILES_LOGOUT, "Log&out");
ret.emplace_back(0, nullptr);
ret.emplace_back(IDM_RA_OPENUSERPAGE, "Open my &User Page");
ret.emplace_back(IDM_RA_OPENGAMEPAGE, "Open this &Game's Page");
ret.emplace_back(0, nullptr);
ret.emplace_back(IDM_RA_HARDCORE_MODE, "&Hardcore Mode");
ret.emplace_back(IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning");
ret.emplace_back(0, nullptr);
ret.emplace_back(IDM_RA_TOGGLELEADERBOARDS, "Enable &Leaderboards");
ret.emplace_back(IDM_RA_OVERLAYSETTINGS, "O&verlay Settings");
ret.emplace_back(0, nullptr);
ret.emplace_back(IDM_RA_FILES_ACHIEVEMENTS, "Assets Li&st");
ret.emplace_back(IDM_RA_FILES_ACHIEVEMENTEDITOR, "Assets &Editor");
ret.emplace_back(IDM_RA_FILES_MEMORYFINDER, "&Memory Inspector");
ret.emplace_back(IDM_RA_FILES_MEMORYBOOKMARKS, "Memory &Bookmarks");
ret.emplace_back(IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor");
ret.emplace_back(0, nullptr);
ret.emplace_back(IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem");
ret.emplace_back(IDM_RA_GETROMCHECKSUM, "View Game H&ash");
}
return ret;
}
void RAIntegration::ActivateMenuItem(int item)
{
RA_InvokeDialog(item);
}
int RACallbackIsActive()
{
return static_cast<int>(HasActiveGame());
}
void RACallbackCauseUnpause()
{
if (System::IsValid())
g_host_interface->PauseSystem(false);
}
void RACallbackCausePause()
{
if (System::IsValid())
g_host_interface->PauseSystem(true);
}
void RACallbackRebuildMenu()
{
// unused, we build the menu on demand
}
void RACallbackEstimateTitle(char* buf)
{
StringUtil::Strlcpy(buf, System::GetRunningTitle(), 256);
}
void RACallbackResetEmulator()
{
g_challenge_mode = RA_HardcoreModeIsActive() != 0;
if (System::IsValid())
g_host_interface->ResetSystem();
}
void RACallbackLoadROM(const char* unused)
{
// unused
UNREFERENCED_PARAMETER(unused);
}
unsigned char RACallbackReadMemory(unsigned int address)
{
if (!System::IsValid())
return 0;
u8 value = 0;
CPU::SafeReadMemoryByte(address, &value);
return value;
}
void RACallbackWriteMemory(unsigned int address, unsigned char value)
{
if (!System::IsValid())
return;
CPU::SafeWriteMemoryByte(address, value);
}
#endif
} // namespace Cheevos

View File

@ -4,6 +4,8 @@
#include <functional>
#include <optional>
#include <string>
#include <utility>
#include <vector>
class CDImage;
class StateWrapper;
@ -51,6 +53,25 @@ extern bool g_active;
extern bool g_challenge_mode;
extern u32 g_game_id;
// RAIntegration only exists for Windows, so no point checking it on other platforms.
#ifdef WITH_RAINTEGRATION
extern bool g_using_raintegration;
static ALWAYS_INLINE bool IsUsingRAIntegration()
{
return g_using_raintegration;
}
#else
static ALWAYS_INLINE bool IsUsingRAIntegration()
{
return false;
}
#endif
ALWAYS_INLINE bool IsActive()
{
return g_active;
@ -123,4 +144,15 @@ TinyString GetAchievementProgressText(const Achievement& achievement);
void UnlockAchievement(u32 achievement_id, bool add_notification = true);
void SubmitLeaderboard(u32 leaderboard_id, int value);
#ifdef WITH_RAINTEGRATION
void SwitchToRAIntegration();
namespace RAIntegration {
void MainWindowChanged(void* new_handle);
void GameChanged();
std::vector<std::pair<int, const char*>> GetMenuItems();
void ActivateMenuItem(int item);
} // namespace RAIntegration
#endif
} // namespace Cheevos

View File

@ -5,10 +5,12 @@
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>WITH_CHEEVOS=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(Platform)'=='x64' Or '$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">WITH_RECOMPILER=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="('$(BuildingForUWP)'!='true' And '$(Platform)'!='ARM64')">WITH_RAINTEGRATION=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="('$(Platform)'=='x64' Or '$(Platform)'=='ARM' Or '$(Platform)'=='ARM64')">WITH_RECOMPILER=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="('$(Platform)'=='x64' Or '$(Platform)'=='ARM64') And ('$(BuildingForUWP)'!='true')">WITH_MMAP_FASTMEM=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="('$(BuildingForUWP)'!='true' And '$(Platform)'!='ARM64')">$(SolutionDir)dep\rainterface;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='x64'">$(SolutionDir)dep\xbyak\xbyak;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">$(SolutionDir)dep\vixl\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
@ -18,6 +20,7 @@
<ItemDefinitionGroup>
<Lib>
<AdditionalDependencies>$(RootBuildDir)rcheevos\rcheevos.lib;$(RootBuildDir)imgui\imgui.lib;$(RootBuildDir)stb\stb.lib;$(RootBuildDir)vulkan-loader\vulkan-loader.lib;$(RootBuildDir)xxhash\xxhash.lib;$(RootBuildDir)zlib\zlib.lib;$(RootBuildDir)common\common.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="('$(BuildingForUWP)'!='true' And '$(Platform)'!='ARM64')">$(RootBuildDir)rainterface\rainterface.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Platform)'=='ARM64'">$(RootBuildDir)vixl\vixl.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Lib>
</ItemDefinitionGroup>

View File

@ -20,6 +20,11 @@
#include "scmversion/scmversion.h"
#include "settingsdialog.h"
#include "settingwidgetbinder.h"
#ifdef WITH_CHEEVOS
#include "core/cheevos.h"
#endif
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
@ -101,6 +106,11 @@ void MainWindow::initializeAndShow()
switchToGameListView();
show();
#ifdef WITH_RAINTEGRATION
if (Cheevos::IsUsingRAIntegration())
Cheevos::RAIntegration::MainWindowChanged((void*)winId());
#endif
}
void MainWindow::reportError(const QString& message)
@ -991,6 +1001,33 @@ void MainWindow::setupAdditionalUi()
connect(action, &QAction::triggered,
[scale]() { QtHostInterface::GetInstance()->requestRenderWindowScale(scale); });
}
#ifdef WITH_RAINTEGRATION
if (Cheevos::IsUsingRAIntegration())
{
QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menuDebug);
connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() {
raMenu->clear();
const auto items = Cheevos::RAIntegration::GetMenuItems();
for (const auto& [id, title] : items)
{
if (id == 0)
{
raMenu->addSeparator();
continue;
}
QAction* raAction = raMenu->addAction(QString::fromUtf8(title));
connect(raAction, &QAction::triggered, this, [id]() {
QtHostInterface::GetInstance()->executeOnEmulationThread(
[id]() { Cheevos::RAIntegration::ActivateMenuItem(id); });
});
}
});
m_ui.menuDebug->insertMenu(m_ui.menuCPUExecutionMode->menuAction(), raMenu);
}
#endif
}
void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode)

View File

@ -18,6 +18,7 @@
#ifdef WITH_CHEEVOS
#include "achievementsettingswidget.h"
#include "core/cheevos.h"
#endif
static constexpr char DEFAULT_SETTING_HELP_TEXT[] = "";
@ -45,7 +46,8 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent
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);
if (!Cheevos::IsUsingRAIntegration())
m_achievement_settings = new AchievementSettingsWidget(host_interface, m_ui.settingsContainer, this);
#endif
m_ui.settingsContainer->insertWidget(static_cast<int>(Category::GeneralSettings), m_general_settings);
@ -62,7 +64,18 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent
m_ui.settingsContainer->insertWidget(static_cast<int>(Category::AudioSettings), m_audio_settings);
#ifdef WITH_CHEEVOS
m_ui.settingsContainer->insertWidget(static_cast<int>(Category::AchievementSettings), m_achievement_settings);
if (Cheevos::IsUsingRAIntegration())
{
QLabel* placeholder_label =
new QLabel(QStringLiteral("RAIntegration is being used, built-in RetroAchievements support is disabled."),
m_ui.settingsContainer);
placeholder_label->setAlignment(Qt::AlignLeft | Qt::AlignTop);
m_ui.settingsContainer->insertWidget(static_cast<int>(Category::AchievementSettings), placeholder_label);
}
else
{
m_ui.settingsContainer->insertWidget(static_cast<int>(Category::AchievementSettings), m_achievement_settings);
}
#else
QLabel* placeholder_label =
new QLabel(tr("This DuckStation build was not compiled with RetroAchievements support."), m_ui.settingsContainer);

View File

@ -113,6 +113,11 @@ bool CommonHostInterface::Initialize()
CreateImGuiContext();
#ifdef WITH_CHEEVOS
#ifdef WITH_RAINTEGRATION
if (GetBoolSettingValue("Cheevos", "UseRAIntegration", false))
Cheevos::SwitchToRAIntegration();
#endif
UpdateCheevosActive();
#endif
@ -3287,6 +3292,10 @@ void CommonHostInterface::SetDefaultSettings(SettingsInterface& si)
si.SetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", true);
si.DeleteValue("Cheevos", "Username");
si.DeleteValue("Cheevos", "Token");
#ifdef WITH_RAINTEGRATION
si.SetBoolValue("Cheevos", "UseRAIntegration", false);
#endif
#endif
}
@ -4377,6 +4386,11 @@ void CommonHostInterface::UpdateCheevosActive()
const bool cheevos_rich_presence = GetBoolSettingValue("Cheevos", "RichPresence", true);
const bool cheevos_hardcore = GetBoolSettingValue("Cheevos", "ChallengeMode", false);
#ifdef WITH_RAINTEGRATION
if (Cheevos::IsUsingRAIntegration())
return;
#endif
if (cheevos_enabled != Cheevos::IsActive() || cheevos_test_mode != Cheevos::IsTestModeActive() ||
cheevos_unofficial_test_mode != Cheevos::IsUnofficialTestModeActive() ||
cheevos_use_first_disc_from_playlist != Cheevos::IsUsingFirstDiscFromPlaylist() ||

View File

@ -2328,6 +2328,17 @@ void DrawSettingsWindow()
case SettingsPage::AchievementsSetings:
{
#ifdef WITH_RAINTEGRATION
if (Cheevos::IsUsingRAIntegration())
{
BeginMenuButtons();
ActiveButton(ICON_FA_BAN " RAIntegration is being used instead of the built-in cheevos implementation.",
false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
EndMenuButtons();
break;
}
#endif
#ifdef WITH_CHEEVOS
BeginMenuButtons();