mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
A skip cutscene cheat for God of War 2 keeps working with RetroAchievements Hardcore active. It is not a user file. It ships in our own patches.zip: gametitle=God of War 2 (SCUS-97481) [Widescreen 16:9] gsaspectratio=16:9 patch=1,EE,00234A48,word,46000406 [Skip Cutscenes] author=Ezedequias patch=1,EE,202D8194,byte,01 Hardcore only ever gated the cheats side. ReloadEnabledLists empties the enabled cheats list, the on-disk walk skips the cheats folder, and the cheat enable call sits behind EnableCheats. The patches list gets none of that: it is re-read verbatim and applied unconditionally, not even behind EnablePatches. Anything filed as a patch has always been exempt, and place=1 reapplies it every vsync. Neither of the obvious rules can separate those two groups. They live in the same pnach, so file location cannot, and the widescreen one writes EE memory too, so "block memory writes" would take widescreen with it. What does separate them is whether the group says what it is for. gsaspectratio or gsinterlacemode means widescreen or no-interlacing, which we deliberately keep working under Hardcore. A group that declares nothing and only writes memory is a cheat whatever its label says. Measured against the shipped database before settling on it: 1284 groups declare presentation and stay, 478 write memory with nothing declared and now stop. The ones that stop read like Car select, Auto-activate analogs and Throttle/brake on right stick. On the reported game, Widescreen 16:9 stays and Skip Cutscenes goes. The check runs at group selection rather than against the enable list, because an unlabelled group never consults that list and would have sailed through a filter applied there. GameDB patches are left alone on purpose. They are a curated compatibility layer, and dropping them under Hardcore would break games instead of stopping cheating. Worth knowing what this does not cover: a pnach author can still launder a cheat by pasting a gsaspectratio line into the group. That stops shipped and accidental content, not somebody determined to cheat themselves.
1902 lines
58 KiB
C++
1902 lines
58 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.
|
|
std::vector<std::string> disk_patch_files;
|
|
if (for_ui || !cheats || !Achievements::IsHardcoreModeActive())
|
|
disk_patch_files = FindPatchFilesOnDisk(serial, crc, cheats, for_ui);
|
|
|
|
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.
|
|
if (cheats || unlabeled_patch_found || !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.
|
|
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<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)));
|
|
|
|
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 "";
|
|
}
|