mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Reported by EddyOP (60 FPS and Widescreen disabled under RetroAchievements
Hardcore) and diagnosed by Jetup, who found that moving the same lines under a
widescreen heading re-enabled them — ARMSX2 issue #541.
The Hardcore gate from 2f3f28faae allows a group through only if it DECLARES
gsaspectratio or gsinterlacemode. That rule was picked to separate a Skip
Cutscenes cheat from a Widescreen patch living in the same pnach, and it does
separate those two. What it does not survive is the rest of the database.
Measured across the shipped 4437 files, the groups it blocked were overwhelmingly
not cheats:
305 50 FPS
177 60 FPS
168 50/60 FPS
102 Remove Blackbars <- a widescreen patch
100 NTSC Mode
31 480p Mode
Almost every real presentation patch changes the picture by writing EE memory and
declares nothing, so the declaration test caught the tail and missed the bulk.
The original commit sampled Car select and Auto-activate analogs and concluded
the blocked set read like cheats; across the whole database it does not.
So the name is now consulted too, matched case-insensitively against the labels
the database actually uses. Matched on the EFFECT rather than the verb — Remove
Blur, Disable Blur and No Blur are the same patch, and listing verbs caught one
spelling while blocking the others.
After: 4673 groups stay by declaration, 1206 more stay by name, 495 remain
blocked. What still blocks reads the way it should — Adjusted triggers
sensitivity, Trigger control mappings, GT3 Chase Camera, and Skip Cutscenes
itself, which is the case the gate was written for.
An unrecognised name still fails CLOSED, which is the safe direction: a new cheat
is blocked by default and only an understood presentation class is let through.
★ Frame-rate patches are the judgement call here. They are in the allowlist
because they are what was reported broken and desktop PCSX2 permits them, but
they are the entry RetroAchievements is most likely to object to. If RA rules
them out, deleting three strings from ALLOWED is the whole change.
1980 lines
62 KiB
C++
1980 lines
62 KiB
C++
// 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 <algorithm>
|
|
#include <cstring>
|
|
#include <span>
|
|
#include <sstream>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
namespace Patch
|
|
{
|
|
template <typename EnumType, class ArrayType>
|
|
static inline std::optional<EnumType> 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<EnumType>(i);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
struct PatchGroup
|
|
{
|
|
std::string name;
|
|
std::optional<float> override_aspect_ratio;
|
|
std::optional<GSInterlaceMode> override_interlace_mode;
|
|
std::vector<PatchCommand> patches;
|
|
std::vector<DynamicPatch> 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<const PatchTextTable>& Table);
|
|
static void LoadPatchLine(PatchGroup* group, const std::string_view line);
|
|
static u32 LoadPatchesFromString(std::vector<PatchGroup>* 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<std::string> FindPatchFilesOnDisk(
|
|
const std::string_view serial, u32 crc, bool cheats, bool all_crcs);
|
|
|
|
static bool ContainsPatchName(const std::vector<PatchInfo>& patches, const std::string_view patchName);
|
|
static bool ContainsPatchName(const std::vector<PatchGroup>& patches, const std::string_view patchName);
|
|
|
|
template <typename F>
|
|
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<PatchInfo>* dst, const std::string& pnach_data, u32* num_unlabelled_patches);
|
|
static void ReloadEnabledLists();
|
|
static u32 EnablePatches(const std::vector<PatchGroup>* patches, const std::vector<std::string>& enable_list,
|
|
const std::vector<std::string>* enable_immediately_list, bool hardcore_safe_only = false);
|
|
|
|
template <typename EEMemory, typename IOPMemory>
|
|
requires std::is_base_of_v<MemoryInterface, EEMemory> &&
|
|
std::is_base_of_v<MemoryInterface, IOPMemory>
|
|
void ApplyPatch(const PatchCommand* p, EEMemory& ee, IOPMemory& iop, ExtendedState& state);
|
|
static void ApplyDynaPatch(const DynamicPatch& patch, u32 address);
|
|
template <typename Memory>
|
|
requires std::is_base_of_v<MemoryInterface, Memory>
|
|
static void writeCheat(Memory& memory, ExtendedState& state);
|
|
template <typename Memory>
|
|
requires std::is_base_of_v<MemoryInterface, Memory>
|
|
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<PatchGroup> s_gamedb_patches;
|
|
static std::vector<PatchGroup> s_game_patches;
|
|
static std::vector<PatchGroup> s_cheat_patches;
|
|
|
|
static u32 s_gamedb_counts = 0;
|
|
static u32 s_patches_counts = 0;
|
|
static u32 s_cheats_counts = 0;
|
|
|
|
static std::vector<const PatchCommand*> s_active_patches;
|
|
static std::vector<DynamicPatch> s_active_gamedb_dynamic_patches;
|
|
static std::vector<DynamicPatch> s_active_pnach_dynamic_patches;
|
|
static std::vector<std::string> s_enabled_cheats;
|
|
static std::vector<std::string> s_enabled_patches;
|
|
static std::vector<std::string> s_just_enabled_cheats;
|
|
static std::vector<std::string> s_just_enabled_patches;
|
|
static u32 s_patches_crc;
|
|
static std::optional<float> s_override_aspect_ratio;
|
|
static std::optional<GSInterlaceMode> 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<PatchGroup>& 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<const PatchTextTable>& 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<PatchGroup>* 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<PatchGroup>::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<u32>(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<std::string> 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<std::string> 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<PatchInfo>& 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 <typename F>
|
|
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<std::string> 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<std::string> 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<std::string> 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<PatchInfo>* dst, const std::string& pnach_data, u32* num_unlabelled_patches)
|
|
{
|
|
std::istringstream ss(pnach_data);
|
|
std::string line;
|
|
PatchInfo current_patch;
|
|
|
|
std::optional<patch_place_type> 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<patch_place_type> place = LookupEnumName<patch_place_type>(
|
|
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::PatchInfo> Patch::GetPatchInfo(const std::string_view serial, u32 crc, bool cheats, bool showAllCRCS, u32* num_unlabelled_patches)
|
|
{
|
|
std::vector<PatchInfo> 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<std::string> 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<std::string> prev_enabled_patches = std::exchange(s_enabled_patches, Host::GetStringListSetting(PATCHES_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY));
|
|
const std::vector<std::string> 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.
|
|
/// Does this group's NAME describe a presentation change rather than a gameplay one?
|
|
///
|
|
/// ★ Needed because "declares gsaspectratio or gsinterlacemode" is not the line it looks like.
|
|
///
|
|
/// That rule was chosen to separate a Skip Cutscenes cheat from a Widescreen patch shipping in
|
|
/// the same pnach, and it does. But measured across the whole shipped database the groups it
|
|
/// blocks are overwhelmingly NOT cheats: 305 "50 FPS", 177 "60 FPS", 168 "50/60 FPS", 102
|
|
/// "Remove Blackbars" (a widescreen patch), 100 "NTSC Mode", 31 "480p Mode", and the blur and
|
|
/// ghosting removers. Almost every real presentation patch changes the picture by writing EE
|
|
/// memory and declares nothing, so the declaration test caught the tail and missed the bulk —
|
|
/// reported by EddyOP and Jetup, who found that moving lines under a widescreen heading was
|
|
/// enough to re-enable them.
|
|
///
|
|
/// Names are matched case-insensitively as substrings, against the labels the community patch
|
|
/// database actually uses. This is deliberately a list of PRESENTATION classes rather than a
|
|
/// blocklist of cheats: an unknown name still fails closed, which is the safe direction.
|
|
static bool IsPresentationPatchName(const std::string& name)
|
|
{
|
|
if (name.empty())
|
|
return false;
|
|
|
|
std::string lower(name);
|
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
static constexpr const std::string_view ALLOWED[] = {
|
|
// Aspect / framing
|
|
"widescreen", "16:9", "16/9", "blackbar", "black bar", "letterbox",
|
|
// Scan-out
|
|
"interlac", "ntsc mode", "pal mode", "480p", "progressive", "scan mode",
|
|
// Frame rate. Contentious elsewhere, but it is what was reported as broken, it is
|
|
// allowed by desktop PCSX2, and it changes how the picture is delivered rather than
|
|
// what the game will let the player do.
|
|
"fps", "frame rate", "framerate",
|
|
// Post-processing the game applies to its own output. Matched on the EFFECT rather than
|
|
// the verb: the database says Remove Blur, Disable Blur and No Blur for the same thing,
|
|
// and listing verbs meant catching one spelling and blocking the others.
|
|
"blur", "bloom", "ghosting", "dither", "noise filter", "depth of field", " dof",
|
|
"color filter", "colour filter", "brown filter", "grain",
|
|
// HUD repositioning ships alongside widescreen and is meaningless without it.
|
|
"correct hud", "hud fix", "fix hud",
|
|
};
|
|
|
|
for (const std::string_view a : ALLOWED)
|
|
{
|
|
if (lower.find(a) != std::string::npos)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool IsHardcoreSafePatchGroup(const Patch::PatchGroup& p)
|
|
{
|
|
if (p.override_aspect_ratio.has_value() || p.override_interlace_mode.has_value())
|
|
return true;
|
|
|
|
// Named as a presentation change. See IsPresentationPatchName for why the declaration test
|
|
// alone is not sufficient.
|
|
if (IsPresentationPatchName(p.name))
|
|
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<PatchGroup>* patches, const std::vector<std::string>& enable_list,
|
|
const std::vector<std::string>* 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("<unlabelled>") : 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("<unknown>") : 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<u32>(p.patches.size()) + static_cast<u32>(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<std::string>(), 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<int>(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<u8>(EmuConfig.GS.AspectRatio)]);
|
|
for (u32 i = 0; i < static_cast<u32>(AspectRatioType::MaxCount); i++)
|
|
{
|
|
if (ar_value == Pcsx2Config::GSOptions::AspectRatioNames[i])
|
|
{
|
|
new_ar = static_cast<AspectRatioType>(i);
|
|
break;
|
|
}
|
|
}
|
|
EmuConfig.GS.AspectRatio = new_ar;
|
|
EmuConfig.GS.InterlaceMode = static_cast<GSInterlaceMode>(Host::GetIntSettingValue(
|
|
"EmuCore/GS", "deinterlace_mode", static_cast<int>(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<std::string_view> 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<patch_place_type> placetopatch = LookupEnumName<patch_place_type>(pieces[0], s_place_to_string);
|
|
const std::optional<patch_cpu_type> cpu = LookupEnumName<patch_cpu_type>(pieces[1], s_cpu_to_string);
|
|
const std::optional<u32> addr = StringUtil::FromChars<u32>(pieces[2], 16, &addr_end);
|
|
const std::optional<patch_data_type> type = LookupEnumName<patch_data_type>(pieces[3], s_type_to_string);
|
|
std::optional<u64> data = StringUtil::FromChars<u64>(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<std::vector<u8>> 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<u8*>(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<float>(dividend) / static_cast<float>(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<int> interlace_mode = StringUtil::FromChars<int>(param);
|
|
if (!interlace_mode.has_value() || interlace_mode.value() < 0 ||
|
|
interlace_mode.value() >= static_cast<int>(GSInterlaceMode::Count))
|
|
{
|
|
Console.Error(fmt::format("Patch error: {} is an unknown interlace mode.", param));
|
|
return;
|
|
}
|
|
|
|
group->override_interlace_mode = static_cast<GSInterlaceMode>(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<std::string_view> 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<u32> dpatch_type = StringUtil::FromChars<u32>(pieces[0]);
|
|
|
|
std::optional<u32> num_patterns = StringUtil::FromChars<u32>(pieces[1], 16, &patterns_end);
|
|
std::optional<u32> num_replacements = StringUtil::FromChars<u32>(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<u32> offset = StringUtil::FromChars<u32>(pieces[3 + (i * 2)], 16);
|
|
std::optional<u32> value = StringUtil::FromChars<u32>(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<u32> offset = StringUtil::FromChars<u32>(pieces[3 + (num_patterns.value() * 2) + (i * 2)], 16);
|
|
std::optional<u32> value = StringUtil::FromChars<u32>(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<const PatchCommand*>& 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<const PatchCommand*>& 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<DynamicPatch>& patches)
|
|
{
|
|
for (const DynamicPatch& it : patches)
|
|
s_active_gamedb_dynamic_patches.push_back(it);
|
|
}
|
|
|
|
template <typename Memory>
|
|
requires std::is_base_of_v<MemoryInterface, Memory>
|
|
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 <typename Memory>
|
|
requires std::is_base_of_v<MemoryInterface, Memory>
|
|
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 <typename EEMemory, typename IOPMemory>
|
|
requires std::is_base_of_v<MemoryInterface, EEMemory> &&
|
|
std::is_base_of_v<MemoryInterface, IOPMemory>
|
|
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<u8>(p->data));
|
|
break;
|
|
}
|
|
case SHORT_T:
|
|
{
|
|
ee.IdempotentWrite16(p->addr, static_cast<u16>(p->data));
|
|
break;
|
|
}
|
|
case WORD_T:
|
|
{
|
|
ee.IdempotentWrite32(p->addr, static_cast<u32>(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<u16>(p->data));
|
|
ee.IdempotentWrite16(p->addr, value);
|
|
break;
|
|
}
|
|
case WORD_BE_T:
|
|
{
|
|
u32 value = ByteSwap(static_cast<u32>(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<u32>(p->data));
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case CPU_IOP:
|
|
{
|
|
switch (p->type)
|
|
{
|
|
case BYTE_T:
|
|
{
|
|
iop.IdempotentWrite(p->addr, static_cast<u8>(p->data));
|
|
break;
|
|
}
|
|
case SHORT_T:
|
|
{
|
|
iop.IdempotentWrite16(p->addr, static_cast<u16>(p->data));
|
|
break;
|
|
}
|
|
case WORD_T:
|
|
{
|
|
iop.IdempotentWrite32(p->addr, static_cast<u32>(p->data));
|
|
break;
|
|
}
|
|
case BYTES_T:
|
|
{
|
|
iop.IdempotentWriteBytes(p->addr, p->data_ptr, static_cast<u32>(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<u32*>(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<patch_place_type> 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 "";
|
|
}
|