// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #define _PC_ // disables MIPS opcode macros. #include "common/Assertions.h" #include "common/ByteSwap.h" #include "common/FileSystem.h" #include "common/Path.h" #include "common/StringUtil.h" #include "common/ZipHelpers.h" #include "Achievements.h" #include "Config.h" #include "GameDatabase.h" #include "Host.h" #include "IopMem.h" #include "Memory.h" #include "Patch.h" #include "R5900.h" #include "IconsFontAwesome.h" #include "fmt/format.h" #include #include #include #include #include #include namespace Patch { template static inline std::optional LookupEnumName(const std::string_view val, const ArrayType& arr) { for (size_t i = 0; i < arr.size(); i++) { if (val == arr[i]) return static_cast(i); } return std::nullopt; } struct PatchGroup { std::string name; std::optional override_aspect_ratio; std::optional override_interlace_mode; std::vector patches; std::vector dpatches; }; struct PatchTextTable { int code; const char* text; void (*func)(PatchGroup* group, const std::string_view cmd, const std::string_view param); }; struct ExtendedState { u32 skip_count = 0; u32 iteration_count = 0; u32 iteration_increment = 0; u32 prev_cheat_type = 0; u32 prev_cheat_addr = 0; u32 last_type = 0; bool null_pointer_encountered = false; }; namespace PatchFunc { static void patch(PatchGroup* group, const std::string_view cmd, const std::string_view param); static void gsaspectratio(PatchGroup* group, const std::string_view cmd, const std::string_view param); static void gsinterlacemode(PatchGroup* group, const std::string_view cmd, const std::string_view param); static void dpatch(PatchGroup* group, const std::string_view cmd, const std::string_view param); } // namespace PatchFunc static void TrimPatchLine(std::string& buffer); static int PatchTableExecute(PatchGroup* group, const std::string_view lhs, const std::string_view rhs, const std::span& Table); static void LoadPatchLine(PatchGroup* group, const std::string_view line); static u32 LoadPatchesFromString(std::vector* patch_list, const std::string& patch_file); static bool OpenPatchesZip(); static std::string GetPnachTemplate( const std::string_view serial, u32 crc, bool include_serial, bool add_wildcard, bool all_crcs); static std::vector FindPatchFilesOnDisk( const std::string_view serial, u32 crc, bool cheats, bool all_crcs); static bool ContainsPatchName(const std::vector& patches, const std::string_view patchName); static bool ContainsPatchName(const std::vector& patches, const std::string_view patchName); template static void EnumeratePnachFiles(const std::string_view serial, u32 crc, bool cheats, bool for_ui, const F& f); static bool PatchStringHasUnlabelledPatch(const std::string& pnach_data); static void ExtractPatchInfo(std::vector* dst, const std::string& pnach_data, u32* num_unlabelled_patches); static void ReloadEnabledLists(); static u32 EnablePatches(const std::vector* patches, const std::vector& enable_list, const std::vector* enable_immediately_list, bool hardcore_safe_only = false); template requires std::is_base_of_v && std::is_base_of_v void ApplyPatch(const PatchCommand* p, EEMemory& ee, IOPMemory& iop, ExtendedState& state); static void ApplyDynaPatch(const DynamicPatch& patch, u32 address); template requires std::is_base_of_v static void writeCheat(Memory& memory, ExtendedState& state); template requires std::is_base_of_v static void handle_extended_t(const PatchCommand* p, Memory& memory, ExtendedState& state); // Name of patches which will be auto-enabled based on global options. static constexpr std::string_view WS_PATCH_NAME = "Widescreen 16:9"; static constexpr std::string_view NI_PATCH_NAME = "No-Interlacing"; static constexpr std::string_view PATCHES_ZIP_NAME = "patches.zip"; const char* PATCHES_CONFIG_SECTION = "Patches"; const char* CHEATS_CONFIG_SECTION = "Cheats"; const char* PATCH_ENABLE_CONFIG_KEY = "Enable"; const char* PATCH_DISABLE_CONFIG_KEY = "Disable"; static zip_t* s_patches_zip; static std::vector s_gamedb_patches; static std::vector s_game_patches; static std::vector s_cheat_patches; static u32 s_gamedb_counts = 0; static u32 s_patches_counts = 0; static u32 s_cheats_counts = 0; static std::vector s_active_patches; static std::vector s_active_gamedb_dynamic_patches; static std::vector s_active_pnach_dynamic_patches; static std::vector s_enabled_cheats; static std::vector s_enabled_patches; static std::vector s_just_enabled_cheats; static std::vector s_just_enabled_patches; static u32 s_patches_crc; static std::optional s_override_aspect_ratio; static std::optional s_override_interlace_mode; static const PatchTextTable s_patch_commands[] = { {0, "patch", &Patch::PatchFunc::patch}, {0, "gsaspectratio", &Patch::PatchFunc::gsaspectratio}, {0, "gsinterlacemode", &Patch::PatchFunc::gsinterlacemode}, {0, "dpatch", &Patch::PatchFunc::dpatch}, {0, nullptr, nullptr}, }; } // namespace Patch void Patch::TrimPatchLine(std::string& buffer) { StringUtil::StripWhitespace(&buffer); if (std::strncmp(buffer.c_str(), "//", 2) == 0) { // comment buffer.clear(); } // check for comments at the end of a line const std::string::size_type pos = buffer.find("//"); if (pos != std::string::npos) buffer.erase(pos); } bool Patch::ContainsPatchName(const std::vector& patch_list, const std::string_view patch_name) { return std::find_if(patch_list.begin(), patch_list.end(), [&patch_name](const PatchGroup& patch) { return patch.name == patch_name; }) != patch_list.end(); } int Patch::PatchTableExecute(PatchGroup* group, const std::string_view lhs, const std::string_view rhs, const std::span& Table) { int i = 0; while (Table[i].text) { if (lhs.compare(Table[i].text) == 0) { if (Table[i].func) Table[i].func(group, lhs, rhs); break; } i++; } return Table[i].code; } // This routine is for executing the commands of the ini file. void Patch::LoadPatchLine(PatchGroup* group, const std::string_view line) { std::string_view key, value; StringUtil::ParseAssignmentString(line, &key, &value); PatchTableExecute(group, key, value, s_patch_commands); } u32 Patch::LoadPatchesFromString(std::vector* patch_list, const std::string& patch_file) { const size_t before = patch_list->size(); PatchGroup current_patch_group; const auto add_current_patch = [patch_list, ¤t_patch_group]() { if (!current_patch_group.patches.empty()) { // Ungrouped/legacy patches should merge with other ungrouped patches. if (current_patch_group.name.empty()) { const std::vector::iterator ungrouped_patch = std::find_if(patch_list->begin(), patch_list->end(), [](const PatchGroup& pg) { return pg.name.empty(); }); if (ungrouped_patch != patch_list->end()) { Console.WriteLn(Color_Gray, fmt::format( "Patch: Merging {} new patch commands into ungrouped list.", current_patch_group.patches.size())); ungrouped_patch->patches.reserve(ungrouped_patch->patches.size() + current_patch_group.patches.size()); for (PatchCommand& cmd : current_patch_group.patches) ungrouped_patch->patches.push_back(std::move(cmd)); } else { // Always add ungrouped patches, no sense to compare empty names. patch_list->push_back(std::move(current_patch_group)); } return; } } if (current_patch_group.patches.empty() && current_patch_group.dpatches.empty()) return; // Don't show patches with duplicate names, prefer the first loaded. if (!ContainsPatchName(*patch_list, current_patch_group.name)) { patch_list->push_back(std::move(current_patch_group)); } else { Console.WriteLn(Color_Gray, fmt::format( "Patch: Skipped loading patch '{}' since a patch with a duplicate name was already loaded.", current_patch_group.name)); } }; std::istringstream ss(patch_file); std::string line; while (std::getline(ss, line)) { TrimPatchLine(line); if (line.empty()) continue; if (line.front() == '[') { if (line.length() < 2 || line.back() != ']') { Console.Error(fmt::format("Malformed patch line: {}", line.c_str())); continue; } if (!current_patch_group.name.empty() || !current_patch_group.patches.empty() || !current_patch_group.dpatches.empty()) { add_current_patch(); current_patch_group = {}; } current_patch_group.name = line.substr(1, line.length() - 2); if (current_patch_group.name.empty()) Console.Error(fmt::format("Malformed patch name: {}", line)); continue; } LoadPatchLine(¤t_patch_group, line); } if (!current_patch_group.name.empty() || !current_patch_group.patches.empty() || !current_patch_group.dpatches.empty()) add_current_patch(); return static_cast(patch_list->size() - before); } bool Patch::OpenPatchesZip() { if (s_patches_zip) return true; const std::string filename = Path::Combine(EmuFolders::Resources, PATCHES_ZIP_NAME); zip_error ze = {}; zip_source_t* zs = zip_source_file_create(filename.c_str(), 0, 0, &ze); if (zs && !(s_patches_zip = zip_open_from_source(zs, ZIP_RDONLY, &ze))) { static bool warning_shown = false; if (!warning_shown) { Host::AddIconOSDMessage("PatchesZipOpenWarning", ICON_FA_BANDAGE, fmt::format(TRANSLATE_FS("Patch", "Failed to open {}. Built-in game patches are not available."), PATCHES_ZIP_NAME), Host::OSD_ERROR_DURATION); warning_shown = true; } // have to clean up source Console.Error("Failed to open %s: %s", filename.c_str(), zip_error_strerror(&ze)); zip_source_free(zs); return false; } std::atexit([]() { zip_close(s_patches_zip); }); return true; } std::string Patch::GetPnachTemplate(const std::string_view serial, u32 crc, bool include_serial, bool add_wildcard, bool all_crcs) { pxAssert(!all_crcs || (include_serial && add_wildcard)); if (!serial.empty()) { if (all_crcs) return fmt::format("{}_*.pnach", serial); else if (include_serial) return fmt::format("{}_{:08X}{}.pnach", serial, crc, add_wildcard ? "*" : ""); } return fmt::format("{:08X}{}.pnach", crc, add_wildcard ? "*" : ""); } std::vector Patch::FindPatchFilesOnDisk(const std::string_view serial, u32 crc, bool cheats, bool all_crcs) { FileSystem::FindResultsArray files; FileSystem::FindFiles(cheats ? EmuFolders::Cheats.c_str() : EmuFolders::Patches.c_str(), GetPnachTemplate(serial, crc, true, true, all_crcs).c_str(), FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files); std::vector ret; ret.reserve(files.size()); for (FILESYSTEM_FIND_DATA& fd : files) ret.push_back(std::move(fd.FileName)); // and patches without serials FileSystem::FindFiles(cheats ? EmuFolders::Cheats.c_str() : EmuFolders::Patches.c_str(), GetPnachTemplate(serial, crc, false, true, false).c_str(), FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files); ret.reserve(ret.size() + files.size()); for (FILESYSTEM_FIND_DATA& fd : files) ret.push_back(std::move(fd.FileName)); return ret; } bool Patch::ContainsPatchName(const std::vector& patches, const std::string_view patchName) { return std::find_if(patches.begin(), patches.end(), [&patchName](const PatchInfo& patch) { return patch.name == patchName; }) != patches.end(); } template void Patch::EnumeratePnachFiles(const std::string_view serial, u32 crc, bool cheats, bool for_ui, const F& f) { // Prefer files on disk over the zip. // // Hardcore blocks CHEATS, not fixes. The old condition dropped every on-disk pnach, // which is asymmetric with the fallback below: the bundled patches.zip stays enabled in // hardcore, so a widescreen/no-interlace/bug-fix patch worked when it shipped in our zip // and silently did nothing when the same patch sat on disk. That killed everything the // in-app Patch Manager writes (it only ever writes to disk) the moment a user turned // hardcore on, with no message explaining why. // // This used to claim cheat pnach files are never enumerated under hardcore. They are, on // a normal boot: UpdateDiscDetails reloads with reload_files before ResetHardcoreMode arms // s_hardcore_mode, so the walk below happens while it is still false and the groups stay // resident for the session. What actually keeps them from applying is ReloadEnabledLists // emptying the enabled list and the cheat enable call sitting behind EnableCheats. The // check here only saves the enumeration on later reloads. // CHEATS are matched across ALL CRCs of the serial (the "{serial}_*.pnach" glob), not just the // running game's CRC. The in-app cheat editor writes "{serial}_00000000.pnach" whenever it can't // read a live CRC (getGameCRC() returns the literal "00000000" with no VM up), and PATCHES the // user pastes/imports are commonly named for a different revision's CRC — either way a // CRC-specific boot-time glob silently drops them ("no cheats found" with the file sitting right // there in the list). Cheats are serial-scoped by nature, so widening them to all_crcs recovers // those files without a re-install. Real fixes/widescreen (cheats == false) stay CRC-specific at // boot so a wrong-revision graphics patch can't auto-apply. std::vector disk_patch_files; if (for_ui || !cheats || !Achievements::IsHardcoreModeActive()) disk_patch_files = FindPatchFilesOnDisk(serial, crc, cheats, for_ui || cheats); bool unlabeled_patch_found = false; if (!disk_patch_files.empty()) { for (const std::string& file : disk_patch_files) { std::optional contents = FileSystem::ReadFileToString(file.c_str()); if (contents.has_value()) { // Catch if unlabeled patches are being loaded so we can disable ZIP patches to prevent conflicts. if (PatchStringHasUnlabelledPatch(contents.value())) { unlabeled_patch_found = true; Console.WriteLn(fmt::format("Patch: Disabling any bundled '{}' patches due to unlabeled patch being loaded. (To avoid conflicts)", PATCHES_ZIP_NAME)); } f(std::move(file), std::move(contents.value())); } } } // Otherwise fall back to the zip. // // "Otherwise" has to include "a disk file was found", which it previously didn't: the guard // ignored disk_patch_files, so the bundled copy loaded ALONGSIDE the user's file every time, // contradicting the "prefer files on disk" comment above. Two user-visible consequences, both // reported: a hand-edited pnach couldn't fully replace the bundled one (dedupe-by-name only // hid the bundled group while the disk name existed), and DELETING a pnach silently promoted // the identically-named bundled group in its place — so a patch the user had removed carried // on applying, with nothing left on disk to explain why. if (cheats || unlabeled_patch_found || !disk_patch_files.empty() || !OpenPatchesZip()) return; // Prefer filename with serial. std::string zip_filename = GetPnachTemplate(serial, crc, true, false, false); std::optional pnach_data(ReadFileInZipToString(s_patches_zip, zip_filename.c_str())); if (!pnach_data.has_value()) { zip_filename = GetPnachTemplate(serial, crc, false, false, false); pnach_data = ReadFileInZipToString(s_patches_zip, zip_filename.c_str()); } if (pnach_data.has_value()) f(std::move(zip_filename), std::move(pnach_data.value())); } bool Patch::PatchStringHasUnlabelledPatch(const std::string& pnach_data) { std::istringstream ss(pnach_data); std::string line; bool foundPatch = false, foundLabel = false; while (std::getline(ss, line)) { TrimPatchLine(line); if (line.empty()) continue; if (line.length() > 2 && line.front() == '[' && line.back() == ']') { if (!foundPatch) return false; foundLabel = true; continue; } std::string_view key, value; StringUtil::ParseAssignmentString(line, &key, &value); if (key == "patch") { if (!foundLabel) return true; foundPatch = true; } } return false; } void Patch::ExtractPatchInfo(std::vector* dst, const std::string& pnach_data, u32* num_unlabelled_patches) { std::istringstream ss(pnach_data); std::string line; PatchInfo current_patch; std::optional last_place; bool unknown_place = false; while (std::getline(ss, line)) { TrimPatchLine(line); if (line.empty()) continue; const bool has_patch = !current_patch.name.empty(); if (line.length() > 2 && line.front() == '[' && line.back() == ']') { if (has_patch) { if (std::none_of(dst->begin(), dst->end(), [¤t_patch](const PatchInfo& pi) { return (pi.name == current_patch.name); })) { // Don't show patches with duplicate names, prefer the first loaded. if (!ContainsPatchName(*dst, current_patch.name)) { dst->push_back(std::move(current_patch)); } else { Console.WriteLn(Color_Gray, fmt::format("Patch: Skipped reading patch '{}' since a patch with a duplicate name was already loaded.", current_patch.name)); } } current_patch = {}; } current_patch.name = line.substr(1, line.length() - 2); last_place = std::nullopt; unknown_place = false; continue; } std::string_view key, value; StringUtil::ParseAssignmentString(line, &key, &value); // Just ignore other directives, who knows what rubbish people have in here. // Use comment for description if it hasn't been otherwise specified. if (key == "author") { current_patch.author = value; } else if (key == "description") { current_patch.description = value; } else if (key == "comment" && current_patch.description.empty()) { current_patch.description = value; } else if (key == "patch") { if (!has_patch && num_unlabelled_patches) (*num_unlabelled_patches)++; // Try to extract the place value of the patch lines so we can // display it in the GUI if they all match. TODO: Don't duplicate // all this parsing logic twice. if (unknown_place) continue; std::string::size_type comma_pos = value.find(","); if (comma_pos == std::string::npos) comma_pos = 0; const std::string_view padded_place = value.substr(0, comma_pos); const std::string_view place_string = StringUtil::StripWhitespace(padded_place); const std::optional place = LookupEnumName( place_string, s_place_to_string); if (!place.has_value() || (last_place.has_value() && place != last_place)) { // This group contains patch lines with different or invalid // place values. current_patch.place = std::nullopt; unknown_place = true; continue; } current_patch.place = place; last_place = place; } else if (key == "dpatch") { current_patch.place = std::nullopt; unknown_place = true; } } // Last one. if (!current_patch.name.empty() && std::none_of(dst->begin(), dst->end(), [¤t_patch](const PatchInfo& pi) { return (pi.name == current_patch.name); })) { dst->push_back(std::move(current_patch)); } } std::string_view Patch::PatchInfo::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 Patch::PatchInfo::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::vector Patch::GetPatchInfo(const std::string_view serial, u32 crc, bool cheats, bool showAllCRCS, u32* num_unlabelled_patches) { std::vector ret; if (num_unlabelled_patches) *num_unlabelled_patches = 0; EnumeratePnachFiles(serial, crc, cheats, showAllCRCS, [&ret, num_unlabelled_patches](const std::string& filename, const std::string& pnach_data) { ExtractPatchInfo(&ret, pnach_data, num_unlabelled_patches); }); return ret; } std::string Patch::GetPnachFilename(const std::string_view serial, u32 crc, bool cheats) { return Path::Combine(cheats ? EmuFolders::Cheats : EmuFolders::Patches, GetPnachTemplate(serial, crc, true, false, false)); } void Patch::ReloadEnabledLists() { const std::vector prev_enabled_cheats = std::move(s_enabled_cheats); if (EmuConfig.EnableCheats && !Achievements::IsHardcoreModeActive()) s_enabled_cheats = Host::GetStringListSetting(CHEATS_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); else s_enabled_cheats = {}; const std::vector prev_enabled_patches = std::exchange(s_enabled_patches, Host::GetStringListSetting(PATCHES_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY)); const std::vector disabled_patches = Host::GetStringListSetting(PATCHES_CONFIG_SECTION, PATCH_DISABLE_CONFIG_KEY); // Name based matching for widescreen/NI settings. if (EmuConfig.EnableWideScreenPatches) { if (std::none_of(s_enabled_patches.begin(), s_enabled_patches.end(), [](const std::string& it) { return (it == WS_PATCH_NAME); })) { s_enabled_patches.emplace_back(WS_PATCH_NAME); } } if (EmuConfig.EnableNoInterlacingPatches) { if (std::none_of(s_enabled_patches.begin(), s_enabled_patches.end(), [](const std::string& it) { return (it == NI_PATCH_NAME); })) { s_enabled_patches.emplace_back(NI_PATCH_NAME); } } for (auto it = s_enabled_patches.begin(); it != s_enabled_patches.end();) { if (std::find(disabled_patches.begin(), disabled_patches.end(), *it) != disabled_patches.end()) { it = s_enabled_patches.erase(it); } else { ++it; } } s_just_enabled_cheats.clear(); s_just_enabled_patches.clear(); for (const auto& p : s_enabled_cheats) { if (std::find(prev_enabled_cheats.begin(), prev_enabled_cheats.end(), p) == prev_enabled_cheats.end()) { s_just_enabled_cheats.emplace_back(p); } } for (const auto& p : s_enabled_patches) { if (std::find(prev_enabled_patches.begin(), prev_enabled_patches.end(), p) == prev_enabled_patches.end()) { s_just_enabled_patches.emplace_back(p); } } } // Under hardcore, a group off the patches path is only allowed through if it declares what it // is for. gsaspectratio or gsinterlacemode means widescreen or no-interlace, which is what the // fork deliberately keeps working; a group that declares nothing and just writes memory is a // cheat whatever the label says. // // It has to be judged by content, not by file or by name. patches.zip files God of War 2's // [Skip Cutscenes] as a patch, in the same pnach as its [Widescreen 16:9], so location cannot // tell them apart. And "no memory writes" cannot either: the widescreen group writes EE memory // too. Declared intent is the only thing that separates them. static bool IsHardcoreSafePatchGroup(const Patch::PatchGroup& p) { if (p.override_aspect_ratio.has_value() || p.override_interlace_mode.has_value()) return true; // Nothing declared and nothing written is inert, so it costs nothing to allow. return p.patches.empty() && p.dpatches.empty(); } u32 Patch::EnablePatches(const std::vector* patches, const std::vector& enable_list, const std::vector* enable_immediately_list, bool hardcore_safe_only) { u32 count = 0; for (const PatchGroup& p : *patches) { // Checked here rather than against the enable list, because an unlabelled group never // consults that list at all -- see the auto-enable below. if (hardcore_safe_only && !IsHardcoreSafePatchGroup(p)) { Console.WriteLn(Color_Orange, fmt::format("Skipping patch under Hardcore: {}", p.name.empty() ? std::string_view("") : std::string_view(p.name))); continue; } // For compatibility, we auto enable anything that's not labelled. // Also for gamedb patches. if (!p.name.empty() && std::find(enable_list.begin(), enable_list.end(), p.name) == enable_list.end()) continue; Console.WriteLn(Color_Green, fmt::format("Enabled patch: {}", p.name.empty() ? std::string_view("") : std::string_view(p.name))); // Indicate that a new group has started so that extended code state // such as the skip counter can be reset. s_active_patches.emplace_back(nullptr); for (const PatchCommand& ip : p.patches) { // print the actual patch lines only in verbose mode (even in devel) if (Log::GetMaxLevel() >= LOGLEVEL_DEV) DevCon.WriteLnFmt(" {}", ip.ToString()); s_active_patches.push_back(&ip); } for (const DynamicPatch& dp : p.dpatches) { s_active_pnach_dynamic_patches.push_back(dp); } if (p.override_aspect_ratio.has_value()) s_override_aspect_ratio = p.override_aspect_ratio; if (p.override_interlace_mode.has_value()) s_override_interlace_mode = p.override_interlace_mode; // Count unlabelled patches once per command, or one patch per group. count += p.name.empty() ? (static_cast(p.patches.size()) + static_cast(p.dpatches.size())) : 1; } // Apply PPT_ON_LOAD_OR_WHEN_ENABLED patches immediately. if (enable_immediately_list && !enable_immediately_list->empty()) { // Don't pass pointers to patch objects themselves here just in case the // patches are reloaded twice in a row before this event makes it. Host::RunOnCPUThread([patches, enable_immediately_list]() { for (const PatchGroup& group : *patches) { const bool apply_immediately = std::find( enable_immediately_list->begin(), enable_immediately_list->end(), group.name) != enable_immediately_list->end(); if (!apply_immediately) continue; EEMemoryInterface ee; IOPMemoryInterface iop; ExtendedState state; for (const PatchCommand& command : group.patches) { if (command.placetopatch != PPT_ON_LOAD_OR_WHEN_ENABLED) continue; ApplyPatch(&command, ee, iop, state); } } }); } return count; } void Patch::ReloadPatches(const std::string& serial, u32 crc, bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) { reload_files |= (s_patches_crc != crc); s_patches_crc = crc; if (reload_files) { s_gamedb_patches.clear(); const GameDatabaseSchema::GameEntry* game = GameDatabase::findGame(serial); if (game) { const std::string* patches = game->findPatch(crc); if (patches) { const u32 patch_count = LoadPatchesFromString(&s_gamedb_patches, *patches); if (patch_count > 0) Console.WriteLn(Color_Green, fmt::format("Found {} game patches in GameDB.", patch_count)); } LoadDynamicPatches(game->dynaPatches); } s_game_patches.clear(); EnumeratePnachFiles( serial, s_patches_crc, false, false, [](const std::string& filename, const std::string& pnach_data) { const u32 patch_count = LoadPatchesFromString(&s_game_patches, pnach_data); if (patch_count > 0) Console.WriteLn(Color_Green, fmt::format("Found {} game patches in {}.", patch_count, filename)); }); s_cheat_patches.clear(); EnumeratePnachFiles( serial, s_patches_crc, true, false, [](const std::string& filename, const std::string& pnach_data) { const u32 patch_count = LoadPatchesFromString(&s_cheat_patches, pnach_data); if (patch_count > 0) Console.WriteLn(Color_Green, fmt::format("Found {} cheats in {}.", patch_count, filename)); }); } UpdateActivePatches(reload_enabled_list, verbose, verbose_if_changed, false); } void Patch::UpdateActivePatches(bool reload_enabled_list, bool verbose, bool verbose_if_changed, bool apply_new_patches) { if (reload_enabled_list) ReloadEnabledLists(); const size_t prev_count = s_active_patches.size(); s_active_patches.clear(); s_override_aspect_ratio.reset(); s_override_interlace_mode.reset(); s_active_pnach_dynamic_patches.clear(); SmallString message; u32 gp_count = 0; if (EmuConfig.EnablePatches) { gp_count = EnablePatches(&s_gamedb_patches, std::vector(), nullptr); s_gamedb_counts = gp_count; if (gp_count > 0) message.append(TRANSLATE_PLURAL_STR("Patch", "%n GameDB patches are active.", "OSD Message", gp_count)); } // The cheats list is emptied under hardcore, but this one never was, and the shipped // patches.zip carries outright cheats filed as patches. So filter it by content instead. // GameDB above is left alone deliberately: it is a curated compatibility layer, and // dropping it under hardcore would break games rather than stop cheating. const u32 p_count = EnablePatches(&s_game_patches, s_enabled_patches, apply_new_patches ? &s_just_enabled_patches : nullptr, Achievements::IsHardcoreModeActive()); s_patches_counts = p_count; if (p_count > 0) message.append_format("{}{}", message.empty() ? "" : "\n", TRANSLATE_PLURAL_STR("Patch", "%n game patches are active.", "OSD Message", p_count)); u32 c_count = 0; if (EmuConfig.EnableCheats) c_count = EnablePatches( &s_cheat_patches, s_enabled_cheats, apply_new_patches ? &s_just_enabled_cheats : nullptr); s_cheats_counts = c_count; if (c_count > 0) message.append_format("{}{}", message.empty() ? "" : "\n", TRANSLATE_PLURAL_STR("Patch", "%n cheat patches are active.", "OSD Message", c_count)); // Display message on first boot when we load patches. // Except when it's just GameDB. const bool just_gamedb = (p_count == 0 && c_count == 0 && gp_count > 0); if (verbose || (verbose_if_changed && prev_count != s_active_patches.size() && !just_gamedb)) { if (!message.empty()) { Host::AddIconOSDMessage("LoadPatches", ICON_FA_BANDAGE, message, Host::OSD_INFO_DURATION); } else { Host::AddIconOSDMessage("LoadPatches", ICON_FA_BANDAGE, TRANSLATE_SV( "Patch", "No cheats or patches (widescreen, compatibility or others) are found / enabled."), Host::OSD_INFO_DURATION); } } if ((!s_active_gamedb_dynamic_patches.empty() || !s_active_pnach_dynamic_patches.empty()) && Cpu) Cpu->Reset(); } void Patch::ApplyPatchSettingOverrides() { // Switch to 16:9 (or any custom aspect ratio) if widescreen patches are enabled, and AR is auto. if (s_override_aspect_ratio.has_value() && EmuConfig.GS.AspectRatio == AspectRatioType::RAuto4_3_3_2) { EmuConfig.CurrentCustomAspectRatio = s_override_aspect_ratio.value(); Console.WriteLn(Color_Gray, fmt::format("Patch: Setting aspect ratio to {} by patch request.", s_override_aspect_ratio.value())); } // Disable interlacing in GS if active. if (s_override_interlace_mode.has_value() && EmuConfig.GS.InterlaceMode == GSInterlaceMode::Automatic) { Console.WriteLn(Color_Gray, fmt::format("Patch: Setting deinterlace mode to {} by patch request.", static_cast(s_override_interlace_mode.value()))); EmuConfig.GS.InterlaceMode = s_override_interlace_mode.value(); } } bool Patch::ReloadPatchAffectingOptions() { // Restore the aspect ratio + interlacing setting the user had set before reloading the patch, // as the custom patch settings only apply if the "Auto" settings are selected. const AspectRatioType current_ar = EmuConfig.GS.AspectRatio; const GSInterlaceMode current_interlace = EmuConfig.GS.InterlaceMode; const float custom_aspect_ratio = EmuConfig.CurrentCustomAspectRatio; // This is pretty gross, but we're not using a config layer, so... AspectRatioType new_ar = Pcsx2Config::GSOptions::DEFAULT_ASPECT_RATIO; const std::string ar_value = Host::GetStringSettingValue("EmuCore/GS", "AspectRatio", Pcsx2Config::GSOptions::AspectRatioNames[static_cast(EmuConfig.GS.AspectRatio)]); for (u32 i = 0; i < static_cast(AspectRatioType::MaxCount); i++) { if (ar_value == Pcsx2Config::GSOptions::AspectRatioNames[i]) { new_ar = static_cast(i); break; } } EmuConfig.GS.AspectRatio = new_ar; EmuConfig.GS.InterlaceMode = static_cast(Host::GetIntSettingValue( "EmuCore/GS", "deinterlace_mode", static_cast(Pcsx2Config::GSOptions::DEFAULT_INTERLACE_MODE))); // Clear the patch-requested aspect before re-deriving it. ApplyPatchSettingOverrides only ever // SETS CurrentCustomAspectRatio, so without this the last widescreen patch's ratio survived // the patch being disabled and GSRenderer kept reading it (it takes any value > 0 in // preference to the real aspect) — i.e. "I turned widescreen off and the game is still 16:9". EmuConfig.CurrentCustomAspectRatio = 0.0f; ApplyPatchSettingOverrides(); // Return true if any config setting changed return current_ar != EmuConfig.GS.AspectRatio || custom_aspect_ratio != EmuConfig.CurrentCustomAspectRatio || current_interlace != EmuConfig.GS.InterlaceMode; } void Patch::UnloadPatches() { s_override_interlace_mode = {}; s_override_aspect_ratio = {}; s_patches_crc = 0; s_active_patches = {}; s_active_pnach_dynamic_patches = {}; s_active_gamedb_dynamic_patches = {}; s_enabled_patches = {}; s_enabled_cheats = {}; decltype(s_cheat_patches)().swap(s_cheat_patches); decltype(s_game_patches)().swap(s_game_patches); decltype(s_gamedb_patches)().swap(s_gamedb_patches); } // PatchFunc Functions. void Patch::PatchFunc::patch(PatchGroup* group, const std::string_view cmd, const std::string_view param) { #define PATCH_ERROR(fstring, ...) \ Console.Error(fmt::format("(Patch) Error Parsing: {}={}: " fstring, cmd, param, __VA_ARGS__)) // [0]=PlaceToPatch,[1]=CpuType,[2]=MemAddr,[3]=OperandSize,[4]=WriteValue const std::vector pieces(StringUtil::SplitString(param, ',', false)); if (pieces.size() != 5) { PATCH_ERROR("Expected 5 data parameters; only found {}", pieces.size()); return; } std::string_view addr_end, data_end; const std::optional placetopatch = LookupEnumName(pieces[0], s_place_to_string); const std::optional cpu = LookupEnumName(pieces[1], s_cpu_to_string); const std::optional addr = StringUtil::FromChars(pieces[2], 16, &addr_end); const std::optional type = LookupEnumName(pieces[3], s_type_to_string); std::optional data = StringUtil::FromChars(pieces[4], 16, &data_end); u8* data_ptr = nullptr; if (!placetopatch.has_value()) { PATCH_ERROR("Invalid 'place' value '{}' (0: on boot only, 1: continuously, 2: on boot and continuously, 3: on boot and when enabled in the GUI)", pieces[0]); return; } if (!addr.has_value() || !addr_end.empty()) { PATCH_ERROR("Malformed address '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[2]); return; } if (!cpu.has_value()) { PATCH_ERROR("Unrecognized CPU Target: '{}'", pieces[1]); return; } if (!type.has_value()) { PATCH_ERROR("Unrecognized Operand Size: '{}'", pieces[3]); return; } if (type.value() != BYTES_T) { if (!data.has_value() || !data_end.empty()) { PATCH_ERROR("Malformed data '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[4]); return; } } else { // bit crappy to copy it, but eh, saves writing a new routine std::optional> bytes = StringUtil::DecodeHex(pieces[4]); if (!bytes.has_value() || bytes->empty()) { PATCH_ERROR("Malformed data '{}', a hex string without prefix (e.g. 0123ABCD) is expected", pieces[4]); return; } data = bytes->size(); data_ptr = static_cast(std::malloc(bytes->size())); std::memcpy(data_ptr, bytes->data(), bytes->size()); } PatchCommand iPatch; iPatch.placetopatch = placetopatch.value(); iPatch.cpu = cpu.value(); iPatch.addr = addr.value(); iPatch.type = type.value(); iPatch.data = data.value(); iPatch.data_ptr = data_ptr; group->patches.push_back(std::move(iPatch)); #undef PATCH_ERROR } void Patch::PatchFunc::gsaspectratio(PatchGroup* group, const std::string_view cmd, const std::string_view param) { std::string str(param); std::istringstream ss(str); uint dividend, divisor; char delimiter; float aspect_ratio = 0.f; ss >> dividend >> delimiter >> divisor; if (!ss.fail() && delimiter == ':' && divisor != 0) { aspect_ratio = static_cast(dividend) / static_cast(divisor); } if (aspect_ratio > 0.f) { group->override_aspect_ratio = aspect_ratio; return; } Console.Error(fmt::format("Patch error: {} is an unknown aspect ratio.", param)); } void Patch::PatchFunc::gsinterlacemode(PatchGroup* group, const std::string_view cmd, const std::string_view param) { const std::optional interlace_mode = StringUtil::FromChars(param); if (!interlace_mode.has_value() || interlace_mode.value() < 0 || interlace_mode.value() >= static_cast(GSInterlaceMode::Count)) { Console.Error(fmt::format("Patch error: {} is an unknown interlace mode.", param)); return; } group->override_interlace_mode = static_cast(interlace_mode.value()); } void Patch::PatchFunc::dpatch(PatchGroup* group, const std::string_view cmd, const std::string_view param) { #define PATCH_ERROR(fstring, ...) \ Console.Error(fmt::format("(dPatch) Error Parsing: {}={}: " fstring, cmd, param, __VA_ARGS__)) // [0]=version/type,[1]=number of patterns,[2]=number of replacements // Each pattern or replacement is [3]=offset,[4]=hex const std::vector pieces(StringUtil::SplitString(param, ',', false)); if (pieces.size() < 3) { PATCH_ERROR("Expected at least 3 data parameters; only found {}", pieces.size()); return; } std::string_view patterns_end, replacements_end; // Implemented for possible future use so we don't have to break backcompat std::optional dpatch_type = StringUtil::FromChars(pieces[0]); std::optional num_patterns = StringUtil::FromChars(pieces[1], 16, &patterns_end); std::optional num_replacements = StringUtil::FromChars(pieces[2], 16, &replacements_end); if (!dpatch_type.has_value()) { PATCH_ERROR("Malformed version/type '{}', a decimal number(e.g. 0,1,2) is expected", pieces[0]); return; } if (dpatch_type.value() != 0) { PATCH_ERROR("Unsupported version/type '{}', only 0 is currently supported", pieces[0]); return; } if (!num_patterns.has_value()) { PATCH_ERROR("Malformed number of patterns '{}', a decimal number is expected", pieces[1]); return; } if (!num_replacements.has_value()) { PATCH_ERROR("Malformed number of replacements '{}', a decimal number is expected", pieces[2]); return; } if (pieces.size() != ((num_patterns.value() * 2) + (num_replacements.value() * 2) + 3)) { PATCH_ERROR("Expected 2 fields for each {} patterns and {} replacements; found {}", num_patterns.value(), num_replacements.value(), pieces.size() - 2); return; } DynamicPatch dpatch; for (u32 i = 0; i < num_patterns.value(); i++) { std::optional offset = StringUtil::FromChars(pieces[3 + (i * 2)], 16); std::optional value = StringUtil::FromChars(pieces[4 + (i * 2)], 16); if (!offset.has_value()) { PATCH_ERROR("Malformed offset '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[3 + (i * 2)]); return; } if (!value.has_value()) { PATCH_ERROR("Malformed value '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[4 + (i * 2)]); return; } DynamicPatchEntry pattern; pattern.offset = offset.value(); pattern.value = value.value(); dpatch.pattern.push_back(pattern); } for (u32 i = 0; i < num_replacements.value(); i++) { std::optional offset = StringUtil::FromChars(pieces[3 + (num_patterns.value() * 2) + (i * 2)], 16); std::optional value = StringUtil::FromChars(pieces[4 + (num_patterns.value() * 2) + (i * 2)], 16); if (!offset.has_value()) { PATCH_ERROR("Malformed offset '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[3 + (num_patterns.value() * 2) + (i * 2)]); return; } if (!value.has_value()) { PATCH_ERROR("Malformed value '{}', a hex number without prefix (e.g. 0123ABCD) is expected", pieces[4 + (num_patterns.value() * 2) + (i * 2)]); return; } DynamicPatchEntry replacement; replacement.offset = offset.value(); replacement.value = value.value(); dpatch.replacement.push_back(replacement); } group->dpatches.push_back(dpatch); } void Patch::ApplyBootPatches() { EEMemoryInterface ee; IOPMemoryInterface iop; ApplyPatches(s_active_patches, PPT_ONCE_ON_LOAD, ee, iop); ApplyPatches(s_active_patches, PPT_COMBINED_0_1, ee, iop); ApplyPatches(s_active_patches, PPT_ON_LOAD_OR_WHEN_ENABLED, ee, iop); } void Patch::ApplyVsyncPatches() { EEMemoryInterface ee; IOPMemoryInterface iop; ApplyPatches(s_active_patches, PPT_CONTINUOUSLY, ee, iop); ApplyPatches(s_active_patches, PPT_COMBINED_0_1, ee, iop); } void Patch::ApplyPatches( const std::vector& patches, patch_place_type place, EEMemoryInterface& ee, IOPMemoryInterface& iop) { ExtendedState state; for (const PatchCommand* patch : patches) { if (!patch) { state = {}; continue; } if (patch->placetopatch != place) continue; ApplyPatch(patch, ee, iop, state); } } void Patch::ApplyPatches( const std::vector& patches, patch_place_type place, MemoryInterface& ee, MemoryInterface& iop) { ExtendedState state; for (const PatchCommand* patch : patches) { if (!patch) { state = {}; continue; } if (patch->placetopatch != place) continue; ApplyPatch(patch, ee, iop, state); } } u32 Patch::GetActiveGameDBPatchesCount() { return s_gamedb_counts; } u32 Patch::GetActivePatchesCount() { return s_patches_counts; } u32 Patch::GetActiveCheatsCount() { return s_cheats_counts; } u32 Patch::GetAllActivePatchesCount() { return s_gamedb_counts + s_patches_counts + s_cheats_counts; } bool Patch::IsGloballyToggleablePatch(const PatchInfo& patch_info) { return patch_info.name == WS_PATCH_NAME || patch_info.name == NI_PATCH_NAME; } void Patch::ApplyDynamicPatches(u32 pc) { for (const auto& dynpatch : s_active_pnach_dynamic_patches) ApplyDynaPatch(dynpatch, pc); for (const auto& dynpatch : s_active_gamedb_dynamic_patches) ApplyDynaPatch(dynpatch, pc); } void Patch::LoadDynamicPatches(const std::vector& patches) { for (const DynamicPatch& it : patches) s_active_gamedb_dynamic_patches.push_back(it); } template requires std::is_base_of_v void Patch::writeCheat(Memory& memory, ExtendedState& state) { switch (state.last_type) { case 0x0: memory.IdempotentWrite8(state.prev_cheat_addr, state.iteration_increment & 0xFF); break; case 0x1: memory.IdempotentWrite16(state.prev_cheat_addr, state.iteration_increment & 0xFFFF); break; case 0x2: memory.IdempotentWrite32(state.prev_cheat_addr, state.iteration_increment); break; default: break; } } template requires std::is_base_of_v void Patch::handle_extended_t(const PatchCommand* p, Memory& memory, ExtendedState& state) { if (state.skip_count > 0) { state.skip_count--; } else switch (state.prev_cheat_type) { case 0x3040: // vvvvvvvv 00000000 Inc { u32 mem = memory.Read32(state.prev_cheat_addr); memory.Write32(state.prev_cheat_addr, mem + (p->addr)); state.prev_cheat_type = 0; break; } case 0x3050: // vvvvvvvv 00000000 Dec { u32 mem = memory.Read32(state.prev_cheat_addr); memory.Write32(state.prev_cheat_addr, mem - (p->addr)); state.prev_cheat_type = 0; break; } case 0x4000: // vvvvvvvv iiiiiiii for (u32 i = 0; i < state.iteration_count; i++) { memory.IdempotentWrite32((u32)(state.prev_cheat_addr + (i * state.iteration_increment)), (u32)(p->addr + ((u32)p->data * i))); } state.prev_cheat_type = 0; break; case 0x5000: // bbbbbbbb 00000000 for (u32 i = 0; i < state.iteration_count; i++) { u8 mem = memory.Read8(state.prev_cheat_addr + i); memory.IdempotentWrite8((p->addr + i) & 0x0FFFFFFF, mem); } state.prev_cheat_type = 0; break; case 0x6000: // 000Xnnnn iiiiiiii { state.null_pointer_encountered = false; // Get Number of pointers if (((u32)p->addr & 0x0000FFFF) == 0) state.iteration_count = 1; else state.iteration_count = (u32)p->addr & 0x0000FFFF; // Read first pointer state.last_type = ((u32)p->addr & 0x000F0000) >> 16; u32 mem = memory.Read32(state.prev_cheat_addr); if (((mem & 0x0FFFFFFF) & 0x3FFFFFFC) == 0) state.null_pointer_encountered = true; state.prev_cheat_addr = mem + (u32)p->data; state.iteration_count--; // Check if needed to read another pointer if (state.iteration_count == 0) { state.prev_cheat_type = 0; if (!state.null_pointer_encountered) writeCheat(memory, state); } else { state.prev_cheat_type = 0x6001; } } break; case 0x6001: // 000Xnnnn iiiiiiii { // Read first pointer u32 mem = 0; if (!state.null_pointer_encountered) { mem = memory.Read32(state.prev_cheat_addr & 0x0FFFFFFF); if (((mem & 0x0FFFFFFF) & 0x3FFFFFFC) == 0) state.null_pointer_encountered = true; } state.prev_cheat_addr = mem + (u32)p->addr; state.iteration_count--; // Check if needed to read another pointer if (state.iteration_count == 0) { state.prev_cheat_type = 0; if (!state.null_pointer_encountered) writeCheat(memory, state); } else { if (!state.null_pointer_encountered) { mem = memory.Read32(state.prev_cheat_addr); if (((mem & 0x0FFFFFFF) & 0x3FFFFFFC) == 0) state.null_pointer_encountered = true; } else { mem = 0; } state.prev_cheat_addr = mem + (u32)p->data; state.iteration_count--; if (state.iteration_count == 0) { state.prev_cheat_type = 0; if (!state.null_pointer_encountered) writeCheat(memory, state); } } } break; default: if ((p->addr & 0xF0000000) == 0x00000000) // 0aaaaaaa 0000000vv { memory.IdempotentWrite8(p->addr & 0x0FFFFFFF, (u8)p->data & 0x000000FF); state.prev_cheat_type = 0; } else if ((p->addr & 0xF0000000) == 0x10000000) // 1aaaaaaa 0000vvvv { memory.IdempotentWrite16(p->addr & 0x0FFFFFFF, (u16)p->data & 0x0000FFFF); state.prev_cheat_type = 0; } else if ((p->addr & 0xF0000000) == 0x20000000) // 2aaaaaaa vvvvvvvv { memory.IdempotentWrite32(p->addr & 0x0FFFFFFF, (u32)p->data); state.prev_cheat_type = 0; } else if ((p->addr & 0xFFFF0000) == 0x30000000) // 300000vv 0aaaaaaa Inc { u8 mem = memory.Read8((u32)p->data); memory.Write8((u32)p->data, mem + (p->addr & 0x000000FF)); state.prev_cheat_type = 0; } else if ((p->addr & 0xFFFF0000) == 0x30100000) // 301000vv 0aaaaaaa Dec { u8 mem = memory.Read8((u32)p->data); memory.Write8((u32)p->data, mem - (p->addr & 0x000000FF)); state.prev_cheat_type = 0; } else if ((p->addr & 0xFFFF0000) == 0x30200000) // 3020vvvv 0aaaaaaa Inc { u16 mem = memory.Read16((u32)p->data); memory.Write16((u32)p->data, mem + (p->addr & 0x0000FFFF)); state.prev_cheat_type = 0; } else if ((p->addr & 0xFFFF0000) == 0x30300000) // 3030vvvv 0aaaaaaa Dec { u16 mem = memory.Read16((u32)p->data); memory.Write16((u32)p->data, mem - (p->addr & 0x0000FFFF)); state.prev_cheat_type = 0; } else if ((p->addr & 0xFFFF0000) == 0x30400000) // 30400000 0aaaaaaa Inc + Another line { state.prev_cheat_type = 0x3040; state.prev_cheat_addr = (u32)p->data; } else if ((p->addr & 0xFFFF0000) == 0x30500000) // 30500000 0aaaaaaa Inc + Another line { state.prev_cheat_type = 0x3050; state.prev_cheat_addr = (u32)p->data; } else if ((p->addr & 0xF0000000) == 0x40000000) // 4aaaaaaa nnnnssss + Another line { state.iteration_count = ((u32)p->data & 0xFFFF0000) >> 16; state.iteration_increment = ((u32)p->data & 0x0000FFFF) * 4; state.prev_cheat_addr = (u32)p->addr & 0x0FFFFFFF; state.prev_cheat_type = 0x4000; } else if ((p->addr & 0xF0000000) == 0x50000000) // 5sssssss nnnnnnnn + Another line { state.prev_cheat_addr = (u32)p->addr & 0x0FFFFFFF; state.iteration_count = ((u32)p->data); state.prev_cheat_type = 0x5000; } else if ((p->addr & 0xF0000000) == 0x60000000) // 6aaaaaaa 000000vv + Another line/s { state.prev_cheat_addr = (u32)p->addr & 0x0FFFFFFF; state.iteration_increment = ((u32)p->data); state.iteration_count = 0; state.prev_cheat_type = 0x6000; } else if ((p->addr & 0xF0000000) == 0x70000000) { if ((p->data & 0x00F00000) == 0x00000000) // 7aaaaaaa 000000vv { u8 mem = memory.Read8((u32)p->addr & 0x0FFFFFFF); memory.Write8((u32)p->addr & 0x0FFFFFFF, (u8)(mem | (p->data & 0x000000FF))); } else if ((p->data & 0x00F00000) == 0x00100000) // 7aaaaaaa 0010vvvv { u16 mem = memory.Read16((u32)p->addr & 0x0FFFFFFF); memory.Write16((u32)p->addr & 0x0FFFFFFF, (u16)(mem | (p->data & 0x0000FFFF))); } else if ((p->data & 0x00F00000) == 0x00200000) // 7aaaaaaa 002000vv { u8 mem = memory.Read8((u32)p->addr & 0x0FFFFFFF); memory.Write8((u32)p->addr & 0x0FFFFFFF, (u8)(mem & (p->data & 0x000000FF))); } else if ((p->data & 0x00F00000) == 0x00300000) // 7aaaaaaa 0030vvvv { u16 mem = memory.Read16((u32)p->addr & 0x0FFFFFFF); memory.Write16((u32)p->addr & 0x0FFFFFFF, (u16)(mem & (p->data & 0x0000FFFF))); } else if ((p->data & 0x00F00000) == 0x00400000) // 7aaaaaaa 004000vv { u8 mem = memory.Read8((u32)p->addr & 0x0FFFFFFF); memory.Write8((u32)p->addr & 0x0FFFFFFF, (u8)(mem ^ (p->data & 0x000000FF))); } else if ((p->data & 0x00F00000) == 0x00500000) // 7aaaaaaa 0050vvvv { u16 mem = memory.Read16((u32)p->addr & 0x0FFFFFFF); memory.Write16((u32)p->addr & 0x0FFFFFFF, (u16)(mem ^ (p->data & 0x0000FFFF))); } } else if ((p->addr & 0xF0000000) == 0xD0000000 || (p->addr & 0xF0000000) == 0xE0000000) { u32 addr = (u32)p->addr; u32 data = (u32)p->data; // Since D-codes now have the additional functionality present in PS2rd which // incorporates E-code-like functionality by making use of the unused bits in // D-codes, the E-codes are now just converted to D-codes to reduce bloat. if ((addr & 0xF0000000) == 0xE0000000) { // Ezyyvvvv taaaaaaa -> Daaaaaaa yytzvvvv addr = 0xD0000000 | ((u32)p->data & 0x0FFFFFFF); data = 0x00000000 | ((u32)p->addr & 0x0000FFFF); data = data | ((u32)p->addr & 0x00FF0000) << 8; data = data | ((u32)p->addr & 0x0F000000) >> 8; data = data | ((u32)p->data & 0xF0000000) >> 8; } const u8 type = (data & 0x000F0000) >> 16; const u8 cond = (data & 0x00F00000) >> 20; if (cond == 0) // Daaaaaaa yy0zvvvv { if (type == 0) // Daaaaaaa yy00vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem != (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy0100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem != (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 1) // Daaaaaaa yy1zvvvv { if (type == 0) // Daaaaaaa yy10vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem == (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy1100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem == (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 2) // Daaaaaaa yy2zvvvv { if (type == 0) // Daaaaaaa yy20vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem >= (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy2100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem >= (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 3) // Daaaaaaa yy3zvvvv { if (type == 0) // Daaaaaaa yy30vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem <= (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy3100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem <= (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 4) // Daaaaaaa yy4zvvvv { if (type == 0) // Daaaaaaa yy40vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem & (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy4100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem & (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 5) // Daaaaaaa yy5zvvvv { if (type == 0) // Daaaaaaa yy50vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (!(mem & (data & 0x0000FFFF))) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy5100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (!(mem & (data & 0x000000FF))) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 6) // Daaaaaaa yy6zvvvv { if (type == 0) // Daaaaaaa yy60vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (mem | (data & 0x0000FFFF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy6100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (mem | (data & 0x000000FF)) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } else if (cond == 7) // Daaaaaaa yy7zvvvv { if (type == 0) // Daaaaaaa yy70vvvv { u16 mem = memory.Read16(addr & 0x0FFFFFFF); if (!(mem | (data & 0x0000FFFF))) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } else if (type == 1) // Daaaaaaa yy7100vv { u8 mem = memory.Read8(addr & 0x0FFFFFFF); if (!(mem | (data & 0x000000FF))) { state.skip_count = (data & 0xFF000000) >> 24; if (!state.skip_count) { state.skip_count = 1; } } state.prev_cheat_type = 0; } } } } } template requires std::is_base_of_v && std::is_base_of_v void Patch::ApplyPatch(const PatchCommand* p, EEMemory& ee, IOPMemory& iop, ExtendedState& state) { switch (p->cpu) { case CPU_EE: { switch (p->type) { case BYTE_T: { ee.IdempotentWrite8(p->addr, static_cast(p->data)); break; } case SHORT_T: { ee.IdempotentWrite16(p->addr, static_cast(p->data)); break; } case WORD_T: { ee.IdempotentWrite32(p->addr, static_cast(p->data)); break; } case DOUBLE_T: { ee.IdempotentWrite64(p->addr, p->data); break; } case EXTENDED_T: { handle_extended_t(p, ee, state); break; } case SHORT_BE_T: { u16 value = ByteSwap(static_cast(p->data)); ee.IdempotentWrite16(p->addr, value); break; } case WORD_BE_T: { u32 value = ByteSwap(static_cast(p->data)); ee.IdempotentWrite32(p->addr, value); break; } case DOUBLE_BE_T: { u64 value = ByteSwap(p->data); ee.IdempotentWrite64(p->addr, value); break; } case BYTES_T: { // We compare before writing so the rec doesn't get upset and invalidate when there's no change. ee.IdempotentWriteBytes(p->addr, p->data_ptr, static_cast(p->data)); break; } default: { break; } } break; } case CPU_IOP: { switch (p->type) { case BYTE_T: { iop.IdempotentWrite(p->addr, static_cast(p->data)); break; } case SHORT_T: { iop.IdempotentWrite16(p->addr, static_cast(p->data)); break; } case WORD_T: { iop.IdempotentWrite32(p->addr, static_cast(p->data)); break; } case BYTES_T: { iop.IdempotentWriteBytes(p->addr, p->data_ptr, static_cast(p->data)); break; } default: { break; } } break; } default: { break; } } } void Patch::ApplyDynaPatch(const DynamicPatch& patch, u32 address) { for (const auto& pattern : patch.pattern) { if (*static_cast(PSM(address + pattern.offset)) != pattern.value) return; } Console.WriteLn("Applying Dynamic Patch to address 0x%08X", address); // If everything passes, apply the patch. for (const auto& replacement : patch.replacement) { memWrite32(address + replacement.offset, replacement.value); } } const char* Patch::PlaceToString(std::optional place) { if (!place.has_value()) //: Time when a patch is applied. return TRANSLATE("Patch", "Unknown"); switch (*place) { case Patch::PPT_ONCE_ON_LOAD: //: Time when a patch is applied. return TRANSLATE("Patch", "Only On Startup"); case Patch::PPT_CONTINUOUSLY: //: Time when a patch is applied. return TRANSLATE("Patch", "Every Frame"); case Patch::PPT_COMBINED_0_1: //: Time when a patch is applied. return TRANSLATE("Patch", "On Startup & Every Frame"); case Patch::PPT_ON_LOAD_OR_WHEN_ENABLED: //: Time when a patch is applied. return TRANSLATE("Patch", "On Startup & When Enabled"); default: { } } return ""; }