diff --git a/CMakeLists.txt b/CMakeLists.txt
index f2fcabd64f..af18192df9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -124,6 +124,11 @@ option(OPROFILING "Enable profiling" OFF)
# TODO: Add DSPSpy
option(DSPTOOL "Build dsptool" OFF)
+# RetroAchievements developer tools require Windows hooks
+if(WIN32)
+ option(RC_CLIENT_SUPPORTS_RAINTEGRATION "Enables RetroAchievements developer tools" ON)
+endif()
+
# Enable SDL by default on operating systems that aren't Android.
if(NOT ANDROID)
option(ENABLE_SDL "Enables SDL as a generic controller backend" ON)
diff --git a/Externals/rcheevos/CMakeLists.txt b/Externals/rcheevos/CMakeLists.txt
index f8d9bee8da..666a5e11ec 100644
--- a/Externals/rcheevos/CMakeLists.txt
+++ b/Externals/rcheevos/CMakeLists.txt
@@ -43,9 +43,12 @@ add_library(rcheevos
rcheevos/src/rhash/hash.c
rcheevos/src/rhash/md5.c
rcheevos/src/rhash/md5.h
+ rcheevos/src/rhash/rc_hash_internal.h
rcheevos/src/rurl/url.c
rcheevos/src/rc_client.c
+ rcheevos/src/rc_client_external.c
rcheevos/src/rc_client_external.h
+ rcheevos/src/rc_client_external_versions.h
rcheevos/src/rc_client_internal.h
rcheevos/src/rc_client_raintegration.c
rcheevos/src/rc_client_raintegration_internal.h
@@ -61,6 +64,11 @@ target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/rcheevo
target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_definitions(rcheevos PRIVATE "RC_DISABLE_LUA=1" "RCHEEVOS_URL_SSL")
target_compile_definitions(rcheevos PRIVATE "RC_CLIENT_SUPPORTS_HASH")
+target_compile_definitions(rcheevos PRIVATE "RC_CLIENT_SUPPORTS_EXTERNAL")
+target_compile_definitions(rcheevos PRIVATE "RC_HASH_NO_ENCRYPTED")
+target_compile_definitions(rcheevos PRIVATE "RC_HASH_NO_ROM")
+target_compile_definitions(rcheevos PRIVATE "RC_HASH_NO_ZIP")
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
+ target_compile_definitions(rcheevos PRIVATE "RC_CLIENT_SUPPORTS_RAINTEGRATION")
target_compile_definitions(rcheevos PRIVATE "_CRT_SECURE_NO_WARNINGS")
endif()
diff --git a/Externals/rcheevos/rcheevos b/Externals/rcheevos/rcheevos
index d54cf8f105..022ac70cff 160000
--- a/Externals/rcheevos/rcheevos
+++ b/Externals/rcheevos/rcheevos
@@ -1 +1 @@
-Subproject commit d54cf8f1059cebc90a6f5ecdf03df69259f22054
+Subproject commit 022ac70cff6cf60c8957de63d6297998904a6f05
diff --git a/Externals/rcheevos/rcheevos.vcxproj b/Externals/rcheevos/rcheevos.vcxproj
index 51a94c7750..eda4fcd77f 100644
--- a/Externals/rcheevos/rcheevos.vcxproj
+++ b/Externals/rcheevos/rcheevos.vcxproj
@@ -41,6 +41,7 @@
+
@@ -68,7 +69,9 @@
+
+
@@ -76,7 +79,7 @@
- RC_DISABLE_LUA;RCHEEVOS_URL_SSL;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions)
+ RC_DISABLE_LUA;RCHEEVOS_URL_SSL;RC_CLIENT_SUPPORTS_HASH;RC_CLIENT_SUPPORTS_EXTERNAL;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_HASH_NO_ENCRYPTED;RC_HASH_NO_ROM;RC_HASH_NO_ZIP;%(PreprocessorDefinitions)
$(ProjectDir)rcheevos\include;%(AdditionalIncludeDirectories)
diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp
index 08df434fa5..01ed2b91a2 100644
--- a/Source/Core/Core/AchievementManager.cpp
+++ b/Source/Core/Core/AchievementManager.cpp
@@ -22,6 +22,7 @@
#include "Common/Image.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
+#include "Common/StringUtil.h"
#include "Common/Version.h"
#include "Common/WorkQueueThread.h"
#include "Core/ActionReplay.h"
@@ -33,6 +34,7 @@
#include "Core/GeckoCode.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/VideoInterface.h"
+#include "Core/Host.h"
#include "Core/PatchEngine.h"
#include "Core/PowerPC/MMU.h"
#include "Core/System.h"
@@ -42,6 +44,12 @@
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoEvents.h"
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+#include
+#include
+#include
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
static const Common::HttpRequest::Headers USER_AGENT_HEADER = {
{"User-Agent", Common::GetUserAgentStr()}};
@@ -51,7 +59,7 @@ AchievementManager& AchievementManager::GetInstance()
return s_instance;
}
-void AchievementManager::Init()
+void AchievementManager::Init(void* hwnd)
{
LoadDefaultBadges();
if (!m_client && Config::Get(Config::RA_ENABLED))
@@ -73,9 +81,19 @@ void AchievementManager::Init()
m_queue.Reset("AchievementManagerQueue", [](const std::function& func) { func(); });
m_image_queue.Reset("AchievementManagerImageQueue",
[](const std::function& func) { func(); });
+
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ // Attempt to load the integration DLL from the directory containing the main client executable.
+ // In x64 build, will look for RA_Integration-x64.dll, then RA_Integration.dll.
+ // In non-x64 build, will only look for RA_Integration.dll.
+ rc_client_begin_load_raintegration(
+ m_client, UTF8ToWString(File::GetExeDirectory()).c_str(), reinterpret_cast(hwnd),
+ "Dolphin", Common::GetScmDescStr().c_str(), LoadIntegrationCallback, NULL);
+#else // RC_CLIENT_SUPPORTS_RAINTEGRATION
if (HasAPIToken())
Login("");
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager Initialized");
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
}
}
@@ -163,12 +181,17 @@ void AchievementManager::LoadGame(const std::string& file_path, const DiscIO::Vo
rc_client_set_unofficial_enabled(m_client, Config::Get(Config::RA_UNOFFICIAL_ENABLED));
rc_client_set_encore_mode_enabled(m_client, Config::Get(Config::RA_ENCORE_ENABLED));
rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED));
- if (volume)
{
std::lock_guard lg{m_lock};
- if (!m_loading_volume)
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ SplitPath(file_path, nullptr, &m_title_estimate, nullptr);
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+ if (volume)
{
- m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader());
+ if (!m_loading_volume)
+ {
+ m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader());
+ }
}
}
std::lock_guard lg{m_filereader_lock};
@@ -292,15 +315,26 @@ void AchievementManager::FetchGameBadges()
void AchievementManager::DoFrame()
{
- if (!IsGameLoaded() || !Core::IsCPUThread())
+ if (!(IsGameLoaded() || m_dll_found) || !Core::IsCPUThread())
return;
{
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ if (m_dll_found)
+ {
+ std::lock_guard lg{m_memory_lock};
+ Core::System* system = m_system.load(std::memory_order_acquire);
+ if (!system)
+ return;
+ Core::CPUThreadGuard thread_guard(*system);
+ u32 ram_size = system->GetMemory().GetRamSizeReal();
+ if (m_cloned_memory.size() != ram_size)
+ m_cloned_memory.resize(ram_size);
+ system->GetMemory().CopyFromEmu(m_cloned_memory.data(), 0, m_cloned_memory.size());
+ }
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
std::lock_guard lg{m_lock};
rc_client_do_frame(m_client);
}
- Core::System* system = m_system.load(std::memory_order_acquire);
- if (!system)
- return;
auto current_time = std::chrono::steady_clock::now();
if (current_time - m_last_rp_time > std::chrono::seconds{10})
{
@@ -621,6 +655,22 @@ std::vector AchievementManager::GetActiveLeaderboards() const
return display_values;
}
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+const rc_client_raintegration_menu_t* AchievementManager::GetDevelopmentMenu()
+{
+ if (!m_dll_found)
+ return nullptr;
+ return rc_client_raintegration_get_menu(m_client);
+}
+
+u32 AchievementManager::ActivateDevMenuItem(u32 menu_item_id)
+{
+ if (!m_dll_found)
+ return 0;
+ return rc_client_raintegration_activate_menu_item(m_client, menu_item_id);
+}
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
void AchievementManager::DoState(PointerWrap& p)
{
if (!m_client || !Config::Get(Config::RA_ENABLED))
@@ -716,6 +766,7 @@ void AchievementManager::Shutdown()
// DON'T log out - keep those credentials for next run.
rc_client_destroy(m_client);
m_client = nullptr;
+ m_dll_found = false;
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager shut down.");
}
}
@@ -921,7 +972,8 @@ void AchievementManager::LeaderboardEntriesCallback(int result, const char* erro
void AchievementManager::LoadGameCallback(int result, const char* error_message,
rc_client_t* client, void* userdata)
{
- AchievementManager::GetInstance().m_loading_volume.reset(nullptr);
+ auto& instance = AchievementManager::GetInstance();
+ instance.m_loading_volume.reset(nullptr);
if (result == RC_API_FAILURE)
{
WARN_LOG_FMT(ACHIEVEMENTS, "Load data request rejected for old Dolphin version.");
@@ -936,6 +988,12 @@ void AchievementManager::LoadGameCallback(int result, const char* error_message,
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load data for current game.");
OSD::AddMessage("Achievements are not supported for this title.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
+ if (instance.m_dll_found && result == RC_NO_GAME_LOADED)
+ {
+ // Allow developer tools for unidentified games
+ rc_client_set_read_memory_function(instance.m_client, MemoryPeeker);
+ instance.m_system.store(&Core::System::GetInstance(), std::memory_order_release);
+ }
return;
}
@@ -949,7 +1007,6 @@ void AchievementManager::LoadGameCallback(int result, const char* error_message,
}
INFO_LOG_FMT(ACHIEVEMENTS, "Loaded data for game ID {}.", game->id);
- auto& instance = AchievementManager::GetInstance();
rc_client_set_read_memory_function(instance.m_client, MemoryPeeker);
instance.m_display_welcome_message = true;
instance.FetchGameBadges();
@@ -1035,6 +1092,7 @@ void AchievementManager::DisplayWelcomeMessage()
void AchievementManager::HandleAchievementTriggeredEvent(const rc_client_event_t* client_event)
{
const auto& instance = AchievementManager::GetInstance();
+
OSD::AddMessage(fmt::format("Unlocked: {} ({})", client_event->achievement->title,
client_event->achievement->points),
OSD::Duration::VERY_LONG,
@@ -1043,6 +1101,30 @@ void AchievementManager::HandleAchievementTriggeredEvent(const rc_client_event_t
&instance.GetAchievementBadge(client_event->achievement->id, false));
AchievementManager::GetInstance().m_update_callback(
UpdatedItems{.achievements = {client_event->achievement->id}});
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ switch (rc_client_raintegration_get_achievement_state(instance.m_client,
+ client_event->achievement->id))
+ {
+ case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_LOCAL:
+ // Achievement only exists locally and has not been uploaded.
+ OSD::AddMessage("Local achievement; not submitted to site.", OSD::Duration::VERY_LONG,
+ OSD::Color::GREEN);
+ break;
+ case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_MODIFIED:
+ // Achievement has been modified locally and differs from the one on the site.
+ OSD::AddMessage("Modified achievement; not submitted to site.", OSD::Duration::VERY_LONG,
+ OSD::Color::GREEN);
+ break;
+ case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_INSECURE:
+ // The player has done something that we consider cheating like modifying the RAM while playing.
+ // Just indicate that the achievement was only unlocked locally, but don't clarify why.
+ OSD::AddMessage("Achievement not submitted to site.", OSD::Duration::VERY_LONG,
+ OSD::Color::GREEN);
+ break;
+ default:
+ break;
+ }
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
}
void AchievementManager::HandleLeaderboardStartedEvent(const rc_client_event_t* client_event)
@@ -1229,16 +1311,33 @@ u32 AchievementManager::MemoryPeeker(u32 address, u8* buffer, u32 num_bytes, rc_
{
if (buffer == nullptr)
return 0u;
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ auto& instance = AchievementManager::GetInstance();
+ if (instance.m_dll_found)
+ {
+ std::lock_guard lg{instance.m_memory_lock};
+ if (u64(address) + num_bytes >= instance.m_cloned_memory.size())
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS,
+ "Attempt to read past memory size: size {} address {} write length {}",
+ instance.m_cloned_memory.size(), address, num_bytes);
+ return 0;
+ }
+ std::copy(instance.m_cloned_memory.begin() + address,
+ instance.m_cloned_memory.begin() + address + num_bytes, buffer);
+ return num_bytes;
+ }
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
auto& system = Core::System::GetInstance();
if (!(Core::IsHostThread() || Core::IsCPUThread()))
{
ASSERT_MSG(ACHIEVEMENTS, false, "MemoryPeeker called from wrong thread");
return 0;
}
- Core::CPUThreadGuard threadguard(system);
+ Core::CPUThreadGuard thread_guard(system);
for (u32 num_read = 0; num_read < num_bytes; num_read++)
{
- auto value = system.GetMMU().HostTryReadU8(threadguard, address + num_read,
+ auto value = system.GetMMU().HostTryReadU8(thread_guard, address + num_read,
PowerPC::RequestedAddressSpace::Physical);
if (!value.has_value())
return num_read;
@@ -1395,4 +1494,97 @@ void AchievementManager::EventHandler(const rc_client_event_t* event, rc_client_
}
}
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+void AchievementManager::LoadIntegrationCallback(int result, const char* error_message,
+ rc_client_t* client, void* userdata)
+{
+ auto& instance = AchievementManager::GetInstance();
+ switch (result)
+ {
+ case RC_OK:
+ INFO_LOG_FMT(ACHIEVEMENTS, "RAIntegration.dll found.");
+ instance.m_dll_found = true;
+ rc_client_raintegration_set_event_handler(instance.m_client, RAIntegrationEventHandler);
+ rc_client_raintegration_set_write_memory_function(instance.m_client, MemoryPoker);
+ rc_client_raintegration_set_get_game_name_function(instance.m_client, GameTitleEstimateHandler);
+ instance.m_dev_menu_callback();
+ // TODO: hook up menu and dll event handlers
+ break;
+
+ case RC_MISSING_VALUE:
+ INFO_LOG_FMT(ACHIEVEMENTS, "RAIntegration.dll not found.");
+ // DLL is not present; do nothing.
+ break;
+
+ default:
+ WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load RAIntegration.dll. {}", error_message);
+ break;
+ }
+
+ if (instance.HasAPIToken())
+ instance.Login("");
+ INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager Initialized");
+}
+
+void AchievementManager::RAIntegrationEventHandler(const rc_client_raintegration_event_t* event,
+ rc_client_t* client)
+{
+ auto& instance = AchievementManager::GetInstance();
+ switch (event->type)
+ {
+ case RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED:
+ case RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED:
+ instance.m_dev_menu_callback();
+ break;
+ case RC_CLIENT_RAINTEGRATION_EVENT_PAUSE:
+ {
+ Core::QueueHostJob([](Core::System& system) { Core::SetState(system, Core::State::Paused); });
+ break;
+ }
+ case RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED:
+ Config::SetBaseOrCurrent(Config::RA_HARDCORE_ENABLED,
+ !Config::Get(Config::RA_HARDCORE_ENABLED));
+ break;
+ default:
+ WARN_LOG_FMT(ACHIEVEMENTS, "Unsupported raintegration event. {}", event->type);
+ break;
+ }
+}
+
+void AchievementManager::MemoryPoker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client)
+{
+ if (buffer == nullptr)
+ return;
+ if (!(Core::IsHostThread() || Core::IsCPUThread()))
+ {
+ Core::QueueHostJob([address, buffer, num_bytes, client](Core::System& system) {
+ MemoryPoker(address, buffer, num_bytes, client);
+ });
+ return;
+ }
+ auto& instance = AchievementManager::GetInstance();
+ if (u64(address) + num_bytes >= instance.m_cloned_memory.size())
+ {
+ ERROR_LOG_FMT(ACHIEVEMENTS,
+ "Attempt to write past memory size: size {} address {} write length {}",
+ instance.m_cloned_memory.size(), address, num_bytes);
+ return;
+ }
+ Core::System* system = instance.m_system.load(std::memory_order_acquire);
+ if (!system)
+ return;
+ Core::CPUThreadGuard thread_guard(*system);
+ std::lock_guard lg{instance.m_memory_lock};
+ system->GetMemory().CopyToEmu(address, buffer, num_bytes);
+ std::copy(buffer, buffer + num_bytes, instance.m_cloned_memory.begin() + address);
+}
+void AchievementManager::GameTitleEstimateHandler(char* buffer, u32 buffer_size,
+ rc_client_t* client)
+{
+ auto& instance = AchievementManager::GetInstance();
+ std::lock_guard lg{instance.m_lock};
+ strncpy(buffer, instance.m_title_estimate.c_str(), static_cast(buffer_size));
+}
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h
index dfa6bb535e..ea7a8bae5e 100644
--- a/Source/Core/Core/AchievementManager.h
+++ b/Source/Core/Core/AchievementManager.h
@@ -34,6 +34,10 @@
#include "DiscIO/Volume.h"
#include "VideoCommon/Assets/CustomTextureData.h"
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+#include
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
namespace Core
{
class CPUThreadGuard;
@@ -113,7 +117,7 @@ public:
using UpdateCallback = std::function;
static AchievementManager& GetInstance();
- void Init();
+ void Init(void* hwnd);
void SetUpdateCallback(UpdateCallback callback);
void Login(const std::string& password);
bool HasAPIToken() const;
@@ -161,6 +165,16 @@ public:
const std::unordered_set& GetActiveChallenges() const;
std::vector GetActiveLeaderboards() const;
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ const rc_client_raintegration_menu_t* GetDevelopmentMenu();
+ u32 ActivateDevMenuItem(u32 menu_item_id);
+ void SetDevMenuUpdateCallback(std::function callback)
+ {
+ m_dev_menu_callback = callback;
+ };
+ bool CheckForModifications() { return rc_client_raintegration_has_modifications(m_client); };
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
void DoState(PointerWrap& p);
void CloseGame();
@@ -235,6 +249,15 @@ private:
const UpdatedItems callback_data);
static void EventHandler(const rc_client_event_t* event, rc_client_t* client);
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ static void LoadIntegrationCallback(int result, const char* error_message, rc_client_t* client,
+ void* userdata);
+ static void RAIntegrationEventHandler(const rc_client_raintegration_event_t* event,
+ rc_client_t* client);
+ static void MemoryPoker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client);
+ static void GameTitleEstimateHandler(char* buffer, u32 buffer_size, rc_client_t* client);
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
rc_runtime_t m_runtime{};
rc_client_t* m_client{};
std::atomic m_system{};
@@ -266,6 +289,14 @@ private:
std::unordered_set m_active_challenges;
std::vector m_active_leaderboards;
+ bool m_dll_found = false;
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ std::function m_dev_menu_callback;
+ std::vector m_cloned_memory;
+ std::recursive_mutex m_memory_lock;
+ std::string m_title_estimate;
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
Common::WorkQueueThread> m_queue;
Common::WorkQueueThread> m_image_queue;
mutable std::recursive_mutex m_lock;
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 09b69a620e..095b1d7a93 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -789,4 +789,7 @@ if(USE_RETRO_ACHIEVEMENTS)
target_link_libraries(core PUBLIC rcheevos)
target_compile_definitions(core PUBLIC -DUSE_RETRO_ACHIEVEMENTS)
target_compile_definitions(core PUBLIC -DRC_CLIENT_SUPPORTS_HASH)
+ if(RC_CLIENT_SUPPORTS_RAINTEGRATION)
+ target_compile_definitions(core PUBLIC -DRC_CLIENT_SUPPORTS_RAINTEGRATION)
+ endif()
endif()
diff --git a/Source/Core/DolphinQt/Achievements/AchievementBox.cpp b/Source/Core/DolphinQt/Achievements/AchievementBox.cpp
index 2b73ba83b0..1fe85131cc 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementBox.cpp
+++ b/Source/Core/DolphinQt/Achievements/AchievementBox.cpp
@@ -22,7 +22,7 @@
static constexpr size_t PROGRESS_LENGTH = 24;
-AchievementBox::AchievementBox(QWidget* parent, rc_client_achievement_t* achievement)
+AchievementBox::AchievementBox(QWidget* parent, const rc_client_achievement_t* achievement)
: QGroupBox(parent), m_achievement(achievement)
{
const auto& instance = AchievementManager::GetInstance();
diff --git a/Source/Core/DolphinQt/Achievements/AchievementBox.h b/Source/Core/DolphinQt/Achievements/AchievementBox.h
index 92b1e75878..7d6e163641 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementBox.h
+++ b/Source/Core/DolphinQt/Achievements/AchievementBox.h
@@ -18,7 +18,7 @@ class AchievementBox final : public QGroupBox
{
Q_OBJECT
public:
- explicit AchievementBox(QWidget* parent, rc_client_achievement_t* achievement);
+ explicit AchievementBox(QWidget* parent, const rc_client_achievement_t* achievement);
void UpdateData();
void UpdateProgress();
@@ -28,7 +28,7 @@ private:
QProgressBar* m_progress_bar;
QLabel* m_progress_label;
- rc_client_achievement_t* m_achievement;
+ const rc_client_achievement_t* m_achievement;
};
#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
index 75152c09ff..6d3ed55ebd 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
+++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp
@@ -34,10 +34,6 @@ AchievementSettingsWidget::AchievementSettingsWidget(QWidget* parent) : QWidget(
connect(&Settings::Instance(), &Settings::ConfigChanged, this,
&AchievementSettingsWidget::LoadSettings);
-
- // If hardcore is enabled when the emulator starts, make sure it turns off what it needs to
- if (Config::Get(Config::RA_HARDCORE_ENABLED))
- UpdateHardcoreMode();
}
void AchievementSettingsWidget::UpdateData(int login_failed_code)
@@ -256,10 +252,9 @@ void AchievementSettingsWidget::ToggleRAIntegration()
auto& instance = AchievementManager::GetInstance();
if (Config::Get(Config::RA_ENABLED))
- instance.Init();
+ instance.Init(reinterpret_cast(winId()));
else
instance.Shutdown();
- UpdateHardcoreMode();
}
void AchievementSettingsWidget::Login()
@@ -297,7 +292,6 @@ void AchievementSettingsWidget::ToggleHardcore()
}
}
SaveSettings();
- UpdateHardcoreMode();
}
void AchievementSettingsWidget::ToggleUnofficial()
@@ -327,14 +321,4 @@ void AchievementSettingsWidget::ToggleProgress()
SaveSettings();
}
-void AchievementSettingsWidget::UpdateHardcoreMode()
-{
- if (Config::Get(Config::RA_HARDCORE_ENABLED))
- {
- Settings::Instance().SetDebugModeEnabled(false);
- }
- emit Settings::Instance().EmulationStateChanged(Core::GetState(Core::System::GetInstance()));
- emit Settings::Instance().HardcoreStateChanged();
-}
-
#endif // USE_RETRO_ACHIEVEMENTS
diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
index 92f360cac9..20a6d03c6d 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
+++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h
@@ -39,8 +39,6 @@ private:
void ToggleDiscordPresence();
void ToggleProgress();
- void UpdateHardcoreMode();
-
QGroupBox* m_common_box;
QVBoxLayout* m_common_layout;
ToolTipCheckBox* m_common_integration_enabled_input;
diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
index 6bb7dea6c2..c9a00a01ed 100644
--- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
+++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp
@@ -39,8 +39,6 @@ AchievementsWindow::AchievementsWindow(QWidget* parent) : QDialog(parent)
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
[this] { m_settings_widget->UpdateData(RC_OK); });
- connect(&Settings::Instance(), &Settings::HardcoreStateChanged, this,
- [this] { AchievementsWindow::UpdateData({.all = true}); });
}
void AchievementsWindow::showEvent(QShowEvent* event)
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index 38d481cdf9..b6e1659d4f 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -677,4 +677,7 @@ endif()
if(USE_RETRO_ACHIEVEMENTS)
target_link_libraries(dolphin-emu PRIVATE rcheevos)
target_compile_definitions(dolphin-emu PRIVATE -DUSE_RETRO_ACHIEVEMENTS)
+ if(RC_CLIENT_SUPPORTS_RAINTEGRATION)
+ target_compile_definitions(dolphin-emu PRIVATE -DRC_CLIENT_SUPPORTS_RAINTEGRATION)
+ endif()
endif()
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 6cb0a39a06..09f2681f94 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -43,6 +43,7 @@
#include "Core/BootManager.h"
#include "Core/CommonTitles.h"
#include "Core/Config/AchievementSettings.h"
+#include "Core/Config/FreeLookSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Config/NetplaySettings.h"
#include "Core/Config/UISettings.h"
@@ -272,9 +273,15 @@ MainWindow::MainWindow(Core::System& system, std::unique_ptr boo
NetPlayInit();
#ifdef USE_RETRO_ACHIEVEMENTS
- AchievementManager::GetInstance().Init();
+ AchievementManager::GetInstance().Init(reinterpret_cast(winId()));
if (AchievementManager::GetInstance().IsHardcoreModeActive())
Settings::Instance().SetDebugModeEnabled(false);
+ // This needs to trigger on both RA_HARDCORE_ENABLED and RA_ENABLED
+ Config::AddConfigChangedCallback(
+ [this]() { QueueOnObject(this, [this] { this->OnHardcoreChanged(); }); });
+ // If hardcore is enabled when the emulator starts, make sure it turns off what it needs to
+ if (Config::Get(Config::RA_HARDCORE_ENABLED))
+ OnHardcoreChanged();
#endif // USE_RETRO_ACHIEVEMENTS
#if defined(__unix__) || defined(__unix) || defined(__APPLE__)
@@ -935,7 +942,11 @@ bool MainWindow::RequestStop()
else
FullScreen();
- if (Config::Get(Config::MAIN_CONFIRM_ON_STOP))
+ bool confirm_on_stop = Config::Get(Config::MAIN_CONFIRM_ON_STOP);
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ confirm_on_stop = confirm_on_stop || AchievementManager::GetInstance().CheckForModifications();
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+ if (confirm_on_stop)
{
if (std::exchange(m_stop_confirm_showing, true))
return true;
@@ -960,13 +971,27 @@ bool MainWindow::RequestStop()
// This is to avoid any "race conditions" between the "Window Activate" message and the
// message box returning, which could break cursor locking depending on the order
m_render_widget->SetWaitingForMessageBox(true);
- auto confirm = ModalMessageBox::question(
- confirm_parent, tr("Confirm"),
- m_stop_requested ? tr("A shutdown is already in progress. Unsaved data "
- "may be lost if you stop the current emulation "
- "before it completes. Force stop?") :
- tr("Do you want to stop the current emulation?"),
- QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton, Qt::ApplicationModal);
+ QString message;
+ if (m_stop_requested)
+ {
+ message = tr("A shutdown is already in progress. Unsaved data "
+ "may be lost if you stop the current emulation "
+ "before it completes. Force stop?");
+ }
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ else if (AchievementManager::GetInstance().CheckForModifications())
+ {
+ message = tr(
+ "Do you want to stop the current emulation? Unsaved achievement modifications detected.");
+ }
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+ else
+ {
+ message = tr("Do you want to stop the current emulation?");
+ }
+ auto confirm = ModalMessageBox::question(confirm_parent, tr("Confirm"), message,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::NoButton, Qt::ApplicationModal);
// If a user confirmed stopping the emulation, we do not capture the cursor again,
// even if the render widget will stay alive for a while.
@@ -1992,6 +2017,13 @@ void MainWindow::ShowAchievementSettings()
ShowAchievementsWindow();
m_achievements_window->ForceSettingsTab();
}
+
+void MainWindow::OnHardcoreChanged()
+{
+ if (Config::Get(Config::RA_HARDCORE_ENABLED))
+ Settings::Instance().SetDebugModeEnabled(false);
+ emit Settings::Instance().EmulationStateChanged(Core::GetState(Core::System::GetInstance()));
+}
#endif // USE_RETRO_ACHIEVEMENTS
void MainWindow::ShowMemcardManager()
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index f9f0f1c95d..a43f98ead0 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -181,6 +181,7 @@ private:
#ifdef USE_RETRO_ACHIEVEMENTS
void ShowAchievementsWindow();
void ShowAchievementSettings();
+ void OnHardcoreChanged();
#endif // USE_RETRO_ACHIEVEMENTS
void NetPlayInit();
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index a4e2b17c0d..232ea1e337 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -64,6 +64,7 @@
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/ParallelProgressDialog.h"
+#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/Updater.h"
@@ -71,6 +72,10 @@
#include "UICommon/AutoUpdate.h"
#include "UICommon/GameFile.h"
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+#include
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
QPointer MenuBar::s_menu_bar;
QString MenuBar::GetSignatureSelector() const
@@ -284,8 +289,14 @@ void MenuBar::AddToolsMenu()
tools_menu->addSeparator();
#ifdef USE_RETRO_ACHIEVEMENTS
- tools_menu->addAction(tr("Achievements"), this, [this] { emit ShowAchievementsWindow(); });
-
+ m_achievements_action =
+ tools_menu->addAction(tr("Achievements"), this, [this] { emit ShowAchievementsWindow(); });
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ m_achievements_dev_menu = tools_menu->addMenu(tr("RetroAchievements Development"));
+ AchievementManager::GetInstance().SetDevMenuUpdateCallback(
+ [this]() { QueueOnObject(this, [this] { this->UpdateAchievementDevelopmentMenu(); }); });
+ m_achievements_dev_menu->menuAction()->setVisible(false);
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
tools_menu->addSeparator();
#endif // USE_RETRO_ACHIEVEMENTS
@@ -1124,6 +1135,38 @@ void MenuBar::UpdateToolsMenu(const Core::State state)
}
}
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+void MenuBar::UpdateAchievementDevelopmentMenu()
+{
+ auto* dev_menu = AchievementManager::GetInstance().GetDevelopmentMenu();
+ if (dev_menu)
+ {
+ m_achievements_dev_menu->menuAction()->setVisible(true);
+ m_achievements_dev_menu->clear();
+ for (u32 i = 0; i < dev_menu->num_items; i++)
+ {
+ const auto& menu_item = dev_menu->items[i];
+ if (menu_item.label == nullptr)
+ {
+ m_achievements_dev_menu->addSeparator();
+ continue;
+ }
+ auto* ra_dev_menu_item = m_achievements_dev_menu->addAction(
+ QString::fromStdString(menu_item.label), this,
+ [menu_item]() { AchievementManager::GetInstance().ActivateDevMenuItem(menu_item.id); });
+ ra_dev_menu_item->setEnabled(menu_item.enabled);
+ // Recommended hardcode by RAIntegration.dll developer Jamiras
+ ra_dev_menu_item->setCheckable(i < 2);
+ ra_dev_menu_item->setChecked(menu_item.checked);
+ }
+ }
+ else
+ {
+ m_achievements_dev_menu->menuAction()->setVisible(false);
+ }
+}
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
+
void MenuBar::InstallWAD()
{
QString wad_file = DolphinFileDialog::getOpenFileName(this, tr("Select Title to Install to NAND"),
diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h
index 29457c15f7..c3309a2f54 100644
--- a/Source/Core/DolphinQt/MenuBar.h
+++ b/Source/Core/DolphinQt/MenuBar.h
@@ -44,6 +44,9 @@ public:
explicit MenuBar(QWidget* parent = nullptr);
void UpdateToolsMenu(Core::State state);
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ void UpdateAchievementDevelopmentMenu();
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
QMenu* GetListColumnsMenu() const { return m_cols_menu; }
@@ -205,6 +208,10 @@ private:
QAction* m_wad_install_action;
QMenu* m_perform_online_update_menu;
QAction* m_perform_online_update_for_current_region;
+ QAction* m_achievements_action;
+#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
+ QMenu* m_achievements_dev_menu;
+#endif // RC_CLIENT_SUPPORTS_RAINTEGRATION
QAction* m_ntscj_ipl;
QAction* m_ntscu_ipl;
QAction* m_pal_ipl;
diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h
index 39337ceb5b..b6c50c9404 100644
--- a/Source/Core/DolphinQt/Settings.h
+++ b/Source/Core/DolphinQt/Settings.h
@@ -222,7 +222,6 @@ signals:
void SDCardInsertionChanged(bool inserted);
void USBKeyboardConnectionChanged(bool connected);
void EnableGfxModsChanged(bool enabled);
- void HardcoreStateChanged();
private:
Settings();
diff --git a/Source/VSProps/Base.Dolphin.props b/Source/VSProps/Base.Dolphin.props
index eb1d50a141..3410b9fef6 100644
--- a/Source/VSProps/Base.Dolphin.props
+++ b/Source/VSProps/Base.Dolphin.props
@@ -46,6 +46,7 @@
HAVE_SDL2;%(PreprocessorDefinitions)
USE_RETRO_ACHIEVEMENTS;%(PreprocessorDefinitions)
RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions)
+ RC_CLIENT_SUPPORTS_RAINTEGRATION;%(PreprocessorDefinitions)
HAVE_CUBEB;%(PreprocessorDefinitions)