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)