diff --git a/Data/Sys/ApprovedInis.json b/Data/Sys/ApprovedInis.json index 2132bd62fe..c8e942b364 100644 --- a/Data/Sys/ApprovedInis.json +++ b/Data/Sys/ApprovedInis.json @@ -160,6 +160,10 @@ "title": "Gladius", "3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC": "Fix freeze in opening cutscene" }, + "GMSE01": { + "title": "Super Mario Sunshine", + "BD718F961DBA5372B1D0257D454D535746C453A0": "Widescreen" + }, "GNHE5d": { "title": "NHL HITZ 2002", "89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF": "Nop Hack" diff --git a/Data/Sys/GameSettings/GMSE01.ini b/Data/Sys/GameSettings/GMSE01.ini index 32eb737097..3b2dc268e2 100644 --- a/Data/Sys/GameSettings/GMSE01.ini +++ b/Data/Sys/GameSettings/GMSE01.ini @@ -175,3 +175,6 @@ C2363138 00000009 7C631670 54A5F0BE 7C630194 7C630214 60000000 00000000 + +[Gecko_RetroAchievements_Verified] +$Widescreen diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 73ace1af34..a40f4aaeb5 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -24,10 +24,12 @@ #include "Common/ScopeGuard.h" #include "Common/Version.h" #include "Common/WorkQueueThread.h" +#include "Core/ActionReplay.h" #include "Core/Config/AchievementSettings.h" #include "Core/Config/FreeLookSettings.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" +#include "Core/GeckoCode.h" #include "Core/HW/Memmap.h" #include "Core/HW/VideoInterface.h" #include "Core/PatchEngine.h" @@ -370,7 +372,6 @@ void AchievementManager::SetHardcoreMode() if (Config::Get(Config::MAIN_EMULATION_SPEED) < 1.0f) Config::SetBaseOrCurrent(Config::MAIN_EMULATION_SPEED, 1.0f); Config::SetBaseOrCurrent(Config::FREE_LOOK_ENABLED, false); - Config::SetBaseOrCurrent(Config::MAIN_ENABLE_CHEATS, false); } } @@ -384,10 +385,11 @@ bool AchievementManager::IsHardcoreModeActive() const return rc_client_is_processing_required(m_client); } -void AchievementManager::FilterApprovedPatches(std::vector& patches, - const std::string& game_ini_id) const +template +void AchievementManager::FilterApprovedIni(std::vector& codes, + const std::string& game_ini_id) const { - if (patches.empty()) + if (codes.empty()) { // There's nothing to verify, so let's save ourselves some work return; @@ -398,46 +400,120 @@ void AchievementManager::FilterApprovedPatches(std::vector& if (!IsHardcoreModeActive()) return; + // Approved codes list failed to hash + if (!m_ini_root->is()) + { + codes.clear(); + return; + } + + for (auto& code : codes) + { + if (code.enabled && !CheckApprovedCode(code, game_ini_id)) + code.enabled = false; + } +} + +template +bool AchievementManager::CheckApprovedCode(const T& code, const std::string& game_ini_id) const +{ + if (!IsHardcoreModeActive()) + return true; + + // Approved codes list failed to hash + if (!m_ini_root->is()) + return false; + const bool known_id = m_ini_root->contains(game_ini_id); - auto patch_itr = patches.begin(); - while (patch_itr != patches.end()) + INFO_LOG_FMT(ACHIEVEMENTS, "Verifying code {}", code.name); + + bool verified = false; + + if (known_id) { - INFO_LOG_FMT(ACHIEVEMENTS, "Verifying patch {}", patch_itr->name); + auto digest = GetCodeHash(code); - bool verified = false; - - if (known_id) - { - auto context = Common::SHA1::CreateContext(); - context->Update(Common::BitCastToArray(static_cast(patch_itr->entries.size()))); - for (const auto& entry : patch_itr->entries) - { - context->Update(Common::BitCastToArray(entry.type)); - context->Update(Common::BitCastToArray(entry.address)); - context->Update(Common::BitCastToArray(entry.value)); - context->Update(Common::BitCastToArray(entry.comparand)); - context->Update(Common::BitCastToArray(entry.conditional)); - } - auto digest = context->Finish(); - - verified = m_ini_root->get(game_ini_id).contains(Common::SHA1::DigestToString(digest)); - } - - if (!verified) - { - patch_itr = patches.erase(patch_itr); - OSD::AddMessage( - fmt::format("Failed to verify patch {} from file {}.", patch_itr->name, game_ini_id), - OSD::Duration::VERY_LONG, OSD::Color::RED); - OSD::AddMessage("Disable hardcore mode to enable this patch.", OSD::Duration::VERY_LONG, - OSD::Color::RED); - } - else - { - patch_itr++; - } + verified = m_ini_root->get(game_ini_id).contains(Common::SHA1::DigestToString(digest)); } + + if (!verified) + { + OSD::AddMessage(fmt::format("Failed to verify code {} from file {}.", code.name, game_ini_id), + OSD::Duration::VERY_LONG, OSD::Color::RED); + OSD::AddMessage("Disable hardcore mode to enable this code.", OSD::Duration::VERY_LONG, + OSD::Color::RED); + } + return verified; +} + +Common::SHA1::Digest AchievementManager::GetCodeHash(const PatchEngine::Patch& patch) const +{ + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(patch.entries.size()))); + for (const auto& entry : patch.entries) + { + context->Update(Common::BitCastToArray(entry.type)); + context->Update(Common::BitCastToArray(entry.address)); + context->Update(Common::BitCastToArray(entry.value)); + context->Update(Common::BitCastToArray(entry.comparand)); + context->Update(Common::BitCastToArray(entry.conditional)); + } + return context->Finish(); +} + +Common::SHA1::Digest AchievementManager::GetCodeHash(const Gecko::GeckoCode& code) const +{ + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.codes.size()))); + for (const auto& entry : code.codes) + { + context->Update(Common::BitCastToArray(entry.address)); + context->Update(Common::BitCastToArray(entry.data)); + } + return context->Finish(); +} + +Common::SHA1::Digest AchievementManager::GetCodeHash(const ActionReplay::ARCode& code) const +{ + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.ops.size()))); + for (const auto& entry : code.ops) + { + context->Update(Common::BitCastToArray(entry.cmd_addr)); + context->Update(Common::BitCastToArray(entry.value)); + } + return context->Finish(); +} + +void AchievementManager::FilterApprovedPatches(std::vector& patches, + const std::string& game_ini_id) const +{ + FilterApprovedIni(patches, game_ini_id); +} + +void AchievementManager::FilterApprovedGeckoCodes(std::vector& codes, + const std::string& game_ini_id) const +{ + FilterApprovedIni(codes, game_ini_id); +} + +void AchievementManager::FilterApprovedARCodes(std::vector& codes, + const std::string& game_ini_id) const +{ + FilterApprovedIni(codes, game_ini_id); +} + +bool AchievementManager::CheckApprovedGeckoCode(const Gecko::GeckoCode& code, + const std::string& game_ini_id) const +{ + return CheckApprovedCode(code, game_ini_id); +} + +bool AchievementManager::CheckApprovedARCode(const ActionReplay::ARCode& code, + const std::string& game_ini_id) const +{ + return CheckApprovedCode(code, game_ini_id); } void AchievementManager::SetSpectatorMode() diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 174b4abce1..9de18f1711 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -45,6 +45,16 @@ namespace PatchEngine struct Patch; } // namespace PatchEngine +namespace Gecko +{ +class GeckoCode; +} // namespace Gecko + +namespace ActionReplay +{ +struct ARCode; +} // namespace ActionReplay + class AchievementManager { public: @@ -70,8 +80,8 @@ public: static constexpr std::string_view BLUE = "#0B71C1"; static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json"; static const inline Common::SHA1::Digest APPROVED_LIST_HASH = { - 0xCC, 0xB4, 0x05, 0x2D, 0x2B, 0xEE, 0xF4, 0x06, 0x4A, 0xC9, - 0x57, 0x5D, 0xA9, 0xE9, 0xDE, 0xB7, 0x98, 0xF8, 0x1A, 0x6D}; + 0xA4, 0x98, 0x59, 0x23, 0x10, 0x56, 0x45, 0x30, 0xA9, 0xC5, + 0x68, 0x5A, 0xB6, 0x47, 0x67, 0xF8, 0xF0, 0x7D, 0x1D, 0x14}; struct LeaderboardEntry { @@ -125,8 +135,16 @@ public: void SetHardcoreMode(); bool IsHardcoreModeActive() const; void SetGameIniId(const std::string& game_ini_id) { m_game_ini_id = game_ini_id; } + void FilterApprovedPatches(std::vector& patches, const std::string& game_ini_id) const; + void FilterApprovedGeckoCodes(std::vector& codes, + const std::string& game_ini_id) const; + void FilterApprovedARCodes(std::vector& codes, + const std::string& game_ini_id) const; + bool CheckApprovedGeckoCode(const Gecko::GeckoCode& code, const std::string& game_ini_id) const; + bool CheckApprovedARCode(const ActionReplay::ARCode& code, const std::string& game_ini_id) const; + void SetSpectatorMode(); std::string_view GetPlayerDisplayName() const; u32 GetPlayerScore() const; @@ -181,6 +199,14 @@ private: void* userdata); void DisplayWelcomeMessage(); + template + void FilterApprovedIni(std::vector& codes, const std::string& game_ini_id) const; + template + bool CheckApprovedCode(const T& code, const std::string& game_ini_id) const; + Common::SHA1::Digest GetCodeHash(const PatchEngine::Patch& patch) const; + Common::SHA1::Digest GetCodeHash(const Gecko::GeckoCode& code) const; + Common::SHA1::Digest GetCodeHash(const ActionReplay::ARCode& code) const; + static void LeaderboardEntriesCallback(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* userdata); @@ -265,6 +291,18 @@ public: constexpr bool IsHardcoreModeActive() { return false; } + constexpr bool CheckApprovedGeckoCode(const Gecko::GeckoCode& code, + const std::string& game_ini_id) + { + return true; + }; + + constexpr bool CheckApprovedARCode(const ActionReplay::ARCode& code, + const std::string& game_ini_id) + { + return true; + }; + constexpr void LoadGame(const std::string&, const DiscIO::Volume*) {} constexpr void SetBackgroundExecutionAllowed(bool allowed) {} diff --git a/Source/Core/Core/ActionReplay.cpp b/Source/Core/Core/ActionReplay.cpp index 3052f06dec..d3171cdd25 100644 --- a/Source/Core/Core/ActionReplay.cpp +++ b/Source/Core/Core/ActionReplay.cpp @@ -39,6 +39,7 @@ #include "Common/MsgHandler.h" #include "Core/ARDecrypt.h" +#include "Core/AchievementManager.h" #include "Core/CheatCodes.h" #include "Core/Config/MainSettings.h" #include "Core/PowerPC/MMU.h" @@ -112,7 +113,7 @@ struct ARAddr // ---------------------- // AR Remote Functions -void ApplyCodes(std::span codes) +void ApplyCodes(std::span codes, const std::string& game_id) { if (!Config::AreCheatsEnabled()) return; @@ -121,7 +122,10 @@ void ApplyCodes(std::span codes) s_disable_logging = false; s_active_codes.clear(); std::copy_if(codes.begin(), codes.end(), std::back_inserter(s_active_codes), - [](const ARCode& code) { return code.enabled; }); + [&game_id](const ARCode& code) { + return code.enabled && + AchievementManager::GetInstance().CheckApprovedARCode(code, game_id); + }); s_active_codes.shrink_to_fit(); } @@ -169,9 +173,10 @@ void AddCode(ARCode code) } } -void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini) +void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini, + const std::string& game_id) { - ApplyCodes(LoadCodes(global_ini, local_ini)); + ApplyCodes(LoadCodes(global_ini, local_ini), game_id); } // Parses the Action Replay section of a game ini file. diff --git a/Source/Core/Core/ActionReplay.h b/Source/Core/Core/ActionReplay.h index ee2cb8b485..1cfbb0a1fe 100644 --- a/Source/Core/Core/ActionReplay.h +++ b/Source/Core/Core/ActionReplay.h @@ -45,12 +45,13 @@ struct ARCode void RunAllActive(const Core::CPUThreadGuard& cpu_guard); -void ApplyCodes(std::span codes); +void ApplyCodes(std::span codes, const std::string& game_id); void SetSyncedCodesAsActive(); void UpdateSyncedCodes(std::span codes); std::vector ApplyAndReturnCodes(std::span codes); void AddCode(ARCode new_code); -void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini); +void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini, + const std::string& game_id); std::vector LoadCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini); void SaveCodes(Common::IniFile* local_ini, std::span codes); diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index c9cd0333a9..0442347fdd 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -751,8 +751,7 @@ bool IsDefaultGCIFolderPathConfigured(ExpansionInterface::Slot slot) bool AreCheatsEnabled() { - return Config::Get(::Config::MAIN_ENABLE_CHEATS) && - !AchievementManager::GetInstance().IsHardcoreModeActive(); + return Config::Get(::Config::MAIN_ENABLE_CHEATS); } bool IsDebuggingEnabled() diff --git a/Source/Core/Core/GeckoCode.cpp b/Source/Core/Core/GeckoCode.cpp index 92f389545d..0d43c5a507 100644 --- a/Source/Core/Core/GeckoCode.cpp +++ b/Source/Core/Core/GeckoCode.cpp @@ -15,6 +15,7 @@ #include "Common/Config/Config.h" #include "Common/FileUtil.h" +#include "Core/AchievementManager.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/Host.h" @@ -49,7 +50,7 @@ static std::vector s_active_codes; static std::vector s_synced_codes; static std::mutex s_active_codes_lock; -void SetActiveCodes(std::span gcodes) +void SetActiveCodes(std::span gcodes, const std::string& game_id) { std::lock_guard lk(s_active_codes_lock); @@ -57,8 +58,12 @@ void SetActiveCodes(std::span gcodes) if (Config::AreCheatsEnabled()) { s_active_codes.reserve(gcodes.size()); + std::copy_if(gcodes.begin(), gcodes.end(), std::back_inserter(s_active_codes), - [](const GeckoCode& code) { return code.enabled; }); + [&game_id](const GeckoCode& code) { + return code.enabled && + AchievementManager::GetInstance().CheckApprovedGeckoCode(code, game_id); + }); } s_active_codes.shrink_to_fit(); diff --git a/Source/Core/Core/GeckoCode.h b/Source/Core/Core/GeckoCode.h index 3600d1b9ba..2931ed3641 100644 --- a/Source/Core/Core/GeckoCode.h +++ b/Source/Core/Core/GeckoCode.h @@ -60,7 +60,7 @@ constexpr u32 HLE_TRAMPOLINE_ADDRESS = INSTALLER_END_ADDRESS - 4; // preserve the emulation performance. constexpr u32 MAGIC_GAMEID = 0xD01F1BAD; -void SetActiveCodes(std::span gcodes); +void SetActiveCodes(std::span gcodes, const std::string& game_id); void SetSyncedCodesAsActive(); void UpdateSyncedCodes(std::span gcodes); std::vector SetAndReturnActiveCodes(std::span gcodes); diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index d4a55cf83e..ae88ba3d82 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -2070,13 +2070,18 @@ bool NetPlayServer::SyncCodes() } // Sync Gecko Codes { + std::vector codes = Gecko::LoadCodes(globalIni, localIni); + +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance().FilterApprovedGeckoCodes(codes, game_id); +#endif // USE_RETRO_ACHIEVEMENTS + // Create a Gecko Code Vector with just the active codes - std::vector s_active_codes = - Gecko::SetAndReturnActiveCodes(Gecko::LoadCodes(globalIni, localIni)); + std::vector active_codes = Gecko::SetAndReturnActiveCodes(codes); // Determine Codelist Size u16 codelines = 0; - for (const Gecko::GeckoCode& active_code : s_active_codes) + for (const Gecko::GeckoCode& active_code : active_codes) { INFO_LOG_FMT(NETPLAY, "Indexing {}", active_code.name); for (const Gecko::GeckoCode::Code& code : active_code.codes) @@ -2104,7 +2109,7 @@ bool NetPlayServer::SyncCodes() pac << MessageID::SyncCodes; pac << SyncCodeID::GeckoData; // Iterate through the active code vector and send each codeline - for (const Gecko::GeckoCode& active_code : s_active_codes) + for (const Gecko::GeckoCode& active_code : active_codes) { INFO_LOG_FMT(NETPLAY, "Sending {}", active_code.name); for (const Gecko::GeckoCode::Code& code : active_code.codes) @@ -2120,13 +2125,16 @@ bool NetPlayServer::SyncCodes() // Sync AR Codes { + std::vector codes = ActionReplay::LoadCodes(globalIni, localIni); +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance().FilterApprovedARCodes(codes, game_id); +#endif // USE_RETRO_ACHIEVEMENTS // Create an AR Code Vector with just the active codes - std::vector s_active_codes = - ActionReplay::ApplyAndReturnCodes(ActionReplay::LoadCodes(globalIni, localIni)); + std::vector active_codes = ActionReplay::ApplyAndReturnCodes(codes); // Determine Codelist Size u16 codelines = 0; - for (const ActionReplay::ARCode& active_code : s_active_codes) + for (const ActionReplay::ARCode& active_code : active_codes) { INFO_LOG_FMT(NETPLAY, "Indexing {}", active_code.name); for (const ActionReplay::AREntry& op : active_code.ops) @@ -2154,7 +2162,7 @@ bool NetPlayServer::SyncCodes() pac << MessageID::SyncCodes; pac << SyncCodeID::ARData; // Iterate through the active code vector and send each codeline - for (const ActionReplay::ARCode& active_code : s_active_codes) + for (const ActionReplay::ARCode& active_code : active_codes) { INFO_LOG_FMT(NETPLAY, "Sending {}", active_code.name); for (const ActionReplay::AREntry& op : active_code.ops) diff --git a/Source/Core/Core/PatchEngine.cpp b/Source/Core/Core/PatchEngine.cpp index 9880977710..e350f1e063 100644 --- a/Source/Core/Core/PatchEngine.cpp +++ b/Source/Core/Core/PatchEngine.cpp @@ -197,8 +197,8 @@ void LoadPatches() } else { - Gecko::SetActiveCodes(Gecko::LoadCodes(globalIni, localIni)); - ActionReplay::LoadAndApplyCodes(globalIni, localIni); + Gecko::SetActiveCodes(Gecko::LoadCodes(globalIni, localIni), sconfig.GetGameID()); + ActionReplay::LoadAndApplyCodes(globalIni, localIni, sconfig.GetGameID()); } } @@ -245,9 +245,6 @@ static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector memory_patch_indices) { - if (AchievementManager::GetInstance().IsHardcoreModeActive()) - return; - std::lock_guard lock(s_on_frame_memory_mutex); for (std::size_t index : memory_patch_indices) { @@ -335,7 +332,7 @@ bool ApplyFramePatches(Core::System& system) void Shutdown() { s_on_frame.clear(); - ActionReplay::ApplyCodes({}); + ActionReplay::ApplyCodes({}, ""); Gecko::Shutdown(); } diff --git a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp index e7d44b8fe6..5089e98806 100644 --- a/Source/Core/DolphinQt/Config/ARCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/ARCodeWidget.cpp @@ -115,7 +115,7 @@ void ARCodeWidget::OnItemChanged(QListWidgetItem* item) m_ar_codes[m_code_list->row(item)].enabled = (item->checkState() == Qt::Checked); if (!m_restart_required) - ActionReplay::ApplyCodes(m_ar_codes); + ActionReplay::ApplyCodes(m_ar_codes, m_game_id); UpdateList(); SaveCodes(); diff --git a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp index 3d2bd020e4..97a93051f5 100644 --- a/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp +++ b/Source/Core/DolphinQt/Config/GeckoCodeWidget.cpp @@ -202,7 +202,7 @@ void GeckoCodeWidget::OnItemChanged(QListWidgetItem* item) m_gecko_codes[index].enabled = (item->checkState() == Qt::Checked); if (!m_restart_required) - Gecko::SetActiveCodes(m_gecko_codes); + Gecko::SetActiveCodes(m_gecko_codes, m_game_id); SaveCodes(); } diff --git a/Source/Core/DolphinQt/Config/HardcoreWarningWidget.cpp b/Source/Core/DolphinQt/Config/HardcoreWarningWidget.cpp index eabec9a46e..257a43c366 100644 --- a/Source/Core/DolphinQt/Config/HardcoreWarningWidget.cpp +++ b/Source/Core/DolphinQt/Config/HardcoreWarningWidget.cpp @@ -35,7 +35,7 @@ void HardcoreWarningWidget::CreateWidgets() auto* icon = new QLabel; icon->setPixmap(warning_icon); - m_text = new QLabel(tr("This feature is disabled in hardcore mode.")); + m_text = new QLabel(tr("Only approved codes will be applied in hardcore mode.")); m_settings_button = new QPushButton(tr("Achievement Settings")); auto* layout = new QHBoxLayout; diff --git a/Source/Core/DolphinQt/Settings/GeneralPane.cpp b/Source/Core/DolphinQt/Settings/GeneralPane.cpp index 06715ceb65..0ab3b263a2 100644 --- a/Source/Core/DolphinQt/Settings/GeneralPane.cpp +++ b/Source/Core/DolphinQt/Settings/GeneralPane.cpp @@ -88,10 +88,9 @@ void GeneralPane::CreateLayout() void GeneralPane::OnEmulationStateChanged(Core::State state) { const bool running = state != Core::State::Uninitialized; - const bool hardcore = AchievementManager::GetInstance().IsHardcoreModeActive(); m_checkbox_dualcore->setEnabled(!running); - m_checkbox_cheats->setEnabled(!running && !hardcore); + m_checkbox_cheats->setEnabled(!running); m_checkbox_override_region_settings->setEnabled(!running); #ifdef USE_DISCORD_PRESENCE m_checkbox_discord_presence->setEnabled(!running); diff --git a/Source/UnitTests/Core/PatchAllowlistTest.cpp b/Source/UnitTests/Core/PatchAllowlistTest.cpp index 779b49791f..9d146a54fa 100644 --- a/Source/UnitTests/Core/PatchAllowlistTest.cpp +++ b/Source/UnitTests/Core/PatchAllowlistTest.cpp @@ -18,7 +18,10 @@ #include "Common/IOFile.h" #include "Common/IniFile.h" #include "Common/JsonUtil.h" +#include "Core/ActionReplay.h" #include "Core/CheatCodes.h" +#include "Core/GeckoCode.h" +#include "Core/GeckoCodeConfig.h" #include "Core/PatchEngine.h" struct GameHashes @@ -27,6 +30,11 @@ struct GameHashes std::map hashes; }; +using AllowList = std::map; + +void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash, + const std::string& patch_name); + TEST(PatchAllowlist, VerifyHashes) { // Load allowlist @@ -42,9 +50,9 @@ TEST(PatchAllowlist, VerifyHashes) const auto& list_filepath = fmt::format("{}{}{}", sys_directory, DIR_SEP, APPROVED_LIST_FILENAME); ASSERT_TRUE(JsonFromFile(list_filepath, &json_tree, &error)) << "Failed to open file at " << list_filepath; - // Parse allowlist - Map + // Parse allowlist - Map> ASSERT_TRUE(json_tree.is()); - std::map allow_list; + AllowList allow_list; for (const auto& entry : json_tree.get()) { ASSERT_TRUE(entry.second.is()); @@ -69,12 +77,25 @@ TEST(PatchAllowlist, VerifyHashes) std::string game_id = file.virtualName.substr(0, file.virtualName.find_first_of('.')); std::vector patches; PatchEngine::LoadPatchSection("OnFrame", &patches, ini_file, Common::IniFile()); + std::vector geckos = Gecko::LoadCodes(Common::IniFile(), ini_file); + std::vector action_replays = + ActionReplay::LoadCodes(Common::IniFile(), ini_file); // Filter patches for RetroAchievements approved - ReadEnabledOrDisabled(ini_file, "OnFrame", false, &patches); + for (auto& patch : patches) + patch.enabled = false; ReadEnabledOrDisabled(ini_file, "Patches_RetroAchievements_Verified", true, &patches); + for (auto& code : geckos) + code.enabled = false; + ReadEnabledOrDisabled(ini_file, "Gecko_RetroAchievements_Verified", true, + &geckos); + for (auto& code : action_replays) + code.enabled = false; + ReadEnabledOrDisabled(ini_file, "AR_RetroAchievements_Verified", true, + &action_replays); // Get game section from allow list auto game_itr = allow_list.find(game_id); + bool itr_end = (game_itr == allow_list.end()); // Iterate over approved patches for (const auto& patch : patches) { @@ -92,38 +113,51 @@ TEST(PatchAllowlist, VerifyHashes) context->Update(Common::BitCastToArray(entry.conditional)); } auto digest = context->Finish(); - std::string hash = Common::SHA1::DigestToString(digest); - // Check patch in list - if (game_itr == allow_list.end()) - { - // Report: no patches in game found in list - ADD_FAILURE() << "Approved hash missing from list." << std::endl - << "Game ID: " << game_id << std::endl - << "Patch: \"" << hash << "\" : \"" << patch.name << "\""; + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), patch.name); + } + // Iterate over approved geckos + for (const auto& code : geckos) + { + if (!code.enabled) continue; - } - auto hash_itr = game_itr->second.hashes.find(hash); - if (hash_itr == game_itr->second.hashes.end()) + // Hash patch + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.codes.size()))); + for (const auto& entry : code.codes) { - // Report: patch not found in list - ADD_FAILURE() << "Approved hash missing from list." << std::endl - << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl - << "Patch: \"" << hash << "\" : \"" << patch.name << "\""; + context->Update(Common::BitCastToArray(entry.address)); + context->Update(Common::BitCastToArray(entry.data)); } - else + auto digest = context->Finish(); + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), code.name); + } + // Iterate over approved AR codes + for (const auto& code : action_replays) + { + if (!code.enabled) + continue; + // Hash patch + auto context = Common::SHA1::CreateContext(); + context->Update(Common::BitCastToArray(static_cast(code.ops.size()))); + for (const auto& entry : code.ops) { - // Remove patch from map if found - game_itr->second.hashes.erase(hash_itr); + context->Update(Common::BitCastToArray(entry.cmd_addr)); + context->Update(Common::BitCastToArray(entry.value)); } + auto digest = context->Finish(); + CheckHash(game_id, itr_end ? nullptr : &game_itr->second, + Common::SHA1::DigestToString(digest), code.name); } // Report missing patches in map - if (game_itr == allow_list.end()) + if (itr_end) continue; for (auto& remaining_hashes : game_itr->second.hashes) { ADD_FAILURE() << "Hash in list not approved in ini." << std::endl << "Game ID: " << game_id << ":" << game_itr->second.game_title << std::endl - << "Patch: " << remaining_hashes.second << ":" << remaining_hashes.first; + << "Code: " << remaining_hashes.second << ":" << remaining_hashes.first; } // Remove section from map allow_list.erase(game_itr); @@ -136,3 +170,30 @@ TEST(PatchAllowlist, VerifyHashes) << remaining_games.second.game_title; } } + +void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash, + const std::string& patch_name) +{ + // Check patch in list + if (game_hashes == nullptr) + { + // Report: no patches in game found in list + ADD_FAILURE() << "Approved hash missing from list." << std::endl + << "Game ID: " << game_id << std::endl + << "Code: \"" << hash << "\": \"" << patch_name << "\""; + return; + } + auto hash_itr = game_hashes->hashes.find(hash); + if (hash_itr == game_hashes->hashes.end()) + { + // Report: patch not found in list + ADD_FAILURE() << "Approved hash missing from list." << std::endl + << "Game ID: " << game_id << ":" << game_hashes->game_title << std::endl + << "Code: \"" << hash << "\": \"" << patch_name << "\""; + } + else + { + // Remove patch from map if found + game_hashes->hashes.erase(hash_itr); + } +}