diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3bd9a7029..41ce19e53 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -85,6 +85,8 @@ add_library(core memory_card.h memory_card_image.cpp memory_card_image.h + memory_scanner.cpp + memory_scanner.h mips_encoder.h multitap.cpp multitap.h diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 186650b15..8661f62ab 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -2,47 +2,1549 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cheats.h" +#include "achievements.h" #include "bus.h" #include "controller.h" #include "cpu_core.h" #include "host.h" #include "system.h" +#include "util/imgui_manager.h" + +#include "common/error.h" #include "common/file_system.h" #include "common/log.h" +#include "common/path.h" #include "common/small_string.h" #include "common/string_util.h" -#include -#include +#include "IconsEmoji.h" +#include "IconsFontAwesome5.h" +#include "fmt/format.h" + #include -#include LOG_CHANNEL(Cheats); -static std::array cht_register; // Used for D7 ,51 & 52 cheat types - -using KeyValuePairVector = std::vector>; - -static bool IsValidScanAddress(PhysicalMemoryAddress address) +namespace { +class CheatFileReader { - if ((address & CPU::SCRATCHPAD_ADDR_MASK) == CPU::SCRATCHPAD_ADDR && - (address & CPU::SCRATCHPAD_OFFSET_MASK) < CPU::SCRATCHPAD_SIZE) +public: + CheatFileReader(const std::string_view contents); + + ALWAYS_INLINE size_t GetCurrentOffset() const { return m_current_offset; } + ALWAYS_INLINE size_t GetCurrentLineOffset() const { return m_current_line_offset; } + ALWAYS_INLINE u32 GetCurrentLineNumber() const { return m_current_line_number; } + + bool GetLine(std::string_view* line); + std::optional GetLine(); + + template + bool LogError(Error* error, bool stop_on_error, fmt::format_string fmt, T&&... args) { - return true; + if (!stop_on_error) + { + Log::WriteFmtArgs(___LogChannel___, Log::Level::Warning, fmt, fmt::make_format_args(args...)); + return true; + } + + if (error) + error->SetString(fmt::vformat(fmt, fmt::make_format_args(args...))); + + return false; } - address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; +private: + const std::string_view m_contents; + size_t m_current_offset = 0; + size_t m_current_line_offset = 0; + u32 m_current_line_number = 0; +}; - if (address < Bus::RAM_MIRROR_END) - return true; +CheatFileReader::CheatFileReader(const std::string_view contents) : m_contents(contents) +{ +} - if (address >= Bus::BIOS_BASE && address < (Bus::BIOS_BASE + Bus::BIOS_SIZE)) - return true; +bool CheatFileReader::GetLine(std::string_view* line) +{ + const size_t length = m_contents.length(); + if (m_current_offset == length) + { + m_current_line_offset = m_current_offset; + return false; + } + + size_t end_position = m_current_offset + 1; + for (; end_position < length; end_position++) + { + // ignore carriage returns + if (m_contents[end_position] == '\r') + continue; + + if (m_contents[end_position] == '\n') + break; + } + + m_current_line_number++; + m_current_line_offset = m_current_offset; + *line = m_contents.substr(m_current_offset, end_position - m_current_offset); + m_current_offset = std::min(end_position + 1, length); + return true; +} + +std::optional CheatFileReader::GetLine() +{ + std::optional ret = std::string_view(); + if (!GetLine(&ret.value())) + ret.reset(); + return ret; +} +} // namespace + +namespace Cheats { + +namespace { +/// Represents a cheat code, after being parsed. +class CheatCode +{ +public: + CheatCode(std::string name, CodeType type, CodeActivation activation); + virtual ~CheatCode(); + + ALWAYS_INLINE const std::string& GetName() const { return m_name; } + ALWAYS_INLINE CodeActivation GetActivation() const { return m_activation; } + ALWAYS_INLINE bool IsManuallyActivated() const { return (m_activation == CodeActivation::Manual); } + + void SetName(std::string name) { m_name = std::move(name); } + void SetType(CodeType type) { m_type = type; } + void SetActivation(CodeActivation activaton) { m_activation = activaton; } + + virtual void Apply() const = 0; + virtual void ApplyOnDisable() const = 0; + +protected: + std::string m_name; + CodeType m_type; + CodeActivation m_activation; +}; +} // namespace + +using CheatCodeList = std::vector>; +using ActiveCodeList = std::vector; +using EnableCodeList = std::vector; + +static std::string GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard); +static std::vector FindPatchFilesOnDisk(const std::string_view serial, std::optional hash, + bool cheats); +static void ExtractCodeInfo(CodeInfoList* dst, const std::string& file_data, bool from_database); +static void AppendCheatToList(CodeInfoList* dst, CodeInfo code); +static std::string FormatCodeForFile(const CodeInfo& code); +static bool UpdateCodeInFile(const char* path, const std::string_view name, const CodeInfo* code, Error* error); + +static bool ShouldLoadDatabaseCheats(); +static void ReloadEnabledLists(); +static u32 EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const std::string_view type); +static void UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed); + +template +static void EnumerateChtFiles(const std::string_view serial, std::optional hash, bool cheats, bool for_ui, + bool load_from_database, const F& f); + +extern void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); + +static Cheats::FileFormat DetectFileFormat(const std::string_view file_contents); +static bool ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); +static bool ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, + Error* error); +static bool ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); + +static std::unique_ptr ParseGamesharkCode(std::string name, CodeActivation activation, + const std::string_view data, Error* error); + +const char* PATCHES_CONFIG_SECTION = "Patches"; +const char* CHEATS_CONFIG_SECTION = "Cheats"; +const char* PATCH_ENABLE_CONFIG_KEY = "Enable"; + +// static zip_t* s_patches_zip; +// static zip_t* s_cheats_zip; +static CheatCodeList s_patch_codes; +static CheatCodeList s_cheat_codes; +static EnableCodeList s_enabled_cheats; +static EnableCodeList s_enabled_patches; + +static ActiveCodeList s_frame_end_codes; +} // namespace Cheats + +Cheats::CheatCode::CheatCode(std::string name, CodeType type, CodeActivation activation) + : m_name(std::move(name)), m_type(type), m_activation(activation) +{ +} + +Cheats::CheatCode::~CheatCode() = default; + +static std::array s_cheat_code_type_names = {{"Gameshark"}}; +static std::array s_cheat_code_type_display_names{{TRANSLATE_NOOP("Cheats", "Gameshark")}}; + +const char* Cheats::GetTypeName(CodeType type) +{ + return s_cheat_code_type_names[static_cast(type)]; +} + +const char* Cheats::GetTypeDisplayName(CodeType type) +{ + return TRANSLATE("Cheats", s_cheat_code_type_display_names[static_cast(type)]); +} + +std::optional Cheats::ParseTypeName(const std::string_view str) +{ + for (size_t i = 0; i < s_cheat_code_type_names.size(); i++) + { + if (str == s_cheat_code_type_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + +static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; +static std::array s_cheat_code_activation_display_names{ + {TRANSLATE_NOOP("Cheats", "Manual"), TRANSLATE_NOOP("Cheats", "Automatic (Frame End)")}}; + +const char* Cheats::GetActivationName(CodeActivation activation) +{ + return s_cheat_code_activation_names[static_cast(activation)]; +} + +const char* Cheats::GetActivationDisplayName(CodeActivation activation) +{ + return TRANSLATE("Cheats", s_cheat_code_activation_display_names[static_cast(activation)]); +} + +std::optional Cheats::ParseActivationName(const std::string_view str) +{ + for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) + { + if (str == s_cheat_code_activation_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + +std::string Cheats::GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard) +{ + if (!hash.has_value()) + return fmt::format("{}{}.cht", serial, add_wildcard ? "*" : ""); + else + return fmt::format("{}_{:016X}{}.cht", serial, hash.value(), add_wildcard ? "*" : ""); +} + +std::vector Cheats::FindPatchFilesOnDisk(const std::string_view serial, std::optional hash, + bool cheats) +{ + std::vector ret; + FileSystem::FindResultsArray files; + FileSystem::FindFiles(cheats ? EmuFolders::Cheats.c_str() : EmuFolders::Patches.c_str(), + GetChtTemplate(serial, hash, true).c_str(), + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files); + ret.reserve(files.size()); + for (FILESYSTEM_FIND_DATA& fd : files) + { + // Skip mismatched hashes. + if (hash.has_value()) + { + if (const std::string_view filename = Path::GetFileTitle(fd.FileName); filename.length() >= serial.length() + 18) + { + const std::string_view filename_hash = filename.substr(serial.length() + 1, 16); + const std::optional filename_parsed_hash = StringUtil::FromChars(filename_hash, 16); + if (filename_parsed_hash.has_value() && filename_parsed_hash.value() != hash.value()) + continue; + } + } + ret.push_back(std::move(fd.FileName)); + } + + return ret; +} + +template +void Cheats::EnumerateChtFiles(const std::string_view serial, std::optional hash, bool cheats, bool for_ui, + bool load_from_database, const F& f) +{ + // Prefer files on disk over the zip. + std::vector disk_patch_files; + if (for_ui || !Achievements::IsHardcoreModeActive()) + disk_patch_files = FindPatchFilesOnDisk(serial, for_ui ? hash : std::nullopt, cheats); + + Error error; + if (!disk_patch_files.empty()) + { + for (const std::string& file : disk_patch_files) + { + const std::optional contents = FileSystem::ReadFileToString(file.c_str(), &error); + if (contents.has_value()) + f(std::move(file), std::move(contents.value()), false); + else + WARNING_LOG("Failed to read cht file '{}': {}", Path::GetFileName(file), error.GetDescription()); + } + } + +#if 0 + // Otherwise fall back to the zip. + if (!(cheats ? OpenCheatsZip() : OpenPatchesZip())) + return; + + // Prefer filename with hash. + const zip_t* zipfile = cheats ? s_cheats_zip : s_patches_zip; + std::string zip_filename = GetChtTemplate(serial, hash, false); + std::optional data = ReadFileInZipToString(zipfile, zip_filename.c_str()); + if (!data.has_value() && hash.has_value()) + { + // Try without the hash. + zip_filename = GetChtTemplate(serial, std::nullopt, false); + data = ReadFileInZipToString(zipfile, zip_filename.c_str()); + } + if (data.has_value()) + f(std::move(zip_filename), std::move(data.value()), true); +#endif +} + +std::string_view Cheats::CodeInfo::GetNamePart() const +{ + const std::string::size_type pos = name.rfind('\\'); + std::string_view ret = name; + if (pos != std::string::npos) + ret = ret.substr(pos + 1); + return ret; +} + +std::string_view Cheats::CodeInfo::GetNameParentPart() const +{ + const std::string::size_type pos = name.rfind('\\'); + std::string_view ret; + if (pos != std::string::npos) + ret = std::string_view(name).substr(0, pos); + return ret; +} + +Cheats::CodeInfoList Cheats::GetCodeInfoList(const std::string_view serial, std::optional hash, bool cheats, + bool load_from_database) +{ + CodeInfoList ret; + + EnumerateChtFiles(serial, hash, cheats, true, load_from_database, + [&ret](const std::string& filename, const std::string& data, bool from_database) { + ExtractCodeInfo(&ret, data, from_database); + }); + + return ret; +} + +const Cheats::CodeInfo* Cheats::FindCodeInInfoList(const CodeInfoList& list, const std::string_view name) +{ + const auto it = std::find_if(list.cbegin(), list.cend(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); + return (it != list.end()) ? &(*it) : nullptr; +} + +Cheats::CodeInfo* Cheats::FindCodeInInfoList(CodeInfoList& list, const std::string_view name) +{ + const auto it = std::find_if(list.begin(), list.end(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); + return (it != list.end()) ? &(*it) : nullptr; +} + +std::string Cheats::FormatCodeForFile(const CodeInfo& code) +{ + return fmt::format("[{}]\n" + "Type = {}\n" + "Activation = {}\n" + "{}\n", + code.name, GetTypeName(code.type), GetActivationName(code.activation), code.body); +} + +bool Cheats::UpdateCodeInFile(const char* path, const std::string_view name, const CodeInfo* code, Error* error) +{ + std::string file_contents; + if (FileSystem::FileExists(path)) + { + std::optional ofile_contents = FileSystem::ReadFileToString(path, error); + if (!ofile_contents.has_value()) + { + Error::AddPrefix(error, "Failed to read existing file: "); + return false; + } + file_contents = std::move(ofile_contents.value()); + } + + // This is a bit crap, we're allocating everything and then tossing it away. + // Hopefully it won't fragment too much at least, because it's freed in reverse order... + std::optional replace_start, replace_end; + if (!file_contents.empty()) + { + CodeInfoList existing_codes_in_file; + ExtractCodeInfo(&existing_codes_in_file, file_contents, false); + + const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, name); + if (existing_code) + { + replace_start = existing_code->file_offset_start; + replace_end = existing_code->file_offset_end; + } + } + + if (replace_start.has_value()) + { + const auto start = file_contents.begin() + replace_start.value(); + const auto end = file_contents.begin() + replace_end.value(); + if (code) + file_contents.replace(start, end, FormatCodeForFile(*code)); + else + file_contents.erase(start, end); + } + else if (code) + { + const std::string code_body = FormatCodeForFile(*code); + file_contents.reserve(file_contents.length() + 1 + code_body.length()); + if (!file_contents.empty() && file_contents.back() != '\n') + file_contents.push_back('\n'); + file_contents.append(code_body); + } + + INFO_LOG("Updating {}...", path); + if (!FileSystem::WriteStringToFile(path, file_contents, error)) + { + Error::AddPrefix(error, "Failed to rewrite file: "); + return false; + } + + return true; +} + +bool Cheats::RemoveCodeFromFile(const char* path, const std::string_view name, Error* error) +{ + return UpdateCodeInFile(path, name, nullptr, error); +} + +bool Cheats::SaveCodeToFile(const char* path, const CodeInfo& code, Error* error) +{ + return UpdateCodeInFile(path, code.name, &code, error); +} + +bool Cheats::SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* error) +{ + std::string file_contents; + if (FileSystem::FileExists(path)) + { + std::optional ofile_contents = FileSystem::ReadFileToString(path, error); + if (!ofile_contents.has_value()) + { + Error::AddPrefix(error, "Failed to read existing file: "); + return false; + } + file_contents = std::move(ofile_contents.value()); + } + + for (const CodeInfo& code : codes) + { + // This is _really_ crap.. but it's only on importing. + std::optional replace_start, replace_end; + if (!file_contents.empty()) + { + CodeInfoList existing_codes_in_file; + ExtractCodeInfo(&existing_codes_in_file, file_contents, false); + + const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, code.name); + if (existing_code) + { + replace_start = existing_code->file_offset_start; + replace_end = existing_code->file_offset_end; + } + } + + if (replace_start.has_value()) + { + const auto start = file_contents.begin() + replace_start.value(); + const auto end = file_contents.begin() + replace_end.value(); + file_contents.replace(start, end, FormatCodeForFile(code)); + } + else + { + const std::string code_body = FormatCodeForFile(code); + file_contents.reserve(file_contents.length() + 1 + code_body.length()); + if (!file_contents.empty() && file_contents.back() != '\n') + file_contents.push_back('\n'); + file_contents.append(code_body); + } + } + + INFO_LOG("Updating {}...", path); + if (!FileSystem::WriteStringToFile(path, file_contents, error)) + { + Error::AddPrefix(error, "Failed to rewrite file: "); + return false; + } + + return true; +} + +void Cheats::MergeCheatList(CodeInfoList* dst, CodeInfoList src) +{ + for (CodeInfo& code : src) + { + CodeInfo* existing_code = FindCodeInInfoList(*dst, code.name); + if (existing_code) + *existing_code = std::move(code); + else + dst->push_back(std::move(code)); + } +} + +std::string Cheats::GetChtFilename(const std::string_view serial, std::optional hash, bool cheats) +{ + return Path::Combine(cheats ? EmuFolders::Cheats : EmuFolders::Patches, GetChtTemplate(serial, hash, false)); +} + +bool Cheats::AreCheatsEnabled() +{ + if (Achievements::IsHardcoreModeActive()) + return false; + + // Only in the gameini. + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + return (sif && sif->GetBoolValue("Cheats", "EnableCheats", false)); +} + +bool Cheats::ShouldLoadDatabaseCheats() +{ + // Only in the gameini. + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + return (sif && sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true)); +} + +void Cheats::ReloadEnabledLists() +{ + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + if (!sif) + { + // no gameini => nothing is going to be enabled. + s_enabled_cheats = {}; + s_enabled_patches = {}; + return; + } + + if (AreCheatsEnabled()) + s_enabled_cheats = sif->GetStringList(CHEATS_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); + else + s_enabled_cheats = {}; + + s_enabled_patches = sif->GetStringList(PATCHES_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); +} + +u32 Cheats::EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const std::string_view type) +{ + u32 count = 0; + for (const std::unique_ptr& p : patches) + { + // ignore manually-activated codes + if (p->IsManuallyActivated()) + continue; + + if (std::find(enable_list.begin(), enable_list.end(), p->GetName()) == enable_list.end()) + continue; + + INFO_LOG("Enabled {}: {}", type, p->GetName()); + + switch (p->GetActivation()) + { + case CodeActivation::EndFrame: + s_frame_end_codes.push_back(p.get()); + break; + + default: + break; + } + + count++; + } + + return count; +} + +void Cheats::ReloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) +{ + for (const CheatCode* code : s_frame_end_codes) + code->ApplyOnDisable(); + + if (reload_files) + { + s_patch_codes.clear(); + s_cheat_codes.clear(); + + if (const std::string& serial = System::GetGameSerial(); !serial.empty()) + { + const GameHash hash = System::GetGameHash(); + + EnumerateChtFiles(serial, hash, false, false, true, + [](const std::string& filename, const std::string& file_contents, bool from_database) { + ParseFile(&s_patch_codes, file_contents); + if (s_patch_codes.size() > 0) + INFO_LOG("Found {} game patches in {}.", s_patch_codes.size(), filename); + }); + + if (AreCheatsEnabled()) + { + EnumerateChtFiles(serial, hash, true, false, ShouldLoadDatabaseCheats(), + [](const std::string& filename, const std::string& file_contents, bool from_database) { + ParseFile(&s_cheat_codes, file_contents); + if (s_cheat_codes.size() > 0) + INFO_LOG("Found {} cheats in {}.", s_cheat_codes.size(), filename); + }); + } + } + } + + UpdateActiveCodes(reload_enabled_list, verbose, verbose_if_changed); +} + +void Cheats::UnloadAll() +{ + s_frame_end_codes = ActiveCodeList(); + s_enabled_patches = EnableCodeList(); + s_enabled_cheats = EnableCodeList(); + s_cheat_codes = CheatCodeList(); + s_patch_codes = CheatCodeList(); +} + +void Cheats::UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed) +{ + if (reload_enabled_list) + ReloadEnabledLists(); + + const size_t prev_count = s_frame_end_codes.size(); + s_frame_end_codes.clear(); + + u32 patch_count = 0; + u32 cheat_count = 0; + + if (!g_settings.disable_all_enhancements) + { + patch_count = EnableCheats(s_patch_codes, s_enabled_patches, "Patch"); + cheat_count = AreCheatsEnabled() ? EnableCheats(s_cheat_codes, s_enabled_cheats, "Cheat") : 0; + } + + // Display message on first boot when we load patches. + // Except when it's just GameDB. + const size_t new_count = s_frame_end_codes.size(); + if (verbose || (verbose_if_changed && prev_count != new_count)) + { + if (patch_count > 0) + { + Host::AddIconOSDMessage("LoadPatches", ICON_FA_BAND_AID, + TRANSLATE_PLURAL_STR("Cheats", "%n game patches are active.", "OSD Message", patch_count), + Host::OSD_INFO_DURATION); + } + if (cheat_count > 0) + { + Host::AddIconOSDMessage( + "LoadCheats", ICON_EMOJI_WARNING, + TRANSLATE_PLURAL_STR("Cheats", "%n cheats are enabled. This may crash games.", "OSD Message", cheat_count), + Host::OSD_WARNING_DURATION); + } + else if (patch_count == 0) + { + Host::RemoveKeyedOSDMessage("LoadPatches"); + Host::AddIconOSDMessage("LoadCheats", ICON_FA_BAND_AID, + TRANSLATE_STR("Cheats", "No cheats/patches are found or enabled."), + Host::OSD_INFO_DURATION); + } + } +} + +void Cheats::ApplyFrameEndCodes() +{ + for (const CheatCode* code : s_frame_end_codes) + code->Apply(); +} + +bool Cheats::EnumerateManualCodes(std::function callback) +{ + for (const std::unique_ptr& code : s_cheat_codes) + { + if (code->IsManuallyActivated()) + { + if (!callback(code->GetName())) + return false; + } + } + return true; +} + +bool Cheats::ApplyManualCode(const std::string_view name) +{ + for (const std::unique_ptr& code : s_cheat_codes) + { + if (code->IsManuallyActivated() && code->GetName() == name) + { + Host::AddIconOSDMessage(code->GetName(), ICON_FA_BAND_AID, + fmt::format(TRANSLATE_FS("Cheats", "Cheat '{}' applied."), code->GetName()), + Host::OSD_INFO_DURATION); + code->Apply(); + return true; + } + } return false; } +////////////////////////////////////////////////////////////////////////// +// File Parsing +////////////////////////////////////////////////////////////////////////// + +void Cheats::ExtractCodeInfo(CodeInfoList* dst, const std::string& file_data, bool from_database) +{ + CodeInfo current_code; + + std::optional legacy_group; + std::optional legacy_type; + std::optional legacy_activation; + + const auto finish_code = [&dst, &file_data, ¤t_code]() { + if (current_code.file_offset_end > current_code.file_offset_body_start) + { + current_code.body = std::string_view(file_data).substr( + current_code.file_offset_body_start, current_code.file_offset_end - current_code.file_offset_body_start); + } + + AppendCheatToList(dst, std::move(current_code)); + }; + + CheatFileReader reader(file_data); + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty()) + continue; + + // legacy metadata parsing + if (linev.starts_with("#group=")) + { + legacy_group = StringUtil::StripWhitespace(linev.substr(7)); + continue; + } + else if (linev.starts_with("#type=")) + { + legacy_type = ParseTypeName(StringUtil::StripWhitespace(linev.substr(6))); + if (!legacy_type.has_value()) [[unlikely]] + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + else if (linev.starts_with("#activation=")) + { + legacy_activation = ParseActivationName(StringUtil::StripWhitespace(linev.substr(12))); + if (!legacy_activation.has_value()) [[unlikely]] + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + // skip comments + if (linev[0] == '#' || linev[0] == ';') + continue; + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + // new code. + if (!current_code.name.empty()) + { + // overwrite existing codes with the same name. + finish_code(); + current_code = CodeInfo(); + } + + const std::string_view name = linev.substr(1, linev.length() - 2); + current_code.name = + legacy_group.has_value() ? fmt::format("{}\\{}", legacy_group.value(), name) : std::string(name); + current_code.type = legacy_type.value_or(CodeType::Gameshark); + current_code.activation = legacy_activation.value_or(CodeActivation::EndFrame); + current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); + current_code.file_offset_end = current_code.file_offset_start; + current_code.file_offset_body_start = current_code.file_offset_start; + current_code.from_database = from_database; + continue; + } + + // metadata? + if (linev.find('=') != std::string_view::npos) + { + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (key == "Description") + { + current_code.description = value; + } + else if (key == "Author") + { + current_code.author = value; + } + else if (key == "Type") + { + const std::optional type = ParseTypeName(value); + if (!type.has_value()) [[unlikely]] + WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); + else + current_code.type = type.value(); + } + else if (key == "Activation") + { + const std::optional activation = ParseActivationName(value); + if (!activation.has_value()) [[unlikely]] + WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); + else + current_code.activation = activation.value(); + } + + // ignore other keys when we're only grabbing info + continue; + } + + if (current_code.name.empty()) + { + WARNING_LOG("Code data specified without name at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (current_code.file_offset_body_start == current_code.file_offset_start) + current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty()) + finish_code(); +} + +void Cheats::AppendCheatToList(CodeInfoList* dst, CodeInfo code) +{ + const auto iter = + std::find_if(dst->begin(), dst->end(), [&code](const CodeInfo& rhs) { return code.name == rhs.name; }); + if (iter != dst->end()) + *iter = std::move(code); + else + dst->push_back(std::move(code)); +} + +void Cheats::ParseFile(CheatCodeList* dst_list, const std::string_view file_contents) +{ + CheatFileReader reader(file_contents); + + std::optional next_code_group; + std::optional next_code_type; + std::optional next_code_activation; + + std::string code_name; + std::optional code_body_start; + + const auto finish_code = [&dst_list, &file_contents, &reader, &code_name, &next_code_group, &next_code_type, + &next_code_activation, &code_body_start]() { + if (!code_body_start.has_value()) + { + WARNING_LOG("Empty cheat body at line {}", reader.GetCurrentLineNumber()); + return; + } + + const std::string_view code_body = + file_contents.substr(code_body_start.value(), reader.GetCurrentLineOffset() - code_body_start.value()); + + const CodeType type = next_code_type.value_or(CodeType::Gameshark); + std::unique_ptr code; + if (type == CodeType::Gameshark) + { + Error error; + code = ParseGamesharkCode(std::move(code_name), next_code_activation.value_or(CodeActivation::EndFrame), + code_body, &error); + if (!code) + { + WARNING_LOG("Failed to parse gameshark code ending on line {}: {}", reader.GetCurrentLineNumber(), + error.GetDescription()); + return; + } + } + else + { + WARNING_LOG("Unknown code type ending at line {}", reader.GetCurrentLineNumber()); + return; + } + + next_code_group.reset(); + next_code_type.reset(); + next_code_activation.reset(); + code_name = {}; + code_body_start.reset(); + + // overwrite existing codes with the same name. + const auto iter = std::find_if(dst_list->begin(), dst_list->end(), [&code](const std::unique_ptr& rhs) { + return code->GetName() == rhs->GetName(); + }); + if (iter != dst_list->end()) + *iter = std::move(code); + else + dst_list->push_back(std::move(code)); + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty()) + continue; + + // legacy metadata parsing + if (linev.starts_with("#group=")) + { + next_code_group = StringUtil::StripWhitespace(linev.substr(7)); + continue; + } + else if (linev.starts_with("#type=")) + { + next_code_type = ParseTypeName(StringUtil::StripWhitespace(linev.substr(6))); + if (!next_code_type.has_value()) + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + + continue; + } + else if (linev.starts_with("#activation=")) + { + next_code_activation = ParseActivationName(StringUtil::StripWhitespace(linev.substr(12))); + if (!next_code_activation.has_value()) + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + + continue; + } + + // skip comments + if (linev[0] == '#' || linev[0] == ';') + continue; + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (!code_name.empty()) + finish_code(); + + // new code. + const std::string_view name = linev.substr(1, linev.length() - 2); + code_name = + next_code_group.has_value() ? fmt::format("{}\\{}", next_code_group.value(), name) : std::string(name); + continue; + } + + // metadata? + if (linev.find('=') != std::string_view::npos) + { + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (key == "Type") + { + const std::optional type = ParseTypeName(value); + if (!type.has_value()) + WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); + + next_code_type = type; + } + else if (key == "Activation") + { + const std::optional activation = ParseActivationName(value); + if (!activation.has_value()) + WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); + + next_code_activation = activation; + } + else if (key == "Author" || key == "Description") + { + // ignored when loading + } + else + { + WARNING_LOG("Unknown parameter {} at line {}", key, reader.GetCurrentLineNumber()); + } + + continue; + } + + if (!code_body_start.has_value()) + code_body_start = reader.GetCurrentLineOffset(); + } + + finish_code(); +} + +////////////////////////////////////////////////////////////////////////// +// File Importing +////////////////////////////////////////////////////////////////////////// + +bool Cheats::ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error) +{ + if (codes.empty()) + { + Error::SetStringView(error, "Code list is empty."); + return false; + } + + auto fp = FileSystem::CreateAtomicRenamedFile(std::move(path), error); + if (!fp) + return false; + + for (const CodeInfo& code : codes) + { + std::string code_body = FormatCodeForFile(code); + + // ensure there's at least two newlines of space between each code + const char* newlines = "\n\n"; + const size_t newline_len = code_body.ends_with("\n\n") ? 0 : (code_body.ends_with("\n") ? 1 : 2); + for (size_t i = 0; i < newline_len; i++) + code_body.push_back('\n'); + + if (std::fwrite(code_body.data(), code_body.length(), 1, fp.get()) != 1) + { + Error::SetErrno(error, "fwrite() failed: ", errno); + FileSystem::DiscardAtomicRenamedFile(fp); + return false; + } + } + + return FileSystem::CommitAtomicRenamedFile(fp, error); +} + +bool Cheats::ImportCodesFromString(CodeInfoList* dst, const std::string_view file_contents, FileFormat file_format, + bool stop_on_error, Error* error) +{ + if (file_format == FileFormat::Unknown) + file_format = DetectFileFormat(file_contents); + + if (file_format == FileFormat::PCSX) + { + if (!ImportPCSXFile(dst, file_contents, stop_on_error, error)) + return false; + } + else if (file_format == FileFormat::Libretro) + { + if (!ImportLibretroFile(dst, file_contents, stop_on_error, error)) + return false; + } + else if (file_format == FileFormat::EPSXe) + { + if (!ImportEPSXeFile(dst, file_contents, stop_on_error, error)) + return false; + } + else + { + Error::SetStringView(error, "Unknown file format."); + return false; + } + + if (dst->empty()) + { + Error::SetStringView(error, "No codes found in file."); + return false; + } + + return true; +} + +Cheats::FileFormat Cheats::DetectFileFormat(const std::string_view file_contents) +{ + CheatFileReader reader(file_contents); + std::string_view line; + while (reader.GetLine(&line)) + { + // skip comments/empty lines + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == ';' || linev[0] == '#') + continue; + + if (linev.starts_with("cheats")) + return FileFormat::Libretro; + + // pcsxr if we see brackets + if (linev[0] == '[') + return FileFormat::PCSX; + + // otherwise if it's a code, it's probably epsxe + if (StringUtil::IsHexDigit(linev[0])) + return FileFormat::EPSXe; + } + + return FileFormat::Unknown; +} + +bool Cheats::ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) +{ + CheatFileReader reader(file_contents); + CodeInfo current_code; + + const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { + if (current_code.file_offset_end <= current_code.file_offset_body_start) + { + if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) + return false; + } + + current_code.body = std::string_view(file_contents) + .substr(current_code.file_offset_body_start, + current_code.file_offset_end - current_code.file_offset_body_start); + + AppendCheatToList(dst, std::move(current_code)); + return true; + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == '#' || linev[0] == ';') + continue; + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), + line)) + { + return false; + } + + continue; + } + + // new code. + if (!current_code.name.empty() && !finish_code()) + return false; + + current_code = CodeInfo(); + current_code.name = linev.substr(1, linev.length() - 2); + current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); + current_code.file_offset_end = current_code.file_offset_start; + current_code.file_offset_body_start = current_code.file_offset_start; + current_code.type = CodeType::Gameshark; + current_code.activation = CodeActivation::EndFrame; + current_code.from_database = false; + continue; + } + + if (current_code.name.empty()) + { + if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", + reader.GetCurrentLineNumber(), line)) + { + return false; + } + + continue; + } + + if (current_code.file_offset_body_start == current_code.file_offset_start) + current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty() && !finish_code()) + return false; + + return true; +} + +bool Cheats::ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, + Error* error) +{ + std::vector> kvp; + + static constexpr auto FindKey = [](const std::vector>& kvp, + const std::string_view search) -> const std::string_view* { + for (const auto& it : kvp) + { + if (StringUtil::EqualNoCase(search, it.first)) + return &it.second; + } + + return nullptr; + }; + + CheatFileReader reader(file_contents); + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty()) + continue; + + // skip comments + if (linev[0] == '#' || linev[0] == ';') + continue; + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line)) + return false; + + continue; + } + + kvp.emplace_back(key, value); + } + + if (kvp.empty()) + { + reader.LogError(error, stop_on_error, "No key/values found."); + return false; + } + + const std::string_view* num_cheats_value = FindKey(kvp, "cheats"); + const u32 num_cheats = num_cheats_value ? StringUtil::FromChars(*num_cheats_value).value_or(0) : 0; + if (num_cheats == 0) + return false; + + for (u32 i = 0; i < num_cheats; i++) + { + const std::string_view* desc = FindKey(kvp, TinyString::from_format("cheat{}_desc", i)); + const std::string_view* code = FindKey(kvp, TinyString::from_format("cheat{}_code", i)); + if (!desc || desc->empty() || !code || code->empty()) + { + if (!reader.LogError(error, stop_on_error, "Missing desc/code for cheat {}", i)) + return false; + + continue; + } + + // Need to convert + to newlines. + CodeInfo info; + info.name = *desc; + info.body = StringUtil::ReplaceAll(*code, '+', '\n'); + info.file_offset_start = 0; + info.file_offset_end = 0; + info.file_offset_body_start = 0; + info.type = CodeType::Gameshark; + info.activation = CodeActivation::EndFrame; + info.from_database = false; + AppendCheatToList(dst, std::move(info)); + } + + return true; +} + +bool Cheats::ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) +{ + CheatFileReader reader(file_contents); + CodeInfo current_code; + + const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { + if (current_code.file_offset_end <= current_code.file_offset_body_start) + { + if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) + return false; + } + + current_code.body = + std::string_view(file_contents).substr(current_code.file_offset_body_start, current_code.file_offset_end); + + AppendCheatToList(dst, std::move(current_code)); + return true; + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == ';') + continue; + + if (linev.front() == '#') + { + if (linev.size() < 2) + { + if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), + line)) + { + return false; + } + + continue; + } + + if (!current_code.name.empty() && !finish_code()) + return false; + + // new code. + current_code = CodeInfo(); + current_code.name = linev.substr(1); + current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); + current_code.file_offset_end = current_code.file_offset_start; + current_code.file_offset_body_start = current_code.file_offset_start; + current_code.type = CodeType::Gameshark; + current_code.activation = CodeActivation::EndFrame; + current_code.from_database = false; + continue; + } + + if (current_code.name.empty()) + { + if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", + reader.GetCurrentLineNumber(), line)) + { + return false; + } + + continue; + } + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty() && !finish_code()) + return false; + + return true; +} + +////////////////////////////////////////////////////////////////////////// +// Gameshark codes +////////////////////////////////////////////////////////////////////////// + +namespace Cheats { +namespace { + +class GamesharkCheatCode final : public CheatCode +{ +public: + GamesharkCheatCode(std::string name, CodeActivation activation); + ~GamesharkCheatCode() override; + + static std::unique_ptr Parse(std::string name, CodeActivation activation, + const std::string_view data, Error* error); + + void Apply() const override; + void ApplyOnDisable() const override; + +private: + enum class InstructionCode : u8 + { + Nop = 0x00, + ConstantWrite8 = 0x30, + ConstantWrite16 = 0x80, + ScratchpadWrite16 = 0x1F, + Increment16 = 0x10, + Decrement16 = 0x11, + Increment8 = 0x20, + Decrement8 = 0x21, + DelayActivation = 0xC1, + SkipIfNotEqual16 = 0xC0, + SkipIfButtonsNotEqual = 0xD5, + SkipIfButtonsEqual = 0xD6, + CompareButtons = 0xD4, + CompareEqual16 = 0xD0, + CompareNotEqual16 = 0xD1, + CompareLess16 = 0xD2, + CompareGreater16 = 0xD3, + CompareEqual8 = 0xE0, + CompareNotEqual8 = 0xE1, + CompareLess8 = 0xE2, + CompareGreater8 = 0xE3, + Slide = 0x50, + MemoryCopy = 0xC2, + ExtImprovedSlide = 0x53, + + // Extension opcodes, not present on original GameShark. + ExtConstantWrite32 = 0x90, + ExtScratchpadWrite32 = 0xA5, + ExtCompareEqual32 = 0xA0, + ExtCompareNotEqual32 = 0xA1, + ExtCompareLess32 = 0xA2, + ExtCompareGreater32 = 0xA3, + ExtSkipIfNotEqual32 = 0xA4, + ExtIncrement32 = 0x60, + ExtDecrement32 = 0x61, + ExtConstantWriteIfMatch16 = 0xA6, + ExtConstantWriteIfMatchWithRestore16 = 0xA7, + ExtConstantForceRange8 = 0xF0, + ExtConstantForceRangeLimits16 = 0xF1, + ExtConstantForceRangeRollRound16 = 0xF2, + ExtConstantForceRange16 = 0xF3, + ExtFindAndReplace = 0xF4, + ExtConstantSwap16 = 0xF5, + + ExtConstantBitSet8 = 0x31, + ExtConstantBitClear8 = 0x32, + ExtConstantBitSet16 = 0x81, + ExtConstantBitClear16 = 0x82, + ExtConstantBitSet32 = 0x91, + ExtConstantBitClear32 = 0x92, + + ExtBitCompareButtons = 0xD7, + ExtSkipIfNotLess8 = 0xC3, + ExtSkipIfNotGreater8 = 0xC4, + ExtSkipIfNotLess16 = 0xC5, + ExtSkipIfNotGreater16 = 0xC6, + ExtMultiConditionals = 0xF6, + + ExtCheatRegisters = 0x51, + ExtCheatRegistersCompare = 0x52, + + ExtCompareBitsSet8 = 0xE4, // Only used inside ExtMultiConditionals + ExtCompareBitsClear8 = 0xE5, // Only used inside ExtMultiConditionals + }; + + union Instruction + { + u64 bits; + + struct + { + u32 second; + u32 first; + }; + + BitField code; + BitField address; + BitField value32; + BitField value16; + BitField value8; + }; + + std::vector instructions; + + u32 GetNextNonConditionalInstruction(u32 index) const; + + static bool IsConditionalInstruction(InstructionCode code); +}; + +} // namespace + +} // namespace Cheats + +Cheats::GamesharkCheatCode::GamesharkCheatCode(std::string name, CodeActivation activation) + : CheatCode(std::move(name), CodeType::Gameshark, activation) +{ +} + +Cheats::GamesharkCheatCode::~GamesharkCheatCode() = default; + +std::unique_ptr Cheats::GamesharkCheatCode::Parse(std::string name, + CodeActivation activation, + const std::string_view data, Error* error) +{ + std::unique_ptr code = std::make_unique(std::move(name), activation); + CheatFileReader reader(data); + std::string_view line; + while (reader.GetLine(&line)) + { + // skip comments/empty lines + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || !StringUtil::IsHexDigit(line[0])) + continue; + + std::string_view next; + const std::optional first = StringUtil::FromChars(linev, 16, &next); + if (!first.has_value()) + { + Error::SetStringFmt(error, "Malformed instruction at line {}: {}", reader.GetCurrentLineNumber(), linev); + code.reset(); + break; + } + + size_t next_offset = 0; + while (next_offset < next.size() && !StringUtil::IsHexDigit(next[next_offset])) + next_offset++; + next = (next_offset < next.size()) ? next.substr(next_offset) : std::string_view(); + + const std::optional second = StringUtil::FromChars(next, 16); + if (!second.has_value()) + { + Error::SetStringFmt(error, "Malformed instruction at line {}: {}", reader.GetCurrentLineNumber(), linev); + code.reset(); + break; + } + + Instruction inst; + inst.first = first.value(); + inst.second = second.value(); + code->instructions.push_back(inst); + } + + if (code->instructions.empty()) + { + Error::SetStringFmt(error, "No instructions in code."); + code.reset(); + } + + return code; +} + +static std::array cht_register; // Used for D7 ,51 & 52 cheat types + template NEVER_INLINE static T DoMemoryRead(VirtualMemoryAddress address) { @@ -167,805 +1669,23 @@ NEVER_INLINE static u32 GetControllerAnalogBits() return bits; } -CheatList::CheatList() = default; - -CheatList::~CheatList() = default; - -static int SignedCharToInt(char ch) -{ - return static_cast(static_cast(ch)); -} - -static const std::string* FindKey(const KeyValuePairVector& kvp, const char* search) -{ - for (const auto& it : kvp) - { - if (StringUtil::Strcasecmp(it.first.c_str(), search) == 0) - return &it.second; - } - - return nullptr; -} - -bool CheatList::LoadFromPCSXRFile(const char* filename) -{ - std::optional str = FileSystem::ReadFileToString(filename); - if (!str.has_value() || str->empty()) - return false; - - return LoadFromPCSXRString(str.value()); -} - -bool CheatList::LoadFromPCSXRString(const std::string& str) -{ - std::istringstream iss(str); - - std::string line; - std::string comments; - std::string group; - CheatCode::Type type = CheatCode::Type::Gameshark; - CheatCode::Activation activation = CheatCode::Activation::EndFrame; - CheatCode current_code; - while (std::getline(iss, line)) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - // DuckStation metadata - if (StringUtil::Strncasecmp(start, "#group=", 7) == 0) - { - group = start + 7; - continue; - } - if (StringUtil::Strncasecmp(start, "#type=", 6) == 0) - { - type = CheatCode::ParseTypeName(start + 6).value_or(CheatCode::Type::Gameshark); - continue; - } - if (StringUtil::Strncasecmp(start, "#activation=", 12) == 0) - { - activation = CheatCode::ParseActivationName(start + 12).value_or(CheatCode::Activation::EndFrame); - continue; - } - - // skip comments and empty line - if (*start == '#' || *start == ';' || *start == '/' || *start == '\"') - { - comments.append(start); - comments += '\n'; - continue; - } - - if (*start == '[' && *end == ']') - { - start++; - *end = '\0'; - - // new cheat - if (current_code.Valid()) - m_codes.push_back(std::move(current_code)); - - current_code = CheatCode(); - if (group.empty()) - group = "Ungrouped"; - - current_code.group = std::move(group); - group = std::string(); - current_code.comments = std::move(comments); - comments = std::string(); - current_code.type = type; - type = CheatCode::Type::Gameshark; - current_code.activation = activation; - activation = CheatCode::Activation::EndFrame; - - if (*start == '*') - { - current_code.enabled = true; - start++; - } - - current_code.description.append(start); - continue; - } - - while (!StringUtil::IsHexDigit(*start) && start != end) - start++; - if (start == end) - continue; - - char* end_ptr; - CheatCode::Instruction inst; - inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); - inst.second = 0; - if (end_ptr) - { - while (!StringUtil::IsHexDigit(*end_ptr) && end_ptr != end) - end_ptr++; - if (end_ptr != end) - inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); - } - current_code.instructions.push_back(inst); - } - - if (current_code.Valid()) - { - // technically this isn't the place for end of file - if (!comments.empty()) - current_code.comments += comments; - m_codes.push_back(std::move(current_code)); - } - - INFO_LOG("Loaded {} cheats (PCSXR format)", m_codes.size()); - return !m_codes.empty(); -} - -bool CheatList::LoadFromLibretroFile(const char* filename) -{ - std::optional str = FileSystem::ReadFileToString(filename); - if (!str.has_value() || str->empty()) - return false; - - return LoadFromLibretroString(str.value()); -} - -bool CheatList::LoadFromLibretroString(const std::string& str) -{ - std::istringstream iss(str); - std::string line; - KeyValuePairVector kvp; - while (std::getline(iss, line)) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0' || *start == '=') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - char* equals = start; - while (*equals != '=' && equals != end) - equals++; - if (equals == end) - continue; - - *equals = '\0'; - - char* key_end = equals - 1; - while (key_end > start && std::isspace(SignedCharToInt(*key_end))) - { - *key_end = '\0'; - key_end--; - } - - char* value_start = equals + 1; - while (*value_start != '\0' && std::isspace(SignedCharToInt(*value_start))) - value_start++; - - if (*value_start == '\0') - continue; - - char* value_end = value_start + std::strlen(value_start) - 1; - while (value_end > value_start && std::isspace(SignedCharToInt(*value_end))) - { - *value_end = '\0'; - value_end--; - } - - if (*value_start == '\"') - { - if (*value_end != '\"') - continue; - - value_start++; - *value_end = '\0'; - } - - kvp.emplace_back(start, value_start); - } - - if (kvp.empty()) - return false; - - const std::string* num_cheats_value = FindKey(kvp, "cheats"); - const u32 num_cheats = num_cheats_value ? StringUtil::FromChars(*num_cheats_value).value_or(0) : 0; - if (num_cheats == 0) - return false; - - for (u32 i = 0; i < num_cheats; i++) - { - const std::string* desc = FindKey(kvp, TinyString::from_format("cheat{}_desc", i)); - const std::string* code = FindKey(kvp, TinyString::from_format("cheat{}_code", i)); - const std::string* enable = FindKey(kvp, TinyString::from_format("cheat{}_enable", i)); - if (!desc || !code || !enable) - { - WARNING_LOG("Missing desc/code/enable for cheat {}", i); - continue; - } - - CheatCode cc; - cc.group = "Ungrouped"; - cc.description = *desc; - cc.enabled = StringUtil::FromChars(*enable).value_or(false); - if (ParseLibretroCheat(&cc, code->c_str())) - m_codes.push_back(std::move(cc)); - } - - INFO_LOG("Loaded {} cheats (libretro format)", m_codes.size()); - return !m_codes.empty(); -} - -bool CheatList::LoadFromEPSXeString(const std::string& str) -{ - std::istringstream iss(str); - - std::string line; - std::string group; - CheatCode::Type type = CheatCode::Type::Gameshark; - CheatCode::Activation activation = CheatCode::Activation::EndFrame; - CheatCode current_code; - while (std::getline(iss, line)) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - // skip comments and empty line - if (*start == ';' || *start == '\0') - continue; - - if (*start == '#') - { - start++; - - // new cheat - if (current_code.Valid()) - m_codes.push_back(std::move(current_code)); - - current_code = CheatCode(); - if (group.empty()) - group = "Ungrouped"; - - current_code.group = std::move(group); - group = std::string(); - current_code.type = type; - type = CheatCode::Type::Gameshark; - current_code.activation = activation; - activation = CheatCode::Activation::EndFrame; - - char* separator = std::strchr(start, '\\'); - if (separator) - { - *separator = 0; - current_code.group = start; - start = separator + 1; - } - - current_code.description.append(start); - continue; - } - - while (!StringUtil::IsHexDigit(*start) && start != end) - start++; - if (start == end) - continue; - - char* end_ptr; - CheatCode::Instruction inst; - inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); - inst.second = 0; - if (end_ptr) - { - while (!StringUtil::IsHexDigit(*end_ptr) && end_ptr != end) - end_ptr++; - if (end_ptr != end) - inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); - } - current_code.instructions.push_back(inst); - } - - if (current_code.Valid()) - m_codes.push_back(std::move(current_code)); - - INFO_LOG("Loaded {} cheats (EPSXe format)", m_codes.size()); - return !m_codes.empty(); -} - -static bool IsLibretroSeparator(char ch) -{ - return (ch == ' ' || ch == '-' || ch == ':' || ch == '+'); -} - -bool CheatList::ParseLibretroCheat(CheatCode* cc, const char* line) -{ - const char* current_ptr = line; - while (current_ptr) - { - char* end_ptr; - CheatCode::Instruction inst; - inst.first = static_cast(std::strtoul(current_ptr, &end_ptr, 16)); - current_ptr = end_ptr; - if (end_ptr) - { - if (!IsLibretroSeparator(*end_ptr)) - { - WARNING_LOG("Malformed code '{}'", line); - break; - } - - end_ptr++; - inst.second = static_cast(std::strtoul(current_ptr, &end_ptr, 16)); - if (end_ptr && *end_ptr == '\0') - end_ptr = nullptr; - - if (end_ptr && *end_ptr != '\0') - { - if (!IsLibretroSeparator(*end_ptr)) - { - WARNING_LOG("Malformed code '{}'", line); - break; - } - - end_ptr++; - } - - current_ptr = end_ptr; - cc->instructions.push_back(inst); - } - } - - return !cc->instructions.empty(); -} - -void CheatList::Apply() -{ - if (!m_master_enable) - return; - - for (const CheatCode& code : m_codes) - { - if (code.enabled) - code.Apply(); - } -} - -void CheatList::AddCode(CheatCode cc) -{ - m_codes.push_back(std::move(cc)); -} - -void CheatList::SetCode(u32 index, CheatCode cc) -{ - if (index > m_codes.size()) - return; - - if (index == m_codes.size()) - { - m_codes.push_back(std::move(cc)); - return; - } - - m_codes[index] = std::move(cc); -} - -void CheatList::RemoveCode(u32 i) -{ - m_codes.erase(m_codes.begin() + i); -} - -std::optional CheatList::DetectFileFormat(const char* filename) -{ - std::optional str = FileSystem::ReadFileToString(filename); - if (!str.has_value() || str->empty()) - return std::nullopt; - - return DetectFileFormat(str.value()); -} - -CheatList::Format CheatList::DetectFileFormat(const std::string& str) -{ - std::istringstream iss(str); - std::string line; - while (std::getline(iss, line)) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - // eat comments - if (start[0] == '#' || start[0] == ';') - continue; - - if (line.starts_with("cheats")) - return Format::Libretro; - - // pcsxr if we see brackets - if (start[0] == '[') - return Format::PCSXR; - - // otherwise if it's a code, it's probably epsxe - if (StringUtil::IsHexDigit(start[0])) - return Format::EPSXe; - } - - return Format::Count; -} - -bool CheatList::LoadFromFile(const char* filename, Format format) -{ - if (!FileSystem::FileExists(filename)) - return false; - - std::optional str = FileSystem::ReadFileToString(filename); - if (!str.has_value()) - return false; - - if (str->empty()) - return true; - - return LoadFromString(str.value(), format); -} - -bool CheatList::LoadFromString(const std::string& str, Format format) -{ - if (format == Format::Autodetect) - format = DetectFileFormat(str); - - if (format == Format::PCSXR) - return LoadFromPCSXRString(str); - else if (format == Format::Libretro) - return LoadFromLibretroString(str); - else if (format == Format::EPSXe) - return LoadFromEPSXeString(str); - else - return false; -} - -bool CheatList::SaveToPCSXRFile(const char* filename) -{ - auto fp = FileSystem::OpenManagedCFile(filename, "wb"); - if (!fp) - return false; - - for (const CheatCode& cc : m_codes) - { - if (!cc.comments.empty()) - std::fputs(cc.comments.c_str(), fp.get()); - std::fprintf(fp.get(), "#group=%s\n", cc.group.c_str()); - std::fprintf(fp.get(), "#type=%s\n", CheatCode::GetTypeName(cc.type)); - std::fprintf(fp.get(), "#activation=%s\n", CheatCode::GetActivationName(cc.activation)); - std::fprintf(fp.get(), "[%s%s]\n", cc.enabled ? "*" : "", cc.description.c_str()); - for (const CheatCode::Instruction& i : cc.instructions) - std::fprintf(fp.get(), "%08X %04X\n", i.first, i.second); - std::fprintf(fp.get(), "\n"); - } - - std::fflush(fp.get()); - return (std::ferror(fp.get()) == 0); -} - -bool CheatList::LoadFromPackage(const std::string& serial) -{ - const std::optional db_string(Host::ReadResourceFileToString("chtdb.txt", false)); - if (!db_string.has_value()) - return false; - - std::istringstream iss(db_string.value()); - std::string line; - while (std::getline(iss, line)) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0' || *start == ';') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - if (start == end) - continue; - - if (start[0] != ':' || std::strcmp(&start[1], serial.c_str()) != 0) - continue; - - // game code match - CheatCode current_code; - while (std::getline(iss, line)) - { - start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0' || *start == ';') - continue; - - end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - if (start == end) - continue; - - // stop adding codes when we hit a different game - if (start[0] == ':' && (!m_codes.empty() || current_code.Valid())) - break; - - if (start[0] == '#') - { - start++; - - if (current_code.Valid()) - { - m_codes.push_back(std::move(current_code)); - current_code = CheatCode(); - } - - // new code - char* slash = std::strrchr(start, '\\'); - if (slash) - { - *slash = '\0'; - current_code.group = start; - start = slash + 1; - } - if (current_code.group.empty()) - current_code.group = "Ungrouped"; - - current_code.description = start; - continue; - } - - while (!StringUtil::IsHexDigit(*start) && start != end) - start++; - if (start == end) - continue; - - char* end_ptr; - CheatCode::Instruction inst; - inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); - inst.second = 0; - if (end_ptr) - { - while (!StringUtil::IsHexDigit(*end_ptr) && end_ptr != end) - end_ptr++; - if (end_ptr != end) - inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); - } - current_code.instructions.push_back(inst); - } - - if (current_code.Valid()) - m_codes.push_back(std::move(current_code)); - - INFO_LOG("Loaded {} codes from package for {}", m_codes.size(), serial); - return !m_codes.empty(); - } - - WARNING_LOG("No codes found in package for {}", serial); - return false; -} - -u32 CheatList::GetEnabledCodeCount() const -{ - u32 count = 0; - for (const CheatCode& cc : m_codes) - { - if (cc.enabled) - count++; - } - - return count; -} - -std::vector CheatList::GetCodeGroups() const -{ - std::vector groups; - for (const CheatCode& cc : m_codes) - { - if (std::any_of(groups.begin(), groups.end(), [cc](const std::string& group) { return (group == cc.group); })) - continue; - - groups.emplace_back(cc.group); - } - - return groups; -} - -void CheatList::SetCodeEnabled(u32 index, bool state) -{ - if (index >= m_codes.size() || m_codes[index].enabled == state) - return; - - m_codes[index].enabled = state; - if (!state) - m_codes[index].ApplyOnDisable(); -} - -void CheatList::EnableCode(u32 index) -{ - SetCodeEnabled(index, true); -} - -void CheatList::DisableCode(u32 index) -{ - SetCodeEnabled(index, false); -} - -void CheatList::ApplyCode(u32 index) -{ - if (index >= m_codes.size()) - return; - - m_codes[index].Apply(); -} - -const CheatCode* CheatList::FindCode(const char* name) const -{ - for (const CheatCode& cc : m_codes) - { - if (cc.description == name) - return &cc; - } - - return nullptr; -} - -const CheatCode* CheatList::FindCode(const char* group, const char* name) const -{ - for (const CheatCode& cc : m_codes) - { - if (cc.group == group && cc.description == name) - return &cc; - } - - return nullptr; -} - -void CheatList::MergeList(const CheatList& cl) -{ - for (const CheatCode& cc : cl.m_codes) - { - if (!FindCode(cc.group.c_str(), cc.description.c_str())) - AddCode(cc); - } -} - -std::string CheatCode::GetInstructionsAsString() const -{ - std::stringstream ss; - - for (const Instruction& inst : instructions) - { - ss << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << inst.first; - ss << " "; - ss << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << inst.second; - ss << '\n'; - } - - return ss.str(); -} - -bool CheatCode::SetInstructionsFromString(const std::string& str) -{ - std::vector new_instructions; - std::istringstream ss(str); - - for (std::string line; std::getline(ss, line);) - { - char* start = line.data(); - while (*start != '\0' && std::isspace(SignedCharToInt(*start))) - start++; - - // skip empty lines - if (*start == '\0') - continue; - - char* end = start + std::strlen(start) - 1; - while (end > start && std::isspace(SignedCharToInt(*end))) - { - *end = '\0'; - end--; - } - - // skip comments and empty line - if (*start == '#' || *start == ';' || *start == '/' || *start == '\"') - continue; - - while (!StringUtil::IsHexDigit(*start) && start != end) - start++; - if (start == end) - continue; - - char* end_ptr; - CheatCode::Instruction inst; - inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); - inst.second = 0; - if (end_ptr) - { - while (!StringUtil::IsHexDigit(*end_ptr) && end_ptr != end) - end_ptr++; - if (end_ptr != end) - inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); - } - new_instructions.push_back(inst); - } - - if (new_instructions.empty()) - return false; - - instructions = std::move(new_instructions); - return true; -} - -static bool IsConditionalInstruction(CheatCode::InstructionCode code) +bool Cheats::GamesharkCheatCode::IsConditionalInstruction(InstructionCode code) { switch (code) { - case CheatCode::InstructionCode::CompareEqual16: // D0 - case CheatCode::InstructionCode::CompareNotEqual16: // D1 - case CheatCode::InstructionCode::CompareLess16: // D2 - case CheatCode::InstructionCode::CompareGreater16: // D3 - case CheatCode::InstructionCode::CompareEqual8: // E0 - case CheatCode::InstructionCode::CompareNotEqual8: // E1 - case CheatCode::InstructionCode::CompareLess8: // E2 - case CheatCode::InstructionCode::CompareGreater8: // E3 - case CheatCode::InstructionCode::CompareButtons: // D4 - case CheatCode::InstructionCode::ExtCompareEqual32: // A0 - case CheatCode::InstructionCode::ExtCompareNotEqual32: // A1 - case CheatCode::InstructionCode::ExtCompareLess32: // A2 - case CheatCode::InstructionCode::ExtCompareGreater32: // A3 + case InstructionCode::CompareEqual16: // D0 + case InstructionCode::CompareNotEqual16: // D1 + case InstructionCode::CompareLess16: // D2 + case InstructionCode::CompareGreater16: // D3 + case InstructionCode::CompareEqual8: // E0 + case InstructionCode::CompareNotEqual8: // E1 + case InstructionCode::CompareLess8: // E2 + case InstructionCode::CompareGreater8: // E3 + case InstructionCode::CompareButtons: // D4 + case InstructionCode::ExtCompareEqual32: // A0 + case InstructionCode::ExtCompareNotEqual32: // A1 + case InstructionCode::ExtCompareLess32: // A2 + case InstructionCode::ExtCompareGreater32: // A3 return true; default: @@ -973,7 +1693,7 @@ static bool IsConditionalInstruction(CheatCode::InstructionCode code) } } -u32 CheatCode::GetNextNonConditionalInstruction(u32 index) const +u32 Cheats::GamesharkCheatCode::GetNextNonConditionalInstruction(u32 index) const { const u32 count = static_cast(instructions.size()); for (; index < count; index++) @@ -988,7 +1708,7 @@ u32 CheatCode::GetNextNonConditionalInstruction(u32 index) const return index; } -void CheatCode::Apply() const +void Cheats::GamesharkCheatCode::Apply() const { const u32 count = static_cast(instructions.size()); u32 index = 0; @@ -1476,11 +2196,11 @@ void CheatCode::Apply() const DoMemoryWrite(cht_register[cht_reg_no1], Truncate8(poke_value & 0xFFu)); break; case 0x03: // Write the u8 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] - // and add the u8 from the address field to it + // and add the u8 from the address field to it cht_register[cht_reg_no1] = Truncate8(cht_register[cht_reg_no2] & 0xFFu) + Truncate8(poke_value & 0xFFu); break; case 0x04: // Write the u8 from the value stored in cht_register[cht_reg_no2] + poke_value to the address - // stored in cht_register[cht_reg_no1] + // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate8(cht_register[cht_reg_no2] & 0xFFu) + Truncate8(poke_value & 0xFFu)); break; @@ -1488,7 +2208,7 @@ void CheatCode::Apply() const cht_register[cht_reg_no1] = Truncate8(poke_value & 0xFFu); break; case 0x06: // Read the u8 value from the address (cht_register[cht_reg_no2] + poke_value) to - // cht_register[cht_reg_no1] + // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; @@ -1502,12 +2222,12 @@ void CheatCode::Apply() const DoMemoryWrite(cht_register[cht_reg_no1], Truncate16(poke_value & 0xFFFFu)); break; case 0x43: // Write the u16 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] - // and add the u16 from the address field to it + // and add the u16 from the address field to it cht_register[cht_reg_no1] = Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) + Truncate16(poke_value & 0xFFFFu); break; case 0x44: // Write the u16 from the value stored in cht_register[cht_reg_no2] + poke_value to the address - // stored in cht_register[cht_reg_no1] + // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], Truncate16(cht_register[cht_reg_no2] & 0xFFFFu) + Truncate16(poke_value & 0xFFFFu)); break; @@ -1515,7 +2235,7 @@ void CheatCode::Apply() const cht_register[cht_reg_no1] = Truncate16(poke_value & 0xFFFFu); break; case 0x46: // Read the u16 value from the address (cht_register[cht_reg_no2] + poke_value) to - // cht_register[cht_reg_no1] + // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; @@ -1529,18 +2249,18 @@ void CheatCode::Apply() const DoMemoryWrite(cht_register[cht_reg_no1], poke_value); break; case 0x83: // Write the u32 from cht_register[cht_reg_no2] to cht_register[cht_reg_no1] - // and add the u32 from the address field to it + // and add the u32 from the address field to it cht_register[cht_reg_no1] = cht_register[cht_reg_no2] + poke_value; break; case 0x84: // Write the u32 from the value stored in cht_register[cht_reg_no2] + poke_value to the address - // stored in cht_register[cht_reg_no1] + // stored in cht_register[cht_reg_no1] DoMemoryWrite(cht_register[cht_reg_no1], cht_register[cht_reg_no2] + poke_value); break; case 0x85: // Write the u32 poke value to cht_register[cht_reg_no1] cht_register[cht_reg_no1] = poke_value; break; case 0x86: // Read the u32 value from the address (cht_register[cht_reg_no2] + poke_value) to - // cht_register[cht_reg_no1] + // cht_register[cht_reg_no1] cht_register[cht_reg_no1] = DoMemoryRead(cht_register[cht_reg_no2] + poke_value); break; @@ -1583,7 +2303,7 @@ void CheatCode::Apply() const case 0xCA: // Reg3 = Reg1 >> X cht_register[cht_reg_no3] = cht_register[cht_reg_no1] >> cht_reg_no2; break; - // Lots of options exist for expanding into this space + // Lots of options exist for expanding into this space default: break; } @@ -2663,7 +3383,7 @@ void CheatCode::Apply() const } } -void CheatCode::ApplyOnDisable() const +void Cheats::GamesharkCheatCode::ApplyOnDisable() const { const u32 count = static_cast(instructions.size()); u32 index = 0; @@ -2709,7 +3429,7 @@ void CheatCode::ApplyOnDisable() const case InstructionCode::ExtFindAndReplace: index += 5; break; - // for conditionals, we don't want to skip over in case it changed at some point + // for conditionals, we don't want to skip over in case it changed at some point case InstructionCode::ExtCompareEqual32: case InstructionCode::ExtCompareNotEqual32: case InstructionCode::ExtCompareLess32: @@ -2726,7 +3446,7 @@ void CheatCode::ApplyOnDisable() const index++; break; - // same deal for block conditionals + // same deal for block conditionals case InstructionCode::SkipIfNotEqual16: // C0 case InstructionCode::ExtSkipIfNotEqual32: // A4 case InstructionCode::SkipIfButtonsNotEqual: // D5 @@ -2764,481 +3484,8 @@ void CheatCode::ApplyOnDisable() const } } -static std::array s_cheat_code_type_names = {{"Gameshark"}}; -static std::array s_cheat_code_type_display_names{{TRANSLATE_NOOP("Cheats", "Gameshark")}}; - -const char* CheatCode::GetTypeName(Type type) +std::unique_ptr Cheats::ParseGamesharkCode(std::string name, CodeActivation activation, + const std::string_view data, Error* error) { - return s_cheat_code_type_names[static_cast(type)]; -} - -const char* CheatCode::GetTypeDisplayName(Type type) -{ - return s_cheat_code_type_display_names[static_cast(type)]; -} - -std::optional CheatCode::ParseTypeName(const char* str) -{ - for (size_t i = 0; i < s_cheat_code_type_names.size(); i++) - { - if (std::strcmp(s_cheat_code_type_names[i], str) == 0) - return static_cast(i); - } - - return std::nullopt; -} - -static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; -static std::array s_cheat_code_activation_display_names{ - {TRANSLATE_NOOP("Cheats", "Manual"), TRANSLATE_NOOP("Cheats", "Automatic (Frame End)")}}; - -const char* CheatCode::GetActivationName(Activation activation) -{ - return s_cheat_code_activation_names[static_cast(activation)]; -} - -const char* CheatCode::GetActivationDisplayName(Activation activation) -{ - return s_cheat_code_activation_display_names[static_cast(activation)]; -} - -std::optional CheatCode::ParseActivationName(const char* str) -{ - for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) - { - if (std::strcmp(s_cheat_code_activation_names[i], str) == 0) - return static_cast(i); - } - - return std::nullopt; -} - -MemoryScan::MemoryScan() = default; - -MemoryScan::~MemoryScan() = default; - -void MemoryScan::ResetSearch() -{ - m_results.clear(); -} - -void MemoryScan::Search() -{ - m_results.clear(); - - switch (m_size) - { - case MemoryAccessSize::Byte: - SearchBytes(); - break; - - case MemoryAccessSize::HalfWord: - SearchHalfwords(); - break; - - case MemoryAccessSize::Word: - SearchWords(); - break; - - default: - break; - } -} - -void MemoryScan::SearchBytes() -{ - for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address++) - { - if (!IsValidScanAddress(address)) - continue; - - const u8 bvalue = DoMemoryRead(address); - - Result res; - res.address = address; - res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - res.last_value = res.value; - res.value_changed = false; - - if (res.Filter(m_operator, m_value, m_signed)) - m_results.push_back(res); - } -} - -void MemoryScan::SearchHalfwords() -{ - for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 2) - { - if (!IsValidScanAddress(address)) - continue; - - const u16 bvalue = DoMemoryRead(address); - - Result res; - res.address = address; - res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - res.last_value = res.value; - res.value_changed = false; - - if (res.Filter(m_operator, m_value, m_signed)) - m_results.push_back(res); - } -} - -void MemoryScan::SearchWords() -{ - for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 4) - { - if (!IsValidScanAddress(address)) - continue; - - Result res; - res.address = address; - res.value = DoMemoryRead(address); - res.last_value = res.value; - res.value_changed = false; - - if (res.Filter(m_operator, m_value, m_signed)) - m_results.push_back(res); - } -} - -void MemoryScan::SearchAgain() -{ - ResultVector new_results; - new_results.reserve(m_results.size()); - for (Result& res : m_results) - { - res.UpdateValue(m_size, m_signed); - - if (res.Filter(m_operator, m_value, m_signed)) - { - res.last_value = res.value; - new_results.push_back(res); - } - } - - m_results.swap(new_results); -} - -void MemoryScan::UpdateResultsValues() -{ - for (Result& res : m_results) - res.UpdateValue(m_size, m_signed); -} - -void MemoryScan::SetResultValue(u32 index, u32 value) -{ - if (index >= m_results.size()) - return; - - Result& res = m_results[index]; - if (res.value == value) - return; - - switch (m_size) - { - case MemoryAccessSize::Byte: - DoMemoryWrite(res.address, Truncate8(value)); - break; - - case MemoryAccessSize::HalfWord: - DoMemoryWrite(res.address, Truncate16(value)); - break; - - case MemoryAccessSize::Word: - CPU::SafeWriteMemoryWord(res.address, value); - break; - } - - res.value = value; - res.value_changed = true; -} - -bool MemoryScan::Result::Filter(Operator op, u32 comp_value, bool is_signed) const -{ - switch (op) - { - case Operator::Equal: - { - return (value == comp_value); - } - - case Operator::NotEqual: - { - return (value != comp_value); - } - - case Operator::GreaterThan: - { - return is_signed ? (static_cast(value) > static_cast(comp_value)) : (value > comp_value); - } - - case Operator::GreaterEqual: - { - return is_signed ? (static_cast(value) >= static_cast(comp_value)) : (value >= comp_value); - } - - case Operator::LessThan: - { - return is_signed ? (static_cast(value) < static_cast(comp_value)) : (value < comp_value); - } - - case Operator::LessEqual: - { - return is_signed ? (static_cast(value) <= static_cast(comp_value)) : (value <= comp_value); - } - - case Operator::IncreasedBy: - { - return is_signed ? ((static_cast(value) - static_cast(last_value)) == static_cast(comp_value)) : - ((value - last_value) == comp_value); - } - - case Operator::DecreasedBy: - { - return is_signed ? ((static_cast(last_value) - static_cast(value)) == static_cast(comp_value)) : - ((last_value - value) == comp_value); - } - - case Operator::ChangedBy: - { - if (is_signed) - return (std::abs(static_cast(last_value) - static_cast(value)) == static_cast(comp_value)); - else - return ((last_value > value) ? (last_value - value) : (value - last_value)) == comp_value; - } - - case Operator::EqualLast: - { - return (value == last_value); - } - - case Operator::NotEqualLast: - { - return (value != last_value); - } - - case Operator::GreaterThanLast: - { - return is_signed ? (static_cast(value) > static_cast(last_value)) : (value > last_value); - } - - case Operator::GreaterEqualLast: - { - return is_signed ? (static_cast(value) >= static_cast(last_value)) : (value >= last_value); - } - - case Operator::LessThanLast: - { - return is_signed ? (static_cast(value) < static_cast(last_value)) : (value < last_value); - } - - case Operator::LessEqualLast: - { - return is_signed ? (static_cast(value) <= static_cast(last_value)) : (value <= last_value); - } - - case Operator::Any: - return true; - - default: - return false; - } -} - -void MemoryScan::Result::UpdateValue(MemoryAccessSize size, bool is_signed) -{ - const u32 old_value = value; - - switch (size) - { - case MemoryAccessSize::Byte: - { - u8 bvalue = DoMemoryRead(address); - value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - } - break; - - case MemoryAccessSize::HalfWord: - { - u16 bvalue = DoMemoryRead(address); - value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - } - break; - - case MemoryAccessSize::Word: - { - CPU::SafeReadMemoryWord(address, &value); - } - break; - } - - value_changed = (value != old_value); -} - -MemoryWatchList::MemoryWatchList() = default; - -MemoryWatchList::~MemoryWatchList() = default; - -const MemoryWatchList::Entry* MemoryWatchList::GetEntryByAddress(u32 address) const -{ - for (const Entry& entry : m_entries) - { - if (entry.address == address) - return &entry; - } - - return nullptr; -} - -bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze) -{ - if (GetEntryByAddress(address)) - return false; - - Entry entry; - entry.description = std::move(description); - entry.address = address; - entry.size = size; - entry.is_signed = is_signed; - entry.freeze = false; - - UpdateEntryValue(&entry); - - entry.changed = false; - entry.freeze = freeze; - - m_entries.push_back(std::move(entry)); - return true; -} - -void MemoryWatchList::RemoveEntry(u32 index) -{ - if (index >= m_entries.size()) - return; - - m_entries.erase(m_entries.begin() + index); -} - -bool MemoryWatchList::RemoveEntryByAddress(u32 address) -{ - for (auto it = m_entries.begin(); it != m_entries.end(); ++it) - { - if (it->address == address) - { - m_entries.erase(it); - return true; - } - } - - return false; -} - -void MemoryWatchList::SetEntryDescription(u32 index, std::string description) -{ - if (index >= m_entries.size()) - return; - - Entry& entry = m_entries[index]; - entry.description = std::move(description); -} - -void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze) -{ - if (index >= m_entries.size()) - return; - - Entry& entry = m_entries[index]; - entry.freeze = freeze; -} - -void MemoryWatchList::SetEntryValue(u32 index, u32 value) -{ - if (index >= m_entries.size()) - return; - - Entry& entry = m_entries[index]; - if (entry.value == value) - return; - - SetEntryValue(&entry, value); -} - -bool MemoryWatchList::RemoveEntryByDescription(const char* description) -{ - bool result = false; - for (auto it = m_entries.begin(); it != m_entries.end();) - { - if (it->description == description) - { - it = m_entries.erase(it); - result = true; - continue; - } - - ++it; - } - - return result; -} - -void MemoryWatchList::UpdateValues() -{ - for (Entry& entry : m_entries) - UpdateEntryValue(&entry); -} - -void MemoryWatchList::SetEntryValue(Entry* entry, u32 value) -{ - switch (entry->size) - { - case MemoryAccessSize::Byte: - DoMemoryWrite(entry->address, Truncate8(value)); - break; - - case MemoryAccessSize::HalfWord: - DoMemoryWrite(entry->address, Truncate16(value)); - break; - - case MemoryAccessSize::Word: - DoMemoryWrite(entry->address, value); - break; - } - - entry->changed = (entry->value != value); - entry->value = value; -} - -void MemoryWatchList::UpdateEntryValue(Entry* entry) -{ - const u32 old_value = entry->value; - - switch (entry->size) - { - case MemoryAccessSize::Byte: - { - u8 bvalue = DoMemoryRead(entry->address); - entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - } - break; - - case MemoryAccessSize::HalfWord: - { - u16 bvalue = DoMemoryRead(entry->address); - entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); - } - break; - - case MemoryAccessSize::Word: - { - entry->value = DoMemoryRead(entry->address); - } - break; - } - - entry->changed = (old_value != entry->value); - - if (entry->freeze && entry->changed) - SetEntryValue(entry, old_value); + return GamesharkCheatCode::Parse(std::move(name), activation, data, error); } diff --git a/src/core/cheats.h b/src/core/cheats.h index 9f6b03730..2c985c808 100644 --- a/src/core/cheats.h +++ b/src/core/cheats.h @@ -7,316 +7,129 @@ #include "types.h" +#include +#include #include #include +#include #include -struct CheatCode +class Error; + +namespace Cheats { +enum class CodeType : u8 { - enum class Type : u8 - { - Gameshark, - Count - }; + Gameshark, + Count +}; - enum class Activation : u8 - { - Manual, - EndFrame, - Count, - }; +enum class CodeActivation : u8 +{ + Manual, + EndFrame, + Count, +}; - enum class InstructionCode : u8 - { - Nop = 0x00, - ConstantWrite8 = 0x30, - ConstantWrite16 = 0x80, - ScratchpadWrite16 = 0x1F, - Increment16 = 0x10, - Decrement16 = 0x11, - Increment8 = 0x20, - Decrement8 = 0x21, - DelayActivation = 0xC1, - SkipIfNotEqual16 = 0xC0, - SkipIfButtonsNotEqual = 0xD5, - SkipIfButtonsEqual = 0xD6, - CompareButtons = 0xD4, - CompareEqual16 = 0xD0, - CompareNotEqual16 = 0xD1, - CompareLess16 = 0xD2, - CompareGreater16 = 0xD3, - CompareEqual8 = 0xE0, - CompareNotEqual8 = 0xE1, - CompareLess8 = 0xE2, - CompareGreater8 = 0xE3, - Slide = 0x50, - MemoryCopy = 0xC2, - ExtImprovedSlide = 0x53, +enum class FileFormat : u8 +{ + Unknown, + PCSX, + Libretro, + EPSXe, + Count +}; - // Extension opcodes, not present on original GameShark. - ExtConstantWrite32 = 0x90, - ExtScratchpadWrite32 = 0xA5, - ExtCompareEqual32 = 0xA0, - ExtCompareNotEqual32 = 0xA1, - ExtCompareLess32 = 0xA2, - ExtCompareGreater32 = 0xA3, - ExtSkipIfNotEqual32 = 0xA4, - ExtIncrement32 = 0x60, - ExtDecrement32 = 0x61, - ExtConstantWriteIfMatch16 = 0xA6, - ExtConstantWriteIfMatchWithRestore16 = 0xA7, - ExtConstantForceRange8 = 0xF0, - ExtConstantForceRangeLimits16 = 0xF1, - ExtConstantForceRangeRollRound16 = 0xF2, - ExtConstantForceRange16 = 0xF3, - ExtFindAndReplace = 0xF4, - ExtConstantSwap16 = 0xF5, - - ExtConstantBitSet8 = 0x31, - ExtConstantBitClear8 = 0x32, - ExtConstantBitSet16 = 0x81, - ExtConstantBitClear16 = 0x82, - ExtConstantBitSet32 = 0x91, - ExtConstantBitClear32 = 0x92, - - ExtBitCompareButtons = 0xD7, - ExtSkipIfNotLess8 = 0xC3, - ExtSkipIfNotGreater8 = 0xC4, - ExtSkipIfNotLess16 = 0xC5, - ExtSkipIfNotGreater16 = 0xC6, - ExtMultiConditionals = 0xF6, - - ExtCheatRegisters = 0x51, - ExtCheatRegistersCompare = 0x52, - - ExtCompareBitsSet8 = 0xE4, //Only used inside ExtMultiConditionals - ExtCompareBitsClear8 = 0xE5, //Only used inside ExtMultiConditionals - }; - - union Instruction - { - u64 bits; - - struct - { - u32 second; - u32 first; - }; - - BitField code; - BitField address; - BitField value32; - BitField value16; - BitField value8; - }; - - std::string group; +/// Contains all the information required to present a cheat code to the user. +struct CodeInfo +{ + std::string name; + std::string author; std::string description; - std::vector instructions; - std::string comments; - Type type = Type::Gameshark; - Activation activation = Activation::EndFrame; - bool enabled = false; + std::string body; + u32 file_offset_start; + u32 file_offset_body_start; + u32 file_offset_end; + CodeType type; + CodeActivation activation; + bool from_database; - ALWAYS_INLINE bool Valid() const { return !instructions.empty() && !description.empty(); } - ALWAYS_INLINE bool IsManuallyActivated() const { return (activation == Activation::Manual); } - - std::string GetInstructionsAsString() const; - bool SetInstructionsFromString(const std::string& str); - - u32 GetNextNonConditionalInstruction(u32 index) const; - - void Apply() const; - void ApplyOnDisable() const; - - static const char* GetTypeName(Type type); - static const char* GetTypeDisplayName(Type type); - static std::optional ParseTypeName(const char* str); - - static const char* GetActivationName(Activation activation); - static const char* GetActivationDisplayName(Activation activation); - static std::optional ParseActivationName(const char* str); + std::string_view GetNamePart() const; + std::string_view GetNameParentPart() const; }; -class CheatList final -{ -public: - enum class Format - { - Autodetect, - PCSXR, - Libretro, - EPSXe, - Count - }; +using CodeInfoList = std::vector; - CheatList(); - ~CheatList(); +/// Returns the internal identifier for a code type. +extern const char* GetTypeName(CodeType type); - ALWAYS_INLINE const CheatCode& GetCode(u32 i) const { return m_codes[i]; } - ALWAYS_INLINE CheatCode& GetCode(u32 i) { return m_codes[i]; } - ALWAYS_INLINE u32 GetCodeCount() const { return static_cast(m_codes.size()); } - ALWAYS_INLINE bool IsCodeEnabled(u32 index) const { return m_codes[index].enabled; } +/// Returns the human-readable name for a code type. +extern const char* GetTypeDisplayName(CodeType type); - ALWAYS_INLINE bool GetMasterEnable() const { return m_master_enable; } - ALWAYS_INLINE void SetMasterEnable(bool enable) { m_master_enable = enable; } +/// Parses an internal identifier, returning the code type. +extern std::optional ParseTypeName(const std::string_view str); - const CheatCode* FindCode(const char* name) const; - const CheatCode* FindCode(const char* group, const char* name) const; +/// Returns the internal identifier for a code activation. +extern const char* GetActivationName(CodeActivation activation); - void AddCode(CheatCode cc); - void SetCode(u32 index, CheatCode cc); - void RemoveCode(u32 i); +/// Returns the human-readable name for a code activation. +extern const char* GetActivationDisplayName(CodeActivation activation); - u32 GetEnabledCodeCount() const; - std::vector GetCodeGroups() const; - void EnableCode(u32 index); - void DisableCode(u32 index); - void SetCodeEnabled(u32 index, bool state); +/// Parses an internal identifier, returning the activation type. +extern std::optional ParseActivationName(const std::string_view str); - static std::optional DetectFileFormat(const char* filename); - static Format DetectFileFormat(const std::string& str); - static bool ParseLibretroCheat(CheatCode* cc, const char* line); +/// Returns a list of all available cheats/patches for a given game. +extern CodeInfoList GetCodeInfoList(const std::string_view serial, std::optional hash, bool cheats, + bool load_from_database); - bool LoadFromFile(const char* filename, Format format); - bool LoadFromPCSXRFile(const char* filename); - bool LoadFromLibretroFile(const char* filename); +/// Searches for a given code by name. +extern const CodeInfo* FindCodeInInfoList(const CodeInfoList& list, const std::string_view name); - bool LoadFromString(const std::string& str, Format format); - bool LoadFromPCSXRString(const std::string& str); - bool LoadFromLibretroString(const std::string& str); - bool LoadFromEPSXeString(const std::string& str); +/// Searches for a given code by name. +extern CodeInfo* FindCodeInInfoList(CodeInfoList& list, const std::string_view name); - bool SaveToPCSXRFile(const char* filename); +/// Imports all codes from the provided string. +extern bool ImportCodesFromString(CodeInfoList* dst, const std::string_view file_contents, FileFormat file_format, + bool stop_on_error, Error* error); - bool LoadFromPackage(const std::string& serial); +/// Exports codes to the given file, in DuckStation format. +extern bool ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error); - void Apply(); +/// Removes the specified code from the file, rewriting it. +extern bool RemoveCodeFromFile(const char* path, const std::string_view name, Error* error); - void ApplyCode(u32 index); +/// Adds or updates the specified code from the file, rewriting it. +extern bool SaveCodeToFile(const char* path, const CodeInfo& code, Error* error); - void MergeList(const CheatList& cl); +/// Updates or adds multiple codes to the file, rewriting it. +extern bool SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* error); -private: - std::vector m_codes; - bool m_master_enable = true; -}; +/// Merges two cheat lists, with any duplicates in the new list taking precedence. +extern void MergeCheatList(CodeInfoList* dst, CodeInfoList src); -class MemoryScan -{ -public: - enum class Operator - { - Any, - LessThanLast, - LessEqualLast, - GreaterThanLast, - GreaterEqualLast, - NotEqualLast, - EqualLast, - DecreasedBy, - IncreasedBy, - ChangedBy, - Equal, - NotEqual, - LessThan, - LessEqual, - GreaterThan, - GreaterEqual - }; +/// Returns the path to a new cheat/patch cht for the specified serial and hash. +extern std::string GetChtFilename(const std::string_view serial, std::optional hash, bool cheats); - struct Result - { - PhysicalMemoryAddress address; - u32 value; - u32 last_value; - bool value_changed; +/// Reloads cheats and game patches. The parameters control the degree to which data is reloaded. +extern void ReloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed); - bool Filter(Operator op, u32 comp_value, bool is_signed) const; - void UpdateValue(MemoryAccessSize size, bool is_signed); - }; +/// Releases all cheat-related state. +extern void UnloadAll(); - using ResultVector = std::vector; +/// Applies all currently-registered frame end cheat codes. +extern void ApplyFrameEndCodes(); - MemoryScan(); - ~MemoryScan(); +/// Returns true if cheats are enabled in the current game's configuration. +extern bool AreCheatsEnabled(); - u32 GetValue() const { return m_value; } - bool GetValueSigned() const { return m_signed; } - MemoryAccessSize GetSize() const { return m_size; } - Operator GetOperator() const { return m_operator; } - PhysicalMemoryAddress GetStartAddress() const { return m_start_address; } - PhysicalMemoryAddress GetEndAddress() const { return m_end_address; } - const ResultVector& GetResults() const { return m_results; } - const Result& GetResult(u32 index) const { return m_results[index]; } - u32 GetResultCount() const { return static_cast(m_results.size()); } +/// Enumerates the names of all manually-activated codes. +extern bool EnumerateManualCodes(std::function callback); - void SetValue(u32 value) { m_value = value; } - void SetValueSigned(bool s) { m_signed = s; } - void SetSize(MemoryAccessSize size) { m_size = size; } - void SetOperator(Operator op) { m_operator = op; } - void SetStartAddress(PhysicalMemoryAddress addr) { m_start_address = addr; } - void SetEndAddress(PhysicalMemoryAddress addr) { m_end_address = addr; } +/// Invokes/applies the specified manually-activated code. +extern bool ApplyManualCode(const std::string_view name); - void ResetSearch(); - void Search(); - void SearchAgain(); - void UpdateResultsValues(); +// Config sections/keys to use to enable patches. +extern const char* PATCHES_CONFIG_SECTION; +extern const char* CHEATS_CONFIG_SECTION; +extern const char* PATCH_ENABLE_CONFIG_KEY; - void SetResultValue(u32 index, u32 value); - -private: - void SearchBytes(); - void SearchHalfwords(); - void SearchWords(); - - u32 m_value = 0; - MemoryAccessSize m_size = MemoryAccessSize::HalfWord; - Operator m_operator = Operator::Equal; - PhysicalMemoryAddress m_start_address = 0; - PhysicalMemoryAddress m_end_address = 0x200000; - ResultVector m_results; - bool m_signed = false; -}; - -class MemoryWatchList -{ -public: - MemoryWatchList(); - ~MemoryWatchList(); - - struct Entry - { - std::string description; - u32 address; - u32 value; - MemoryAccessSize size; - bool is_signed; - bool freeze; - bool changed; - }; - - using EntryVector = std::vector; - - const Entry* GetEntryByAddress(u32 address) const; - const EntryVector& GetEntries() const { return m_entries; } - const Entry& GetEntry(u32 index) const { return m_entries[index]; } - u32 GetEntryCount() const { return static_cast(m_entries.size()); } - - bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze); - void RemoveEntry(u32 index); - bool RemoveEntryByDescription(const char* description); - bool RemoveEntryByAddress(u32 address); - - void SetEntryDescription(u32 index, std::string description); - void SetEntryFreeze(u32 index, bool freeze); - void SetEntryValue(u32 index, u32 value); - - void UpdateValues(); - -private: - static void SetEntryValue(Entry* entry, u32 value); - static void UpdateEntryValue(Entry* entry); - - EntryVector m_entries; -}; +} // namespace Cheats diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index e05bab768..a0bb58bc1 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -65,6 +65,7 @@ + @@ -145,6 +146,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 4672c9bba..8978026ea 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -67,6 +67,7 @@ + @@ -140,6 +141,7 @@ + diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index a5cf930c0..9475f2baf 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -1209,6 +1209,7 @@ void FullscreenUI::DoChangeDisc() void FullscreenUI::DoCheatsMenu() { +#if 0 CheatList* cl = System::GetCheatList(); if (!cl) { @@ -1247,6 +1248,7 @@ void FullscreenUI::DoCheatsMenu() System::SetCheatCodeState(static_cast(index), checked); }; OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_FROWN, "Cheat List"), true, std::move(options), std::move(callback)); +#endif } void FullscreenUI::DoToggleAnalogMode() @@ -5349,12 +5351,14 @@ void FullscreenUI::DrawPauseMenu() s_current_main_window = MainWindowType::None; } +#if 0 if (ActiveButton(FSUI_ICONSTR(ICON_FA_FROWN_OPEN, "Cheat List"), false, !System::GetGameSerial().empty() && g_settings.enable_cheats)) { s_current_main_window = MainWindowType::None; DoCheatsMenu(); } +#endif if (ActiveButton(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Toggle Analog"), false)) { diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp index 80026b16c..ca5dd224d 100644 --- a/src/core/game_database.cpp +++ b/src/core/game_database.cpp @@ -214,7 +214,7 @@ std::string GameDatabase::GetSerialForPath(const char* path) const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image) { std::string id; - System::GameHash hash; + GameHash hash; System::GetGameDetailsFromImage(image, &id, &hash); const Entry* entry = GetEntryForGameDetails(id, hash); if (entry) diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 0b22c9f72..0a1590850 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -81,7 +81,7 @@ struct MemcardTimestampCacheEntry using CacheMap = PreferUnorderedStringMap; using PlayedTimeMap = PreferUnorderedStringMap; -static_assert(std::is_same_v); +static_assert(std::is_same_v); static bool GetExeListEntry(const std::string& path, Entry* entry); static bool GetPsfListEntry(const std::string& path, Entry* entry); @@ -192,7 +192,7 @@ bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry) return false; } - const System::GameHash hash = System::GetGameHashFromFile(path.c_str()); + const GameHash hash = System::GetGameHashFromFile(path.c_str()); entry->serial = hash ? System::GetGameHashId(hash) : std::string(); entry->title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path)); diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index 6d800756a..e0143abd2 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -285,20 +285,6 @@ DEFINE_HOTKEY("Rewind", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hot System::SetRewindState(pressed > 0); }) -#ifndef __ANDROID__ -DEFINE_HOTKEY("ToggleCheats", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Cheats"), - [](s32 pressed) { - if (!pressed) - System::DoToggleCheats(); - }) -#else -DEFINE_HOTKEY("TogglePatchCodes", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Patch Codes"), - [](s32 pressed) { - if (!pressed) - System::DoToggleCheats(); - }) -#endif - DEFINE_HOTKEY("ToggleOverclocking", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Clock Speed Control (Overclocking)"), [](s32 pressed) { if (!pressed && System::IsValid()) diff --git a/src/core/memory_scanner.cpp b/src/core/memory_scanner.cpp new file mode 100644 index 000000000..4a75150fd --- /dev/null +++ b/src/core/memory_scanner.cpp @@ -0,0 +1,473 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "memory_scanner.h" +#include "bus.h" +#include "cpu_core.h" + +#include "common/log.h" + +#include "fmt/format.h" + +LOG_CHANNEL(Cheats); + +static bool IsValidScanAddress(PhysicalMemoryAddress address) +{ + if ((address & CPU::SCRATCHPAD_ADDR_MASK) == CPU::SCRATCHPAD_ADDR && + (address & CPU::SCRATCHPAD_OFFSET_MASK) < CPU::SCRATCHPAD_SIZE) + { + return true; + } + + address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + + if (address < Bus::RAM_MIRROR_END) + return true; + + if (address >= Bus::BIOS_BASE && address < (Bus::BIOS_BASE + Bus::BIOS_SIZE)) + return true; + + return false; +} + +MemoryScan::MemoryScan() = default; + +MemoryScan::~MemoryScan() = default; + +void MemoryScan::ResetSearch() +{ + m_results.clear(); +} + +void MemoryScan::Search() +{ + m_results.clear(); + + switch (m_size) + { + case MemoryAccessSize::Byte: + SearchBytes(); + break; + + case MemoryAccessSize::HalfWord: + SearchHalfwords(); + break; + + case MemoryAccessSize::Word: + SearchWords(); + break; + + default: + break; + } +} + +void MemoryScan::SearchBytes() +{ + for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address++) + { + if (!IsValidScanAddress(address)) + continue; + + u8 bvalue = 0; + if (!CPU::SafeReadMemoryByte(address, &bvalue)) [[unlikely]] + continue; + + Result res; + res.address = address; + res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + res.last_value = res.value; + res.value_changed = false; + + if (res.Filter(m_operator, m_value, m_signed)) + m_results.push_back(res); + } +} + +void MemoryScan::SearchHalfwords() +{ + for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 2) + { + if (!IsValidScanAddress(address)) + continue; + + u16 bvalue = 0; + if (!CPU::SafeReadMemoryHalfWord(address, &bvalue)) [[unlikely]] + continue; + + Result res; + res.address = address; + res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + res.last_value = res.value; + res.value_changed = false; + + if (res.Filter(m_operator, m_value, m_signed)) + m_results.push_back(res); + } +} + +void MemoryScan::SearchWords() +{ + for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 4) + { + if (!IsValidScanAddress(address)) + continue; + + u32 bvalue = 0; + if (!CPU::SafeReadMemoryWord(address, &bvalue)) [[unlikely]] + continue; + + Result res; + res.address = address; + res.value = bvalue; + res.last_value = res.value; + res.value_changed = false; + + if (res.Filter(m_operator, m_value, m_signed)) + m_results.push_back(res); + } +} + +void MemoryScan::SearchAgain() +{ + ResultVector new_results; + new_results.reserve(m_results.size()); + for (Result& res : m_results) + { + res.UpdateValue(m_size, m_signed); + + if (res.Filter(m_operator, m_value, m_signed)) + { + res.last_value = res.value; + new_results.push_back(res); + } + } + + m_results.swap(new_results); +} + +void MemoryScan::UpdateResultsValues() +{ + for (Result& res : m_results) + res.UpdateValue(m_size, m_signed); +} + +void MemoryScan::SetResultValue(u32 index, u32 value) +{ + if (index >= m_results.size()) + return; + + Result& res = m_results[index]; + if (res.value == value) + return; + + switch (m_size) + { + case MemoryAccessSize::Byte: + CPU::SafeWriteMemoryByte(res.address, Truncate8(value)); + break; + + case MemoryAccessSize::HalfWord: + CPU::SafeWriteMemoryHalfWord(res.address, Truncate16(value)); + break; + + case MemoryAccessSize::Word: + CPU::SafeWriteMemoryWord(res.address, value); + break; + } + + res.value = value; + res.value_changed = true; +} + +bool MemoryScan::Result::Filter(Operator op, u32 comp_value, bool is_signed) const +{ + switch (op) + { + case Operator::Equal: + { + return (value == comp_value); + } + + case Operator::NotEqual: + { + return (value != comp_value); + } + + case Operator::GreaterThan: + { + return is_signed ? (static_cast(value) > static_cast(comp_value)) : (value > comp_value); + } + + case Operator::GreaterEqual: + { + return is_signed ? (static_cast(value) >= static_cast(comp_value)) : (value >= comp_value); + } + + case Operator::LessThan: + { + return is_signed ? (static_cast(value) < static_cast(comp_value)) : (value < comp_value); + } + + case Operator::LessEqual: + { + return is_signed ? (static_cast(value) <= static_cast(comp_value)) : (value <= comp_value); + } + + case Operator::IncreasedBy: + { + return is_signed ? ((static_cast(value) - static_cast(last_value)) == static_cast(comp_value)) : + ((value - last_value) == comp_value); + } + + case Operator::DecreasedBy: + { + return is_signed ? ((static_cast(last_value) - static_cast(value)) == static_cast(comp_value)) : + ((last_value - value) == comp_value); + } + + case Operator::ChangedBy: + { + if (is_signed) + return (std::abs(static_cast(last_value) - static_cast(value)) == static_cast(comp_value)); + else + return ((last_value > value) ? (last_value - value) : (value - last_value)) == comp_value; + } + + case Operator::EqualLast: + { + return (value == last_value); + } + + case Operator::NotEqualLast: + { + return (value != last_value); + } + + case Operator::GreaterThanLast: + { + return is_signed ? (static_cast(value) > static_cast(last_value)) : (value > last_value); + } + + case Operator::GreaterEqualLast: + { + return is_signed ? (static_cast(value) >= static_cast(last_value)) : (value >= last_value); + } + + case Operator::LessThanLast: + { + return is_signed ? (static_cast(value) < static_cast(last_value)) : (value < last_value); + } + + case Operator::LessEqualLast: + { + return is_signed ? (static_cast(value) <= static_cast(last_value)) : (value <= last_value); + } + + case Operator::Any: + return true; + + default: + return false; + } +} + +void MemoryScan::Result::UpdateValue(MemoryAccessSize size, bool is_signed) +{ + const u32 old_value = value; + + switch (size) + { + case MemoryAccessSize::Byte: + { + u8 bvalue = 0; + if (CPU::SafeReadMemoryByte(address, &bvalue)) [[likely]] + value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::HalfWord: + { + u16 bvalue = 0; + if (CPU::SafeReadMemoryHalfWord(address, &bvalue)) [[likely]] + value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::Word: + { + CPU::SafeReadMemoryWord(address, &value); + } + break; + } + + value_changed = (value != old_value); +} + +MemoryWatchList::MemoryWatchList() = default; + +MemoryWatchList::~MemoryWatchList() = default; + +const MemoryWatchList::Entry* MemoryWatchList::GetEntryByAddress(u32 address) const +{ + for (const Entry& entry : m_entries) + { + if (entry.address == address) + return &entry; + } + + return nullptr; +} + +bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze) +{ + if (GetEntryByAddress(address)) + return false; + + Entry entry; + entry.description = std::move(description); + entry.address = address; + entry.size = size; + entry.is_signed = is_signed; + entry.freeze = false; + + UpdateEntryValue(&entry); + + entry.changed = false; + entry.freeze = freeze; + + m_entries.push_back(std::move(entry)); + return true; +} + +void MemoryWatchList::RemoveEntry(u32 index) +{ + if (index >= m_entries.size()) + return; + + m_entries.erase(m_entries.begin() + index); +} + +bool MemoryWatchList::RemoveEntryByAddress(u32 address) +{ + for (auto it = m_entries.begin(); it != m_entries.end(); ++it) + { + if (it->address == address) + { + m_entries.erase(it); + return true; + } + } + + return false; +} + +void MemoryWatchList::SetEntryDescription(u32 index, std::string description) +{ + if (index >= m_entries.size()) + return; + + Entry& entry = m_entries[index]; + entry.description = std::move(description); +} + +void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze) +{ + if (index >= m_entries.size()) + return; + + Entry& entry = m_entries[index]; + entry.freeze = freeze; +} + +void MemoryWatchList::SetEntryValue(u32 index, u32 value) +{ + if (index >= m_entries.size()) + return; + + Entry& entry = m_entries[index]; + if (entry.value == value) + return; + + SetEntryValue(&entry, value); +} + +bool MemoryWatchList::RemoveEntryByDescription(const char* description) +{ + bool result = false; + for (auto it = m_entries.begin(); it != m_entries.end();) + { + if (it->description == description) + { + it = m_entries.erase(it); + result = true; + continue; + } + + ++it; + } + + return result; +} + +void MemoryWatchList::UpdateValues() +{ + for (Entry& entry : m_entries) + UpdateEntryValue(&entry); +} + +void MemoryWatchList::SetEntryValue(Entry* entry, u32 value) +{ + switch (entry->size) + { + case MemoryAccessSize::Byte: + CPU::SafeWriteMemoryByte(entry->address, Truncate8(value)); + break; + + case MemoryAccessSize::HalfWord: + CPU::SafeWriteMemoryHalfWord(entry->address, Truncate16(value)); + break; + + case MemoryAccessSize::Word: + CPU::SafeWriteMemoryWord(entry->address, value); + break; + } + + entry->changed = (entry->value != value); + entry->value = value; +} + +void MemoryWatchList::UpdateEntryValue(Entry* entry) +{ + const u32 old_value = entry->value; + + switch (entry->size) + { + case MemoryAccessSize::Byte: + { + u8 bvalue = 0; + if (CPU::SafeReadMemoryByte(entry->address, &bvalue)) [[likely]] + entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::HalfWord: + { + u16 bvalue = 0; + if (CPU::SafeReadMemoryHalfWord(entry->address, &bvalue)) [[likely]] + entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::Word: + { + CPU::SafeReadMemoryWord(entry->address, &entry->value); + } + break; + } + + entry->changed = (old_value != entry->value); + + if (entry->freeze && entry->changed) + SetEntryValue(entry, old_value); +} diff --git a/src/core/memory_scanner.h b/src/core/memory_scanner.h new file mode 100644 index 000000000..5189f88a4 --- /dev/null +++ b/src/core/memory_scanner.h @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "types.h" + +#include +#include +#include +#include + +class MemoryScan +{ +public: + enum class Operator + { + Any, + LessThanLast, + LessEqualLast, + GreaterThanLast, + GreaterEqualLast, + NotEqualLast, + EqualLast, + DecreasedBy, + IncreasedBy, + ChangedBy, + Equal, + NotEqual, + LessThan, + LessEqual, + GreaterThan, + GreaterEqual + }; + + struct Result + { + PhysicalMemoryAddress address; + u32 value; + u32 last_value; + bool value_changed; + + bool Filter(Operator op, u32 comp_value, bool is_signed) const; + void UpdateValue(MemoryAccessSize size, bool is_signed); + }; + + using ResultVector = std::vector; + + MemoryScan(); + ~MemoryScan(); + + u32 GetValue() const { return m_value; } + bool GetValueSigned() const { return m_signed; } + MemoryAccessSize GetSize() const { return m_size; } + Operator GetOperator() const { return m_operator; } + PhysicalMemoryAddress GetStartAddress() const { return m_start_address; } + PhysicalMemoryAddress GetEndAddress() const { return m_end_address; } + const ResultVector& GetResults() const { return m_results; } + const Result& GetResult(u32 index) const { return m_results[index]; } + u32 GetResultCount() const { return static_cast(m_results.size()); } + + void SetValue(u32 value) { m_value = value; } + void SetValueSigned(bool s) { m_signed = s; } + void SetSize(MemoryAccessSize size) { m_size = size; } + void SetOperator(Operator op) { m_operator = op; } + void SetStartAddress(PhysicalMemoryAddress addr) { m_start_address = addr; } + void SetEndAddress(PhysicalMemoryAddress addr) { m_end_address = addr; } + + void ResetSearch(); + void Search(); + void SearchAgain(); + void UpdateResultsValues(); + + void SetResultValue(u32 index, u32 value); + +private: + void SearchBytes(); + void SearchHalfwords(); + void SearchWords(); + + u32 m_value = 0; + MemoryAccessSize m_size = MemoryAccessSize::HalfWord; + Operator m_operator = Operator::Equal; + PhysicalMemoryAddress m_start_address = 0; + PhysicalMemoryAddress m_end_address = 0x200000; + ResultVector m_results; + bool m_signed = false; +}; + +class MemoryWatchList +{ +public: + MemoryWatchList(); + ~MemoryWatchList(); + + struct Entry + { + std::string description; + u32 address; + u32 value; + MemoryAccessSize size; + bool is_signed; + bool freeze; + bool changed; + }; + + using EntryVector = std::vector; + + const Entry* GetEntryByAddress(u32 address) const; + const EntryVector& GetEntries() const { return m_entries; } + const Entry& GetEntry(u32 index) const { return m_entries[index]; } + u32 GetEntryCount() const { return static_cast(m_entries.size()); } + + bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze); + void RemoveEntry(u32 index); + bool RemoveEntryByDescription(const char* description); + bool RemoveEntryByAddress(u32 address); + + void SetEntryDescription(u32 index, std::string description); + void SetEntryFreeze(u32 index, bool freeze); + void SetEntryValue(u32 index, u32 value); + + void UpdateValues(); + +private: + static void SetEntryValue(Entry* entry, u32 value); + static void UpdateEntryValue(Entry* entry); + + EntryVector m_entries; +}; diff --git a/src/core/settings.cpp b/src/core/settings.cpp index d502f8f2f..b7ba916f2 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -168,7 +168,6 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si) load_devices_from_save_states = si.GetBoolValue("Main", "LoadDevicesFromSaveStates", false); apply_compatibility_settings = si.GetBoolValue("Main", "ApplyCompatibilitySettings", true); apply_game_settings = si.GetBoolValue("Main", "ApplyGameSettings", true); - enable_cheats = si.GetBoolValue("Console", "EnableCheats", false); disable_all_enhancements = si.GetBoolValue("Main", "DisableAllEnhancements", false); enable_discord_presence = si.GetBoolValue("Main", "EnableDiscordPresence", false); rewind_enable = si.GetBoolValue("Main", "RewindEnable", false); @@ -514,7 +513,6 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const } si.SetBoolValue("Main", "LoadDevicesFromSaveStates", load_devices_from_save_states); - si.SetBoolValue("Console", "EnableCheats", enable_cheats); si.SetBoolValue("Main", "DisableAllEnhancements", disable_all_enhancements); si.SetBoolValue("Main", "RewindEnable", rewind_enable); si.SetFloatValue("Main", "RewindFrequency", rewind_save_frequency); @@ -928,7 +926,6 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages) g_settings.cpu_overclock_enable = false; g_settings.cpu_overclock_active = false; g_settings.enable_8mb_ram = false; - g_settings.enable_cheats = false; g_settings.gpu_resolution_scale = 1; g_settings.gpu_multisamples = 1; g_settings.gpu_per_sample_shading = false; @@ -1041,7 +1038,6 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages) (g_settings.fast_forward_speed != 0.0f) ? std::max(g_settings.fast_forward_speed, 1.0f) : 0.0f; g_settings.turbo_speed = (g_settings.turbo_speed != 0.0f) ? std::max(g_settings.turbo_speed, 1.0f) : 0.0f; g_settings.rewind_enable = false; - g_settings.enable_cheats = false; if (g_settings.cpu_overclock_enable && g_settings.GetCPUOverclockPercent() < 100) { g_settings.cpu_overclock_enable = false; @@ -2125,6 +2121,7 @@ std::string EmuFolders::GameIcons; std::string EmuFolders::GameSettings; std::string EmuFolders::InputProfiles; std::string EmuFolders::MemoryCards; +std::string EmuFolders::Patches; std::string EmuFolders::Resources; std::string EmuFolders::SaveStates; std::string EmuFolders::Screenshots; @@ -2144,6 +2141,7 @@ void EmuFolders::SetDefaults() GameSettings = Path::Combine(DataRoot, "gamesettings"); InputProfiles = Path::Combine(DataRoot, "inputprofiles"); MemoryCards = Path::Combine(DataRoot, "memcards"); + Patches = Path::Combine(DataRoot, "patches"); SaveStates = Path::Combine(DataRoot, "savestates"); Screenshots = Path::Combine(DataRoot, "screenshots"); Shaders = Path::Combine(DataRoot, "shaders"); @@ -2175,6 +2173,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) GameSettings = LoadPathFromSettings(si, DataRoot, "Folders", "GameSettings", "gamesettings"); InputProfiles = LoadPathFromSettings(si, DataRoot, "Folders", "InputProfiles", "inputprofiles"); MemoryCards = LoadPathFromSettings(si, DataRoot, "MemoryCards", "Directory", "memcards"); + Patches = LoadPathFromSettings(si, DataRoot, "Folders", "Patches", "patches"); SaveStates = LoadPathFromSettings(si, DataRoot, "Folders", "SaveStates", "savestates"); Screenshots = LoadPathFromSettings(si, DataRoot, "Folders", "Screenshots", "screenshots"); Shaders = LoadPathFromSettings(si, DataRoot, "Folders", "Shaders", "shaders"); @@ -2191,6 +2190,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) DEV_LOG("Game Settings Directory: {}", GameSettings); DEV_LOG("Input Profile Directory: {}", InputProfiles); DEV_LOG("MemoryCards Directory: {}", MemoryCards); + DEV_LOG("Patches Directory: {}", Patches); DEV_LOG("Resources Directory: {}", Resources); DEV_LOG("SaveStates Directory: {}", SaveStates); DEV_LOG("Screenshots Directory: {}", Screenshots); @@ -2212,6 +2212,7 @@ void EmuFolders::Save(SettingsInterface& si) si.SetStringValue("Folders", "GameSettings", Path::MakeRelative(GameSettings, DataRoot).c_str()); si.SetStringValue("Folders", "InputProfiles", Path::MakeRelative(InputProfiles, DataRoot).c_str()); si.SetStringValue("MemoryCards", "Directory", Path::MakeRelative(MemoryCards, DataRoot).c_str()); + si.SetStringValue("Folders", "Patches", Path::MakeRelative(Patches, DataRoot).c_str()); si.SetStringValue("Folders", "SaveStates", Path::MakeRelative(SaveStates, DataRoot).c_str()); si.SetStringValue("Folders", "Screenshots", Path::MakeRelative(Screenshots, DataRoot).c_str()); si.SetStringValue("Folders", "Shaders", Path::MakeRelative(Shaders, DataRoot).c_str()); @@ -2252,6 +2253,7 @@ bool EmuFolders::EnsureFoldersExist() result = FileSystem::EnsureDirectoryExists(GameSettings.c_str(), false) && result; result = FileSystem::EnsureDirectoryExists(InputProfiles.c_str(), false) && result; result = FileSystem::EnsureDirectoryExists(MemoryCards.c_str(), false) && result; + result = FileSystem::EnsureDirectoryExists(Patches.c_str(), false) && result; result = FileSystem::EnsureDirectoryExists(SaveStates.c_str(), false) && result; result = FileSystem::EnsureDirectoryExists(Screenshots.c_str(), false) && result; result = FileSystem::EnsureDirectoryExists(Shaders.c_str(), false) && result; diff --git a/src/core/settings.h b/src/core/settings.h index 4a5faa884..eae012a84 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -88,7 +88,6 @@ struct Settings bool load_devices_from_save_states : 1 = false; bool apply_compatibility_settings : 1 = true; bool apply_game_settings : 1 = true; - bool enable_cheats : 1 = false; bool disable_all_enhancements : 1 = false; bool enable_discord_presence : 1 = false; @@ -575,6 +574,7 @@ extern std::string GameIcons; extern std::string GameSettings; extern std::string InputProfiles; extern std::string MemoryCards; +extern std::string Patches; extern std::string Resources; extern std::string SaveStates; extern std::string Screenshots; diff --git a/src/core/system.cpp b/src/core/system.cpp index 38fb375e8..ad916a473 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -252,7 +252,7 @@ static std::string s_running_game_serial; static std::string s_running_game_title; static std::string s_exe_override; static const GameDatabase::Entry* s_running_game_entry = nullptr; -static System::GameHash s_running_game_hash; +static GameHash s_running_game_hash; static System::BootMode s_boot_mode = System::BootMode::None; static bool s_running_game_custom_title = false; @@ -311,7 +311,6 @@ static Common::Timer s_fps_timer; static Common::Timer s_frame_timer; static Threading::ThreadHandle s_cpu_thread_handle; -static std::unique_ptr s_cheat_list; static std::unique_ptr s_media_capture; // temporary save state, created when loading, used to undo load state @@ -714,7 +713,7 @@ const GameDatabase::Entry* System::GetGameDatabaseEntry() return s_running_game_entry; } -System::GameHash System::GetGameHash() +GameHash System::GetGameHash() { return s_running_game_hash; } @@ -922,7 +921,7 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash return true; } -System::GameHash System::GetGameHashFromFile(const char* path) +GameHash System::GetGameHashFromFile(const char* path) { const std::optional> data = FileSystem::ReadBinaryFile(path); if (!data) @@ -1061,8 +1060,8 @@ bool System::ReadExecutableFromImage(IsoReader& iso, std::string* out_executable return true; } -System::GameHash System::GetGameHashFromBuffer(std::string_view exe_name, std::span exe_buffer, - const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length) +GameHash System::GetGameHashFromBuffer(std::string_view exe_name, std::span exe_buffer, + const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length) { XXH64_state_t* state = XXH64_createState(); XXH64_reset(state, 0x4242D00C); @@ -1496,9 +1495,8 @@ void System::ResetSystem() if (Achievements::ResetHardcoreMode(false)) { - // Make sure a pre-existing cheat file hasn't been loaded when resetting - // after enabling HC mode. - s_cheat_list.reset(); + // Make sure a pre-existing cheat file hasn't been loaded when resetting after enabling HC mode. + Cheats::ReloadCheats(true, true, false, true); ApplySettings(false); } @@ -1712,6 +1710,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) Error::SetStringFmt(error, "File '{}' is not a valid executable to boot.", Path::GetFileName(parameters.override_exe)); s_state = State::Shutdown; + Cheats::UnloadAll(); + ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); return false; @@ -1726,6 +1726,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (!CheckForSBIFile(disc.get(), error)) { s_state = State::Shutdown; + Cheats::UnloadAll(); ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); @@ -1763,6 +1764,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (cancelled) { s_state = State::Shutdown; + Cheats::UnloadAll(); ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); @@ -1776,6 +1778,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (!SetBootMode(boot_mode, disc_region, error)) { s_state = State::Shutdown; + Cheats::UnloadAll(); ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); @@ -1787,6 +1790,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) { s_boot_mode = System::BootMode::None; s_state = State::Shutdown; + Cheats::UnloadAll(); ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); @@ -1981,6 +1985,7 @@ void System::DestroySystem() ClearMemorySaveStates(); + Cheats::UnloadAll(); PCDrv::Shutdown(); SIO::Shutdown(); MDEC::Shutdown(); @@ -2013,7 +2018,6 @@ void System::DestroySystem() s_bios_image_info = nullptr; s_exe_override = {}; s_boot_mode = BootMode::None; - s_cheat_list.reset(); s_state = State::Shutdown; @@ -2085,8 +2089,7 @@ void System::FrameDone() // TODO: when running ahead, we can skip this (and the flush above) SPU::GeneratePendingSamples(); - if (s_cheat_list) - s_cheat_list->Apply(); + Cheats::ApplyFrameEndCodes(); if (Achievements::IsActive()) Achievements::FrameUpdate(); @@ -3598,33 +3601,6 @@ void System::DoFrameStep() PauseSystem(false); } -void System::DoToggleCheats() -{ - if (!System::IsValid()) - return; - - if (Achievements::IsHardcoreModeActive()) - { - Achievements::ConfirmHardcoreModeDisableAsync("Toggling cheats", [](bool approved) { DoToggleCheats(); }); - return; - } - - CheatList* cl = GetCheatList(); - if (!cl) - { - Host::AddKeyedOSDMessage("ToggleCheats", TRANSLATE_STR("OSDMessage", "No cheats are loaded."), 10.0f); - return; - } - - cl->SetMasterEnable(!cl->GetMasterEnable()); - Host::AddIconOSDMessage( - "ToggleCheats", ICON_FA_EXCLAMATION_TRIANGLE, - cl->GetMasterEnable() ? - TRANSLATE_PLURAL_STR("System", "%n cheat(s) are now active.", "", cl->GetEnabledCodeCount()) : - TRANSLATE_PLURAL_STR("System", "%n cheat(s) are now inactive.", "", cl->GetEnabledCodeCount()), - Host::OSD_QUICK_DURATION); -} - #if 0 // currently not used until EXP1 is implemented @@ -4117,9 +4093,7 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool UpdateGameSettingsLayer(); ApplySettings(true); - s_cheat_list.reset(); - if (g_settings.enable_cheats) - LoadCheatList(); + Cheats::ReloadCheats(true, true, false, true); if (s_running_game_serial != prev_serial) UpdateSessionTime(prev_serial); @@ -4248,40 +4222,6 @@ bool System::SwitchMediaSubImage(u32 index) return true; } -bool System::HasCheatList() -{ - return static_cast(s_cheat_list); -} - -CheatList* System::GetCheatList() -{ - return s_cheat_list.get(); -} - -void System::ApplyCheatCode(const CheatCode& code) -{ - Assert(!IsShutdown()); - code.Apply(); -} - -void System::SetCheatList(std::unique_ptr cheats) -{ - Assert(!IsShutdown()); - s_cheat_list = std::move(cheats); - - if (s_cheat_list && s_cheat_list->GetEnabledCodeCount() > 0) - { - Host::AddIconOSDMessage("CheatsLoadWarning", ICON_FA_EXCLAMATION_TRIANGLE, - TRANSLATE_PLURAL_STR("System", "%n cheat(s) are enabled. This may crash games.", "", - s_cheat_list->GetEnabledCodeCount()), - Host::OSD_WARNING_DURATION); - } - else - { - Host::RemoveKeyedOSDMessage("CheatsLoadWarning"); - } -} - void System::CheckForSettingsChanges(const Settings& old_settings) { if (IsValid() && @@ -4390,14 +4330,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings) Bus::RemapFastmemViews(); } - if (g_settings.enable_cheats != old_settings.enable_cheats) - { - if (g_settings.enable_cheats) - LoadCheatList(); - else - SetCheatList(nullptr); - } - SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume()); if (g_settings.gpu_resolution_scale != old_settings.gpu_resolution_scale || @@ -4688,8 +4620,10 @@ void System::WarnAboutUnsafeSettings() APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Overclock disabled.")); if (g_settings.enable_8mb_ram) APPEND_SUBMESSAGE(TRANSLATE_SV("System", "8MB RAM disabled.")); - if (g_settings.enable_cheats) + if (s_game_settings_interface && s_game_settings_interface->GetBoolValue("Cheats", "EnableCheats", false)) APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Cheats disabled.")); + if (s_game_settings_interface && s_game_settings_interface->ContainsValue("Patches", "Enable")) + APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Patches disabled.")); if (g_settings.gpu_resolution_scale != 1) APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Resolution scale set to 1x.")); if (g_settings.gpu_multisamples != 1) @@ -5585,154 +5519,6 @@ std::string System::GetCheatFileName() return ret; } -bool System::LoadCheatList() -{ - // Called when booting, needs to test for shutdown. - if (IsShutdown() || !g_settings.enable_cheats) - return false; - - const std::string filename(GetCheatFileName()); - if (filename.empty() || !FileSystem::FileExists(filename.c_str())) - return false; - - std::unique_ptr cl = std::make_unique(); - if (!cl->LoadFromFile(filename.c_str(), CheatList::Format::Autodetect)) - { - Host::AddIconOSDMessage( - "cheats_loaded", ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Failed to load cheats from '{}'."), Path::GetFileName(filename))); - return false; - } - - SetCheatList(std::move(cl)); - return true; -} - -bool System::LoadCheatListFromDatabase() -{ - if (IsShutdown() || s_running_game_serial.empty() || Achievements::IsHardcoreModeActive()) - return false; - - std::unique_ptr cl = std::make_unique(); - if (!cl->LoadFromPackage(s_running_game_serial)) - return false; - - INFO_LOG("Loaded {} cheats from database.", cl->GetCodeCount()); - SetCheatList(std::move(cl)); - return true; -} - -bool System::SaveCheatList() -{ - if (!System::IsValid() || !System::HasCheatList()) - return false; - - const std::string filename(GetCheatFileName()); - if (filename.empty()) - return false; - - if (!System::GetCheatList()->SaveToPCSXRFile(filename.c_str())) - { - Host::AddIconOSDMessage( - "CheatSaveError", ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Failed to save cheat list to '{}'."), Path::GetFileName(filename)), - Host::OSD_ERROR_DURATION); - } - - return true; -} - -bool System::DeleteCheatList() -{ - if (!System::IsValid()) - return false; - - const std::string filename(GetCheatFileName()); - if (!filename.empty()) - { - if (!FileSystem::DeleteFile(filename.c_str())) - return false; - - Host::AddIconOSDMessage( - "CheatDelete", ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Deleted cheat list '{}'."), Path::GetFileName(filename)), - Host::OSD_INFO_DURATION); - } - - System::SetCheatList(nullptr); - return true; -} - -void System::ClearCheatList(bool save_to_file) -{ - if (!System::IsValid()) - return; - - CheatList* cl = System::GetCheatList(); - if (!cl) - return; - - while (cl->GetCodeCount() > 0) - cl->RemoveCode(cl->GetCodeCount() - 1); - - if (save_to_file) - SaveCheatList(); -} - -void System::SetCheatCodeState(u32 index, bool enabled) -{ - if (!System::IsValid() || !System::HasCheatList()) - return; - - CheatList* cl = System::GetCheatList(); - if (index >= cl->GetCodeCount()) - return; - - CheatCode& cc = cl->GetCode(index); - if (cc.enabled == enabled) - return; - - cc.enabled = enabled; - if (!enabled) - cc.ApplyOnDisable(); - - if (enabled) - { - Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Cheat '{}' enabled."), cc.description), - Host::OSD_INFO_DURATION); - } - else - { - Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Cheat '{}' disabled."), cc.description), - Host::OSD_INFO_DURATION); - } - - SaveCheatList(); -} - -void System::ApplyCheatCode(u32 index) -{ - if (!System::HasCheatList() || index >= System::GetCheatList()->GetCodeCount()) - return; - - const CheatCode& cc = System::GetCheatList()->GetCode(index); - if (!cc.enabled) - { - cc.Apply(); - Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Applied cheat '{}'."), cc.description), - Host::OSD_INFO_DURATION); - } - else - { - Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE, - fmt::format(TRANSLATE_FS("System", "Cheat '{}' is already enabled."), cc.description), - Host::OSD_INFO_DURATION); - } -} - void System::ToggleWidescreen() { g_settings.gpu_widescreen_hack = !g_settings.gpu_widescreen_hack; diff --git a/src/core/system.h b/src/core/system.h index 179301b65..0179c8943 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -23,9 +23,6 @@ enum class GPUVSyncMode : u8; class Controller; -struct CheatCode; -class CheatList; - class GPUTexture; class MediaCapture; @@ -107,8 +104,6 @@ enum class BootMode BootPSF, }; -using GameHash = u64; - extern TickCount g_ticks_per_second; /// Returns true if the filename is a PlayStation executable we can inject. @@ -315,18 +310,6 @@ std::string GetMediaSubImageTitle(u32 index); /// Switches to the specified media/disc playlist index. bool SwitchMediaSubImage(u32 index); -/// Returns true if there is currently a cheat list. -bool HasCheatList(); - -/// Accesses the current cheat list. -CheatList* GetCheatList(); - -/// Applies a single cheat code. -void ApplyCheatCode(const CheatCode& code); - -/// Sets or clears the provided cheat list, applying every frame. -void SetCheatList(std::unique_ptr cheats); - /// Updates throttler. void UpdateSpeedLimiterState(); @@ -343,7 +326,6 @@ bool IsRewinding(); void SetRewindState(bool enabled); void DoFrameStep(); -void DoToggleCheats(); /// Returns the path to a save state file. Specifying an index of -1 is the "resume" save state. std::string GetGameSaveStateFileName(std::string_view serial, s32 slot); @@ -405,27 +387,6 @@ bool StartMediaCapture(std::string path = {}); bool StartMediaCapture(std::string path, bool capture_video, bool capture_audio); void StopMediaCapture(); -/// Loads the cheat list for the current game title from the user directory. -bool LoadCheatList(); - -/// Loads the cheat list for the current game code from the built-in code database. -bool LoadCheatListFromDatabase(); - -/// Saves the current cheat list to the game title's file. -bool SaveCheatList(); - -/// Deletes the cheat list, if present. -bool DeleteCheatList(); - -/// Removes all cheats from the cheat list. -void ClearCheatList(bool save_to_file); - -/// Enables/disabled the specified cheat code. -void SetCheatCodeState(u32 index, bool enabled); - -/// Immediately applies the specified cheat code. -void ApplyCheatCode(u32 index); - /// Toggle Widescreen Hack and Aspect Ratio void ToggleWidescreen(); diff --git a/src/core/types.h b/src/core/types.h index 21b46d28d..828720739 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -22,6 +22,7 @@ enum class MemoryAccessSize : u32 using TickCount = s32; using GlobalTicks = u64; +using GameHash = u64; enum class ConsoleRegion : u8 { diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 034050eeb..d39e1db70 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -32,9 +32,6 @@ set(SRCS cheatcodeeditordialog.cpp cheatcodeeditordialog.h cheatcodeeditordialog.ui - cheatmanagerwindow.cpp - cheatmanagerwindow.h - cheatmanagerwindow.ui colorpickerbutton.cpp colorpickerbutton.h consolesettingswidget.cpp @@ -79,6 +76,13 @@ set(SRCS foldersettingswidget.cpp foldersettingswidget.h foldersettingswidget.ui + gamecheatsettingswidget.cpp + gamecheatsettingswidget.h + gamecheatsettingswidget.ui + gamepatchdetailswidget.ui + gamepatchsettingswidget.cpp + gamepatchsettingswidget.h + gamepatchsettingswidget.ui gamelistmodel.cpp gamelistmodel.h gamelistrefreshthread.cpp diff --git a/src/duckstation-qt/cheatcodeeditordialog.cpp b/src/duckstation-qt/cheatcodeeditordialog.cpp index 0516f327c..ef308022e 100644 --- a/src/duckstation-qt/cheatcodeeditordialog.cpp +++ b/src/duckstation-qt/cheatcodeeditordialog.cpp @@ -2,9 +2,17 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cheatcodeeditordialog.h" +#include "gamecheatsettingswidget.h" +#include "qtutils.h" + +#include "core/cheats.h" + +#include "common/error.h" + #include -CheatCodeEditorDialog::CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent) +CheatCodeEditorDialog::CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo& code, + const QStringList& group_names) : QDialog(parent), m_code(code) { m_ui.setupUi(this); @@ -24,16 +32,25 @@ void CheatCodeEditorDialog::saveClicked() return; } - if (!m_code->SetInstructionsFromString(m_ui.instructions->toPlainText().toStdString())) + std::string new_body = m_ui.instructions->toPlainText().toStdString(); + if (new_body.empty()) { - QMessageBox::critical(this, tr("Error"), tr("Instructions are invalid.")); + QMessageBox::critical(this, tr("Error"), tr("Instructions cannot be empty.")); return; } - m_code->description = std::move(new_description); - m_code->type = static_cast(m_ui.type->currentIndex()); - m_code->activation = static_cast(m_ui.activation->currentIndex()); - m_code->group = m_ui.group->currentText().toStdString(); + // m_code.description + m_code.type = static_cast(m_ui.type->currentIndex()); + m_code.activation = static_cast(m_ui.activation->currentIndex()); + m_code.body = std::move(new_body); + + std::string path = m_parent->getPathForSavingCheats(); + Error error; + if (!Cheats::SaveCodeToFile(path.c_str(), m_code, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to save cheat code:\n%1").arg(QString::fromStdString(error.GetDescription()))); + } done(1); } @@ -45,28 +62,25 @@ void CheatCodeEditorDialog::cancelClicked() void CheatCodeEditorDialog::setupAdditionalUi(const QStringList& group_names) { - for (u32 i = 0; i < static_cast(CheatCode::Type::Count); i++) - { - m_ui.type->addItem(qApp->translate("Cheats", CheatCode::GetTypeDisplayName(static_cast(i)))); - } + for (u32 i = 0; i < static_cast(Cheats::CodeType::Count); i++) + m_ui.type->addItem(Cheats::GetTypeDisplayName(static_cast(i))); - for (u32 i = 0; i < static_cast(CheatCode::Activation::Count); i++) - { - m_ui.activation->addItem( - qApp->translate("Cheats", CheatCode::GetActivationDisplayName(static_cast(i)))); - } + for (u32 i = 0; i < static_cast(Cheats::CodeActivation::Count); i++) + m_ui.activation->addItem(Cheats::GetActivationDisplayName(static_cast(i))); + + m_ui.group->addItem(QStringLiteral("Ungrouped"), QVariant(QString())); if (!group_names.isEmpty()) m_ui.group->addItems(group_names); - else - m_ui.group->addItem(QStringLiteral("Ungrouped")); + + m_ui.group->addItem(QStringLiteral("New...")); } void CheatCodeEditorDialog::fillUi() { - m_ui.description->setText(QString::fromStdString(m_code->description)); + m_ui.description->setText(QtUtils::StringViewToQString(m_code.GetNamePart())); - const QString group_qstr(QString::fromStdString(m_code->group)); + const QString group_qstr(QtUtils::StringViewToQString(m_code.GetNameParentPart())); int index = m_ui.group->findText(group_qstr); if (index >= 0) { @@ -79,10 +93,10 @@ void CheatCodeEditorDialog::fillUi() m_ui.group->setCurrentIndex(index); } - m_ui.type->setCurrentIndex(static_cast(m_code->type)); - m_ui.activation->setCurrentIndex(static_cast(m_code->activation)); + m_ui.type->setCurrentIndex(static_cast(m_code.type)); + m_ui.activation->setCurrentIndex(static_cast(m_code.activation)); - m_ui.instructions->setPlainText(QString::fromStdString(m_code->GetInstructionsAsString())); + m_ui.instructions->setPlainText(QString::fromStdString(m_code.body)); } void CheatCodeEditorDialog::connectUi() diff --git a/src/duckstation-qt/cheatcodeeditordialog.h b/src/duckstation-qt/cheatcodeeditordialog.h index 954f6b878..0e08ac9a6 100644 --- a/src/duckstation-qt/cheatcodeeditordialog.h +++ b/src/duckstation-qt/cheatcodeeditordialog.h @@ -2,15 +2,23 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #pragma once -#include "core/cheats.h" + #include "ui_cheatcodeeditordialog.h" +#include + +namespace Cheats { +struct CodeInfo; +} + +class GameCheatSettingsWidget; + class CheatCodeEditorDialog : public QDialog { Q_OBJECT public: - CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent); + CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo& code, const QStringList& group_names); ~CheatCodeEditorDialog(); private Q_SLOTS: @@ -22,7 +30,9 @@ private: void fillUi(); void connectUi(); - CheatCode* m_code; + GameCheatSettingsWidget* m_parent; + + Cheats::CodeInfo& m_code; Ui::CheatCodeEditorDialog m_ui; }; diff --git a/src/duckstation-qt/cheatmanagerwindow.cpp b/src/duckstation-qt/cheatmanagerwindow.cpp deleted file mode 100644 index f32172b48..000000000 --- a/src/duckstation-qt/cheatmanagerwindow.cpp +++ /dev/null @@ -1,581 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. -// SPDX-License-Identifier: CC-BY-NC-ND-4.0 - -#include "cheatmanagerwindow.h" -#include "cheatcodeeditordialog.h" -#include "mainwindow.h" -#include "qthost.h" -#include "qtutils.h" - -#include "core/bus.h" -#include "core/cpu_core.h" -#include "core/host.h" -#include "core/system.h" - -#include "common/assert.h" -#include "common/string_util.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CheatManagerWindow::CheatManagerWindow() : QWidget() -{ - m_ui.setupUi(this); - - QtUtils::RestoreWindowGeometry("CheatManagerWindow", this); - - connectUi(); - - updateCheatList(); -} - -CheatManagerWindow::~CheatManagerWindow() = default; - -void CheatManagerWindow::connectUi() -{ - connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged); - connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated); - connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged); - connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked); - connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked); - connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked); - connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked); - connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked); - connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked); - connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked); - connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked); - connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked); - - connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState); - connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList); -} - -void CheatManagerWindow::showEvent(QShowEvent* event) -{ - QWidget::showEvent(event); - resizeColumns(); -} - -void CheatManagerWindow::closeEvent(QCloseEvent* event) -{ - QtUtils::SaveWindowGeometry("CheatManagerWindow", this); - QWidget::closeEvent(event); - emit closed(); -} - -void CheatManagerWindow::resizeEvent(QResizeEvent* event) -{ - QWidget::resizeEvent(event); - resizeColumns(); -} - -void CheatManagerWindow::resizeColumns() -{ - QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100}); -} - -QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const -{ - QTreeWidgetItemIterator iter(m_ui.cheatList); - while (*iter) - { - QTreeWidgetItem* item = *iter; - const QVariant item_data(item->data(0, Qt::UserRole)); - if (item_data.isValid() && item_data.toUInt() == index) - return item; - - ++iter; - } - - return nullptr; -} - -QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const -{ - const int count = m_ui.cheatList->topLevelItemCount(); - for (int i = 0; i < count; i++) - { - QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); - if (item->text(0) == group_name) - return item; - } - - return nullptr; -} - -QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const -{ - QTreeWidgetItem* group = new QTreeWidgetItem(); - group->setFlags(group->flags() | Qt::ItemIsUserCheckable); - group->setText(0, group_name); - m_ui.cheatList->addTopLevelItem(group); - return group; -} - -QStringList CheatManagerWindow::getCheatGroupNames() const -{ - QStringList group_names; - - const int count = m_ui.cheatList->topLevelItemCount(); - for (int i = 0; i < count; i++) - { - QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); - group_names.push_back(item->text(0)); - } - - return group_names; -} - -static int getCheatIndexFromItem(QTreeWidgetItem* item) -{ - QVariant item_data(item->data(0, Qt::UserRole)); - if (!item_data.isValid()) - return -1; - - return static_cast(item_data.toUInt()); -} - -int CheatManagerWindow::getSelectedCheatIndex() const -{ - QList sel = m_ui.cheatList->selectedItems(); - if (sel.isEmpty()) - return -1; - - return static_cast(getCheatIndexFromItem(sel.first())); -} - -CheatList* CheatManagerWindow::getCheatList() const -{ - return System::IsValid() ? System::GetCheatList() : nullptr; -} - -void CheatManagerWindow::updateCheatList() -{ - QSignalBlocker sb(m_ui.cheatList); - while (m_ui.cheatList->topLevelItemCount() > 0) - delete m_ui.cheatList->takeTopLevelItem(0); - - m_ui.cheatList->setEnabled(false); - m_ui.cheatListAdd->setEnabled(false); - m_ui.cheatListNewCategory->setEnabled(false); - m_ui.cheatListEdit->setEnabled(false); - m_ui.cheatListRemove->setEnabled(false); - m_ui.cheatListActivate->setText(tr("Activate")); - m_ui.cheatListActivate->setEnabled(false); - m_ui.cheatListImport->setEnabled(false); - m_ui.cheatListExport->setEnabled(false); - m_ui.cheatListClear->setEnabled(false); - m_ui.cheatListReset->setEnabled(false); - - Host::RunOnCPUThread([]() { - if (!System::IsValid()) - return; - - CheatList* list = System::GetCheatList(); - if (!list) - { - System::LoadCheatList(); - list = System::GetCheatList(); - } - if (!list) - { - System::LoadCheatListFromDatabase(); - list = System::GetCheatList(); - } - if (!list) - { - System::SetCheatList(std::make_unique()); - list = System::GetCheatList(); - } - - // still racey... - QtHost::RunOnUIThread([list]() { - if (!QtHost::IsSystemValid()) - return; - - CheatManagerWindow* cm = g_main_window->getCheatManagerWindow(); - if (!cm) - return; - - QSignalBlocker sb(cm->m_ui.cheatList); - - const std::vector groups = list->GetCodeGroups(); - for (const std::string& group_name : groups) - { - QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name)); - - const u32 count = list->GetCodeCount(); - bool all_enabled = true; - for (u32 i = 0; i < count; i++) - { - const CheatCode& code = list->GetCode(i); - if (code.group != group_name) - continue; - - QTreeWidgetItem* item = new QTreeWidgetItem(group); - cm->fillItemForCheatCode(item, i, code); - - all_enabled &= code.enabled; - } - - group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked); - group->setExpanded(true); - } - - cm->m_ui.cheatList->setEnabled(true); - cm->m_ui.cheatListAdd->setEnabled(true); - cm->m_ui.cheatListNewCategory->setEnabled(true); - cm->m_ui.cheatListImport->setEnabled(true); - cm->m_ui.cheatListClear->setEnabled(true); - cm->m_ui.cheatListReset->setEnabled(true); - cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0); - }); - }); -} - -void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code) -{ - item->setData(0, Qt::UserRole, QVariant(static_cast(index))); - if (code.IsManuallyActivated()) - { - item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable)); - } - else - { - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked); - } - item->setText(0, QString::fromStdString(code.description)); - item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type))); - item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation))); - item->setText(3, QString::number(static_cast(code.instructions.size()))); -} - -void CheatManagerWindow::saveCheatList() -{ - Host::RunOnCPUThread([]() { System::SaveCheatList(); }); -} - -void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) -{ - const int cheat_index = current ? getCheatIndexFromItem(current) : -1; - const bool has_current = (cheat_index >= 0); - m_ui.cheatListEdit->setEnabled(has_current); - m_ui.cheatListRemove->setEnabled(has_current); - m_ui.cheatListActivate->setEnabled(has_current); - - if (!has_current) - { - m_ui.cheatListActivate->setText(tr("Activate")); - } - else - { - const bool manual_activation = getCheatList()->GetCode(static_cast(cheat_index)).IsManuallyActivated(); - m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle")); - } -} - -void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item) -{ - if (!item) - return; - - const int index = getCheatIndexFromItem(item); - if (index >= 0) - activateCheat(static_cast(index)); -} - -void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column) -{ - if (!item || column != 0) - return; - - CheatList* list = getCheatList(); - - const int index = getCheatIndexFromItem(item); - if (index < 0) - { - // we're probably a parent/group node - const int child_count = item->childCount(); - const Qt::CheckState cs = item->checkState(0); - for (int i = 0; i < child_count; i++) - item->child(i)->setCheckState(0, cs); - - return; - } - - if (static_cast(index) >= list->GetCodeCount()) - return; - - CheatCode& cc = list->GetCode(static_cast(index)); - if (cc.IsManuallyActivated()) - return; - - const bool new_enabled = (item->checkState(0) == Qt::Checked); - if (cc.enabled == new_enabled) - return; - - Host::RunOnCPUThread([index, new_enabled]() { - System::GetCheatList()->SetCodeEnabled(static_cast(index), new_enabled); - System::SaveCheatList(); - }); -} - -void CheatManagerWindow::activateCheat(u32 index) -{ - CheatList* list = getCheatList(); - if (index >= list->GetCodeCount()) - return; - - CheatCode& cc = list->GetCode(index); - if (cc.IsManuallyActivated()) - { - g_emu_thread->applyCheat(index); - return; - } - - const bool new_enabled = !cc.enabled; - setCheatCheckState(index, new_enabled); - - Host::RunOnCPUThread([index, new_enabled]() { - System::GetCheatList()->SetCodeEnabled(index, new_enabled); - System::SaveCheatList(); - }); -} - -void CheatManagerWindow::setCheatCheckState(u32 index, bool checked) -{ - QTreeWidgetItem* item = getItemForCheatIndex(index); - if (item) - { - QSignalBlocker sb(m_ui.cheatList); - item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); - } -} - -void CheatManagerWindow::newCategoryClicked() -{ - QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:")); - if (group_name.isEmpty()) - return; - - if (getItemForCheatGroup(group_name) != nullptr) - { - QMessageBox::critical(this, tr("Error"), tr("This group name already exists.")); - return; - } - - createItemForCheatGroup(group_name); -} - -void CheatManagerWindow::addCodeClicked() -{ - CheatList* list = getCheatList(); - - CheatCode new_code; - new_code.group = "Ungrouped"; - - CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); - if (editor.exec() > 0) - { - const QString group_name_qstr(QString::fromStdString(new_code.group)); - QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); - if (!group_item) - group_item = createItemForCheatGroup(group_name_qstr); - - QTreeWidgetItem* item = new QTreeWidgetItem(group_item); - fillItemForCheatCode(item, list->GetCodeCount(), new_code); - group_item->setExpanded(true); - - Host::RunOnCPUThread( - [&new_code]() { - System::GetCheatList()->AddCode(std::move(new_code)); - System::SaveCheatList(); - }, - true); - } -} - -void CheatManagerWindow::editCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - CheatList* list = getCheatList(); - if (static_cast(index) >= list->GetCodeCount()) - return; - - CheatCode new_code = list->GetCode(static_cast(index)); - CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); - if (editor.exec() > 0) - { - QTreeWidgetItem* item = getItemForCheatIndex(static_cast(index)); - if (item) - { - if (new_code.group != list->GetCode(static_cast(index)).group) - { - item = item->parent()->takeChild(item->parent()->indexOfChild(item)); - - const QString group_name_qstr(QString::fromStdString(new_code.group)); - QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); - if (!group_item) - group_item = createItemForCheatGroup(group_name_qstr); - group_item->addChild(item); - group_item->setExpanded(true); - } - - fillItemForCheatCode(item, static_cast(index), new_code); - } - else - { - // shouldn't happen... - updateCheatList(); - } - - Host::RunOnCPUThread( - [index, &new_code]() { - System::GetCheatList()->SetCode(static_cast(index), std::move(new_code)); - System::SaveCheatList(); - }, - true); - } -} - -void CheatManagerWindow::deleteCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - CheatList* list = getCheatList(); - if (static_cast(index) >= list->GetCodeCount()) - return; - - if (QMessageBox::question(this, tr("Delete Code"), - tr("Are you sure you wish to delete the selected code? This action is not reversible."), - QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread( - [index]() { - System::GetCheatList()->RemoveCode(static_cast(index)); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerWindow::activateCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - activateCheat(static_cast(index)); -} - -void CheatManagerWindow::importClicked() -{ - QMenu menu(this); - connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered); - connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered); - menu.exec(QCursor::pos()); -} - -void CheatManagerWindow::importFromFileTriggered() -{ - const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)")); - const QString filename = - QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter)); - if (filename.isEmpty()) - return; - - CheatList new_cheats; - if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect)) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); - return; - } - - Host::RunOnCPUThread( - [&new_cheats]() { - DebugAssert(System::HasCheatList()); - System::GetCheatList()->MergeList(new_cheats); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerWindow::importFromTextTriggered() -{ - const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:")); - if (text.isEmpty()) - return; - - CheatList new_cheats; - if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect)) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); - return; - } - - Host::RunOnCPUThread( - [&new_cheats]() { - DebugAssert(System::HasCheatList()); - System::GetCheatList()->MergeList(new_cheats); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerWindow::exportClicked() -{ - const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)")); - const QString filename = - QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter)); - if (filename.isEmpty()) - return; - - if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData())) - QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information.")); -} - -void CheatManagerWindow::clearClicked() -{ - if (QMessageBox::question(this, tr("Confirm Clear"), - tr("Are you sure you want to remove all cheats? This is not reversible.")) != - QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true); - updateCheatList(); -} - -void CheatManagerWindow::resetClicked() -{ - if (QMessageBox::question( - this, tr("Confirm Reset"), - tr( - "Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) != - QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true); - updateCheatList(); -} diff --git a/src/duckstation-qt/cheatmanagerwindow.h b/src/duckstation-qt/cheatmanagerwindow.h deleted file mode 100644 index a50f8fb1e..000000000 --- a/src/duckstation-qt/cheatmanagerwindow.h +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin -// SPDX-License-Identifier: CC-BY-NC-ND-4.0 - -#pragma once - -#include "ui_cheatmanagerwindow.h" - -#include "core/cheats.h" - -#include -#include -#include -#include -#include -#include -#include - -class CheatManagerWindow : public QWidget -{ - Q_OBJECT - -public: - CheatManagerWindow(); - ~CheatManagerWindow(); - -Q_SIGNALS: - void closed(); - -protected: - void showEvent(QShowEvent* event); - void closeEvent(QCloseEvent* event); - void resizeEvent(QResizeEvent* event); - void resizeColumns(); - -private Q_SLOTS: - CheatList* getCheatList() const; - void updateCheatList(); - void saveCheatList(); - void cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); - void cheatListItemActivated(QTreeWidgetItem* item); - void cheatListItemChanged(QTreeWidgetItem* item, int column); - void activateCheat(u32 index); - void setCheatCheckState(u32 index, bool checked); - void newCategoryClicked(); - void addCodeClicked(); - void editCodeClicked(); - void deleteCodeClicked(); - void activateCodeClicked(); - void importClicked(); - void importFromFileTriggered(); - void importFromTextTriggered(); - void exportClicked(); - void clearClicked(); - void resetClicked(); - -private: - enum : int - { - MAX_DISPLAYED_SCAN_RESULTS = 5000 - }; - - void connectUi(); - void fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code); - - QTreeWidgetItem* getItemForCheatIndex(u32 index) const; - QTreeWidgetItem* getItemForCheatGroup(const QString& group_name) const; - QTreeWidgetItem* createItemForCheatGroup(const QString& group_name) const; - QStringList getCheatGroupNames() const; - int getSelectedCheatIndex() const; - - Ui::CheatManagerWindow m_ui; - - QTimer* m_update_timer = nullptr; -}; diff --git a/src/duckstation-qt/cheatmanagerwindow.ui b/src/duckstation-qt/cheatmanagerwindow.ui deleted file mode 100644 index 4fe46fd49..000000000 --- a/src/duckstation-qt/cheatmanagerwindow.ui +++ /dev/null @@ -1,144 +0,0 @@ - - - CheatManagerWindow - - - - 0 - 0 - 817 - 462 - - - - Cheat Manager - - - - :/icons/duck.png:/icons/duck.png - - - - - - - - &Add Group... - - - - - - - &Add Code... - - - - - - - &Edit Code... - - - - - - - false - - - &Delete Code - - - - - - - false - - - Activate - - - - - - - Import... - - - - - - - false - - - Export... - - - - - - - Clear - - - - - - - Reset - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - QAbstractItemView::SelectionMode::SingleSelection - - - QAbstractItemView::SelectionBehavior::SelectRows - - - - Name - - - - - Type - - - - - Activation - - - - - Instructions - - - - - - - - - diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 9a5d77201..44bafcbcc 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -9,7 +9,6 @@ - @@ -21,7 +20,9 @@ + + @@ -58,7 +59,6 @@ - @@ -85,6 +85,8 @@ + + @@ -139,9 +141,6 @@ Document - - Document - Document @@ -222,7 +221,6 @@ - @@ -233,10 +231,12 @@ + + @@ -342,6 +342,15 @@ Document + + Document + + + Document + + + Document + @@ -395,7 +404,6 @@ QT_NO_EXCEPTIONS=1;%(PreprocessorDefinitions) 4127;%(DisableSpecificWarnings) %(AdditionalIncludeDirectories);$(SolutionDir)dep\minizip\include - true %(AdditionalOptions) /clang:-frtti diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index c46c6434f..38f69a187 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -23,7 +23,6 @@ - @@ -68,9 +67,6 @@ moc - - moc - moc @@ -175,6 +171,14 @@ + + + + moc + + + moc + @@ -216,7 +220,6 @@ - @@ -237,6 +240,8 @@ + + @@ -252,7 +257,6 @@ - @@ -285,6 +289,9 @@ + + + diff --git a/src/duckstation-qt/gamecheatsettingswidget.cpp b/src/duckstation-qt/gamecheatsettingswidget.cpp new file mode 100644 index 000000000..ddb8c7756 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.cpp @@ -0,0 +1,354 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "gamecheatsettingswidget.h" +#include "cheatcodeeditordialog.h" +#include "mainwindow.h" +#include "qthost.h" +#include "qtutils.h" +#include "settingswindow.h" +#include "settingwidgetbinder.h" + +#include "core/cheats.h" + +#include "common/error.h" + +#include + +GameCheatSettingsWidget::GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog) +{ + m_ui.setupUi(this); + + reloadList(); + + SettingsInterface* sif = m_dialog->getSettingsInterface(); + + // We don't use the binder here, because they're binary - either enabled, or not in the file. + m_ui.enableCheats->setChecked(sif->GetBoolValue("Cheats", "EnableCheats", false)); + m_ui.loadDatabaseCheats->setChecked(sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true)); + updateListEnabled(); + + connect(m_ui.enableCheats, &QCheckBox::checkStateChanged, this, &GameCheatSettingsWidget::onEnableCheatsChanged); + connect(m_ui.loadDatabaseCheats, &QCheckBox::checkStateChanged, this, + &GameCheatSettingsWidget::onLoadDatabaseCheatsChanged); + connect(m_ui.cheatList, &QTreeWidget::itemDoubleClicked, this, + &GameCheatSettingsWidget::onCheatListItemDoubleClicked); + connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged); + connect(m_ui.reloadCheats, &QToolButton::clicked, this, &GameCheatSettingsWidget::onReloadClicked); + connect(m_ui.disableAll, &QToolButton::clicked, this, [this]() { setStateForAll(false); }); + connect(m_ui.importCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onImportClicked); + connect(m_ui.exportCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onExportClicked); +} + +GameCheatSettingsWidget::~GameCheatSettingsWidget() = default; + +std::string GameCheatSettingsWidget::getPathForSavingCheats() const +{ + // Check for the path without the hash first. If we have one of those, keep using it. + std::string path = Cheats::GetChtFilename(m_dialog->getGameSerial(), std::nullopt, true); + if (!FileSystem::FileExists(path.c_str())) + path = Cheats::GetChtFilename(m_dialog->getGameSerial(), m_dialog->getGameHash(), true); + return path; +} + +QStringList GameCheatSettingsWidget::getGroupNames() const +{ +#if 0 +#endif + return QStringList(); +} + +void GameCheatSettingsWidget::onEnableCheatsChanged(Qt::CheckState state) +{ + if (state == Qt::Checked) + m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "EnableCheats", true); + else + m_dialog->getSettingsInterface()->DeleteValue("Cheats", "EnableCheats"); + saveAndReload(true, true, false, true); + updateListEnabled(); +} + +void GameCheatSettingsWidget::onLoadDatabaseCheatsChanged(Qt::CheckState state) +{ + // Default is enabled. + if (state == Qt::Checked) + m_dialog->getSettingsInterface()->DeleteValue("Cheats", "LoadCheatsFromDatabase"); + else + m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "LoadCheatsFromDatabase", false); + saveAndReload(true, true, false, true); + updateListEnabled(); + reloadList(); +} + +void GameCheatSettingsWidget::onCheatListItemDoubleClicked(QTreeWidgetItem* item, int column) +{ + const QVariant item_data = item->data(0, Qt::UserRole); + if (!item_data.isValid()) + return; + + const std::string cheat_name = item_data.toString().toStdString(); + Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, cheat_name); + if (!code) + return; + + CheatCodeEditorDialog dlg(this, *code, getGroupNames()); + if (dlg.exec()) + reloadList(); +} + +void GameCheatSettingsWidget::onCheatListItemChanged(QTreeWidgetItem* item, int column) +{ + const QVariant item_data = item->data(0, Qt::UserRole); + if (!item_data.isValid()) + return; + + std::string cheat_name = item_data.toString().toStdString(); + const bool current_enabled = + (std::find(m_enabled_codes.begin(), m_enabled_codes.end(), cheat_name) != m_enabled_codes.end()); + const bool current_checked = (item->checkState(0) == Qt::Checked); + if (current_enabled == current_checked) + return; + + setCheatEnabled(std::move(cheat_name), current_checked, true); +} + +void GameCheatSettingsWidget::onReloadClicked() +{ + reloadList(); + g_emu_thread->reloadPatches(true, false, true, true); +} + +bool GameCheatSettingsWidget::shouldLoadFromDatabase() const +{ + return m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true); +} + +void GameCheatSettingsWidget::updateListEnabled() +{ + const bool cheats_enabled = m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "EnableCheats", false); + m_ui.cheatList->setEnabled(cheats_enabled); + m_ui.add->setEnabled(cheats_enabled); + m_ui.remove->setEnabled(cheats_enabled); + m_ui.disableAll->setEnabled(cheats_enabled); + m_ui.reloadCheats->setEnabled(cheats_enabled); +} + +void GameCheatSettingsWidget::disableAllCheats() +{ + SettingsInterface* sif = m_dialog->getSettingsInterface(); + sif->RemoveSection(Cheats::CHEATS_CONFIG_SECTION); + saveAndReload(false, true, false, true); +} + +void GameCheatSettingsWidget::saveAndReload(bool reload_files, bool reload_enabled_list, bool verbose, + bool verbose_if_changed) +{ + QtHost::SaveGameSettings(m_dialog->getSettingsInterface(), true); + g_emu_thread->reloadPatches(reload_files, reload_enabled_list, verbose, verbose_if_changed); +} + +void GameCheatSettingsWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {320, 100, -1}); +} + +void GameCheatSettingsWidget::setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings) +{ + SettingsInterface* si = m_dialog->getSettingsInterface(); + const auto it = std::find(m_enabled_codes.begin(), m_enabled_codes.end(), name); + + if (enabled) + { + si->AddToStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str()); + if (it == m_enabled_codes.end()) + m_enabled_codes.push_back(std::move(name)); + } + else + { + si->RemoveFromStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str()); + if (it != m_enabled_codes.end()) + m_enabled_codes.erase(it); + } + + if (save_and_reload_settings) + saveAndReload(false, true, false, true); +} + +void GameCheatSettingsWidget::setStateForAll(bool enabled) +{ + QSignalBlocker sb(m_ui.cheatList); + setStateRecursively(nullptr, enabled); + saveAndReload(false, true, false, true); +} + +void GameCheatSettingsWidget::setStateRecursively(QTreeWidgetItem* parent, bool enabled) +{ + const int count = parent ? parent->childCount() : m_ui.cheatList->topLevelItemCount(); + for (int i = 0; i < count; i++) + { + QTreeWidgetItem* item = parent ? parent->child(i) : m_ui.cheatList->topLevelItem(i); + const QVariant item_data = item->data(0, Qt::UserRole); + if (item_data.isValid()) + { + if ((item->checkState(0) == Qt::Checked) != enabled) + { + item->setCheckState(0, enabled ? Qt::Checked : Qt::Unchecked); + setCheatEnabled(item_data.toString().toStdString(), enabled, false); + } + } + else + { + setStateRecursively(item, enabled); + } + } +} + +void GameCheatSettingsWidget::reloadList() +{ + // Show all hashes, since the ini is shared. + m_codes = Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, true, shouldLoadFromDatabase()); + m_enabled_codes = + m_dialog->getSettingsInterface()->GetStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY); + + m_parent_map.clear(); + while (m_ui.cheatList->topLevelItemCount() > 0) + delete m_ui.cheatList->takeTopLevelItem(0); + + for (const Cheats::CodeInfo& ci : m_codes) + { + const bool enabled = (std::find(m_enabled_codes.begin(), m_enabled_codes.end(), ci.name) != m_enabled_codes.end()); + + const std::string_view parent_part = ci.GetNameParentPart(); + + QTreeWidgetItem* parent = getTreeWidgetParent(parent_part); + QTreeWidgetItem* item = new QTreeWidgetItem(); + populateTreeWidgetItem(item, ci, enabled); + if (parent) + parent->addChild(item); + else + m_ui.cheatList->addTopLevelItem(item); + } + + // Hide root indicator when there's no groups, frees up some whitespace. + m_ui.cheatList->setRootIsDecorated(!m_parent_map.empty()); +} + +void GameCheatSettingsWidget::onImportClicked() +{ + QMenu menu(this); + connect(menu.addAction(tr("From File...")), &QAction::triggered, this, + &GameCheatSettingsWidget::onImportFromFileTriggered); + connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, + &GameCheatSettingsWidget::onImportFromTextTriggered); + menu.exec(QCursor::pos()); +} + +void GameCheatSettingsWidget::onImportFromFileTriggered() +{ + const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)")); + const QString filename = + QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter)); + if (filename.isEmpty()) + return; + + Error error; + const std::optional file_contents = FileSystem::ReadFileToString(filename.toStdString().c_str(), &error); + if (!file_contents.has_value()) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to read file:\n%1").arg(QString::fromStdString(error.GetDescription()))); + return; + } + + importCodes(file_contents.value()); +} + +void GameCheatSettingsWidget::onImportFromTextTriggered() +{ + const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:")); + if (text.isEmpty()) + return; + + importCodes(text.toStdString()); +} + +void GameCheatSettingsWidget::importCodes(const std::string& file_contents) +{ + Error error; + Cheats::CodeInfoList new_codes; + if (!Cheats::ImportCodesFromString(&new_codes, file_contents, Cheats::FileFormat::Unknown, true, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to parse file:\n%1").arg(QString::fromStdString(error.GetDescription()))); + return; + } + + Cheats::MergeCheatList(&m_codes, std::move(new_codes)); + if (!Cheats::SaveCodesToFile(getPathForSavingCheats().c_str(), m_codes, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription()))); + } + + reloadList(); +} + +void GameCheatSettingsWidget::onExportClicked() +{ + const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)")); + const QString filename = + QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter)); + if (filename.isEmpty()) + return; + + Error error; + if (!Cheats::ExportCodesToFile(filename.toStdString(), m_codes, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to save cheat file:\n%1").arg(QString::fromStdString(error.GetDescription()))); + } +} + +QTreeWidgetItem* GameCheatSettingsWidget::getTreeWidgetParent(const std::string_view parent) +{ + if (parent.empty()) + return nullptr; + + auto it = m_parent_map.find(parent); + if (it != m_parent_map.end()) + return it->second; + + std::string_view this_part = parent; + QTreeWidgetItem* parent_to_this = nullptr; + const std::string_view::size_type pos = parent.rfind('\\'); + if (pos != std::string::npos && pos != (parent.size() - 1)) + { + // go up the chain until we find the real parent, then back down + parent_to_this = getTreeWidgetParent(parent.substr(0, pos)); + this_part = parent.substr(pos + 1); + } + + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setText(0, QString::fromUtf8(this_part.data(), this_part.length())); + + if (parent_to_this) + parent_to_this->addChild(item); + else + m_ui.cheatList->addTopLevelItem(item); + + // Must be called after adding. + item->setExpanded(true); + m_parent_map.emplace(parent, item); + return item; +} + +void GameCheatSettingsWidget::populateTreeWidgetItem(QTreeWidgetItem* item, const Cheats::CodeInfo& pi, bool enabled) +{ + const std::string_view name_part = pi.GetNamePart(); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + item->setCheckState(0, enabled ? Qt::Checked : Qt::Unchecked); + item->setData(0, Qt::UserRole, QString::fromStdString(pi.name)); + if (!name_part.empty()) + item->setText(0, QtUtils::StringViewToQString(name_part)); +} diff --git a/src/duckstation-qt/gamecheatsettingswidget.h b/src/duckstation-qt/gamecheatsettingswidget.h new file mode 100644 index 000000000..120a279e1 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "ui_gamecheatsettingswidget.h" + +#include "core/cheats.h" + +#include "common/heterogeneous_containers.h" + +#include +#include + +#include +#include +#include + +namespace GameList { +struct Entry; +} + +class SettingsWindow; + +class GameCheatSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent); + ~GameCheatSettingsWidget(); + + std::string getPathForSavingCheats() const; + QStringList getGroupNames() const; + void disableAllCheats(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private Q_SLOTS: + void onEnableCheatsChanged(Qt::CheckState state); + void onLoadDatabaseCheatsChanged(Qt::CheckState state); + void onCheatListItemDoubleClicked(QTreeWidgetItem* item, int column); + void onCheatListItemChanged(QTreeWidgetItem* item, int column); + void onReloadClicked(); + void updateListEnabled(); + void reloadList(); + void onImportClicked(); + void onImportFromFileTriggered(); + void onImportFromTextTriggered(); + void onExportClicked(); + +private: + bool shouldLoadFromDatabase() const; + + QTreeWidgetItem* getTreeWidgetParent(const std::string_view parent); + void populateTreeWidgetItem(QTreeWidgetItem* item, const Cheats::CodeInfo& pi, bool enabled); + void setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings); + void setStateForAll(bool enabled); + void setStateRecursively(QTreeWidgetItem* parent, bool enabled); + void saveAndReload(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed); + void importCodes(const std::string& file_contents); + + Ui::GameCheatSettingsWidget m_ui; + SettingsWindow* m_dialog; + + UnorderedStringMap m_parent_map; + Cheats::CodeInfoList m_codes; + std::vector m_enabled_codes; +}; diff --git a/src/duckstation-qt/gamecheatsettingswidget.ui b/src/duckstation-qt/gamecheatsettingswidget.ui new file mode 100644 index 000000000..9e968bd04 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.ui @@ -0,0 +1,168 @@ + + + GameCheatSettingsWidget + + + + 0 + 0 + 821 + 401 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted saves. Cheats can persist through save states even after being disabled, please remember to reset/reboot the game after turning off any codes. + + + true + + + + + + + + + Enable Cheats + + + + + + + Load Database Cheats + + + + + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionMode::NoSelection + + + QAbstractItemView::SelectionBehavior::SelectItems + + + true + + + + Name + + + + + + + + + + + 0 + 0 + + + + Add Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + + 0 + 0 + + + + Remove Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + Disable All Cheats + + + + + + + + + + Reload Cheats + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Import... + + + + + + + Export... + + + + + + + + + + diff --git a/src/duckstation-qt/gamepatchdetailswidget.ui b/src/duckstation-qt/gamepatchdetailswidget.ui new file mode 100644 index 000000000..a68410fee --- /dev/null +++ b/src/duckstation-qt/gamepatchdetailswidget.ui @@ -0,0 +1,87 @@ + + + GamePatchDetailsWidget + + + + 0 + 0 + 541 + 112 + + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 12 + true + + + + Patch Title + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Enabled + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Author: </span>Patch Author</p><p>Description would go here</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + diff --git a/src/duckstation-qt/gamepatchsettingswidget.cpp b/src/duckstation-qt/gamepatchsettingswidget.cpp new file mode 100644 index 000000000..b674de9f4 --- /dev/null +++ b/src/duckstation-qt/gamepatchsettingswidget.cpp @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "gamepatchsettingswidget.h" +#include "mainwindow.h" +#include "qthost.h" +#include "qtutils.h" +#include "settingswindow.h" +#include "settingwidgetbinder.h" + +#include "core/cheats.h" + +#include "common/assert.h" + +#include + +GamePatchDetailsWidget::GamePatchDetailsWidget(std::string name, const std::string& author, + const std::string& description, bool enabled, SettingsWindow* dialog, + QWidget* parent) + : QWidget(parent), m_dialog(dialog), m_name(name) +{ + m_ui.setupUi(this); + + m_ui.name->setText(QString::fromStdString(name)); + m_ui.description->setText( + tr("Author: %1
%2") + .arg(author.empty() ? tr("Unknown") : QString::fromStdString(author)) + .arg(description.empty() ? tr("No description provided.") : QString::fromStdString(description))); + + DebugAssert(dialog->getSettingsInterface()); + m_ui.enabled->setChecked(enabled); + connect(m_ui.enabled, &QCheckBox::checkStateChanged, this, &GamePatchDetailsWidget::onEnabledStateChanged); +} + +GamePatchDetailsWidget::~GamePatchDetailsWidget() = default; + +void GamePatchDetailsWidget::onEnabledStateChanged(int state) +{ + SettingsInterface* si = m_dialog->getSettingsInterface(); + if (state == Qt::Checked) + si->AddToStringList("Patches", "Enable", m_name.c_str()); + else + si->RemoveFromStringList("Patches", "Enable", m_name.c_str()); + + si->Save(); + g_emu_thread->reloadGameSettings(); +} + +GamePatchSettingsWidget::GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog) +{ + m_ui.setupUi(this); + m_ui.scrollArea->setFrameShape(QFrame::WinPanel); + m_ui.scrollArea->setFrameShadow(QFrame::Sunken); + + connect(m_ui.reload, &QPushButton::clicked, this, &GamePatchSettingsWidget::onReloadClicked); + connect(m_ui.disableAllPatches, &QPushButton::clicked, this, &GamePatchSettingsWidget::disableAllPatches); + + reloadList(); +} + +GamePatchSettingsWidget::~GamePatchSettingsWidget() = default; + +void GamePatchSettingsWidget::onReloadClicked() +{ + reloadList(); + + // reload it on the emu thread too, so it picks up any changes + g_emu_thread->reloadPatches(true, false, true, true); +} + +void GamePatchSettingsWidget::disableAllPatches() +{ + SettingsInterface* sif = m_dialog->getSettingsInterface(); + sif->RemoveSection(Cheats::PATCHES_CONFIG_SECTION); + QtHost::SaveGameSettings(sif, true); + g_emu_thread->reloadPatches(false, true, false, true); + reloadList(); +} + +void GamePatchSettingsWidget::reloadList() +{ + const std::vector patches = + Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, false, true); + const std::vector enabled_list = + m_dialog->getSettingsInterface()->GetStringList(Cheats::PATCHES_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY); + + delete m_ui.scrollArea->takeWidget(); + + QWidget* container = new QWidget(m_ui.scrollArea); + QVBoxLayout* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + + if (!patches.empty()) + { + bool first = true; + + for (const Cheats::CodeInfo& pi : patches) + { + if (!first) + { + QFrame* frame = new QFrame(container); + frame->setFrameShape(QFrame::HLine); + frame->setFrameShadow(QFrame::Sunken); + layout->addWidget(frame); + } + else + { + first = false; + } + + const bool enabled = (std::find(enabled_list.begin(), enabled_list.end(), pi.name) != enabled_list.end()); + GamePatchDetailsWidget* it = + new GamePatchDetailsWidget(std::move(pi.name), pi.author, pi.description, enabled, m_dialog, container); + layout->addWidget(it); + } + } + else + { + QLabel* label = new QLabel(tr("There are no patches available for this game."), container); + layout->addWidget(label); + } + + layout->addStretch(1); + + m_ui.scrollArea->setWidget(container); +} diff --git a/src/duckstation-qt/gamepatchsettingswidget.h b/src/duckstation-qt/gamepatchsettingswidget.h new file mode 100644 index 000000000..7f2e118c7 --- /dev/null +++ b/src/duckstation-qt/gamepatchsettingswidget.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "ui_gamepatchdetailswidget.h" +#include "ui_gamepatchsettingswidget.h" + +#include + +namespace GameList { +struct Entry; +} + +class SettingsWindow; + +class GamePatchDetailsWidget : public QWidget +{ + Q_OBJECT + +public: + GamePatchDetailsWidget(std::string name, const std::string& author, const std::string& description, bool enabled, + SettingsWindow* dialog, QWidget* parent); + ~GamePatchDetailsWidget(); + +private Q_SLOTS: + void onEnabledStateChanged(int state); + +private: + Ui::GamePatchDetailsWidget m_ui; + SettingsWindow* m_dialog; + std::string m_name; +}; + +class GamePatchSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent); + ~GamePatchSettingsWidget(); + +public Q_SLOTS: + void disableAllPatches(); + +private Q_SLOTS: + void onReloadClicked(); + +private: + void reloadList(); + + Ui::GamePatchSettingsWidget m_ui; + SettingsWindow* m_dialog; +}; diff --git a/src/duckstation-qt/gamepatchsettingswidget.ui b/src/duckstation-qt/gamepatchsettingswidget.ui new file mode 100644 index 000000000..df0f02140 --- /dev/null +++ b/src/duckstation-qt/gamepatchsettingswidget.ui @@ -0,0 +1,81 @@ + + + GamePatchSettingsWidget + + + + 0 + 0 + 766 + 392 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Activating game patches can cause unpredictable behavior, crashing, soft-locks, or broken saved games. Use patches at your own risk, no support will be provided to users who have enabled game patches. + + + true + + + + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + true + + + + + + + + + Disable All Patches + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Reload Patches + + + + + + + + + + diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 5b047d4c2..a22252466 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -5,7 +5,6 @@ #include "aboutdialog.h" #include "achievementlogindialog.h" #include "autoupdaterdialog.h" -#include "cheatmanagerwindow.h" #include "coverdownloaddialog.h" #include "debuggerwindow.h" #include "displaywidget.h" @@ -23,6 +22,7 @@ #include "settingwidgetbinder.h" #include "core/achievements.h" +#include "core/cheats.h" #include "core/game_list.h" #include "core/host.h" #include "core/memory_card.h" @@ -787,7 +787,6 @@ void MainWindow::destroySubWindows() { QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); QtUtils::CloseAndDeleteWindow(m_debugger_window); - QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window); QtUtils::CloseAndDeleteWindow(m_controller_settings_window); QtUtils::CloseAndDeleteWindow(m_settings_window); @@ -1061,62 +1060,47 @@ void MainWindow::onCheatsActionTriggered() void MainWindow::onCheatsMenuAboutToShow() { m_ui.menuCheats->clear(); - connect(m_ui.menuCheats->addAction(tr("Cheat Manager")), &QAction::triggered, this, &MainWindow::openCheatManager); + connect(m_ui.menuCheats->addAction(tr("Select Cheats...")), &QAction::triggered, this, + [this]() { openGamePropertiesForCurrentGame("Cheats"); }); m_ui.menuCheats->addSeparator(); populateCheatsMenu(m_ui.menuCheats); } void MainWindow::populateCheatsMenu(QMenu* menu) { - const bool has_cheat_list = (s_system_valid && System::HasCheatList()); + Host::RunOnCPUThread([menu]() { + if (!System::IsValid()) + return; - QMenu* enabled_menu = menu->addMenu(tr("&Enabled Cheats")); - enabled_menu->setEnabled(s_system_valid); - QMenu* apply_menu = menu->addMenu(tr("&Apply Cheats")); - apply_menu->setEnabled(s_system_valid); - - if (has_cheat_list) - { - CheatList* cl = System::GetCheatList(); - for (const std::string& group : cl->GetCodeGroups()) + if (!Cheats::AreCheatsEnabled()) { - QMenu* enabled_submenu = nullptr; - QMenu* apply_submenu = nullptr; - - for (u32 i = 0; i < cl->GetCodeCount(); i++) - { - CheatCode& cc = cl->GetCode(i); - if (cc.group != group) - continue; - - QString desc(QString::fromStdString(cc.description)); - if (cc.IsManuallyActivated()) - { - if (!apply_submenu) - { - apply_menu->setEnabled(true); - apply_submenu = apply_menu->addMenu(QString::fromStdString(group)); - } - - QAction* action = apply_submenu->addAction(desc); - connect(action, &QAction::triggered, [i]() { g_emu_thread->applyCheat(i); }); - } - else - { - if (!enabled_submenu) - { - enabled_menu->setEnabled(true); - enabled_submenu = enabled_menu->addMenu(QString::fromStdString(group)); - } - - QAction* action = enabled_submenu->addAction(desc); - action->setCheckable(true); - action->setChecked(cc.enabled); - connect(action, &QAction::toggled, [i](bool enabled) { g_emu_thread->setCheatEnabled(i, enabled); }); - } - } + QAction* action = menu->addAction(tr("Cheats are not enabled.")); + action->setEnabled(false); + return; } - } + + QStringList names; + Cheats::EnumerateManualCodes([&names](const std::string& name) { + names.append(QString::fromStdString(name)); + return true; + }); + if (names.empty()) + return; + + QtHost::RunOnUIThread([menu, names = std::move(names)]() { + QMenu* apply_submenu = menu->addMenu(tr("&Apply Cheat")); + for (const QString& name : names) + { + const QAction* action = apply_submenu->addAction(name); + connect(action, &QAction::triggered, apply_submenu, [action]() { + Host::RunOnCPUThread([name = action->text().toStdString()]() { + if (System::IsValid()) + Cheats::ApplyManualCode(name); + }); + }); + } + }); + }); } const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry, @@ -1375,23 +1359,6 @@ void MainWindow::onViewSystemDisplayTriggered() switchToEmulationView(); } -void MainWindow::onViewGamePropertiesActionTriggered() -{ - if (!s_system_valid) - return; - - Host::RunOnCPUThread([]() { - const std::string& path = System::GetDiscPath(); - const std::string& serial = System::GetGameSerial(); - if (path.empty() || serial.empty()) - return; - - QtHost::RunOnUIThread([path = path, serial = serial]() { - SettingsWindow::openGamePropertiesDialog(path, System::GetGameTitle(), serial, System::GetDiscRegion()); - }); - }); -} - void MainWindow::onGitHubRepositoryActionTriggered() { QtUtils::OpenURL(this, "https://github.com/stenzek/duckstation/"); @@ -1486,7 +1453,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) if (!entry->IsDiscSet()) { connect(menu.addAction(tr("Properties...")), &QAction::triggered, [entry]() { - SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->region); + SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->hash, entry->region); }); connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() { @@ -1556,7 +1523,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) if (first_disc) { SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->title, first_disc->serial, - first_disc->region); + first_disc->hash, first_disc->region); } }); @@ -2081,7 +2048,7 @@ void MainWindow::connectSignals() connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered); connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered); connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered); - connect(m_ui.actionViewGameProperties, &QAction::triggered, this, &MainWindow::onViewGamePropertiesActionTriggered); + connect(m_ui.actionViewGameProperties, &QAction::triggered, this, [this]() { openGamePropertiesForCurrentGame(); }); connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered); connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); connect(m_ui.actionViewThirdPartyNotices, &QAction::triggered, this, @@ -2096,8 +2063,10 @@ void MainWindow::connectSignals() connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); - connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenTextureDirectoryTriggered); - connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread, &EmuThread::reloadTextureReplacements); + connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this, + &MainWindow::onToolsOpenTextureDirectoryTriggered); + connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread, + &EmuThread::reloadTextureReplacements); connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets); connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); @@ -2336,6 +2305,25 @@ void MainWindow::doSettings(const char* category /* = nullptr */) dlg->setCategory(category); } +void MainWindow::openGamePropertiesForCurrentGame(const char* category /* = nullptr */) +{ + if (!s_system_valid) + return; + + Host::RunOnCPUThread([category]() { + const std::string& path = System::GetDiscPath(); + const std::string& serial = System::GetGameSerial(); + if (path.empty() || serial.empty()) + return; + + QtHost::RunOnUIThread([title = std::string(System::GetGameTitle()), path = std::string(path), + serial = std::string(serial), hash = System::GetGameHash(), region = System::GetDiscRegion(), + category]() { + SettingsWindow::openGamePropertiesDialog(path, title, std::move(serial), hash, region, category); + }); + }); +} + ControllerSettingsWindow* MainWindow::getControllerSettingsWindow() { if (!m_controller_settings_window) @@ -2710,7 +2698,6 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled) { if (enabled) { - QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); QtUtils::CloseAndDeleteWindow(m_debugger_window); QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); } @@ -2782,23 +2769,6 @@ void MainWindow::onToolsMemoryScannerTriggered() QtUtils::ShowOrRaiseWindow(m_memory_scanner_window); } -void MainWindow::openCheatManager() -{ - if (Achievements::IsHardcoreModeActive()) - return; - - if (!m_cheat_manager_window) - { - m_cheat_manager_window = new CheatManagerWindow(); - connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() { - m_cheat_manager_window->deleteLater(); - m_cheat_manager_window = nullptr; - }); - } - - QtUtils::ShowOrRaiseWindow(m_cheat_manager_window); -} - void MainWindow::openCPUDebugger() { if (!m_debugger_window) diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 039ebddff..d8ef980af 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -29,7 +29,6 @@ class GameListWidget; class EmuThread; class AutoUpdaterDialog; class MemoryCardEditorWindow; -class CheatManagerWindow; class DebuggerWindow; class MemoryScannerWindow; @@ -96,9 +95,6 @@ public: ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; } ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; } - /// Accessors for child windows. - CheatManagerWindow* getCheatManagerWindow() const { return m_cheat_manager_window; } - /// Opens the editor for a specific input profile. void openInputProfileEditor(const std::string_view name); @@ -167,7 +163,6 @@ private Q_SLOTS: void onViewGameListActionTriggered(); void onViewGameGridActionTriggered(); void onViewSystemDisplayTriggered(); - void onViewGamePropertiesActionTriggered(); void onGitHubRepositoryActionTriggered(); void onIssueTrackerActionTriggered(); void onDiscordServerActionTriggered(); @@ -189,7 +184,6 @@ private Q_SLOTS: void onUpdateCheckComplete(); - void openCheatManager(); void openCPUDebugger(); protected: @@ -242,6 +236,7 @@ private: SettingsWindow* getSettingsWindow(); void doSettings(const char* category = nullptr); + void openGamePropertiesForCurrentGame(const char* category = nullptr); ControllerSettingsWindow* getControllerSettingsWindow(); void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count); @@ -300,7 +295,6 @@ private: AutoUpdaterDialog* m_auto_updater_dialog = nullptr; MemoryCardEditorWindow* m_memory_card_editor_window = nullptr; - CheatManagerWindow* m_cheat_manager_window = nullptr; DebuggerWindow* m_debugger_window = nullptr; MemoryScannerWindow* m_memory_scanner_window = nullptr; diff --git a/src/duckstation-qt/memoryscannerwindow.h b/src/duckstation-qt/memoryscannerwindow.h index df941746b..0b47d7e02 100644 --- a/src/duckstation-qt/memoryscannerwindow.h +++ b/src/duckstation-qt/memoryscannerwindow.h @@ -5,7 +5,7 @@ #include "ui_memoryscannerwindow.h" -#include "core/cheats.h" +#include "core/memory_scanner.h" #include #include diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 35c6c7c0d..108f6a841 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1293,28 +1293,34 @@ void EmuThread::changeDiscFromPlaylist(quint32 index) errorReported(tr("Error"), tr("Failed to switch to subimage %1").arg(index)); } -void EmuThread::setCheatEnabled(quint32 index, bool enabled) +void EmuThread::reloadPatches(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) { if (!isOnThread()) { - QMetaObject::invokeMethod(this, "setCheatEnabled", Qt::QueuedConnection, Q_ARG(quint32, index), - Q_ARG(bool, enabled)); + QMetaObject::invokeMethod(this, "reloadPatches", Qt::QueuedConnection, Q_ARG(bool, reload_files), + Q_ARG(bool, reload_enabled_list), Q_ARG(bool, verbose), Q_ARG(bool, verbose_if_changed)); return; } - System::SetCheatCodeState(index, enabled); - emit cheatEnabled(index, enabled); + if (System::IsValid()) + { + // If the reloaded list is being enabled, we also need to reload the gameini file. + if (reload_enabled_list) + System::ReloadGameSettings(verbose); + Cheats::ReloadCheats(reload_files, reload_enabled_list, verbose, verbose_if_changed); + } } -void EmuThread::applyCheat(quint32 index) +void EmuThread::applyCheat(const QString& name) { if (!isOnThread()) { - QMetaObject::invokeMethod(this, "applyCheat", Qt::QueuedConnection, Q_ARG(quint32, index)); + QMetaObject::invokeMethod(this, "applyCheat", Qt::QueuedConnection, Q_ARG(const QString&, name)); return; } - System::ApplyCheatCode(index); + if (System::IsValid()) + Cheats::ApplyManualCode(name.toStdString()); } void EmuThread::reloadPostProcessingShaders() diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index f2881b168..bec47617a 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -158,6 +158,7 @@ public Q_SLOTS: void setDefaultSettings(bool system = true, bool controller = true); void applySettings(bool display_osd_messages = false); void reloadGameSettings(bool display_osd_messages = false); + void reloadPatches(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed); void updateEmuFolders(); void updateControllerSettings(); void reloadInputSources(); @@ -194,8 +195,7 @@ public Q_SLOTS: void setFullscreen(bool fullscreen, bool allow_render_to_main); void setSurfaceless(bool surfaceless); void requestDisplaySize(float scale); - void setCheatEnabled(quint32 index, bool enabled); - void applyCheat(quint32 index); + void applyCheat(const QString& name); void reloadPostProcessingShaders(); void updatePostProcessingSettings(); void clearInputBindStateFromSource(InputBindingKey key); diff --git a/src/duckstation-qt/resources/duckstation-qt.qrc b/src/duckstation-qt/resources/duckstation-qt.qrc index fb54c25e6..88e25b99a 100644 --- a/src/duckstation-qt/resources/duckstation-qt.qrc +++ b/src/duckstation-qt/resources/duckstation-qt.qrc @@ -33,6 +33,7 @@ icons/black/svg/artboard-2-line.svg icons/black/svg/cheats-line.svg icons/black/svg/checkbox-multiple-blank-line.svg + icons/black/svg/chat-off-line.svg icons/black/svg/chip-2-line.svg icons/black/svg/chip-line.svg icons/black/svg/close-line.svg @@ -98,6 +99,7 @@ icons/black/svg/settings-3-line.svg icons/black/svg/shut-down-line.svg icons/black/svg/sparkle-fill.svg + icons/black/svg/sparkling-line.svg icons/black/svg/sun-fill.svg icons/black/svg/trash-fill.svg icons/black/svg/trophy-line.svg @@ -263,6 +265,7 @@ icons/white/svg/artboard-2-line.svg icons/white/svg/cheats-line.svg icons/white/svg/checkbox-multiple-blank-line.svg + icons/white/svg/chat-off-line.svg icons/white/svg/chip-2-line.svg icons/white/svg/chip-line.svg icons/white/svg/close-line.svg @@ -328,6 +331,7 @@ icons/white/svg/settings-3-line.svg icons/white/svg/shut-down-line.svg icons/white/svg/sparkle-fill.svg + icons/white/svg/sparkling-line.svg icons/white/svg/sun-fill.svg icons/white/svg/trash-fill.svg icons/white/svg/trophy-line.svg diff --git a/src/duckstation-qt/resources/icons/black/svg/chat-off-line.svg b/src/duckstation-qt/resources/icons/black/svg/chat-off-line.svg new file mode 100644 index 000000000..760ca3c2c --- /dev/null +++ b/src/duckstation-qt/resources/icons/black/svg/chat-off-line.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/black/svg/sparkling-line.svg b/src/duckstation-qt/resources/icons/black/svg/sparkling-line.svg new file mode 100644 index 000000000..a04b4badd --- /dev/null +++ b/src/duckstation-qt/resources/icons/black/svg/sparkling-line.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/white/svg/chat-off-line.svg b/src/duckstation-qt/resources/icons/white/svg/chat-off-line.svg new file mode 100644 index 000000000..c11100226 --- /dev/null +++ b/src/duckstation-qt/resources/icons/white/svg/chat-off-line.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/white/svg/sparkling-line.svg b/src/duckstation-qt/resources/icons/white/svg/sparkling-line.svg new file mode 100644 index 000000000..ca5e6b5c5 --- /dev/null +++ b/src/duckstation-qt/resources/icons/white/svg/sparkling-line.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/duckstation-qt/settingswindow.cpp b/src/duckstation-qt/settingswindow.cpp index ecf776017..53c812c6d 100644 --- a/src/duckstation-qt/settingswindow.cpp +++ b/src/duckstation-qt/settingswindow.cpp @@ -9,7 +9,9 @@ #include "consolesettingswidget.h" #include "emulationsettingswidget.h" #include "foldersettingswidget.h" +#include "gamecheatsettingswidget.h" #include "gamelistsettingswidget.h" +#include "gamepatchsettingswidget.h" #include "gamesummarywidget.h" #include "graphicssettingswidget.h" #include "interfacesettingswidget.h" @@ -46,9 +48,9 @@ SettingsWindow::SettingsWindow() : QWidget() connectUi(); } -SettingsWindow::SettingsWindow(const std::string& path, const std::string& serial, DiscRegion region, +SettingsWindow::SettingsWindow(const std::string& path, const std::string& serial, GameHash hash, DiscRegion region, const GameDatabase::Entry* entry, std::unique_ptr sif) - : QWidget(), m_sif(std::move(sif)), m_database_entry(entry) + : QWidget(), m_sif(std::move(sif)), m_database_entry(entry), m_serial(serial), m_hash(hash) { m_ui.setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -108,6 +110,18 @@ void SettingsWindow::addPages() QStringLiteral("emulation-line"), tr("Emulation Settings
These options determine the speed and runahead behavior of the " "system.

Mouse over an option for additional information, and Shift+Wheel to scroll this panel.")); + + if (isPerGameSettings()) + { + addWidget(m_game_patch_settings_widget = new GamePatchSettingsWidget(this, m_ui.settingsContainer), tr("Patches"), + QStringLiteral("sparkling-line"), + tr("Patches
This section allows you to select optional patches to apply to the game, " + "which may provide performance, visual, or gameplay improvements.")); + addWidget(m_game_cheat_settings_widget = new GameCheatSettingsWidget(this, m_ui.settingsContainer), tr("Cheats"), + QStringLiteral("cheats-line"), + tr("Cheats
This section allows you to select which cheats you wish to enable.")); + } + addWidget( m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"), QStringLiteral("memcard-line"), @@ -635,8 +649,9 @@ bool SettingsWindow::hasGameTrait(GameDatabase::Trait trait) m_sif->GetBoolValue("Main", "ApplyCompatibilitySettings", true)); } -void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std::string& title, - const std::string& serial, DiscRegion region) +SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path, const std::string& title, + std::string serial, GameHash hash, DiscRegion region, + const char* category /* = nullptr */) { const GameDatabase::Entry* dentry = nullptr; if (!System::IsExeFileName(path) && !System::IsPsfFileName(path)) @@ -669,7 +684,9 @@ void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std dialog->raise(); dialog->activateWindow(); dialog->setFocus(); - return; + if (category) + dialog->setCategory(category); + return dialog; } } @@ -677,8 +694,11 @@ void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std if (FileSystem::FileExists(sif->GetFileName().c_str())) sif->Load(); - SettingsWindow* dialog = new SettingsWindow(path, real_serial, region, dentry, std::move(sif)); + SettingsWindow* dialog = new SettingsWindow(path, real_serial, hash, region, dentry, std::move(sif)); dialog->show(); + if (category) + dialog->setCategory(category); + return dialog; } void SettingsWindow::closeGamePropertiesDialogs() diff --git a/src/duckstation-qt/settingswindow.h b/src/duckstation-qt/settingswindow.h index 9414abaa1..12aee995d 100644 --- a/src/duckstation-qt/settingswindow.h +++ b/src/duckstation-qt/settingswindow.h @@ -6,12 +6,13 @@ #include "util/ini_settings_interface.h" -#include "common/types.h" +#include "core/types.h" #include #include #include #include +#include class QWheelEvent; @@ -25,6 +26,8 @@ struct Entry; class InterfaceSettingsWidget; class BIOSSettingsWidget; class GameListSettingsWidget; +class GamePatchSettingsWidget; +class GameCheatSettingsWidget; class ConsoleSettingsWidget; class EmulationSettingsWidget; class MemoryCardSettingsWidget; @@ -41,12 +44,12 @@ class SettingsWindow final : public QWidget public: SettingsWindow(); - SettingsWindow(const std::string& path, const std::string& serial, DiscRegion region, + SettingsWindow(const std::string& path, const std::string& serial, GameHash hash, DiscRegion region, const GameDatabase::Entry* entry, std::unique_ptr sif); ~SettingsWindow(); - static void openGamePropertiesDialog(const std::string& path, const std::string& title, const std::string& serial, - DiscRegion region); + static SettingsWindow* openGamePropertiesDialog(const std::string& path, const std::string& title, std::string serial, + GameHash hash, DiscRegion region, const char* category = nullptr); static void closeGamePropertiesDialogs(); // Helper for externally setting fields in game settings ini. @@ -56,6 +59,8 @@ public: ALWAYS_INLINE bool isPerGameSettings() const { return static_cast(m_sif); } ALWAYS_INLINE INISettingsInterface* getSettingsInterface() const { return m_sif.get(); } + ALWAYS_INLINE const std::string& getGameSerial() const { return m_serial; } + ALWAYS_INLINE const std::optional& getGameHash() const { return m_hash; } ALWAYS_INLINE InterfaceSettingsWidget* getInterfaceSettingsWidget() const { return m_interface_settings; } ALWAYS_INLINE BIOSSettingsWidget* getBIOSSettingsWidget() const { return m_bios_settings; } @@ -93,7 +98,6 @@ public: bool containsSettingValue(const char* section, const char* key) const; void removeSettingValue(const char* section, const char* key); void saveAndReloadGameSettings(); - void reloadGameSettingsFromIni(); bool hasGameTrait(GameDatabase::Trait trait); @@ -117,7 +121,7 @@ protected: private: enum : u32 { - MAX_SETTINGS_WIDGETS = 12 + MAX_SETTINGS_WIDGETS = 13 }; void connectUi(); @@ -137,6 +141,8 @@ private: ConsoleSettingsWidget* m_console_settings = nullptr; EmulationSettingsWidget* m_emulation_settings = nullptr; GameListSettingsWidget* m_game_list_settings = nullptr; + GamePatchSettingsWidget* m_game_patch_settings_widget = nullptr; + GameCheatSettingsWidget* m_game_cheat_settings_widget = nullptr; MemoryCardSettingsWidget* m_memory_card_settings = nullptr; GraphicsSettingsWidget* m_graphics_settings = nullptr; PostProcessingSettingsWidget* m_post_processing_settings = nullptr; @@ -150,5 +156,6 @@ private: QObject* m_current_help_widget = nullptr; QMap m_widget_help_text_map; - std::string m_game_list_filename; + std::string m_serial; + std::optional m_hash; }; diff --git a/src/util/imgui_glyph_ranges.inl b/src/util/imgui_glyph_ranges.inl index ffb70aedb..c8e437ba0 100644 --- a/src/util/imgui_glyph_ranges.inl +++ b/src/util/imgui_glyph_ranges.inl @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 -static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe06f,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf025,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf050,0xf050,0xf05e,0xf05e,0xf062,0xf063,0xf067,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0e7,0xf0e7,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f2,0xf2f2,0xf3fd,0xf3fd,0xf410,0xf410,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf545,0xf545,0xf547,0xf548,0xf57a,0xf57a,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5e7,0xf5e7,0xf65d,0xf65e,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 }; +static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe06f,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf025,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf050,0xf050,0xf05e,0xf05e,0xf062,0xf063,0xf067,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0e7,0xf0e7,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f2,0xf2f2,0xf3fd,0xf3fd,0xf410,0xf410,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf545,0xf545,0xf547,0xf548,0xf57a,0xf57a,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5e7,0xf5e7,0xf65d,0xf65e,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 }; static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x227a,0x227f,0x2284,0x2284,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2460,0x246b,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x278a,0x278e,0x27fc,0x27fc,0xe001,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 };