// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cheats.h" #include "achievements.h" #include "bus.h" #include "controller.h" #include "cpu_core.h" #include "game_database.h" #include "host.h" #include "system.h" #include "util/imgui_manager.h" #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" #include "common/small_string.h" #include "common/string_util.h" #include "common/zip_helpers.h" #include "IconsEmoji.h" #include "IconsFontAwesome5.h" #include "fmt/format.h" LOG_CHANNEL(Cheats); namespace { class CheatFileReader { public: CheatFileReader(const std::string_view contents) : m_contents(contents) {} ALWAYS_INLINE size_t GetCurrentOffset() const { return m_current_offset; } ALWAYS_INLINE size_t GetCurrentLineOffset() const { return m_current_line_offset; } ALWAYS_INLINE u32 GetCurrentLineNumber() const { return m_current_line_number; } bool GetLine(std::string_view* line) { const size_t length = m_contents.length(); if (m_current_offset == length) { m_current_line_offset = m_current_offset; return false; } size_t end_position = m_current_offset; for (; end_position < length; end_position++) { // ignore carriage returns if (m_contents[end_position] == '\r') continue; if (m_contents[end_position] == '\n') break; } m_current_line_number++; m_current_line_offset = m_current_offset; *line = m_contents.substr(m_current_offset, end_position - m_current_offset); m_current_offset = std::min(end_position + 1, length); return true; } std::optional GetLine() { std::optional ret = std::string_view(); if (!GetLine(&ret.value())) ret.reset(); return ret; } template bool LogError(Error* error, bool stop_on_error, fmt::format_string fmt, T&&... args) { if (!stop_on_error) { Log::WriteFmtArgs(___LogChannel___, Log::Level::Warning, fmt, fmt::make_format_args(args...)); return true; } if (error) error->SetString(fmt::vformat(fmt, fmt::make_format_args(args...))); return false; } private: const std::string_view m_contents; size_t m_current_offset = 0; size_t m_current_line_offset = 0; u32 m_current_line_number = 0; }; class CheatArchive { public: ~CheatArchive() { // zip has to be destroyed before data m_zip.reset(); m_data.deallocate(); } ALWAYS_INLINE bool IsOpen() const { return static_cast(m_zip); } bool Open(const char* name) { if (m_zip) return true; Error error; std::optional> data = Host::ReadResourceFile(name, false, &error); if (!data.has_value()) { ERROR_LOG("Failed to read cheat archive {}: {}", name, error.GetDescription()); return false; } m_data = std::move(data.value()); m_zip = ZipHelpers::OpenManagedZipBuffer(m_data.data(), m_data.size(), 0, false, &error); if (!m_zip) [[unlikely]] { ERROR_LOG("Failed to open cheat archive {}: {}", name, error.GetDescription()); return false; } return true; } std::optional ReadFile(const char* name) const { Error error; std::optional ret = ZipHelpers::ReadFileInZipToString(m_zip.get(), name, true, &error); if (!ret.has_value()) DEV_LOG("Failed to read {} from zip: {}", name, error.GetDescription()); return ret; } private: // Maybe counter-intuitive, but it ends up faster for reading a single game's cheats if we keep a // copy of the archive in memory, as opposed to reading from disk. DynamicHeapArray m_data; ZipHelpers::ManagedZipT m_zip; }; } // namespace namespace Cheats { namespace { /// Represents a cheat code, after being parsed. class CheatCode { public: /// Additional metadata to a cheat code, present for all types. struct Metadata { std::string name; CodeType type = CodeType::Gameshark; CodeActivation activation = CodeActivation::EndFrame; std::optional override_cpu_overclock; std::optional override_aspect_ratio; bool has_options : 1; bool disable_widescreen_rendering : 1; bool disallow_for_achievements : 1; }; public: CheatCode(Metadata metadata); virtual ~CheatCode(); ALWAYS_INLINE const Metadata& GetMetadata() const { return m_metadata; } ALWAYS_INLINE const std::string& GetName() const { return m_metadata.name; } ALWAYS_INLINE CodeActivation GetActivation() const { return m_metadata.activation; } ALWAYS_INLINE bool IsManuallyActivated() const { return (m_metadata.activation == CodeActivation::Manual); } ALWAYS_INLINE bool HasOptions() const { return m_metadata.has_options; } void ApplySettingOverrides(); virtual void SetOptionValue(u32 value) = 0; virtual void Apply() const = 0; virtual void ApplyOnDisable() const = 0; protected: Metadata m_metadata; }; } // namespace using CheatCodeList = std::vector>; using ActiveCodeList = std::vector; using EnableCodeList = std::vector; static std::string GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard); static std::vector FindChtFilesOnDisk(const std::string_view serial, std::optional hash, bool cheats); static void ExtractCodeInfo(CodeInfoList* dst, const std::string& file_data, bool from_database); static void AppendCheatToList(CodeInfoList* dst, CodeInfo code); static std::string FormatCodeForFile(const CodeInfo& code); static bool ShouldLoadDatabaseCheats(); static bool AreAnyPatchesEnabled(); static void ReloadEnabledLists(); static u32 EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const char* section, bool hc_mode_active); static void UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed); template bool SearchCheatArchive(CheatArchive& archive, std::string_view serial, std::optional hash, const F& f); template static void EnumerateChtFiles(const std::string_view serial, std::optional hash, bool cheats, bool for_ui, bool load_from_disk, bool load_from_database, const F& f); static std::optional ParseOption(const std::string_view value); static bool ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end); extern void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); static Cheats::FileFormat DetectFileFormat(const std::string_view file_contents); static bool ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); static bool ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); static bool ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); static bool ImportOldChtFile(const std::string_view serial); static std::unique_ptr ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, Error* error); const char* PATCHES_CONFIG_SECTION = "Patches"; const char* CHEATS_CONFIG_SECTION = "Cheats"; const char* PATCH_ENABLE_CONFIG_KEY = "Enable"; static std::mutex s_zip_mutex; static CheatArchive s_patches_zip; static CheatArchive s_cheats_zip; static CheatCodeList s_patch_codes; static CheatCodeList s_cheat_codes; static EnableCodeList s_enabled_cheats; static EnableCodeList s_enabled_patches; static ActiveCodeList s_frame_end_codes; static u32 s_active_patch_count = 0; static u32 s_active_cheat_count = 0; static bool s_patches_enabled = false; static bool s_cheats_enabled = false; static bool s_database_cheat_codes_enabled = false; } // namespace Cheats Cheats::CheatCode::CheatCode(Metadata metadata) : m_metadata(std::move(metadata)) { } void Cheats::CheatCode::ApplySettingOverrides() { if (m_metadata.disable_widescreen_rendering && g_settings.gpu_widescreen_hack) { DEV_LOG("Disabling widescreen rendering from {} patch.", GetName()); g_settings.gpu_widescreen_hack = false; } if (m_metadata.override_aspect_ratio.has_value() && g_settings.display_aspect_ratio == DisplayAspectRatio::Auto) { DEV_LOG("Setting aspect ratio to {} from {} patch.", Settings::GetDisplayAspectRatioName(m_metadata.override_aspect_ratio.value()), GetName()); g_settings.display_aspect_ratio = m_metadata.override_aspect_ratio.value(); } if (m_metadata.override_cpu_overclock.has_value()) { DEV_LOG("Setting CPU overclock to {} from {} patch.", m_metadata.override_cpu_overclock.value(), GetName()); g_settings.SetCPUOverclockPercent(m_metadata.override_cpu_overclock.value()); g_settings.cpu_overclock_enable = true; g_settings.UpdateOverclockActive(); } } Cheats::CheatCode::~CheatCode() = default; static std::array s_cheat_code_type_names = {{"Gameshark"}}; static std::array s_cheat_code_type_display_names{{TRANSLATE_NOOP("Cheats", "Gameshark")}}; const char* Cheats::GetTypeName(CodeType type) { return s_cheat_code_type_names[static_cast(type)]; } const char* Cheats::GetTypeDisplayName(CodeType type) { return TRANSLATE("Cheats", s_cheat_code_type_display_names[static_cast(type)]); } std::optional Cheats::ParseTypeName(const std::string_view str) { for (size_t i = 0; i < s_cheat_code_type_names.size(); i++) { if (str == s_cheat_code_type_names[i]) return static_cast(i); } return std::nullopt; } static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; static std::array s_cheat_code_activation_display_names{ {TRANSLATE_NOOP("Cheats", "Manual"), TRANSLATE_NOOP("Cheats", "Automatic (Frame End)")}}; const char* Cheats::GetActivationName(CodeActivation activation) { return s_cheat_code_activation_names[static_cast(activation)]; } const char* Cheats::GetActivationDisplayName(CodeActivation activation) { return TRANSLATE("Cheats", s_cheat_code_activation_display_names[static_cast(activation)]); } std::optional Cheats::ParseActivationName(const std::string_view str) { for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) { if (str == s_cheat_code_activation_names[i]) return static_cast(i); } return std::nullopt; } std::string Cheats::GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard) { if (!hash.has_value()) return fmt::format("{}{}.cht", serial, add_wildcard ? "*" : ""); else return fmt::format("{}_{:016X}{}.cht", serial, hash.value(), add_wildcard ? "*" : ""); } std::vector Cheats::FindChtFilesOnDisk(const std::string_view serial, std::optional hash, bool cheats) { std::vector ret; FileSystem::FindResultsArray files; FileSystem::FindFiles(cheats ? EmuFolders::Cheats.c_str() : EmuFolders::Patches.c_str(), GetChtTemplate(serial, hash, true).c_str(), FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files); ret.reserve(files.size()); for (FILESYSTEM_FIND_DATA& fd : files) { // Skip mismatched hashes. if (hash.has_value()) { if (const std::string_view filename = Path::GetFileTitle(fd.FileName); filename.length() >= serial.length() + 18) { const std::string_view filename_hash = filename.substr(serial.length() + 1, 16); const std::optional filename_parsed_hash = StringUtil::FromChars(filename_hash, 16); if (filename_parsed_hash.has_value() && filename_parsed_hash.value() != hash.value()) continue; } } ret.push_back(std::move(fd.FileName)); } return ret; } template bool Cheats::SearchCheatArchive(CheatArchive& archive, std::string_view serial, std::optional hash, const F& f) { // Prefer filename with hash. std::string zip_filename = GetChtTemplate(serial, hash, false); std::optional data = archive.ReadFile(zip_filename.c_str()); if (!data.has_value() && hash.has_value()) { // Try without the hash. zip_filename = GetChtTemplate(serial, std::nullopt, false); data = archive.ReadFile(zip_filename.c_str()); } if (data.has_value()) { f(std::move(zip_filename), std::move(data.value()), true); return true; } return false; } template void Cheats::EnumerateChtFiles(const std::string_view serial, std::optional hash, bool cheats, bool for_ui, bool load_from_files, bool load_from_database, const F& f) { // Prefer files on disk over the zip, so we have to load the zip first. if (load_from_database) { const std::unique_lock lock(s_zip_mutex); CheatArchive& archive = cheats ? s_cheats_zip : s_patches_zip; if (!archive.IsOpen()) { const char* archive_name = cheats ? "cheats.zip" : "patches.zip"; archive.Open(archive_name); } if (archive.IsOpen()) { if (!SearchCheatArchive(archive, serial, hash, f)) { // Is this game part of a disc set? Try codes for the other discs. const GameDatabase::Entry* gentry = GameDatabase::GetEntryForSerial(serial); if (gentry && gentry->disc_set_serials.size() > 1) { for (const std::string& set_serial : gentry->disc_set_serials) { if (set_serial == serial) continue; else if (SearchCheatArchive(archive, set_serial, std::nullopt, f)) break; } } } } } if (load_from_files) { std::vector disk_patch_files; if (for_ui || !Achievements::IsHardcoreModeActive()) { disk_patch_files = FindChtFilesOnDisk(serial, for_ui ? hash : std::nullopt, cheats); if (cheats && disk_patch_files.empty()) { // Check if there's an old-format titled file. if (ImportOldChtFile(serial)) disk_patch_files = FindChtFilesOnDisk(serial, for_ui ? hash : std::nullopt, cheats); } } Error error; if (!disk_patch_files.empty()) { for (const std::string& file : disk_patch_files) { const std::optional contents = FileSystem::ReadFileToString(file.c_str(), &error); if (contents.has_value()) f(std::move(file), std::move(contents.value()), false); else WARNING_LOG("Failed to read cht file '{}': {}", Path::GetFileName(file), error.GetDescription()); } } } } std::string_view Cheats::CodeInfo::GetNamePart() const { const std::string::size_type pos = name.rfind('\\'); std::string_view ret = name; if (pos != std::string::npos) ret = ret.substr(pos + 1); return ret; } std::string_view Cheats::CodeInfo::GetNameParentPart() const { const std::string::size_type pos = name.rfind('\\'); std::string_view ret; if (pos != std::string::npos) ret = std::string_view(name).substr(0, pos); return ret; } std::string_view Cheats::CodeInfo::MapOptionValueToName(u32 value) const { std::string_view ret; if (!options.empty()) ret = options.front().first; for (const Cheats::CodeOption& opt : options) { if (opt.second == value) { ret = opt.first; break; } } return ret; } std::string_view Cheats::CodeInfo::MapOptionValueToName(const std::string_view value) const { const std::optional value_uint = StringUtil::FromChars(value); return MapOptionValueToName(value_uint.value_or(options.empty() ? 0 : options.front().second)); } u32 Cheats::CodeInfo::MapOptionNameToValue(const std::string_view opt_name) const { for (const Cheats::CodeOption& opt : options) { if (opt.first == opt_name) return opt.second; } return options.empty() ? 0 : options.front().second; } Cheats::CodeInfoList Cheats::GetCodeInfoList(const std::string_view serial, std::optional hash, bool cheats, bool load_from_database, bool sort_by_name) { CodeInfoList ret; EnumerateChtFiles(serial, hash, cheats, true, true, load_from_database, [&ret](const std::string& filename, const std::string& data, bool from_database) { ExtractCodeInfo(&ret, data, from_database); }); if (sort_by_name) std::sort(ret.begin(), ret.end(), [](const CodeInfo& lhs, const CodeInfo& rhs) { // ungrouped cheats go together first if (const int lhs_group = static_cast(lhs.name.find('\\') != std::string::npos), rhs_group = static_cast(rhs.name.find('\\') != std::string::npos); lhs_group != rhs_group) { return (lhs_group < rhs_group); } return lhs.name < rhs.name; }); return ret; } std::vector Cheats::GetCodeListUniquePrefixes(const CodeInfoList& list, bool include_empty) { std::vector ret; for (const Cheats::CodeInfo& code : list) { const std::string_view prefix = code.GetNameParentPart(); if (prefix.empty()) { if (include_empty && (ret.empty() || !ret.front().empty())) ret.insert(ret.begin(), std::string_view()); continue; } if (std::find(ret.begin(), ret.end(), prefix) == ret.end()) ret.push_back(prefix); } return ret; } const Cheats::CodeInfo* Cheats::FindCodeInInfoList(const CodeInfoList& list, const std::string_view name) { const auto it = std::find_if(list.cbegin(), list.cend(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); return (it != list.end()) ? &(*it) : nullptr; } Cheats::CodeInfo* Cheats::FindCodeInInfoList(CodeInfoList& list, const std::string_view name) { const auto it = std::find_if(list.begin(), list.end(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); return (it != list.end()) ? &(*it) : nullptr; } std::string Cheats::FormatCodeForFile(const CodeInfo& code) { fmt::memory_buffer buf; auto appender = std::back_inserter(buf); fmt::format_to(appender, "[{}]\n", code.name); if (!code.description.empty()) fmt::format_to(appender, "Description = {}\n", code.description); fmt::format_to(appender, "Type = {}\n", GetTypeName(code.type)); fmt::format_to(appender, "Activation = {}\n", GetActivationName(code.activation)); if (code.HasOptionChoices()) { for (const CodeOption& opt : code.options) fmt::format_to(appender, "Option = {}:{}\n", opt.first, opt.second); } else if (code.HasOptionRange()) { fmt::format_to(appender, "OptionRange = {}:{}\n", code.option_range_start, code.option_range_end); } fmt::format_to(appender, "{}\n", code.body); return std::string(buf.begin(), buf.end()); } bool Cheats::UpdateCodeInFile(const char* path, const std::string_view name, const CodeInfo* code, Error* error) { std::string file_contents; if (FileSystem::FileExists(path)) { std::optional ofile_contents = FileSystem::ReadFileToString(path, error); if (!ofile_contents.has_value()) { Error::AddPrefix(error, "Failed to read existing file: "); return false; } file_contents = std::move(ofile_contents.value()); } // This is a bit crap, we're allocating everything and then tossing it away. // Hopefully it won't fragment too much at least, because it's freed in reverse order... std::optional replace_start, replace_end; if (!file_contents.empty() && !name.empty()) { CodeInfoList existing_codes_in_file; ExtractCodeInfo(&existing_codes_in_file, file_contents, false); const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, name); if (existing_code) { replace_start = existing_code->file_offset_start; replace_end = existing_code->file_offset_end; } } if (replace_start.has_value()) { const auto start = file_contents.begin() + replace_start.value(); const auto end = file_contents.begin() + replace_end.value(); if (code) file_contents.replace(start, end, FormatCodeForFile(*code)); else file_contents.erase(start, end); } else if (code) { const std::string code_body = FormatCodeForFile(*code); file_contents.reserve(file_contents.length() + 1 + code_body.length()); if (!file_contents.empty() && file_contents.back() != '\n') file_contents.push_back('\n'); file_contents.append(code_body); } INFO_LOG("Updating {}...", path); if (!FileSystem::WriteStringToFile(path, file_contents, error)) { Error::AddPrefix(error, "Failed to rewrite file: "); return false; } return true; } bool Cheats::SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* error) { std::string file_contents; if (FileSystem::FileExists(path)) { std::optional ofile_contents = FileSystem::ReadFileToString(path, error); if (!ofile_contents.has_value()) { Error::AddPrefix(error, "Failed to read existing file: "); return false; } file_contents = std::move(ofile_contents.value()); } for (const CodeInfo& code : codes) { // This is _really_ crap.. but it's only on importing. std::optional replace_start, replace_end; if (!file_contents.empty()) { CodeInfoList existing_codes_in_file; ExtractCodeInfo(&existing_codes_in_file, file_contents, false); const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, code.name); if (existing_code) { replace_start = existing_code->file_offset_start; replace_end = existing_code->file_offset_end; } } if (replace_start.has_value()) { const auto start = file_contents.begin() + replace_start.value(); const auto end = file_contents.begin() + replace_end.value(); file_contents.replace(start, end, FormatCodeForFile(code)); } else { const std::string code_body = FormatCodeForFile(code); file_contents.reserve(file_contents.length() + 1 + code_body.length()); if (!file_contents.empty() && file_contents.back() != '\n') file_contents.push_back('\n'); file_contents.append(code_body); } } INFO_LOG("Updating {}...", path); if (!FileSystem::WriteStringToFile(path, file_contents, error)) { Error::AddPrefix(error, "Failed to rewrite file: "); return false; } return true; } void Cheats::MergeCheatList(CodeInfoList* dst, CodeInfoList src) { for (CodeInfo& code : src) { CodeInfo* existing_code = FindCodeInInfoList(*dst, code.name); if (existing_code) *existing_code = std::move(code); else dst->push_back(std::move(code)); } } std::string Cheats::GetChtFilename(const std::string_view serial, std::optional hash, bool cheats) { return Path::Combine(cheats ? EmuFolders::Cheats : EmuFolders::Patches, GetChtTemplate(serial, hash, false)); } bool Cheats::AreCheatsEnabled() { if (Achievements::IsHardcoreModeActive() || g_settings.disable_all_enhancements) return false; // Only in the gameini. const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); return (sif && sif->GetBoolValue("Cheats", "EnableCheats", false)); } bool Cheats::ShouldLoadDatabaseCheats() { // Only in the gameini. const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); return (sif && sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true)); } bool Cheats::AreAnyPatchesEnabled() { if (g_settings.disable_all_enhancements) return false; // Only in the gameini. const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); return (sif && sif->ContainsValue("Patches", "Enable")); } void Cheats::ReloadEnabledLists() { const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); if (!sif) { // no gameini => nothing is going to be enabled. s_enabled_cheats = {}; s_enabled_patches = {}; return; } if (AreCheatsEnabled()) s_enabled_cheats = sif->GetStringList(CHEATS_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); else s_enabled_cheats = {}; s_enabled_patches = sif->GetStringList(PATCHES_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); } u32 Cheats::EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const char* section, bool hc_mode_active) { u32 count = 0; for (const std::unique_ptr& p : patches) { // ignore manually-activated codes if (p->IsManuallyActivated()) continue; // don't load banned patches if (p->GetMetadata().disallow_for_achievements && hc_mode_active) continue; if (std::find(enable_list.begin(), enable_list.end(), p->GetName()) == enable_list.end()) continue; INFO_LOG("Enabled code from {}: {}", section, p->GetName()); switch (p->GetActivation()) { case CodeActivation::EndFrame: s_frame_end_codes.push_back(p.get()); break; default: break; } if (p->HasOptions()) { // need to extract the option from the ini SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); if (sif) [[likely]] { if (const std::optional value = sif->GetOptionalUIntValue(section, p->GetName().c_str(), std::nullopt)) { DEV_LOG("Setting {} option value to 0x{:X}", p->GetName(), value.value()); p->SetOptionValue(value.value()); } } } count++; } return count; } void Cheats::ReloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) { for (const CheatCode* code : s_frame_end_codes) code->ApplyOnDisable(); // Reload files if cheats or patches are enabled, and they were not previously. const bool patches_are_enabled = AreAnyPatchesEnabled(); const bool cheats_are_enabled = AreCheatsEnabled(); const bool cheatdb_is_enabled = cheats_are_enabled && ShouldLoadDatabaseCheats(); reload_files = reload_files || (s_patches_enabled != patches_are_enabled); reload_files = reload_files || (s_cheats_enabled != cheats_are_enabled); reload_files = reload_files || (s_database_cheat_codes_enabled != cheatdb_is_enabled); if (reload_files) { s_patch_codes.clear(); s_cheat_codes.clear(); if (const std::string& serial = System::GetGameSerial(); !serial.empty()) { const GameHash hash = System::GetGameHash(); s_patches_enabled = patches_are_enabled; if (patches_are_enabled) { EnumerateChtFiles(serial, hash, false, false, !Achievements::IsHardcoreModeActive(), true, [](const std::string& filename, const std::string& file_contents, bool from_database) { ParseFile(&s_patch_codes, file_contents); if (s_patch_codes.size() > 0) INFO_LOG("Found {} game patches in {}.", s_patch_codes.size(), filename); }); } s_cheats_enabled = cheats_are_enabled; s_database_cheat_codes_enabled = cheatdb_is_enabled; if (cheats_are_enabled) { EnumerateChtFiles(serial, hash, true, false, true, cheatdb_is_enabled, [](const std::string& filename, const std::string& file_contents, bool from_database) { ParseFile(&s_cheat_codes, file_contents); if (s_cheat_codes.size() > 0) INFO_LOG("Found {} cheats in {}.", s_cheat_codes.size(), filename); }); } } } UpdateActiveCodes(reload_enabled_list, verbose, verbose_if_changed); } void Cheats::UnloadAll() { s_active_cheat_count = 0; s_active_patch_count = 0; s_frame_end_codes = ActiveCodeList(); s_enabled_patches = EnableCodeList(); s_enabled_cheats = EnableCodeList(); s_cheat_codes = CheatCodeList(); s_patch_codes = CheatCodeList(); s_patches_enabled = false; s_cheats_enabled = false; s_database_cheat_codes_enabled = false; } void Cheats::ApplySettingOverrides() { // only need to check patches for this for (const std::string& name : s_enabled_patches) { for (std::unique_ptr& code : s_patch_codes) { if (name == code->GetName()) { code->ApplySettingOverrides(); break; } } } } void Cheats::UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed) { if (reload_enabled_list) ReloadEnabledLists(); const size_t prev_count = s_frame_end_codes.size(); s_frame_end_codes.clear(); s_active_patch_count = 0; s_active_cheat_count = 0; if (!g_settings.disable_all_enhancements) { const bool hc_mode_active = Achievements::IsHardcoreModeActive(); s_active_patch_count = EnableCheats(s_patch_codes, s_enabled_patches, "Patches", hc_mode_active); s_active_cheat_count = AreCheatsEnabled() ? EnableCheats(s_cheat_codes, s_enabled_cheats, "Cheats", hc_mode_active) : 0; } // Display message on first boot when we load patches. // Except when it's just GameDB. const size_t new_count = s_frame_end_codes.size(); if (verbose || (verbose_if_changed && prev_count != new_count)) { if (s_active_patch_count > 0) { System::SetTaint(System::Taint::Patches); Host::AddIconOSDMessage( "LoadPatches", ICON_FA_BAND_AID, TRANSLATE_PLURAL_STR("Cheats", "%n game patches are active.", "OSD Message", s_active_patch_count), Host::OSD_INFO_DURATION); } if (s_active_cheat_count > 0) { System::SetTaint(System::Taint::Cheats); Host::AddIconOSDMessage("LoadCheats", ICON_EMOJI_WARNING, TRANSLATE_PLURAL_STR("Cheats", "%n cheats are enabled. This may crash games.", "OSD Message", s_active_cheat_count), Host::OSD_WARNING_DURATION); } else if (s_active_patch_count == 0) { Host::RemoveKeyedOSDMessage("LoadPatches"); Host::AddIconOSDMessage("LoadCheats", ICON_FA_BAND_AID, TRANSLATE_STR("Cheats", "No cheats/patches are found or enabled."), Host::OSD_INFO_DURATION); } } } void Cheats::ApplyFrameEndCodes() { for (const CheatCode* code : s_frame_end_codes) code->Apply(); } bool Cheats::EnumerateManualCodes(std::function callback) { for (const std::unique_ptr& code : s_cheat_codes) { if (code->IsManuallyActivated()) { if (!callback(code->GetName())) return false; } } return true; } bool Cheats::ApplyManualCode(const std::string_view name) { for (const std::unique_ptr& code : s_cheat_codes) { if (code->IsManuallyActivated() && code->GetName() == name) { Host::AddIconOSDMessage(code->GetName(), ICON_FA_BAND_AID, fmt::format(TRANSLATE_FS("Cheats", "Cheat '{}' applied."), code->GetName()), Host::OSD_INFO_DURATION); code->Apply(); return true; } } return false; } u32 Cheats::GetActivePatchCount() { return s_active_patch_count; } u32 Cheats::GetActiveCheatCount() { return s_active_cheat_count; } ////////////////////////////////////////////////////////////////////////// // File Parsing ////////////////////////////////////////////////////////////////////////// void Cheats::ExtractCodeInfo(CodeInfoList* dst, const std::string& file_data, bool from_database) { CodeInfo current_code; std::optional legacy_group; std::optional legacy_type; std::optional legacy_activation; const auto finish_code = [&dst, &file_data, ¤t_code]() { if (current_code.file_offset_end > current_code.file_offset_body_start) { current_code.body = std::string_view(file_data).substr( current_code.file_offset_body_start, current_code.file_offset_end - current_code.file_offset_body_start); } AppendCheatToList(dst, std::move(current_code)); }; CheatFileReader reader(file_data); std::string_view line; while (reader.GetLine(&line)) { std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty()) continue; // legacy metadata parsing if (linev.starts_with("#group=")) { legacy_group = StringUtil::StripWhitespace(linev.substr(7)); continue; } else if (linev.starts_with("#type=")) { legacy_type = ParseTypeName(StringUtil::StripWhitespace(linev.substr(6))); if (!legacy_type.has_value()) [[unlikely]] WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } else if (linev.starts_with("#activation=")) { legacy_activation = ParseActivationName(StringUtil::StripWhitespace(linev.substr(12))); if (!legacy_activation.has_value()) [[unlikely]] WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } // skip comments if (linev[0] == '#' || linev[0] == ';') continue; if (linev.front() == '[') { if (linev.size() < 3 || linev.back() != ']') { WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } // new code. if (!current_code.name.empty()) { // overwrite existing codes with the same name. finish_code(); current_code = CodeInfo(); } const std::string_view name = linev.substr(1, linev.length() - 2); current_code.name = legacy_group.has_value() ? fmt::format("{}\\{}", legacy_group.value(), name) : std::string(name); current_code.type = legacy_type.value_or(CodeType::Gameshark); current_code.activation = legacy_activation.value_or(CodeActivation::EndFrame); current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); current_code.file_offset_end = current_code.file_offset_start; current_code.file_offset_body_start = current_code.file_offset_start; current_code.from_database = from_database; continue; } // strip comments off end of lines const std::string_view::size_type comment_pos = linev.find_last_of("#;"); if (comment_pos != std::string_view::npos) { linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); if (linev.empty()) continue; } // metadata? if (linev.find('=') != std::string_view::npos) { std::string_view key, value; if (!StringUtil::ParseAssignmentString(linev, &key, &value)) { WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } if (key == "Description") { current_code.description = value; } else if (key == "Author") { current_code.author = value; } else if (key == "Type") { const std::optional type = ParseTypeName(value); if (type.has_value()) [[unlikely]] current_code.type = type.value(); else WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); } else if (key == "Activation") { const std::optional activation = ParseActivationName(value); if (activation.has_value()) [[unlikely]] current_code.activation = activation.value(); else WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); } else if (key == "Option") { if (std::optional opt = ParseOption(value)) current_code.options.push_back(std::move(opt.value())); else WARNING_LOG("Invalid option declaration at line {}: {}", reader.GetCurrentLineNumber(), line); } else if (key == "OptionRange") { if (!ParseOptionRange(value, ¤t_code.option_range_start, ¤t_code.option_range_end)) WARNING_LOG("Invalid option range declaration at line {}: {}", reader.GetCurrentLineNumber(), line); } // ignore other keys when we're only grabbing info continue; } if (current_code.name.empty()) { WARNING_LOG("Code data specified without name at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } if (current_code.file_offset_body_start == current_code.file_offset_start) current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); // if it's a code line, update the ending point current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); } // last code. if (!current_code.name.empty()) finish_code(); } void Cheats::AppendCheatToList(CodeInfoList* dst, CodeInfo code) { const auto iter = std::find_if(dst->begin(), dst->end(), [&code](const CodeInfo& rhs) { return code.name == rhs.name; }); if (iter != dst->end()) *iter = std::move(code); else dst->push_back(std::move(code)); } void Cheats::ParseFile(CheatCodeList* dst_list, const std::string_view file_contents) { CheatFileReader reader(file_contents); std::string_view next_code_group; CheatCode::Metadata next_code_metadata; std::optional code_body_start; const auto finish_code = [&dst_list, &file_contents, &reader, &next_code_group, &next_code_metadata, &code_body_start]() { if (!code_body_start.has_value()) { WARNING_LOG("Empty cheat body at line {}", reader.GetCurrentLineNumber()); next_code_metadata = CheatCode::Metadata(); return; } const std::string_view code_body = file_contents.substr(code_body_start.value(), reader.GetCurrentLineOffset() - code_body_start.value()); std::unique_ptr code; if (next_code_metadata.type == CodeType::Gameshark) { Error error; code = ParseGamesharkCode(std::move(next_code_metadata), code_body, &error); if (!code) { WARNING_LOG("Failed to parse gameshark code ending on line {}: {}", reader.GetCurrentLineNumber(), error.GetDescription()); return; } } else { WARNING_LOG("Unknown code type ending at line {}", reader.GetCurrentLineNumber()); return; } next_code_group = {}; next_code_metadata = CheatCode::Metadata(); code_body_start.reset(); // overwrite existing codes with the same name. const auto iter = std::find_if(dst_list->begin(), dst_list->end(), [&code](const std::unique_ptr& rhs) { return code->GetName() == rhs->GetName(); }); if (iter != dst_list->end()) *iter = std::move(code); else dst_list->push_back(std::move(code)); }; std::string_view line; while (reader.GetLine(&line)) { std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty()) continue; // legacy metadata parsing if (linev.starts_with("#group=")) { next_code_group = StringUtil::StripWhitespace(linev.substr(7)); continue; } else if (linev.starts_with("#type=")) { const std::optional type = ParseTypeName(StringUtil::StripWhitespace(linev.substr(6))); if (!type.has_value()) WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.type = type.value(); continue; } else if (linev.starts_with("#activation=")) { const std::optional activation = ParseActivationName(StringUtil::StripWhitespace(linev.substr(12))); if (!activation.has_value()) WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.activation = activation.value(); continue; } // skip comments if (linev[0] == '#' || linev[0] == ';') continue; if (linev.front() == '[') { if (linev.size() < 3 || linev.back() != ']') { WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } if (!next_code_metadata.name.empty()) finish_code(); // new code. const std::string_view name = linev.substr(1, linev.length() - 2); next_code_metadata.name = next_code_group.empty() ? std::string(name) : fmt::format("{}\\{}", next_code_group, name); continue; } // strip comments off end of lines const std::string_view::size_type comment_pos = linev.find_last_of("#;"); if (comment_pos != std::string_view::npos) { linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); if (linev.empty()) continue; } // metadata? if (linev.find('=') != std::string_view::npos) { std::string_view key, value; if (!StringUtil::ParseAssignmentString(linev, &key, &value)) { WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); continue; } if (key == "Type") { const std::optional type = ParseTypeName(value); if (!type.has_value()) WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.type = type.value(); } else if (key == "Activation") { const std::optional activation = ParseActivationName(value); if (!activation.has_value()) WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.activation = activation.value(); } else if (key == "OverrideAspectRatio") { const std::optional aspect_ratio = Settings::ParseDisplayAspectRatio(TinyString(value).c_str()); if (!aspect_ratio.has_value()) WARNING_LOG("Unknown aspect ratio at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.override_aspect_ratio = aspect_ratio; } else if (key == "OverrideCPUOverclock") { const std::optional ocvalue = StringUtil::FromChars(value); if (!ocvalue.has_value() || ocvalue.value() == 0) WARNING_LOG("Invalid CPU overclock at line {}: {}", reader.GetCurrentLineNumber(), line); else next_code_metadata.override_cpu_overclock = ocvalue.value(); } else if (key == "DisableWidescreenRendering") { next_code_metadata.disable_widescreen_rendering = StringUtil::FromChars(value).value_or(false); } else if (key == "DisallowForAchievements") { next_code_metadata.disallow_for_achievements = StringUtil::FromChars(value).value_or(false); } else if (key == "Option" || key == "OptionRange") { // we don't care about the actual values, we load them from the config next_code_metadata.has_options = true; } else if (key == "Author" || key == "Description") { // ignored when loading } else { WARNING_LOG("Unknown parameter {} at line {}", key, reader.GetCurrentLineNumber()); } continue; } if (!code_body_start.has_value()) code_body_start = reader.GetCurrentLineOffset(); } finish_code(); } std::optional Cheats::ParseOption(const std::string_view value) { // Option = Value1:0x1 std::optional ret; if (const std::string_view::size_type pos = value.rfind(':'); pos != std::string_view::npos) { const std::string_view opt_name = StringUtil::StripWhitespace(value.substr(0, pos)); const std::optional opt_value = StringUtil::FromCharsWithOptionalBase(StringUtil::StripWhitespace(value.substr(pos + 1))); if (opt_value.has_value()) ret = CodeOption(opt_name, opt_value.value()); } return ret; } bool Cheats::ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end) { // OptionRange = 0:255 if (const std::string_view::size_type pos = value.rfind(':'); pos != std::string_view::npos) { const std::optional start = StringUtil::FromChars(StringUtil::StripWhitespace(value.substr(0, pos))); const std::optional end = StringUtil::FromChars(StringUtil::StripWhitespace(value.substr(pos + 1))); if (start.has_value() && end.has_value() && start.value() <= std::numeric_limits::max() && end.value() <= std::numeric_limits::max() && end.value() > start.value()) { *out_range_start = static_cast(start.value()); *out_range_end = static_cast(end.value()); return true; } } return false; } ////////////////////////////////////////////////////////////////////////// // File Importing ////////////////////////////////////////////////////////////////////////// bool Cheats::ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error) { if (codes.empty()) { Error::SetStringView(error, "Code list is empty."); return false; } auto fp = FileSystem::CreateAtomicRenamedFile(std::move(path), error); if (!fp) return false; for (const CodeInfo& code : codes) { std::string code_body = FormatCodeForFile(code); // ensure there's at least two newlines of space between each code const size_t newline_len = code_body.ends_with("\n\n") ? 0 : (code_body.ends_with("\n") ? 1 : 2); for (size_t i = 0; i < newline_len; i++) code_body.push_back('\n'); if (std::fwrite(code_body.data(), code_body.length(), 1, fp.get()) != 1) { Error::SetErrno(error, "fwrite() failed: ", errno); FileSystem::DiscardAtomicRenamedFile(fp); return false; } } return FileSystem::CommitAtomicRenamedFile(fp, error); } bool Cheats::ImportCodesFromString(CodeInfoList* dst, const std::string_view file_contents, FileFormat file_format, bool stop_on_error, Error* error) { if (file_format == FileFormat::Unknown) file_format = DetectFileFormat(file_contents); if (file_format == FileFormat::PCSX) { if (!ImportPCSXFile(dst, file_contents, stop_on_error, error)) return false; } else if (file_format == FileFormat::Libretro) { if (!ImportLibretroFile(dst, file_contents, stop_on_error, error)) return false; } else if (file_format == FileFormat::EPSXe) { if (!ImportEPSXeFile(dst, file_contents, stop_on_error, error)) return false; } else { Error::SetStringView(error, "Unknown file format."); return false; } if (dst->empty()) { Error::SetStringView(error, "No codes found in file."); return false; } return true; } Cheats::FileFormat Cheats::DetectFileFormat(const std::string_view file_contents) { CheatFileReader reader(file_contents); std::string_view line; while (reader.GetLine(&line)) { // skip comments/empty lines std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty() || linev[0] == ';' || linev[0] == '#') continue; if (linev.starts_with("cheats")) return FileFormat::Libretro; // pcsxr if we see brackets if (linev[0] == '[') return FileFormat::PCSX; // otherwise if it's a code, it's probably epsxe if (StringUtil::IsHexDigit(linev[0])) return FileFormat::EPSXe; } return FileFormat::Unknown; } bool Cheats::ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) { CheatFileReader reader(file_contents); CodeInfo current_code; const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { if (current_code.file_offset_end <= current_code.file_offset_body_start) { if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) return false; } current_code.body = std::string_view(file_contents) .substr(current_code.file_offset_body_start, current_code.file_offset_end - current_code.file_offset_body_start); AppendCheatToList(dst, std::move(current_code)); return true; }; std::string_view line; while (reader.GetLine(&line)) { std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty() || linev[0] == '#' || linev[0] == ';') continue; if (linev.front() == '[') { if (linev.size() < 3 || linev.back() != ']' || (linev[1] == '*' && linev.size() < 4)) { if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line)) { return false; } continue; } // new code. if (!current_code.name.empty() && !finish_code()) return false; current_code = CodeInfo(); current_code.name = (linev[1] == '*') ? linev.substr(2, linev.length() - 3) : linev.substr(1, linev.length() - 2); current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); current_code.file_offset_end = current_code.file_offset_start; current_code.file_offset_body_start = current_code.file_offset_start; current_code.type = CodeType::Gameshark; current_code.activation = CodeActivation::EndFrame; current_code.from_database = false; continue; } // strip comments off end of lines const std::string_view::size_type comment_pos = linev.find_last_of("#;"); if (comment_pos != std::string_view::npos) { linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); if (linev.empty()) continue; } if (current_code.name.empty()) { if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", reader.GetCurrentLineNumber(), line)) { return false; } continue; } if (current_code.file_offset_body_start == current_code.file_offset_start) current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); // if it's a code line, update the ending point current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); } // last code. if (!current_code.name.empty() && !finish_code()) return false; return true; } bool Cheats::ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) { std::vector> kvp; static constexpr auto FindKey = [](const std::vector>& kvp, const std::string_view search) -> const std::string_view* { for (const auto& it : kvp) { if (StringUtil::EqualNoCase(search, it.first)) return &it.second; } return nullptr; }; CheatFileReader reader(file_contents); std::string_view line; while (reader.GetLine(&line)) { const std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty()) continue; // skip comments if (linev[0] == '#' || linev[0] == ';') continue; std::string_view key, value; if (!StringUtil::ParseAssignmentString(linev, &key, &value)) { if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line)) return false; continue; } kvp.emplace_back(key, value); } if (kvp.empty()) { reader.LogError(error, stop_on_error, "No key/values found."); return false; } const std::string_view* num_cheats_value = FindKey(kvp, "cheats"); const u32 num_cheats = num_cheats_value ? StringUtil::FromChars(*num_cheats_value).value_or(0) : 0; if (num_cheats == 0) return false; for (u32 i = 0; i < num_cheats; i++) { const std::string_view* desc = FindKey(kvp, TinyString::from_format("cheat{}_desc", i)); const std::string_view* code = FindKey(kvp, TinyString::from_format("cheat{}_code", i)); if (!desc || desc->empty() || !code || code->empty()) { if (!reader.LogError(error, stop_on_error, "Missing desc/code for cheat {}", i)) return false; continue; } // Need to convert + to newlines. CodeInfo info; info.name = *desc; info.body = StringUtil::ReplaceAll(*code, '+', '\n'); info.file_offset_start = 0; info.file_offset_end = 0; info.file_offset_body_start = 0; info.type = CodeType::Gameshark; info.activation = CodeActivation::EndFrame; info.from_database = false; AppendCheatToList(dst, std::move(info)); } return true; } bool Cheats::ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) { CheatFileReader reader(file_contents); CodeInfo current_code; const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { if (current_code.file_offset_end <= current_code.file_offset_body_start) { if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) return false; } current_code.body = std::string_view(file_contents).substr(current_code.file_offset_body_start, current_code.file_offset_end); AppendCheatToList(dst, std::move(current_code)); return true; }; std::string_view line; while (reader.GetLine(&line)) { std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty() || linev[0] == ';') continue; if (linev.front() == '#') { if (linev.size() < 2) { if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line)) { return false; } continue; } if (!current_code.name.empty() && !finish_code()) return false; // new code. current_code = CodeInfo(); current_code.name = linev.substr(1); current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); current_code.file_offset_end = current_code.file_offset_start; current_code.file_offset_body_start = current_code.file_offset_start; current_code.type = CodeType::Gameshark; current_code.activation = CodeActivation::EndFrame; current_code.from_database = false; continue; } if (current_code.name.empty()) { if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", reader.GetCurrentLineNumber(), line)) { return false; } continue; } // if it's a code line, update the ending point current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); } // last code. if (!current_code.name.empty() && !finish_code()) return false; return true; } bool Cheats::ImportOldChtFile(const std::string_view serial) { const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(serial); if (!dbentry || dbentry->title.empty()) return false; const std::string old_path = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.cht", EmuFolders::Cheats, dbentry->title); if (!FileSystem::FileExists(old_path.c_str())) return false; Error error; std::optional old_data = FileSystem::ReadFileToString(old_path.c_str(), &error); if (!old_data.has_value()) { ERROR_LOG("Failed to open old cht file '{}' for importing: {}", Path::GetFileName(old_path), error.GetDescription()); return false; } CodeInfoList new_codes; if (!ImportCodesFromString(&new_codes, old_data.value(), FileFormat::Unknown, false, &error) || new_codes.empty()) { ERROR_LOG("Failed to import old cht file '{}': {}", Path::GetFileName(old_path), error.GetDescription()); return false; } const std::string new_path = GetChtFilename(serial, std::nullopt, true); if (!SaveCodesToFile(new_path.c_str(), new_codes, &error)) { ERROR_LOG("Failed to write new cht file '{}': {}", Path::GetFileName(new_path), error.GetDescription()); return false; } INFO_LOG("Imported {} codes from {}.", new_codes.size(), Path::GetFileName(old_path)); return true; } ////////////////////////////////////////////////////////////////////////// // Gameshark codes ////////////////////////////////////////////////////////////////////////// namespace Cheats { namespace { class GamesharkCheatCode final : public CheatCode { public: GamesharkCheatCode(Metadata metadata); ~GamesharkCheatCode() override; static std::unique_ptr Parse(Metadata metadata, const std::string_view data, Error* error); void SetOptionValue(u32 value) override; void Apply() const override; void ApplyOnDisable() const override; private: enum class InstructionCode : u8 { Nop = 0x00, ConstantWrite8 = 0x30, ConstantWrite16 = 0x80, ScratchpadWrite16 = 0x1F, Increment16 = 0x10, Decrement16 = 0x11, Increment8 = 0x20, Decrement8 = 0x21, DelayActivation = 0xC1, SkipIfNotEqual16 = 0xC0, SkipIfButtonsNotEqual = 0xD5, SkipIfButtonsEqual = 0xD6, CompareButtons = 0xD4, CompareEqual16 = 0xD0, CompareNotEqual16 = 0xD1, CompareLess16 = 0xD2, CompareGreater16 = 0xD3, CompareEqual8 = 0xE0, CompareNotEqual8 = 0xE1, CompareLess8 = 0xE2, CompareGreater8 = 0xE3, Slide = 0x50, MemoryCopy = 0xC2, ExtImprovedSlide = 0x53, // Extension opcodes, not present on original GameShark. ExtConstantWrite32 = 0x90, ExtScratchpadWrite32 = 0xA5, ExtCompareEqual32 = 0xA0, ExtCompareNotEqual32 = 0xA1, ExtCompareLess32 = 0xA2, ExtCompareGreater32 = 0xA3, ExtSkipIfNotEqual32 = 0xA4, ExtIncrement32 = 0x60, ExtDecrement32 = 0x61, ExtConstantWriteIfMatch16 = 0xA6, ExtConstantWriteIfMatchWithRestore16 = 0xA7, ExtConstantForceRange8 = 0xF0, ExtConstantForceRangeLimits16 = 0xF1, ExtConstantForceRangeRollRound16 = 0xF2, ExtConstantForceRange16 = 0xF3, ExtFindAndReplace = 0xF4, ExtConstantSwap16 = 0xF5, ExtConstantBitSet8 = 0x31, ExtConstantBitClear8 = 0x32, ExtConstantBitSet16 = 0x81, ExtConstantBitClear16 = 0x82, ExtConstantBitSet32 = 0x91, ExtConstantBitClear32 = 0x92, ExtBitCompareButtons = 0xD7, ExtSkipIfNotLess8 = 0xC3, ExtSkipIfNotGreater8 = 0xC4, ExtSkipIfNotLess16 = 0xC5, ExtSkipIfNotGreater16 = 0xC6, ExtMultiConditionals = 0xF6, ExtCheatRegisters = 0x51, ExtCheatRegistersCompare = 0x52, ExtCompareBitsSet8 = 0xE4, // Only used inside ExtMultiConditionals ExtCompareBitsClear8 = 0xE5, // Only used inside ExtMultiConditionals }; union Instruction { u64 bits; struct { u32 second; u32 first; }; BitField code; BitField address; BitField value32; BitField value16; BitField value8; }; std::vector instructions; std::vector> option_instruction_values; u32 GetNextNonConditionalInstruction(u32 index) const; static bool IsConditionalInstruction(InstructionCode code); }; } // namespace } // namespace Cheats Cheats::GamesharkCheatCode::GamesharkCheatCode(Metadata metadata) : CheatCode(std::move(metadata)) { } Cheats::GamesharkCheatCode::~GamesharkCheatCode() = default; static std::optional ParseHexOptionMask(const std::string_view str, u8* out_option_start, u8* out_option_count) { if (str.length() > 8) return std::nullopt; const u32 num_nibbles = static_cast(str.size()); std::array nibble_values; u32 option_nibble_start = 0; u32 option_nibble_count = 0; bool last_was_option = false; for (u32 i = 0; i < num_nibbles; i++) { if (str[i] == '?') { if (option_nibble_count == 0) { option_nibble_start = i; } else if (!last_was_option) { // ? must be consecutive return false; } option_nibble_count++; last_was_option = true; nibble_values[i] = '0'; } else if (StringUtil::IsHexDigit(str[i])) { last_was_option = false; nibble_values[i] = str[i]; } else { // not a valid hex digit return false; } } // use stringutil to decode it, it has zeros in the place const std::optional parsed = StringUtil::FromChars(std::string_view(nibble_values.data(), num_nibbles), 16); if (!parsed.has_value()) [[unlikely]] return std::nullopt; // LSB comes first, so reverse *out_option_start = static_cast((num_nibbles - option_nibble_start - option_nibble_count) * 4); *out_option_count = static_cast(option_nibble_count * 4); return parsed; } std::unique_ptr Cheats::GamesharkCheatCode::Parse(Metadata metadata, const std::string_view data, Error* error) { std::unique_ptr code = std::make_unique(std::move(metadata)); CheatFileReader reader(data); std::string_view line; while (reader.GetLine(&line)) { // skip comments/empty lines std::string_view linev = StringUtil::StripWhitespace(line); if (linev.empty() || !StringUtil::IsHexDigit(linev[0])) continue; std::string_view next; const std::optional first = StringUtil::FromChars(linev, 16, &next); if (!first.has_value()) { Error::SetStringFmt(error, "Malformed instruction at line {}: {}", reader.GetCurrentLineNumber(), linev); code.reset(); break; } size_t next_offset = 0; while (next_offset < next.size() && !StringUtil::IsHexDigit(next[next_offset])) next_offset++; next = (next_offset < next.size()) ? next.substr(next_offset) : std::string_view(); std::optional second; if (next.find('?') != std::string_view::npos) { u8 option_bitpos, option_bitcount; second = ParseHexOptionMask(next, &option_bitpos, &option_bitcount); if (second.has_value()) { code->option_instruction_values.emplace_back(static_cast(code->instructions.size()), option_bitpos, option_bitcount); } } else { second = StringUtil::FromChars(next, 16); } if (!second.has_value()) { Error::SetStringFmt(error, "Malformed instruction at line {}: {}", reader.GetCurrentLineNumber(), linev); code.reset(); break; } Instruction inst; inst.first = first.value(); inst.second = second.value(); code->instructions.push_back(inst); } if (code->instructions.empty()) { Error::SetStringFmt(error, "No instructions in code."); code.reset(); } return code; } static std::array cht_register; // Used for D7 ,51 & 52 cheat types template NEVER_INLINE static T DoMemoryRead(VirtualMemoryAddress address) { using UnsignedType = typename std::make_unsigned_t; static_assert(std::is_same_v || std::is_same_v || std::is_same_v); T result; if constexpr (std::is_same_v) return CPU::SafeReadMemoryByte(address, &result) ? result : static_cast(0); else if constexpr (std::is_same_v) return CPU::SafeReadMemoryHalfWord(address, &result) ? result : static_cast(0); else // if constexpr (std::is_same_v) return CPU::SafeReadMemoryWord(address, &result) ? result : static_cast(0); } template NEVER_INLINE static void DoMemoryWrite(PhysicalMemoryAddress address, T value) { using UnsignedType = typename std::make_unsigned_t; static_assert(std::is_same_v || std::is_same_v || std::is_same_v); if constexpr (std::is_same_v) CPU::SafeWriteMemoryByte(address, value); else if constexpr (std::is_same_v) CPU::SafeWriteMemoryHalfWord(address, value); else // if constexpr (std::is_same_v) CPU::SafeWriteMemoryWord(address, value); } NEVER_INLINE static u32 GetControllerButtonBits() { static constexpr std::array button_mapping = {{ 0x0100, // Select 0x0200, // L3 0x0400, // R3 0x0800, // Start 0x1000, // Up 0x2000, // Right 0x4000, // Down 0x8000, // Left 0x0001, // L2 0x0002, // R2 0x0004, // L1 0x0008, // R1 0x0010, // Triangle 0x0020, // Circle 0x0040, // Cross 0x0080, // Square }}; u32 bits = 0; for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Controller* controller = System::GetController(i); if (!controller) continue; bits |= controller->GetButtonStateBits(); } u32 translated_bits = 0; for (u32 i = 0, bit = 1; i < static_cast(button_mapping.size()); i++, bit <<= 1) { if (bits & bit) translated_bits |= button_mapping[i]; } return translated_bits; } NEVER_INLINE static u32 GetControllerAnalogBits() { // 0x010000 - Right Thumb Up // 0x020000 - Right Thumb Right // 0x040000 - Right Thumb Down // 0x080000 - Right Thumb Left // 0x100000 - Left Thumb Up // 0x200000 - Left Thumb Right // 0x400000 - Left Thumb Down // 0x800000 - Left Thumb Left u32 bits = 0; u8 l_ypos = 0; u8 l_xpos = 0; u8 r_ypos = 0; u8 r_xpos = 0; std::optional analog = 0; for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) { Controller* controller = System::GetController(i); if (!controller) continue; analog = controller->GetAnalogInputBytes(); if (analog.has_value()) { l_ypos = Truncate8((analog.value() & 0xFF000000u) >> 24); l_xpos = Truncate8((analog.value() & 0x00FF0000u) >> 16); r_ypos = Truncate8((analog.value() & 0x0000FF00u) >> 8); r_xpos = Truncate8(analog.value() & 0x000000FFu); if (l_ypos < 0x50) bits |= 0x100000; else if (l_ypos > 0xA0) bits |= 0x400000; if (l_xpos < 0x50) bits |= 0x800000; else if (l_xpos > 0xA0) bits |= 0x200000; if (r_ypos < 0x50) bits |= 0x10000; else if (r_ypos > 0xA0) bits |= 0x40000; if (r_xpos < 0x50) bits |= 0x80000; else if (r_xpos > 0xA0) bits |= 0x20000; } } return bits; } bool Cheats::GamesharkCheatCode::IsConditionalInstruction(InstructionCode code) { switch (code) { case InstructionCode::CompareEqual16: // D0 case InstructionCode::CompareNotEqual16: // D1 case InstructionCode::CompareLess16: // D2 case InstructionCode::CompareGreater16: // D3 case InstructionCode::CompareEqual8: // E0 case InstructionCode::CompareNotEqual8: // E1 case InstructionCode::CompareLess8: // E2 case InstructionCode::CompareGreater8: // E3 case InstructionCode::CompareButtons: // D4 case InstructionCode::ExtCompareEqual32: // A0 case InstructionCode::ExtCompareNotEqual32: // A1 case InstructionCode::ExtCompareLess32: // A2 case InstructionCode::ExtCompareGreater32: // A3 return true; default: return false; } } u32 Cheats::GamesharkCheatCode::GetNextNonConditionalInstruction(u32 index) const { const u32 count = static_cast(instructions.size()); for (; index < count; index++) { if (!IsConditionalInstruction(instructions[index].code)) { // we've found the first non conditional instruction in the chain, so skip over the instruction following it return index + 1; } } return index; } void Cheats::GamesharkCheatCode::Apply() const { const u32 count = static_cast(instructions.size()); u32 index = 0; for (; index < count;) { const Instruction& inst = instructions[index]; switch (inst.code) { case InstructionCode::Nop: { index++; } break; case InstructionCode::ConstantWrite8: { DoMemoryWrite(inst.address, inst.value8); index++; } break; case InstructionCode::ConstantWrite16: { DoMemoryWrite(inst.address, inst.value16); index++; } break; case InstructionCode::ExtConstantWrite32: { DoMemoryWrite(inst.address, inst.value32); index++; } break; case InstructionCode::ExtConstantBitSet8: { const u8 value = DoMemoryRead(inst.address) | inst.value8; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ExtConstantBitSet16: { const u16 value = DoMemoryRead(inst.address) | inst.value16; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ExtConstantBitSet32: { const u32 value = DoMemoryRead(inst.address) | inst.value32; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ExtConstantBitClear8: { const u8 value = DoMemoryRead(inst.address) & ~inst.value8; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ExtConstantBitClear16: { const u16 value = DoMemoryRead(inst.address) & ~inst.value16; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ExtConstantBitClear32: { const u32 value = DoMemoryRead(inst.address) & ~inst.value32; DoMemoryWrite(inst.address, value); index++; } break; case InstructionCode::ScratchpadWrite16: { DoMemoryWrite(CPU::SCRATCHPAD_ADDR | (inst.address & CPU::SCRATCHPAD_OFFSET_MASK), inst.value16); index++; } break; case InstructionCode::ExtScratchpadWrite32: { DoMemoryWrite(CPU::SCRATCHPAD_ADDR | (inst.address & CPU::SCRATCHPAD_OFFSET_MASK), inst.value32); index++; } break; case InstructionCode::ExtIncrement32: { const u32 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value + inst.value32); index++; } break; case InstructionCode::ExtDecrement32: { const u32 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value - inst.value32); index++; } break; case InstructionCode::Increment16: { const u16 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value + inst.value16); index++; } break; case InstructionCode::Decrement16: { const u16 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value - inst.value16); index++; } break; case InstructionCode::Increment8: { const u8 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value + inst.value8); index++; } break; case InstructionCode::Decrement8: { const u8 value = DoMemoryRead(inst.address); DoMemoryWrite(inst.address, value - inst.value8); index++; } break; case InstructionCode::ExtCompareEqual32: { const u32 value = DoMemoryRead(inst.address); if (value == inst.value32) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::ExtCompareNotEqual32: { const u32 value = DoMemoryRead(inst.address); if (value != inst.value32) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::ExtCompareLess32: { const u32 value = DoMemoryRead(inst.address); if (value < inst.value32) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::ExtCompareGreater32: { const u32 value = DoMemoryRead(inst.address); if (value > inst.value32) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::ExtConstantWriteIfMatch16: case InstructionCode::ExtConstantWriteIfMatchWithRestore16: { const u16 value = DoMemoryRead(inst.address); const u16 comparevalue = Truncate16(inst.value32 >> 16); const u16 newvalue = Truncate16(inst.value32 & 0xFFFFu); if (value == comparevalue) DoMemoryWrite(inst.address, newvalue); index++; } break; case InstructionCode::ExtConstantForceRange8: { const u8 value = DoMemoryRead(inst.address); const u8 min = Truncate8(inst.value32 & 0x000000FFu); const u8 max = Truncate8((inst.value32 & 0x0000FF00u) >> 8); const u8 overmin = Truncate8((inst.value32 & 0x00FF0000u) >> 16); const u8 overmax = Truncate8((inst.value32 & 0xFF000000u) >> 24); if ((value < min) || (value < min && min == 0x00u && max < 0xFEu)) DoMemoryWrite(inst.address, overmin); // also handles a min value of 0x00 else if (value > max) DoMemoryWrite(inst.address, overmax); index++; } break; case InstructionCode::ExtConstantForceRangeLimits16: { const u16 value = DoMemoryRead(inst.address); const u16 min = Truncate16(inst.value32 & 0x0000FFFFu); const u16 max = Truncate16((inst.value32 & 0xFFFF0000u) >> 16); if ((value < min) || (value < min && min == 0x0000u && max < 0xFFFEu)) DoMemoryWrite(inst.address, min); // also handles a min value of 0x0000 else if (value > max) DoMemoryWrite(inst.address, max); index++; } break; case InstructionCode::ExtConstantForceRangeRollRound16: { const u16 value = DoMemoryRead(inst.address); const u16 min = Truncate16(inst.value32 & 0x0000FFFFu); const u16 max = Truncate16((inst.value32 & 0xFFFF0000u) >> 16); if ((value < min) || (value < min && min == 0x0000u && max < 0xFFFEu)) DoMemoryWrite(inst.address, max); // also handles a min value of 0x0000 else if (value > max) DoMemoryWrite(inst.address, min); index++; } break; case InstructionCode::ExtConstantForceRange16: { const u16 min = Truncate16(inst.value32 & 0x0000FFFFu); const u16 max = Truncate16((inst.value32 & 0xFFFF0000u) >> 16); const u16 value = DoMemoryRead(inst.address); const Instruction& inst2 = instructions[index + 1]; const u16 overmin = Truncate16(inst2.value32 & 0x0000FFFFu); const u16 overmax = Truncate16((inst2.value32 & 0xFFFF0000u) >> 16); if ((value < min) || (value < min && min == 0x0000u && max < 0xFFFEu)) DoMemoryWrite(inst.address, overmin); // also handles a min value of 0x0000 else if (value > max) DoMemoryWrite(inst.address, overmax); index += 2; } break; case InstructionCode::ExtConstantSwap16: { const u16 value1 = Truncate16(inst.value32 & 0x0000FFFFu); const u16 value2 = Truncate16((inst.value32 & 0xFFFF0000u) >> 16); const u16 value = DoMemoryRead(inst.address); if (value == value1) DoMemoryWrite(inst.address, value2); else if (value == value2) DoMemoryWrite(inst.address, value1); index++; } break; case InstructionCode::ExtFindAndReplace: { if ((index + 4) >= instructions.size()) { ERROR_LOG("Incomplete find/replace instruction"); return; } const Instruction& inst2 = instructions[index + 1]; const Instruction& inst3 = instructions[index + 2]; const Instruction& inst4 = instructions[index + 3]; const Instruction& inst5 = instructions[index + 4]; const u32 offset = Truncate16(inst.value32 & 0x0000FFFFu) << 1; const u8 wildcard = Truncate8((inst.value32 & 0x00FF0000u) >> 16); const u32 minaddress = inst.address - offset; const u32 maxaddress = inst.address + offset; const u8 f1 = Truncate8((inst2.first & 0xFF000000u) >> 24); const u8 f2 = Truncate8((inst2.first & 0x00FF0000u) >> 16); const u8 f3 = Truncate8((inst2.first & 0x0000FF00u) >> 8); const u8 f4 = Truncate8(inst2.first & 0x000000FFu); const u8 f5 = Truncate8((inst2.value32 & 0xFF000000u) >> 24); const u8 f6 = Truncate8((inst2.value32 & 0x00FF0000u) >> 16); const u8 f7 = Truncate8((inst2.value32 & 0x0000FF00u) >> 8); const u8 f8 = Truncate8(inst2.value32 & 0x000000FFu); const u8 f9 = Truncate8((inst3.first & 0xFF000000u) >> 24); const u8 f10 = Truncate8((inst3.first & 0x00FF0000u) >> 16); const u8 f11 = Truncate8((inst3.first & 0x0000FF00u) >> 8); const u8 f12 = Truncate8(inst3.first & 0x000000FFu); const u8 f13 = Truncate8((inst3.value32 & 0xFF000000u) >> 24); const u8 f14 = Truncate8((inst3.value32 & 0x00FF0000u) >> 16); const u8 f15 = Truncate8((inst3.value32 & 0x0000FF00u) >> 8); const u8 f16 = Truncate8(inst3.value32 & 0x000000FFu); const u8 r1 = Truncate8((inst4.first & 0xFF000000u) >> 24); const u8 r2 = Truncate8((inst4.first & 0x00FF0000u) >> 16); const u8 r3 = Truncate8((inst4.first & 0x0000FF00u) >> 8); const u8 r4 = Truncate8(inst4.first & 0x000000FFu); const u8 r5 = Truncate8((inst4.value32 & 0xFF000000u) >> 24); const u8 r6 = Truncate8((inst4.value32 & 0x00FF0000u) >> 16); const u8 r7 = Truncate8((inst4.value32 & 0x0000FF00u) >> 8); const u8 r8 = Truncate8(inst4.value32 & 0x000000FFu); const u8 r9 = Truncate8((inst5.first & 0xFF000000u) >> 24); const u8 r10 = Truncate8((inst5.first & 0x00FF0000u) >> 16); const u8 r11 = Truncate8((inst5.first & 0x0000FF00u) >> 8); const u8 r12 = Truncate8(inst5.first & 0x000000FFu); const u8 r13 = Truncate8((inst5.value32 & 0xFF000000u) >> 24); const u8 r14 = Truncate8((inst5.value32 & 0x00FF0000u) >> 16); const u8 r15 = Truncate8((inst5.value32 & 0x0000FF00u) >> 8); const u8 r16 = Truncate8(inst5.value32 & 0x000000FFu); for (u32 address = minaddress; address <= maxaddress; address += 2) { if ((DoMemoryRead(address) == f1 || f1 == wildcard) && (DoMemoryRead(address + 1) == f2 || f2 == wildcard) && (DoMemoryRead(address + 2) == f3 || f3 == wildcard) && (DoMemoryRead(address + 3) == f4 || f4 == wildcard) && (DoMemoryRead(address + 4) == f5 || f5 == wildcard) && (DoMemoryRead(address + 5) == f6 || f6 == wildcard) && (DoMemoryRead(address + 6) == f7 || f7 == wildcard) && (DoMemoryRead(address + 7) == f8 || f8 == wildcard) && (DoMemoryRead(address + 8) == f9 || f9 == wildcard) && (DoMemoryRead(address + 9) == f10 || f10 == wildcard) && (DoMemoryRead(address + 10) == f11 || f11 == wildcard) && (DoMemoryRead(address + 11) == f12 || f12 == wildcard) && (DoMemoryRead(address + 12) == f13 || f13 == wildcard) && (DoMemoryRead(address + 13) == f14 || f14 == wildcard) && (DoMemoryRead(address + 14) == f15 || f15 == wildcard) && (DoMemoryRead(address + 15) == f16 || f16 == wildcard)) { if (r1 != wildcard) DoMemoryWrite(address, r1); if (r2 != wildcard) DoMemoryWrite(address + 1, r2); if (r3 != wildcard) DoMemoryWrite(address + 2, r3); if (r4 != wildcard) DoMemoryWrite(address + 3, r4); if (r5 != wildcard) DoMemoryWrite(address + 4, r5); if (r6 != wildcard) DoMemoryWrite(address + 5, r6); if (r7 != wildcard) DoMemoryWrite(address + 6, r7); if (r8 != wildcard) DoMemoryWrite(address + 7, r8); if (r9 != wildcard) DoMemoryWrite(address + 8, r9); if (r10 != wildcard) DoMemoryWrite(address + 9, r10); if (r11 != wildcard) DoMemoryWrite(address + 10, r11); if (r12 != wildcard) DoMemoryWrite(address + 11, r12); if (r13 != wildcard) DoMemoryWrite(address + 12, r13); if (r14 != wildcard) DoMemoryWrite(address + 13, r14); if (r15 != wildcard) DoMemoryWrite(address + 14, r15); if (r16 != wildcard) DoMemoryWrite(address + 15, r16); address = address + 16; } } index += 5; } break; case InstructionCode::CompareEqual16: { const u16 value = DoMemoryRead(inst.address); if (value == inst.value16) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareNotEqual16: { const u16 value = DoMemoryRead(inst.address); if (value != inst.value16) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareLess16: { const u16 value = DoMemoryRead(inst.address); if (value < inst.value16) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareGreater16: { const u16 value = DoMemoryRead(inst.address); if (value > inst.value16) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareEqual8: { const u8 value = DoMemoryRead(inst.address); if (value == inst.value8) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareNotEqual8: { const u8 value = DoMemoryRead(inst.address); if (value != inst.value8) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareLess8: { const u8 value = DoMemoryRead(inst.address); if (value < inst.value8) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareGreater8: { const u8 value = DoMemoryRead(inst.address); if (value > inst.value8) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::CompareButtons: // D4 { if (inst.value16 == GetControllerButtonBits()) index++; else index = GetNextNonConditionalInstruction(index); } break; case InstructionCode::ExtCheatRegisters: // 51 { const u32 poke_value = inst.value32; const u8 cht_reg_no1 = Truncate8(inst.address & 0xFFu); const u8 cht_reg_no2 = Truncate8((inst.address & 0xFF00u) >> 8); const u8 cht_reg_no3 = Truncate8(inst.value32 & 0xFFu); const u8 sub_type = Truncate8((inst.address & 0xFF0000u) >> 16); switch (sub_type) { case 0x00: // Write the u8 from cht_register[cht_reg_no1] to address DoMemoryWrite(inst.value32, Truncate8(cht_register[cht_reg_no1]) & 0xFFu); break; case 0x01: // Read the u8 from address to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(inst.value32); break; case 0x02: // Write the u8 from address field to the address stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate8(poke_value & 0xFFu)); break; case 0x03: // Write the u8 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] // and add the u8 from the address field to it cht_register[cht_reg_no1] = Truncate8(cht_register[cht_reg_no2] & 0xFFu) + Truncate8(poke_value & 0xFFu); break; case 0x04: // Write the u8 from the value stored in cht_register[cht_reg_no2] + poke_value to the address // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate8(cht_register[cht_reg_no2] & 0xFFu) + Truncate8(poke_value & 0xFFu)); break; case 0x05: // Write the u8 poke value to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = Truncate8(poke_value & 0xFFu); break; case 0x06: // Read the u8 value from the address (cht_register[cht_reg_no2] + poke_value) to // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; case 0x40: // Write the u16 from cht_register[cht_reg_no1] to address DoMemoryWrite(inst.value32, Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x41: // Read the u16 from address to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(inst.value32); break; case 0x42: // Write the u16 from address field to the address stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate16(poke_value & 0xFFFFu)); break; case 0x43: // Write the u16 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] // and add the u16 from the address field to it cht_register[cht_reg_no1] = Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) + Truncate16(poke_value & 0xFFFFu); break; case 0x44: // Write the u16 from the value stored in cht_register[cht_reg_no2] + poke_value to the address // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) + Truncate16(poke_value & 0xFFFFu)); break; case 0x45: // Write the u16 poke value to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = Truncate16(poke_value & 0xFFFFu); break; case 0x46: // Read the u16 value from the address (cht_register[cht_reg_no2] + poke_value) to // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; case 0x80: // Write the u32 from cht_register[cht_reg_no1] to address DoMemoryWrite(inst.value32, cht_register[cht_reg_no1]); break; case 0x81: // Read the u32 from address to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(inst.value32); break; case 0x82: // Write the u32 from address field to the address stored in cht_register[cht_reg_no] DoMemoryWrite(cht_register[cht_reg_no1], poke_value); break; case 0x83: // Write the u32 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] // and add the u32 from the address field to it cht_register[cht_reg_no1] = cht_register[cht_reg_no2] + poke_value; break; case 0x84: // Write the u32 from the value stored in cht_register[cht_reg_no2] + poke_value to the address // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], cht_register[cht_reg_no2] + poke_value); break; case 0x85: // Write the u32 poke value to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = poke_value; break; case 0x86: // Read the u32 value from the address (cht_register[cht_reg_no2] + poke_value) to // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; case 0xC0: // Reg3 = Reg2 + Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] + cht_register[cht_reg_no1]; break; case 0xC1: // Reg3 = Reg2 - Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] - cht_register[cht_reg_no1]; break; case 0xC2: // Reg3 = Reg2 * Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] * cht_register[cht_reg_no1]; break; case 0xC3: // Reg3 = Reg2 / Reg1 with DIV0 handling if (cht_register[cht_reg_no1] == 0) cht_register[cht_reg_no3] = 0; else cht_register[cht_reg_no3] = cht_register[cht_reg_no2] / cht_register[cht_reg_no1]; break; case 0xC4: // Reg3 = Reg2 % Reg1 (with DIV0 handling) if (cht_register[cht_reg_no1] == 0) cht_register[cht_reg_no3] = cht_register[cht_reg_no2]; else cht_register[cht_reg_no3] = cht_register[cht_reg_no2] % cht_register[cht_reg_no1]; break; case 0xC5: // Reg3 = Reg2 & Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] & cht_register[cht_reg_no1]; break; case 0xC6: // Reg3 = Reg2 | Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] | cht_register[cht_reg_no1]; break; case 0xC7: // Reg3 = Reg2 ^ Reg1 cht_register[cht_reg_no3] = cht_register[cht_reg_no2] ^ cht_register[cht_reg_no1]; break; case 0xC8: // Reg3 = ~Reg1 cht_register[cht_reg_no3] = ~cht_register[cht_reg_no1]; break; case 0xC9: // Reg3 = Reg1 << X cht_register[cht_reg_no3] = cht_register[cht_reg_no1] << cht_reg_no2; break; case 0xCA: // Reg3 = Reg1 >> X cht_register[cht_reg_no3] = cht_register[cht_reg_no1] >> cht_reg_no2; break; // Lots of options exist for expanding into this space default: break; } index++; } break; case InstructionCode::SkipIfNotEqual16: // C0 case InstructionCode::ExtSkipIfNotEqual32: // A4 case InstructionCode::SkipIfButtonsNotEqual: // D5 case InstructionCode::SkipIfButtonsEqual: // D6 case InstructionCode::ExtSkipIfNotLess8: // C3 case InstructionCode::ExtSkipIfNotGreater8: // C4 case InstructionCode::ExtSkipIfNotLess16: // C5 case InstructionCode::ExtSkipIfNotGreater16: // C6 case InstructionCode::ExtMultiConditionals: // F6 { index++; bool activate_codes; switch (inst.code) { case InstructionCode::SkipIfNotEqual16: // C0 activate_codes = (DoMemoryRead(inst.address) == inst.value16); break; case InstructionCode::ExtSkipIfNotEqual32: // A4 activate_codes = (DoMemoryRead(inst.address) == inst.value32); break; case InstructionCode::SkipIfButtonsNotEqual: // D5 activate_codes = (GetControllerButtonBits() == inst.value16); break; case InstructionCode::SkipIfButtonsEqual: // D6 activate_codes = (GetControllerButtonBits() != inst.value16); break; case InstructionCode::ExtSkipIfNotLess8: // C3 activate_codes = (DoMemoryRead(inst.address) < inst.value8); break; case InstructionCode::ExtSkipIfNotGreater8: // C4 activate_codes = (DoMemoryRead(inst.address) > inst.value8); break; case InstructionCode::ExtSkipIfNotLess16: // C5 activate_codes = (DoMemoryRead(inst.address) < inst.value16); break; case InstructionCode::ExtSkipIfNotGreater16: // C6 activate_codes = (DoMemoryRead(inst.address) > inst.value16); break; case InstructionCode::ExtMultiConditionals: // F6 { // Ensure any else if or else that are hit outside the if context are skipped if ((inst.value32 & 0xFFFFFF00u) != 0x1F000000) { activate_codes = false; break; } for (;;) { const u8 totalConds = Truncate8(instructions[index - 1].value32 & 0x000000FFu); const u8 conditionType = Truncate8(instructions[index - 1].address & 0x000000FFu); bool conditions_check; if (conditionType == 0x00 && totalConds > 0) // AND { conditions_check = true; for (int i = 1; totalConds >= i; index++, i++) { switch (instructions[index].code) { case InstructionCode::CompareEqual16: // D0 conditions_check &= (DoMemoryRead(instructions[index].address) == instructions[index].value16); break; case InstructionCode::CompareNotEqual16: // D1 conditions_check &= (DoMemoryRead(instructions[index].address) != instructions[index].value16); break; case InstructionCode::CompareLess16: // D2 conditions_check &= (DoMemoryRead(instructions[index].address) < instructions[index].value16); break; case InstructionCode::CompareGreater16: // D3 conditions_check &= (DoMemoryRead(instructions[index].address) > instructions[index].value16); break; case InstructionCode::CompareEqual8: // E0 conditions_check &= (DoMemoryRead(instructions[index].address) == instructions[index].value8); break; case InstructionCode::CompareNotEqual8: // E1 conditions_check &= (DoMemoryRead(instructions[index].address) != instructions[index].value8); break; case InstructionCode::CompareLess8: // E2 conditions_check &= (DoMemoryRead(instructions[index].address) < instructions[index].value8); break; case InstructionCode::CompareGreater8: // E3 conditions_check &= (DoMemoryRead(instructions[index].address) > instructions[index].value8); break; case InstructionCode::ExtCompareEqual32: // A0 conditions_check &= (DoMemoryRead(instructions[index].address) == instructions[index].value32); break; case InstructionCode::ExtCompareNotEqual32: // A1 conditions_check &= (DoMemoryRead(instructions[index].address) != instructions[index].value32); break; case InstructionCode::ExtCompareLess32: // A2 conditions_check &= (DoMemoryRead(instructions[index].address) < instructions[index].value32); break; case InstructionCode::ExtCompareGreater32: // A3 conditions_check &= (DoMemoryRead(instructions[index].address) > instructions[index].value32); break; case InstructionCode::ExtCompareBitsSet8: // E4 Internal to F6 conditions_check &= (instructions[index].value8 == (DoMemoryRead(instructions[index].address) & instructions[index].value8)); break; case InstructionCode::ExtCompareBitsClear8: // E5 Internal to F6 conditions_check &= ((DoMemoryRead(instructions[index].address) & instructions[index].value8) == 0); break; case InstructionCode::ExtBitCompareButtons: // D7 { const u32 frame_compare_value = instructions[index].address & 0xFFFFu; const u8 cht_reg_no = Truncate8((instructions[index].value32 & 0xFF000000u) >> 24); const bool bit_comparison_type = ((instructions[index].address & 0x100000u) >> 20); const u8 frame_comparison = Truncate8((instructions[index].address & 0xF0000u) >> 16); const u32 check_value = (instructions[index].value32 & 0xFFFFFFu); const u32 value1 = GetControllerButtonBits(); const u32 value2 = GetControllerAnalogBits(); u32 value = value1 | value2; if ((bit_comparison_type == false && check_value == (value & check_value)) // Check Bits are set || (bit_comparison_type == true && check_value != (value & check_value))) // Check Bits are clear { cht_register[cht_reg_no] += 1; switch (frame_comparison) { case 0x0: // No comparison on frame count, just do it conditions_check &= true; break; case 0x1: // Check if frame_compare_value == current count conditions_check &= (cht_register[cht_reg_no] == frame_compare_value); break; case 0x2: // Check if frame_compare_value < current count conditions_check &= (cht_register[cht_reg_no] < frame_compare_value); break; case 0x3: // Check if frame_compare_value > current count conditions_check &= (cht_register[cht_reg_no] > frame_compare_value); break; case 0x4: // Check if frame_compare_value != current count conditions_check &= (cht_register[cht_reg_no] != frame_compare_value); break; default: conditions_check &= false; break; } } else { cht_register[cht_reg_no] = 0; conditions_check &= false; } break; } default: ERROR_LOG("Incorrect conditional instruction (see chtdb.txt for supported instructions)"); return; } } } else if (conditionType == 0x01 && totalConds > 0) // OR { conditions_check = false; for (int i = 1; totalConds >= i; index++, i++) { switch (instructions[index].code) { case InstructionCode::CompareEqual16: // D0 conditions_check |= (DoMemoryRead(instructions[index].address) == instructions[index].value16); break; case InstructionCode::CompareNotEqual16: // D1 conditions_check |= (DoMemoryRead(instructions[index].address) != instructions[index].value16); break; case InstructionCode::CompareLess16: // D2 conditions_check |= (DoMemoryRead(instructions[index].address) < instructions[index].value16); break; case InstructionCode::CompareGreater16: // D3 conditions_check |= (DoMemoryRead(instructions[index].address) > instructions[index].value16); break; case InstructionCode::CompareEqual8: // E0 conditions_check |= (DoMemoryRead(instructions[index].address) == instructions[index].value8); break; case InstructionCode::CompareNotEqual8: // E1 conditions_check |= (DoMemoryRead(instructions[index].address) != instructions[index].value8); break; case InstructionCode::CompareLess8: // E2 conditions_check |= (DoMemoryRead(instructions[index].address) < instructions[index].value8); break; case InstructionCode::CompareGreater8: // E3 conditions_check |= (DoMemoryRead(instructions[index].address) > instructions[index].value8); break; case InstructionCode::ExtCompareEqual32: // A0 conditions_check |= (DoMemoryRead(instructions[index].address) == instructions[index].value32); break; case InstructionCode::ExtCompareNotEqual32: // A1 conditions_check |= (DoMemoryRead(instructions[index].address) != instructions[index].value32); break; case InstructionCode::ExtCompareLess32: // A2 conditions_check |= (DoMemoryRead(instructions[index].address) < instructions[index].value32); break; case InstructionCode::ExtCompareGreater32: // A3 conditions_check |= (DoMemoryRead(instructions[index].address) > instructions[index].value32); break; case InstructionCode::ExtCompareBitsSet8: // E4 Internal to F6 conditions_check |= (instructions[index].value8 == (DoMemoryRead(instructions[index].address) & instructions[index].value8)); break; case InstructionCode::ExtCompareBitsClear8: // E5 Internal to F6 conditions_check |= ((DoMemoryRead(instructions[index].address) & instructions[index].value8) == 0); break; case InstructionCode::ExtBitCompareButtons: // D7 { const u32 frame_compare_value = instructions[index].address & 0xFFFFu; const u8 cht_reg_no = Truncate8((instructions[index].value32 & 0xFF000000u) >> 24); const bool bit_comparison_type = ((instructions[index].address & 0x100000u) >> 20); const u8 frame_comparison = Truncate8((instructions[index].address & 0xF0000u) >> 16); const u32 check_value = (instructions[index].value32 & 0xFFFFFFu); const u32 value1 = GetControllerButtonBits(); const u32 value2 = GetControllerAnalogBits(); u32 value = value1 | value2; if ((bit_comparison_type == false && check_value == (value & check_value)) // Check Bits are set || (bit_comparison_type == true && check_value != (value & check_value))) // Check Bits are clear { cht_register[cht_reg_no] += 1; switch (frame_comparison) { case 0x0: // No comparison on frame count, just do it conditions_check |= true; break; case 0x1: // Check if frame_compare_value == current count conditions_check |= (cht_register[cht_reg_no] == frame_compare_value); break; case 0x2: // Check if frame_compare_value < current count conditions_check |= (cht_register[cht_reg_no] < frame_compare_value); break; case 0x3: // Check if frame_compare_value > current count conditions_check |= (cht_register[cht_reg_no] > frame_compare_value); break; case 0x4: // Check if frame_compare_value != current count conditions_check |= (cht_register[cht_reg_no] != frame_compare_value); break; default: conditions_check |= false; break; } } else { cht_register[cht_reg_no] = 0; conditions_check |= false; } break; } default: ERROR_LOG("Incorrect conditional instruction (see chtdb.txt for supported instructions)"); return; } } } else { ERROR_LOG("Incomplete multi conditional instruction"); return; } if (conditions_check == true) { activate_codes = true; break; } else { // parse through to 00000000 FFFF and peek if next line is a F6 type associated with a ELSE activate_codes = false; // skip to the next separator (00000000 FFFF), or end constexpr u64 separator_value = UINT64_C(0x000000000000FFFF); constexpr u64 else_value = UINT64_C(0x00000000E15E0000); constexpr u64 elseif_value = UINT64_C(0x00000000E15E1F00); while (index < count) { const u64 bits = instructions[index++].bits; if (bits == separator_value) { const u64 bits_ahead = instructions[index].bits; if ((bits_ahead & 0xFFFFFF00u) == elseif_value) { break; } if ((bits_ahead & 0xFFFF0000u) == else_value) { // index++; activate_codes = true; break; } index--; break; } if ((bits & 0xFFFFFF00u) == elseif_value) { // index--; break; } if ((bits & 0xFFFFFFFFu) == else_value) { // index++; activate_codes = true; break; } } if (activate_codes == true) break; } } break; } default: activate_codes = false; break; } if (activate_codes) { // execute following instructions continue; } // skip to the next separator (00000000 FFFF), or end constexpr u64 separator_value = UINT64_C(0x000000000000FFFF); while (index < count) { // we don't want to execute the separator instruction const u64 bits = instructions[index++].bits; if (bits == separator_value) break; } } break; case InstructionCode::ExtBitCompareButtons: // D7 { index++; bool activate_codes; const u32 frame_compare_value = inst.address & 0xFFFFu; const u8 cht_reg_no = Truncate8((inst.value32 & 0xFF000000u) >> 24); const bool bit_comparison_type = ((inst.address & 0x100000u) >> 20); const u8 frame_comparison = Truncate8((inst.address & 0xF0000u) >> 16); const u32 check_value = (inst.value32 & 0xFFFFFFu); const u32 value1 = GetControllerButtonBits(); const u32 value2 = GetControllerAnalogBits(); u32 value = value1 | value2; if ((bit_comparison_type == false && check_value == (value & check_value)) // Check Bits are set || (bit_comparison_type == true && check_value != (value & check_value))) // Check Bits are clear { cht_register[cht_reg_no] += 1; switch (frame_comparison) { case 0x0: // No comparison on frame count, just do it activate_codes = true; break; case 0x1: // Check if frame_compare_value == current count activate_codes = (cht_register[cht_reg_no] == frame_compare_value); break; case 0x2: // Check if frame_compare_value < current count activate_codes = (cht_register[cht_reg_no] < frame_compare_value); break; case 0x3: // Check if frame_compare_value > current count activate_codes = (cht_register[cht_reg_no] > frame_compare_value); break; case 0x4: // Check if frame_compare_value != current count activate_codes = (cht_register[cht_reg_no] != frame_compare_value); break; default: activate_codes = false; break; } } else { cht_register[cht_reg_no] = 0; activate_codes = false; } if (activate_codes) { // execute following instructions continue; } // skip to the next separator (00000000 FFFF), or end constexpr u64 separator_value = UINT64_C(0x000000000000FFFF); while (index < count) { // we don't want to execute the separator instruction const u64 bits = instructions[index++].bits; if (bits == separator_value) break; } } break; case InstructionCode::ExtCheatRegistersCompare: // 52 { index++; bool activate_codes = false; const u8 cht_reg_no1 = Truncate8(inst.address & 0xFFu); const u8 cht_reg_no2 = Truncate8((inst.address & 0xFF00u) >> 8); const u8 sub_type = Truncate8((inst.first & 0xFF0000u) >> 16); switch (sub_type) { case 0x00: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) == Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x01: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) != Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x02: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) > Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x03: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) >= Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x04: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) < Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x05: activate_codes = (Truncate8(cht_register[cht_reg_no2] & 0xFFu) <= Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x06: activate_codes = ((Truncate8(cht_register[cht_reg_no2] & 0xFFu) & Truncate8(cht_register[cht_reg_no1] & 0xFFu)) == (Truncate8(cht_register[cht_reg_no1] & 0xFFu))); break; case 0x07: activate_codes = ((Truncate8(cht_register[cht_reg_no2] & 0xFFu) & Truncate8(cht_register[cht_reg_no1] & 0xFFu)) != (Truncate8(cht_register[cht_reg_no1] & 0xFFu))); break; case 0x0A: activate_codes = ((Truncate8(cht_register[cht_reg_no2] & 0xFFu) & Truncate8(cht_register[cht_reg_no1] & 0xFFu)) == (Truncate8(cht_register[cht_reg_no2] & 0xFFu))); break; case 0x0B: activate_codes = ((Truncate8(cht_register[cht_reg_no2] & 0xFFu) & Truncate8(cht_register[cht_reg_no1] & 0xFFu)) != (Truncate8(cht_register[cht_reg_no2] & 0xFFu))); break; case 0x10: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) == inst.value8); break; case 0x11: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) != inst.value8); break; case 0x12: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) > inst.value8); break; case 0x13: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) >= inst.value8); break; case 0x14: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) < inst.value8); break; case 0x15: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) <= inst.value8); break; case 0x16: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & inst.value8) == inst.value8); break; case 0x17: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & inst.value8) != inst.value8); break; case 0x18: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) > inst.value8) && (Truncate8(cht_register[cht_reg_no1] & 0xFFu) < Truncate8((inst.value32 & 0xFF0000u) >> 16))); break; case 0x19: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) >= inst.value8) && (Truncate8(cht_register[cht_reg_no1] & 0xFFu) <= Truncate8((inst.value32 & 0xFF0000u) >> 16))); break; case 0x1A: activate_codes = ((Truncate8(cht_register[cht_reg_no2] & 0xFFu) & inst.value8) == Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x1B: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & inst.value8) != Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x20: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x21: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x22: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) > DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x23: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) >= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x24: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) < DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x25: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) <= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x26: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value8) == inst.value8); break; case 0x27: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value8) != inst.value8); break; case 0x28: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) > inst.value8) && (DoMemoryRead(cht_register[cht_reg_no1]) < Truncate8((inst.value32 & 0xFF0000u) >> 16))); break; case 0x29: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) >= inst.value8) && (DoMemoryRead(cht_register[cht_reg_no1]) <= Truncate8((inst.value32 & 0xFF0000u) >> 16))); break; case 0x2A: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value8) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x2B: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value8) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x30: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) == DoMemoryRead(inst.value32)); break; case 0x31: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) != DoMemoryRead(inst.value32)); break; case 0x32: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) > DoMemoryRead(inst.value32)); break; case 0x33: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) >= DoMemoryRead(inst.value32)); break; case 0x34: activate_codes = (Truncate8(cht_register[cht_reg_no1] & 0xFFu) < DoMemoryRead(inst.value32)); break; case 0x36: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & DoMemoryRead(inst.value32)) == DoMemoryRead(inst.value32)); break; case 0x37: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & DoMemoryRead(inst.value32)) != DoMemoryRead(inst.value32)); break; case 0x3A: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & DoMemoryRead(inst.value32)) == Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x3B: activate_codes = ((Truncate8(cht_register[cht_reg_no1] & 0xFFu) & DoMemoryRead(inst.value32)) != Truncate8(cht_register[cht_reg_no1] & 0xFFu)); break; case 0x40: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) == Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x41: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) != Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x42: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) > Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x43: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) >= Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x44: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) < Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x45: activate_codes = (Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) <= Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x46: activate_codes = ((Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) & Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)) == Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x47: activate_codes = ((Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) & Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)) != Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x4A: activate_codes = ((Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) & Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)) == Truncate16(cht_register[cht_reg_no2] & 0xFFFFu)); break; case 0x4B: activate_codes = ((Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) & Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)) != Truncate16(cht_register[cht_reg_no2] & 0xFFFFu)); break; case 0x50: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) == inst.value16); break; case 0x51: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) != inst.value16); break; case 0x52: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) > inst.value16); break; case 0x53: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) >= inst.value16); break; case 0x54: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) < inst.value16); break; case 0x55: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) <= inst.value16); break; case 0x56: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & inst.value16) == inst.value16); break; case 0x57: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & inst.value16) != inst.value16); break; case 0x58: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) > inst.value16) && (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) < Truncate16((inst.value32 & 0xFFFF0000u) >> 16))); break; case 0x59: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) >= inst.value16) && (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) <= Truncate16((inst.value32 & 0xFFFF0000u) >> 16))); break; case 0x5A: activate_codes = ((Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) & inst.value16) == Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x5B: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & inst.value16) != Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x60: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x61: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x62: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) > DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x63: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) >= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x64: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) < DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x65: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) <= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x66: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value16) == inst.value16); break; case 0x67: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value16) != inst.value16); break; case 0x68: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) > inst.value16) && (DoMemoryRead(cht_register[cht_reg_no1]) < Truncate16((inst.value32 & 0xFFFF0000u) >> 16))); break; case 0x69: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) >= inst.value16) && (DoMemoryRead(cht_register[cht_reg_no1]) <= Truncate16((inst.value32 & 0xFFFF0000u) >> 16))); break; case 0x6A: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value16) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x6B: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value16) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0x70: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) == DoMemoryRead(inst.value32)); break; case 0x71: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) != DoMemoryRead(inst.value32)); break; case 0x72: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) > DoMemoryRead(inst.value32)); break; case 0x73: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) >= DoMemoryRead(inst.value32)); break; case 0x74: activate_codes = (Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) < DoMemoryRead(inst.value32)); break; case 0x76: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & DoMemoryRead(inst.value32)) == DoMemoryRead(inst.value32)); break; case 0x77: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & DoMemoryRead(inst.value32)) != DoMemoryRead(inst.value32)); break; case 0x7A: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & DoMemoryRead(inst.value32)) == Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x7B: activate_codes = ((Truncate16(cht_register[cht_reg_no1] & 0xFFFFu) & DoMemoryRead(inst.value32)) != Truncate16(cht_register[cht_reg_no1] & 0xFFFFu)); break; case 0x80: activate_codes = (cht_register[cht_reg_no2] == cht_register[cht_reg_no1]); break; case 0x81: activate_codes = (cht_register[cht_reg_no2] != cht_register[cht_reg_no1]); break; case 0x82: activate_codes = (cht_register[cht_reg_no2] > cht_register[cht_reg_no1]); break; case 0x83: activate_codes = (cht_register[cht_reg_no2] >= cht_register[cht_reg_no1]); break; case 0x84: activate_codes = (cht_register[cht_reg_no2] < cht_register[cht_reg_no1]); break; case 0x85: activate_codes = (cht_register[cht_reg_no2] <= cht_register[cht_reg_no1]); break; case 0x86: activate_codes = ((cht_register[cht_reg_no2] & cht_register[cht_reg_no1]) == cht_register[cht_reg_no1]); break; case 0x87: activate_codes = ((cht_register[cht_reg_no2] & cht_register[cht_reg_no1]) != cht_register[cht_reg_no1]); break; case 0x8A: activate_codes = ((cht_register[cht_reg_no2] & cht_register[cht_reg_no1]) == cht_register[cht_reg_no2]); break; case 0x8B: activate_codes = ((cht_register[cht_reg_no2] & cht_register[cht_reg_no1]) != cht_register[cht_reg_no2]); break; case 0x90: activate_codes = (cht_register[cht_reg_no1] == inst.value32); break; case 0x91: activate_codes = (cht_register[cht_reg_no1] != inst.value32); break; case 0x92: activate_codes = (cht_register[cht_reg_no1] > inst.value32); break; case 0x93: activate_codes = (cht_register[cht_reg_no1] >= inst.value32); break; case 0x94: activate_codes = (cht_register[cht_reg_no1] < inst.value32); break; case 0x95: activate_codes = (cht_register[cht_reg_no1] <= inst.value32); break; case 0x96: activate_codes = ((cht_register[cht_reg_no1] & inst.value32) == inst.value32); break; case 0x97: activate_codes = ((cht_register[cht_reg_no1] & inst.value32) != inst.value32); break; case 0x9A: activate_codes = ((cht_register[cht_reg_no2] & inst.value32) == cht_register[cht_reg_no1]); break; case 0x9B: activate_codes = ((cht_register[cht_reg_no1] & inst.value32) != cht_register[cht_reg_no1]); break; case 0xA0: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA1: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA2: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) > DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA3: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) >= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA4: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) < DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA5: activate_codes = (DoMemoryRead(cht_register[cht_reg_no2]) <= DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xA6: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value32) == inst.value32); break; case 0xA7: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value32) != inst.value32); break; case 0xAA: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value32) == DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xAB: activate_codes = ((DoMemoryRead(cht_register[cht_reg_no1]) & inst.value32) != DoMemoryRead(cht_register[cht_reg_no1])); break; case 0xB0: activate_codes = (cht_register[cht_reg_no1] == DoMemoryRead(inst.value32)); break; case 0xB1: activate_codes = (cht_register[cht_reg_no1] != DoMemoryRead(inst.value32)); break; case 0xB2: activate_codes = (cht_register[cht_reg_no1] > DoMemoryRead(inst.value32)); break; case 0xB3: activate_codes = (cht_register[cht_reg_no1] >= DoMemoryRead(inst.value32)); break; case 0xB4: activate_codes = (cht_register[cht_reg_no1] < DoMemoryRead(inst.value32)); break; case 0xB6: activate_codes = ((cht_register[cht_reg_no1] & DoMemoryRead(inst.value32)) == DoMemoryRead(inst.value32)); break; case 0xB7: activate_codes = ((cht_register[cht_reg_no1] & DoMemoryRead(inst.value32)) != DoMemoryRead(inst.value32)); break; case 0xBA: activate_codes = ((cht_register[cht_reg_no1] & DoMemoryRead(inst.value32)) == cht_register[cht_reg_no1]); break; case 0xBB: activate_codes = ((cht_register[cht_reg_no1] & DoMemoryRead(inst.value32)) != cht_register[cht_reg_no1]); break; default: activate_codes = false; break; } if (activate_codes) { // execute following instructions continue; } // skip to the next separator (00000000 FFFF), or end constexpr u64 separator_value = UINT64_C(0x000000000000FFFF); while (index < count) { // we don't want to execute the separator instruction const u64 bits = instructions[index++].bits; if (bits == separator_value) break; } } break; case InstructionCode::DelayActivation: // C1 { // A value of around 4000 or 5000 will usually give you a good 20-30 second delay before codes are activated. // Frame number * 0.3 -> (20 * 60) * 10 / 3 => 4000 const u32 comp_value = (System::GetFrameNumber() * 10) / 3; if (comp_value < inst.value16) index = count; else index++; } break; case InstructionCode::Slide: { if ((index + 1) >= instructions.size()) { ERROR_LOG("Incomplete slide instruction"); return; } const u32 slide_count = (inst.first >> 8) & 0xFFu; const u32 address_increment = inst.first & 0xFFu; const u16 value_increment = Truncate16(inst.second); const Instruction& inst2 = instructions[index + 1]; const InstructionCode write_type = inst2.code; u32 address = inst2.address; u16 value = inst2.value16; if (write_type == InstructionCode::ConstantWrite8) { for (u32 i = 0; i < slide_count; i++) { DoMemoryWrite(address, Truncate8(value)); address += address_increment; value += value_increment; } } else if (write_type == InstructionCode::ConstantWrite16) { for (u32 i = 0; i < slide_count; i++) { DoMemoryWrite(address, value); address += address_increment; value += value_increment; } } else { ERROR_LOG("Invalid command in second slide parameter 0x{:02X}", static_cast(write_type)); } index += 2; } break; case InstructionCode::ExtImprovedSlide: { if ((index + 1) >= instructions.size()) { ERROR_LOG("Incomplete slide instruction"); return; } const u32 slide_count = inst.first & 0xFFFFu; const u32 address_change = (inst.second >> 16) & 0xFFFFu; const u16 value_change = Truncate16(inst.second); const Instruction& inst2 = instructions[index + 1]; const InstructionCode write_type = inst2.code; const bool address_change_negative = (inst.first >> 20) & 0x1u; const bool value_change_negative = (inst.first >> 16) & 0x1u; u32 address = inst2.address; u32 value = inst2.value32; if (write_type == InstructionCode::ConstantWrite8) { for (u32 i = 0; i < slide_count; i++) { DoMemoryWrite(address, Truncate8(value)); if (address_change_negative) address -= address_change; else address += address_change; if (value_change_negative) value -= value_change; else value += value_change; } } else if (write_type == InstructionCode::ConstantWrite16) { for (u32 i = 0; i < slide_count; i++) { DoMemoryWrite(address, Truncate16(value)); if (address_change_negative) address -= address_change; else address += address_change; if (value_change_negative) value -= value_change; else value += value_change; } } else if (write_type == InstructionCode::ExtConstantWrite32) { for (u32 i = 0; i < slide_count; i++) { DoMemoryWrite(address, value); if (address_change_negative) address -= address_change; else address += address_change; if (value_change_negative) value -= value_change; else value += value_change; } } else { ERROR_LOG("Invalid command in second slide parameter 0x{:02X}", static_cast(write_type)); } index += 2; } break; case InstructionCode::MemoryCopy: { if ((index + 1) >= instructions.size()) { ERROR_LOG("Incomplete memory copy instruction"); return; } const Instruction& inst2 = instructions[index + 1]; const u32 byte_count = inst.value16; u32 src_address = inst.address; u32 dst_address = inst2.address; for (u32 i = 0; i < byte_count; i++) { u8 value = DoMemoryRead(src_address); DoMemoryWrite(dst_address, value); src_address++; dst_address++; } index += 2; } break; default: { ERROR_LOG("Unhandled instruction code 0x{:02X} ({:08X} {:08X})", static_cast(inst.code.GetValue()), inst.first, inst.second); index++; } break; } } } void Cheats::GamesharkCheatCode::ApplyOnDisable() const { const u32 count = static_cast(instructions.size()); u32 index = 0; for (; index < count;) { const Instruction& inst = instructions[index]; switch (inst.code) { case InstructionCode::Nop: case InstructionCode::ConstantWrite8: case InstructionCode::ConstantWrite16: case InstructionCode::ExtConstantWrite32: case InstructionCode::ExtConstantBitSet8: case InstructionCode::ExtConstantBitSet16: case InstructionCode::ExtConstantBitSet32: case InstructionCode::ExtConstantBitClear8: case InstructionCode::ExtConstantBitClear16: case InstructionCode::ExtConstantBitClear32: case InstructionCode::ScratchpadWrite16: case InstructionCode::ExtScratchpadWrite32: case InstructionCode::ExtIncrement32: case InstructionCode::ExtDecrement32: case InstructionCode::Increment16: case InstructionCode::Decrement16: case InstructionCode::Increment8: case InstructionCode::Decrement8: case InstructionCode::ExtConstantForceRange8: case InstructionCode::ExtConstantForceRangeLimits16: case InstructionCode::ExtConstantForceRangeRollRound16: case InstructionCode::ExtConstantSwap16: case InstructionCode::DelayActivation: // C1 case InstructionCode::ExtConstantWriteIfMatch16: case InstructionCode::ExtCheatRegisters: index++; break; case InstructionCode::ExtConstantForceRange16: case InstructionCode::Slide: case InstructionCode::ExtImprovedSlide: case InstructionCode::MemoryCopy: index += 2; break; case InstructionCode::ExtFindAndReplace: index += 5; break; // for conditionals, we don't want to skip over in case it changed at some point case InstructionCode::ExtCompareEqual32: case InstructionCode::ExtCompareNotEqual32: case InstructionCode::ExtCompareLess32: case InstructionCode::ExtCompareGreater32: case InstructionCode::CompareEqual16: case InstructionCode::CompareNotEqual16: case InstructionCode::CompareLess16: case InstructionCode::CompareGreater16: case InstructionCode::CompareEqual8: case InstructionCode::CompareNotEqual8: case InstructionCode::CompareLess8: case InstructionCode::CompareGreater8: case InstructionCode::CompareButtons: // D4 index++; break; // same deal for block conditionals case InstructionCode::SkipIfNotEqual16: // C0 case InstructionCode::ExtSkipIfNotEqual32: // A4 case InstructionCode::SkipIfButtonsNotEqual: // D5 case InstructionCode::SkipIfButtonsEqual: // D6 case InstructionCode::ExtBitCompareButtons: // D7 case InstructionCode::ExtSkipIfNotLess8: // C3 case InstructionCode::ExtSkipIfNotGreater8: // C4 case InstructionCode::ExtSkipIfNotLess16: // C5 case InstructionCode::ExtSkipIfNotGreater16: // C6 case InstructionCode::ExtMultiConditionals: // F6 case InstructionCode::ExtCheatRegistersCompare: // 52 index++; break; case InstructionCode::ExtConstantWriteIfMatchWithRestore16: { const u16 value = DoMemoryRead(inst.address); const u16 comparevalue = Truncate16(inst.value32 >> 16); const u16 newvalue = Truncate16(inst.value32 & 0xFFFFu); if (value == newvalue) DoMemoryWrite(inst.address, comparevalue); index++; } break; [[unlikely]] default: { ERROR_LOG("Unhandled instruction code 0x{:02X} ({:08X} {:08X})", static_cast(inst.code.GetValue()), inst.first, inst.second); index++; } break; } } } void Cheats::GamesharkCheatCode::SetOptionValue(u32 value) { for (const auto& [index, bitpos_start, bit_count] : option_instruction_values) { Instruction& inst = instructions[index]; const u32 value_mask = ((1u << bit_count) - 1); ; const u32 fixed_mask = ~(value_mask << bitpos_start); inst.second = (inst.second & fixed_mask) | ((value & value_mask) << bitpos_start); } } std::unique_ptr Cheats::ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, Error* error) { return GamesharkCheatCode::Parse(std::move(metadata), data, error); }