From 2f0a5101082e398b85fb3d9c3bcf1a69a1e0f14f Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 7 Oct 2024 01:44:13 +1000 Subject: [PATCH] Cheats: Rewrite cheat management system --- src/core/CMakeLists.txt | 4 +- src/core/cheats.cpp | 3259 ++++++++++------- src/core/cheats.h | 377 +- src/core/core.vcxproj | 8 +- src/core/core.vcxproj.filters | 2 + src/core/fullscreen_ui.cpp | 316 +- src/core/game_database.cpp | 2 +- src/core/game_list.cpp | 4 +- src/core/hotkeys.cpp | 14 - src/core/memory_scanner.cpp | 473 +++ src/core/memory_scanner.h | 130 + src/core/settings.cpp | 10 +- src/core/settings.h | 2 +- src/core/system.cpp | 261 +- src/core/system.h | 39 - src/core/types.h | 1 + src/duckstation-qt/CMakeLists.txt | 15 +- src/duckstation-qt/cheatcodeeditordialog.cpp | 92 - src/duckstation-qt/cheatcodeeditordialog.h | 28 - src/duckstation-qt/cheatcodeeditordialog.ui | 74 - src/duckstation-qt/cheatmanagerwindow.cpp | 581 --- src/duckstation-qt/cheatmanagerwindow.h | 74 - src/duckstation-qt/cheatmanagerwindow.ui | 144 - src/duckstation-qt/duckstation-qt.vcxproj | 30 +- .../duckstation-qt.vcxproj.filters | 27 +- .../gamecheatcodechoiceeditordialog.ui | 84 + .../gamecheatcodeeditordialog.ui | 142 + .../gamecheatsettingswidget.cpp | 948 +++++ src/duckstation-qt/gamecheatsettingswidget.h | 131 + src/duckstation-qt/gamecheatsettingswidget.ui | 175 + src/duckstation-qt/gamepatchdetailswidget.ui | 87 + .../gamepatchsettingswidget.cpp | 125 + src/duckstation-qt/gamepatchsettingswidget.h | 54 + src/duckstation-qt/gamepatchsettingswidget.ui | 71 + src/duckstation-qt/mainwindow.cpp | 259 +- src/duckstation-qt/mainwindow.h | 9 +- src/duckstation-qt/mainwindow.ui | 9 - src/duckstation-qt/memoryscannerwindow.cpp | 1 - src/duckstation-qt/memoryscannerwindow.h | 2 +- src/duckstation-qt/qthost.cpp | 22 +- src/duckstation-qt/qthost.h | 4 +- .../resources/duckstation-qt.qrc | 8 + .../icons/black/svg/chat-off-line.svg | 3 + .../resources/icons/black/svg/export-line.svg | 1 + .../resources/icons/black/svg/import-line.svg | 1 + .../icons/black/svg/sparkling-line.svg | 3 + .../icons/white/svg/chat-off-line.svg | 3 + .../resources/icons/white/svg/export-line.svg | 1 + .../resources/icons/white/svg/import-line.svg | 1 + .../icons/white/svg/sparkling-line.svg | 3 + src/duckstation-qt/settingswindow.cpp | 37 +- src/duckstation-qt/settingswindow.h | 21 +- src/util/imgui_glyph_ranges.inl | 2 +- 53 files changed, 4997 insertions(+), 3177 deletions(-) create mode 100644 src/core/memory_scanner.cpp create mode 100644 src/core/memory_scanner.h delete mode 100644 src/duckstation-qt/cheatcodeeditordialog.cpp delete mode 100644 src/duckstation-qt/cheatcodeeditordialog.h delete mode 100644 src/duckstation-qt/cheatcodeeditordialog.ui delete mode 100644 src/duckstation-qt/cheatmanagerwindow.cpp delete mode 100644 src/duckstation-qt/cheatmanagerwindow.h delete mode 100644 src/duckstation-qt/cheatmanagerwindow.ui create mode 100644 src/duckstation-qt/gamecheatcodechoiceeditordialog.ui create mode 100644 src/duckstation-qt/gamecheatcodeeditordialog.ui create mode 100644 src/duckstation-qt/gamecheatsettingswidget.cpp create mode 100644 src/duckstation-qt/gamecheatsettingswidget.h create mode 100644 src/duckstation-qt/gamecheatsettingswidget.ui create mode 100644 src/duckstation-qt/gamepatchdetailswidget.ui create mode 100644 src/duckstation-qt/gamepatchsettingswidget.cpp create mode 100644 src/duckstation-qt/gamepatchsettingswidget.h create mode 100644 src/duckstation-qt/gamepatchsettingswidget.ui create mode 100644 src/duckstation-qt/resources/icons/black/svg/chat-off-line.svg create mode 100644 src/duckstation-qt/resources/icons/black/svg/export-line.svg create mode 100644 src/duckstation-qt/resources/icons/black/svg/import-line.svg create mode 100644 src/duckstation-qt/resources/icons/black/svg/sparkling-line.svg create mode 100644 src/duckstation-qt/resources/icons/white/svg/chat-off-line.svg create mode 100644 src/duckstation-qt/resources/icons/white/svg/export-line.svg create mode 100644 src/duckstation-qt/resources/icons/white/svg/import-line.svg create mode 100644 src/duckstation-qt/resources/icons/white/svg/sparkling-line.svg diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3bd9a7029..78baf0d26 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 @@ -136,7 +138,7 @@ target_precompile_headers(core PRIVATE "pch.h") target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_link_libraries(core PUBLIC Threads::Threads common util) -target_link_libraries(core PRIVATE xxhash imgui rapidyaml rcheevos cpuinfo::cpuinfo ZLIB::ZLIB Zstd::Zstd) +target_link_libraries(core PRIVATE xxhash imgui minizip rapidyaml rcheevos cpuinfo::cpuinfo ZLIB::ZLIB Zstd::Zstd) if(CPU_ARCH_X64) target_compile_definitions(core PUBLIC "ENABLE_RECOMPILER=1" "ENABLE_NEWREC=1" "ENABLE_MMAP_FASTMEM=1") diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 186650b15..ad28beffa 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -2,47 +2,1955 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cheats.h" +#include "achievements.h" #include "bus.h" #include "controller.h" #include "cpu_core.h" +#include "game_database.h" #include "host.h" #include "system.h" +#include "util/imgui_manager.h" + +#include "common/assert.h" +#include "common/error.h" #include "common/file_system.h" #include "common/log.h" +#include "common/minizip_helpers.h" +#include "common/path.h" #include "common/small_string.h" #include "common/string_util.h" -#include -#include -#include -#include +#include "IconsEmoji.h" +#include "IconsFontAwesome5.h" +#include "fmt/format.h" 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) : m_contents(contents) {} + + ALWAYS_INLINE size_t GetCurrentOffset() const { return m_current_offset; } + ALWAYS_INLINE size_t GetCurrentLineOffset() const { return m_current_line_offset; } + ALWAYS_INLINE u32 GetCurrentLineNumber() const { return m_current_line_number; } + + bool GetLine(std::string_view* line) { + const size_t length = m_contents.length(); + if (m_current_offset == length) + { + m_current_line_offset = m_current_offset; + return false; + } + + size_t end_position = m_current_offset + 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; } - address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + std::optional GetLine() + { + std::optional ret = std::string_view(); + if (!GetLine(&ret.value())) + ret.reset(); + return ret; + } - if (address < Bus::RAM_MIRROR_END) - return true; + template + bool LogError(Error* error, bool stop_on_error, fmt::format_string fmt, T&&... args) + { + if (!stop_on_error) + { + Log::WriteFmtArgs(___LogChannel___, Log::Level::Warning, fmt, fmt::make_format_args(args...)); + return true; + } + + if (error) + error->SetString(fmt::vformat(fmt, fmt::make_format_args(args...))); + + return false; + } + +private: + const std::string_view m_contents; + size_t m_current_offset = 0; + size_t m_current_line_offset = 0; + u32 m_current_line_number = 0; +}; + +class CheatArchive +{ +public: + ~CheatArchive() + { + if (m_unz_file) + unzClose(m_unz_file); + } + + ALWAYS_INLINE bool IsOpen() const { return (m_unz_file != nullptr); } + + bool Open(const char* name) + { + if (m_unz_file) + return true; + + std::optional> data = Host::ReadResourceFile(name, false); + if (!data.has_value()) + { + ERROR_LOG("Failed to read cheat archive {}.", name); + return false; + } + + m_data = std::move(data.value()); + m_unz_file = MinizipHelpers::OpenUnzMemoryFile(m_data.data(), m_data.size()); + if (!m_unz_file) [[unlikely]] + { + ERROR_LOG("Failed to open cheat archive {}.", name); + return false; + } - if (address >= Bus::BIOS_BASE && address < (Bus::BIOS_BASE + Bus::BIOS_SIZE)) return true; + } + + std::optional ReadFile(const char* name) const + { + Error error; + std::optional ret = MinizipHelpers::ReadZipFileToString(m_unz_file, name, false, &error); + if (!ret.has_value()) + DEV_LOG("Failed to read {} from zip: {}", name, error.GetDescription()); + return ret; + } + +private: + // Maybe counter-intuitive, but it ends up faster for reading a single game's cheats if we keep a + // copy of the archive in memory, as opposed to reading from disk. + DynamicHeapArray m_data; + unzFile m_unz_file = nullptr; +}; + +} // namespace + +namespace Cheats { + +namespace { +/// Represents a cheat code, after being parsed. +class CheatCode +{ +public: + /// Additional metadata to a cheat code, present for all types. + struct Metadata + { + std::string name; + CodeType type = CodeType::Gameshark; + CodeActivation activation = CodeActivation::EndFrame; + std::optional override_cpu_overclock; + std::optional override_aspect_ratio; + bool has_options : 1; + bool disable_widescreen_rendering : 1; + bool disallow_for_achievements : 1; + }; + +public: + CheatCode(Metadata metadata); + virtual ~CheatCode(); + + ALWAYS_INLINE const Metadata& GetMetadata() const { return m_metadata; } + ALWAYS_INLINE const std::string& GetName() const { return m_metadata.name; } + ALWAYS_INLINE CodeActivation GetActivation() const { return m_metadata.activation; } + ALWAYS_INLINE bool IsManuallyActivated() const { return (m_metadata.activation == CodeActivation::Manual); } + ALWAYS_INLINE bool HasOptions() const { return m_metadata.has_options; } + + void ApplySettingOverrides(); + + virtual void SetOptionValue(u32 value) = 0; + + virtual void Apply() const = 0; + virtual void ApplyOnDisable() const = 0; + +protected: + Metadata m_metadata; +}; +} // namespace + +using CheatCodeList = std::vector>; +using ActiveCodeList = std::vector; +using EnableCodeList = std::vector; + +static std::string GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard); +static std::vector FindChtFilesOnDisk(const std::string_view serial, std::optional hash, + bool cheats); +static void ExtractCodeInfo(CodeInfoList* dst, const std::string& file_data, bool from_database); +static void AppendCheatToList(CodeInfoList* dst, CodeInfo code); +static std::string FormatCodeForFile(const CodeInfo& code); + +static bool ShouldLoadDatabaseCheats(); +static bool AreAnyPatchesEnabled(); +static void ReloadEnabledLists(); +static u32 EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const char* section, + bool hc_mode_active); +static void UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed); + +template +static void EnumerateChtFiles(const std::string_view serial, std::optional hash, bool cheats, bool for_ui, + bool load_from_database, const F& f); + +static std::optional ParseOption(const std::string_view value); +static bool ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end); +extern void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); + +static Cheats::FileFormat DetectFileFormat(const std::string_view file_contents); +static bool ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); +static bool ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, + Error* error); +static bool ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); +static bool ImportOldChtFile(const std::string_view serial); + +static std::unique_ptr ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, + Error* error); + +const char* PATCHES_CONFIG_SECTION = "Patches"; +const char* CHEATS_CONFIG_SECTION = "Cheats"; +const char* PATCH_ENABLE_CONFIG_KEY = "Enable"; + +static CheatArchive s_patches_zip; +static CheatArchive s_cheats_zip; +static CheatCodeList s_patch_codes; +static CheatCodeList s_cheat_codes; +static EnableCodeList s_enabled_cheats; +static EnableCodeList s_enabled_patches; + +static ActiveCodeList s_frame_end_codes; + +static bool s_patches_enabled = false; +static bool s_cheats_enabled = false; +static bool s_database_cheat_codes_enabled = false; + +} // namespace Cheats + +Cheats::CheatCode::CheatCode(Metadata metadata) : m_metadata(std::move(metadata)) +{ +} + +void Cheats::CheatCode::ApplySettingOverrides() +{ + if (m_metadata.disable_widescreen_rendering && g_settings.gpu_widescreen_hack) + { + DEV_LOG("Disabling widescreen rendering from {} patch.", GetName()); + g_settings.gpu_widescreen_hack = false; + } + if (m_metadata.override_aspect_ratio.has_value() && g_settings.display_aspect_ratio == DisplayAspectRatio::Auto) + { + DEV_LOG("Setting aspect ratio to {} from {} patch.", + Settings::GetDisplayAspectRatioName(m_metadata.override_aspect_ratio.value()), GetName()); + g_settings.display_aspect_ratio = m_metadata.override_aspect_ratio.value(); + } + if (m_metadata.override_cpu_overclock.has_value()) + { + DEV_LOG("Setting CPU overclock to {} from {} patch.", m_metadata.override_cpu_overclock.value(), GetName()); + g_settings.SetCPUOverclockPercent(m_metadata.override_cpu_overclock.value()); + g_settings.cpu_overclock_enable = true; + g_settings.UpdateOverclockActive(); + } +} + +Cheats::CheatCode::~CheatCode() = default; + +static std::array s_cheat_code_type_names = {{"Gameshark"}}; +static std::array s_cheat_code_type_display_names{{TRANSLATE_NOOP("Cheats", "Gameshark")}}; + +const char* Cheats::GetTypeName(CodeType type) +{ + return s_cheat_code_type_names[static_cast(type)]; +} + +const char* Cheats::GetTypeDisplayName(CodeType type) +{ + return TRANSLATE("Cheats", s_cheat_code_type_display_names[static_cast(type)]); +} + +std::optional Cheats::ParseTypeName(const std::string_view str) +{ + for (size_t i = 0; i < s_cheat_code_type_names.size(); i++) + { + if (str == s_cheat_code_type_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + +static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; +static std::array s_cheat_code_activation_display_names{ + {TRANSLATE_NOOP("Cheats", "Manual"), TRANSLATE_NOOP("Cheats", "Automatic (Frame End)")}}; + +const char* Cheats::GetActivationName(CodeActivation activation) +{ + return s_cheat_code_activation_names[static_cast(activation)]; +} + +const char* Cheats::GetActivationDisplayName(CodeActivation activation) +{ + return TRANSLATE("Cheats", s_cheat_code_activation_display_names[static_cast(activation)]); +} + +std::optional Cheats::ParseActivationName(const std::string_view str) +{ + for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) + { + if (str == s_cheat_code_activation_names[i]) + return static_cast(i); + } + + return std::nullopt; +} + +std::string Cheats::GetChtTemplate(const std::string_view serial, std::optional hash, bool add_wildcard) +{ + if (!hash.has_value()) + return fmt::format("{}{}.cht", serial, add_wildcard ? "*" : ""); + else + return fmt::format("{}_{:016X}{}.cht", serial, hash.value(), add_wildcard ? "*" : ""); +} + +std::vector Cheats::FindChtFilesOnDisk(const std::string_view serial, std::optional hash, + bool cheats) +{ + std::vector ret; + FileSystem::FindResultsArray files; + FileSystem::FindFiles(cheats ? EmuFolders::Cheats.c_str() : EmuFolders::Patches.c_str(), + GetChtTemplate(serial, hash, true).c_str(), + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &files); + ret.reserve(files.size()); + for (FILESYSTEM_FIND_DATA& fd : files) + { + // Skip mismatched hashes. + if (hash.has_value()) + { + if (const std::string_view filename = Path::GetFileTitle(fd.FileName); filename.length() >= serial.length() + 18) + { + const std::string_view filename_hash = filename.substr(serial.length() + 1, 16); + const std::optional filename_parsed_hash = StringUtil::FromChars(filename_hash, 16); + if (filename_parsed_hash.has_value() && filename_parsed_hash.value() != hash.value()) + continue; + } + } + ret.push_back(std::move(fd.FileName)); + } + + return ret; +} + +template +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, so we have to load the zip first. + if (load_from_database) + { + CheatArchive& archive = cheats ? s_cheats_zip : s_patches_zip; + if (!archive.IsOpen()) + { + const char* archive_name = cheats ? "cheats.zip" : "patches.zip"; + archive.Open(archive_name); + } + + if (archive.IsOpen()) + { + // Prefer filename with hash. + std::string zip_filename = GetChtTemplate(serial, hash, false); + std::optional data = archive.ReadFile(zip_filename.c_str()); + if (!data.has_value() && hash.has_value()) + { + // Try without the hash. + zip_filename = GetChtTemplate(serial, std::nullopt, false); + data = archive.ReadFile(zip_filename.c_str()); + } + if (data.has_value()) + f(std::move(zip_filename), std::move(data.value()), true); + } + } + + std::vector disk_patch_files; + if (for_ui || !Achievements::IsHardcoreModeActive()) + { + disk_patch_files = FindChtFilesOnDisk(serial, for_ui ? hash : std::nullopt, cheats); + if (cheats && disk_patch_files.empty()) + { + // Check if there's an old-format titled file. + if (ImportOldChtFile(serial)) + disk_patch_files = FindChtFilesOnDisk(serial, for_ui ? hash : std::nullopt, cheats); + } + } + + Error error; + if (!disk_patch_files.empty()) + { + for (const std::string& file : disk_patch_files) + { + const std::optional contents = FileSystem::ReadFileToString(file.c_str(), &error); + if (contents.has_value()) + f(std::move(file), std::move(contents.value()), false); + else + WARNING_LOG("Failed to read cht file '{}': {}", Path::GetFileName(file), error.GetDescription()); + } + } +} + +std::string_view Cheats::CodeInfo::GetNamePart() const +{ + const std::string::size_type pos = name.rfind('\\'); + std::string_view ret = name; + if (pos != std::string::npos) + ret = ret.substr(pos + 1); + return ret; +} + +std::string_view Cheats::CodeInfo::GetNameParentPart() const +{ + const std::string::size_type pos = name.rfind('\\'); + std::string_view ret; + if (pos != std::string::npos) + ret = std::string_view(name).substr(0, pos); + return ret; +} + +std::string_view Cheats::CodeInfo::MapOptionValueToName(u32 value) const +{ + std::string_view ret; + if (!options.empty()) + ret = options.front().first; + + for (const Cheats::CodeOption& opt : options) + { + if (opt.second == value) + { + ret = opt.first; + break; + } + } + + return ret; +} + +std::string_view Cheats::CodeInfo::MapOptionValueToName(const std::string_view value) const +{ + const std::optional value_uint = StringUtil::FromChars(value); + return MapOptionValueToName(value_uint.value_or(options.empty() ? 0 : options.front().second)); +} + +u32 Cheats::CodeInfo::MapOptionNameToValue(const std::string_view opt_name) const +{ + for (const Cheats::CodeOption& opt : options) + { + if (opt.first == opt_name) + return opt.second; + } + + return options.empty() ? 0 : options.front().second; +} + +Cheats::CodeInfoList Cheats::GetCodeInfoList(const std::string_view serial, std::optional hash, bool cheats, + bool load_from_database, bool sort_by_name) +{ + CodeInfoList ret; + + EnumerateChtFiles(serial, hash, cheats, true, load_from_database, + [&ret](const std::string& filename, const std::string& data, bool from_database) { + ExtractCodeInfo(&ret, data, from_database); + }); + + if (sort_by_name) + std::sort(ret.begin(), ret.end(), [](const CodeInfo& lhs, const CodeInfo& rhs) { + // ungrouped cheats go together first + if (const int lhs_group = static_cast(lhs.name.find('\\') != std::string::npos), + rhs_group = static_cast(rhs.name.find('\\') != std::string::npos); + lhs_group != rhs_group) + { + return (lhs_group < rhs_group); + } + + return lhs.name < rhs.name; + }); + + return ret; +} + +std::vector Cheats::GetCodeListUniquePrefixes(const CodeInfoList& list, bool include_empty) +{ + std::vector ret; + for (const Cheats::CodeInfo& code : list) + { + const std::string_view prefix = code.GetNameParentPart(); + if (prefix.empty()) + { + if (include_empty && (ret.empty() || !ret.front().empty())) + ret.insert(ret.begin(), std::string_view()); + + continue; + } + + if (std::find(ret.begin(), ret.end(), prefix) == ret.end()) + ret.push_back(prefix); + } + return ret; +} + +const Cheats::CodeInfo* Cheats::FindCodeInInfoList(const CodeInfoList& list, const std::string_view name) +{ + const auto it = std::find_if(list.cbegin(), list.cend(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); + return (it != list.end()) ? &(*it) : nullptr; +} + +Cheats::CodeInfo* Cheats::FindCodeInInfoList(CodeInfoList& list, const std::string_view name) +{ + const auto it = std::find_if(list.begin(), list.end(), [&name](const CodeInfo& rhs) { return name == rhs.name; }); + return (it != list.end()) ? &(*it) : nullptr; +} + +std::string Cheats::FormatCodeForFile(const CodeInfo& code) +{ + fmt::memory_buffer buf; + auto appender = std::back_inserter(buf); + fmt::format_to(appender, "[{}]\n", code.name); + if (!code.description.empty()) + fmt::format_to(appender, "Description = {}\n", code.description); + fmt::format_to(appender, "Type = {}\n", GetTypeName(code.type)); + fmt::format_to(appender, "Activation = {}\n", GetActivationName(code.activation)); + if (code.HasOptionChoices()) + { + for (const CodeOption& opt : code.options) + fmt::format_to(appender, "Option = {}:{}\n", opt.first, opt.second); + } + else if (code.HasOptionRange()) + { + fmt::format_to(appender, "OptionRange = {}:{}\n", code.option_range_start, code.option_range_end); + } + + fmt::format_to(appender, "{}\n", code.body); + return std::string(buf.begin(), buf.end()); +} + +bool Cheats::UpdateCodeInFile(const char* path, const std::string_view name, const CodeInfo* code, Error* error) +{ + std::string file_contents; + if (FileSystem::FileExists(path)) + { + std::optional ofile_contents = FileSystem::ReadFileToString(path, error); + if (!ofile_contents.has_value()) + { + Error::AddPrefix(error, "Failed to read existing file: "); + return false; + } + file_contents = std::move(ofile_contents.value()); + } + + // This is a bit crap, we're allocating everything and then tossing it away. + // Hopefully it won't fragment too much at least, because it's freed in reverse order... + std::optional replace_start, replace_end; + if (!file_contents.empty() && !name.empty()) + { + CodeInfoList existing_codes_in_file; + ExtractCodeInfo(&existing_codes_in_file, file_contents, false); + + const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, name); + if (existing_code) + { + replace_start = existing_code->file_offset_start; + replace_end = existing_code->file_offset_end; + } + } + + if (replace_start.has_value()) + { + const auto start = file_contents.begin() + replace_start.value(); + const auto end = file_contents.begin() + replace_end.value(); + if (code) + file_contents.replace(start, end, FormatCodeForFile(*code)); + else + file_contents.erase(start, end); + } + else if (code) + { + const std::string code_body = FormatCodeForFile(*code); + file_contents.reserve(file_contents.length() + 1 + code_body.length()); + if (!file_contents.empty() && file_contents.back() != '\n') + file_contents.push_back('\n'); + file_contents.append(code_body); + } + + INFO_LOG("Updating {}...", path); + if (!FileSystem::WriteStringToFile(path, file_contents, error)) + { + Error::AddPrefix(error, "Failed to rewrite file: "); + return false; + } + + return true; +} + +bool Cheats::SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* error) +{ + std::string file_contents; + if (FileSystem::FileExists(path)) + { + std::optional ofile_contents = FileSystem::ReadFileToString(path, error); + if (!ofile_contents.has_value()) + { + Error::AddPrefix(error, "Failed to read existing file: "); + return false; + } + file_contents = std::move(ofile_contents.value()); + } + + for (const CodeInfo& code : codes) + { + // This is _really_ crap.. but it's only on importing. + std::optional replace_start, replace_end; + if (!file_contents.empty()) + { + CodeInfoList existing_codes_in_file; + ExtractCodeInfo(&existing_codes_in_file, file_contents, false); + + const CodeInfo* existing_code = FindCodeInInfoList(existing_codes_in_file, code.name); + if (existing_code) + { + replace_start = existing_code->file_offset_start; + replace_end = existing_code->file_offset_end; + } + } + + if (replace_start.has_value()) + { + const auto start = file_contents.begin() + replace_start.value(); + const auto end = file_contents.begin() + replace_end.value(); + file_contents.replace(start, end, FormatCodeForFile(code)); + } + else + { + const std::string code_body = FormatCodeForFile(code); + file_contents.reserve(file_contents.length() + 1 + code_body.length()); + if (!file_contents.empty() && file_contents.back() != '\n') + file_contents.push_back('\n'); + file_contents.append(code_body); + } + } + + INFO_LOG("Updating {}...", path); + if (!FileSystem::WriteStringToFile(path, file_contents, error)) + { + Error::AddPrefix(error, "Failed to rewrite file: "); + return false; + } + + return true; +} + +void Cheats::MergeCheatList(CodeInfoList* dst, CodeInfoList src) +{ + for (CodeInfo& code : src) + { + CodeInfo* existing_code = FindCodeInInfoList(*dst, code.name); + if (existing_code) + *existing_code = std::move(code); + else + dst->push_back(std::move(code)); + } +} + +std::string Cheats::GetChtFilename(const std::string_view serial, std::optional hash, bool cheats) +{ + return Path::Combine(cheats ? EmuFolders::Cheats : EmuFolders::Patches, GetChtTemplate(serial, hash, false)); +} + +bool Cheats::AreCheatsEnabled() +{ + if (Achievements::IsHardcoreModeActive() || g_settings.disable_all_enhancements) + return false; + + // Only in the gameini. + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + return (sif && sif->GetBoolValue("Cheats", "EnableCheats", false)); +} + +bool Cheats::ShouldLoadDatabaseCheats() +{ + // Only in the gameini. + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + return (sif && sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true)); +} + +bool Cheats::AreAnyPatchesEnabled() +{ + if (g_settings.disable_all_enhancements) + return false; + + // Only in the gameini. + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + return (sif && sif->ContainsValue("Patches", "Enable")); +} + +void Cheats::ReloadEnabledLists() +{ + const SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + if (!sif) + { + // no gameini => nothing is going to be enabled. + s_enabled_cheats = {}; + s_enabled_patches = {}; + return; + } + + if (AreCheatsEnabled()) + s_enabled_cheats = sif->GetStringList(CHEATS_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); + else + s_enabled_cheats = {}; + + s_enabled_patches = sif->GetStringList(PATCHES_CONFIG_SECTION, PATCH_ENABLE_CONFIG_KEY); +} + +u32 Cheats::EnableCheats(const CheatCodeList& patches, const EnableCodeList& enable_list, const char* section, + bool hc_mode_active) +{ + u32 count = 0; + for (const std::unique_ptr& p : patches) + { + // ignore manually-activated codes + if (p->IsManuallyActivated()) + continue; + + // don't load banned patches + if (p->GetMetadata().disallow_for_achievements && hc_mode_active) + continue; + + if (std::find(enable_list.begin(), enable_list.end(), p->GetName()) == enable_list.end()) + continue; + + INFO_LOG("Enabled code from {}: {}", section, p->GetName()); + + switch (p->GetActivation()) + { + case CodeActivation::EndFrame: + s_frame_end_codes.push_back(p.get()); + break; + + default: + break; + } + + if (p->HasOptions()) + { + // need to extract the option from the ini + SettingsInterface* sif = Host::Internal::GetGameSettingsLayer(); + if (sif) [[likely]] + { + if (const std::optional value = sif->GetOptionalUIntValue(section, p->GetName().c_str(), std::nullopt)) + { + DEV_LOG("Setting {} option value to 0x{:X}", p->GetName(), value.value()); + p->SetOptionValue(value.value()); + } + } + } + + count++; + } + + return count; +} + +void Cheats::ReloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed) +{ + for (const CheatCode* code : s_frame_end_codes) + code->ApplyOnDisable(); + + // Reload files if cheats or patches are enabled, and they were not previously. + const bool patches_are_enabled = AreAnyPatchesEnabled(); + const bool cheats_are_enabled = AreCheatsEnabled(); + const bool cheatdb_is_enabled = cheats_are_enabled && ShouldLoadDatabaseCheats(); + reload_files = reload_files || (s_patches_enabled != patches_are_enabled); + reload_files = reload_files || (s_cheats_enabled != cheats_are_enabled); + reload_files = reload_files || (s_database_cheat_codes_enabled != cheatdb_is_enabled); + + if (reload_files) + { + s_patch_codes.clear(); + s_cheat_codes.clear(); + + if (const std::string& serial = System::GetGameSerial(); !serial.empty()) + { + const GameHash hash = System::GetGameHash(); + + s_patches_enabled = patches_are_enabled; + if (patches_are_enabled) + { + EnumerateChtFiles(serial, hash, false, false, true, + [](const std::string& filename, const std::string& file_contents, bool from_database) { + ParseFile(&s_patch_codes, file_contents); + if (s_patch_codes.size() > 0) + INFO_LOG("Found {} game patches in {}.", s_patch_codes.size(), filename); + }); + } + + s_cheats_enabled = cheats_are_enabled; + s_database_cheat_codes_enabled = cheatdb_is_enabled; + if (cheats_are_enabled) + { + EnumerateChtFiles(serial, hash, true, false, cheatdb_is_enabled, + [](const std::string& filename, const std::string& file_contents, bool from_database) { + ParseFile(&s_cheat_codes, file_contents); + if (s_cheat_codes.size() > 0) + INFO_LOG("Found {} cheats in {}.", s_cheat_codes.size(), filename); + }); + } + } + } + + UpdateActiveCodes(reload_enabled_list, verbose, verbose_if_changed); +} + +void Cheats::UnloadAll() +{ + s_frame_end_codes = ActiveCodeList(); + s_enabled_patches = EnableCodeList(); + s_enabled_cheats = EnableCodeList(); + s_cheat_codes = CheatCodeList(); + s_patch_codes = CheatCodeList(); + s_patches_enabled = false; + s_cheats_enabled = false; + s_database_cheat_codes_enabled = false; +} + +void Cheats::ApplySettingOverrides() +{ + // only need to check patches for this + for (const std::string& name : s_enabled_patches) + { + for (std::unique_ptr& code : s_patch_codes) + { + if (name == code->GetName()) + { + code->ApplySettingOverrides(); + break; + } + } + } +} + +void Cheats::UpdateActiveCodes(bool reload_enabled_list, bool verbose, bool verbose_if_changed) +{ + if (reload_enabled_list) + ReloadEnabledLists(); + + const size_t prev_count = s_frame_end_codes.size(); + s_frame_end_codes.clear(); + + u32 patch_count = 0; + u32 cheat_count = 0; + + if (!g_settings.disable_all_enhancements) + { + const bool hc_mode_active = Achievements::IsHardcoreModeActive(); + patch_count = EnableCheats(s_patch_codes, s_enabled_patches, "Patches", hc_mode_active); + cheat_count = AreCheatsEnabled() ? EnableCheats(s_cheat_codes, s_enabled_cheats, "Cheats", hc_mode_active) : 0; + } + + // Display message on first boot when we load patches. + // Except when it's just GameDB. + const size_t new_count = s_frame_end_codes.size(); + if (verbose || (verbose_if_changed && prev_count != new_count)) + { + if (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; + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + // new code. + if (!current_code.name.empty()) + { + // overwrite existing codes with the same name. + finish_code(); + current_code = CodeInfo(); + } + + const std::string_view name = linev.substr(1, linev.length() - 2); + current_code.name = + legacy_group.has_value() ? fmt::format("{}\\{}", legacy_group.value(), name) : std::string(name); + current_code.type = legacy_type.value_or(CodeType::Gameshark); + current_code.activation = legacy_activation.value_or(CodeActivation::EndFrame); + current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); + current_code.file_offset_end = current_code.file_offset_start; + current_code.file_offset_body_start = current_code.file_offset_start; + current_code.from_database = from_database; + continue; + } + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + // metadata? + if (linev.find('=') != std::string_view::npos) + { + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (key == "Description") + { + current_code.description = value; + } + else if (key == "Author") + { + current_code.author = value; + } + else if (key == "Type") + { + const std::optional type = ParseTypeName(value); + if (type.has_value()) [[unlikely]] + current_code.type = type.value(); + else + WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); + } + else if (key == "Activation") + { + const std::optional activation = ParseActivationName(value); + if (activation.has_value()) [[unlikely]] + current_code.activation = activation.value(); + else + WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); + } + else if (key == "Option") + { + if (std::optional opt = ParseOption(value)) + current_code.options.push_back(std::move(opt.value())); + else + WARNING_LOG("Invalid option declaration at line {}: {}", reader.GetCurrentLineNumber(), line); + } + else if (key == "OptionRange") + { + if (!ParseOptionRange(value, ¤t_code.option_range_start, ¤t_code.option_range_end)) + WARNING_LOG("Invalid option range declaration at line {}: {}", reader.GetCurrentLineNumber(), line); + } + + // ignore other keys when we're only grabbing info + continue; + } + + if (current_code.name.empty()) + { + WARNING_LOG("Code data specified without name at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (current_code.file_offset_body_start == current_code.file_offset_start) + current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty()) + finish_code(); +} + +void Cheats::AppendCheatToList(CodeInfoList* dst, CodeInfo code) +{ + const auto iter = + std::find_if(dst->begin(), dst->end(), [&code](const CodeInfo& rhs) { return code.name == rhs.name; }); + if (iter != dst->end()) + *iter = std::move(code); + else + dst->push_back(std::move(code)); +} + +void Cheats::ParseFile(CheatCodeList* dst_list, const std::string_view file_contents) +{ + CheatFileReader reader(file_contents); + + std::string_view next_code_group; + CheatCode::Metadata next_code_metadata; + std::optional code_body_start; + + const auto finish_code = [&dst_list, &file_contents, &reader, &next_code_group, &next_code_metadata, + &code_body_start]() { + if (!code_body_start.has_value()) + { + WARNING_LOG("Empty cheat body at line {}", reader.GetCurrentLineNumber()); + next_code_metadata = CheatCode::Metadata(); + return; + } + + const std::string_view code_body = + file_contents.substr(code_body_start.value(), reader.GetCurrentLineOffset() - code_body_start.value()); + + std::unique_ptr code; + if (next_code_metadata.type == CodeType::Gameshark) + { + Error error; + code = ParseGamesharkCode(std::move(next_code_metadata), code_body, &error); + if (!code) + { + WARNING_LOG("Failed to parse gameshark code ending on line {}: {}", reader.GetCurrentLineNumber(), + error.GetDescription()); + return; + } + } + else + { + WARNING_LOG("Unknown code type ending at line {}", reader.GetCurrentLineNumber()); + return; + } + + next_code_group = {}; + next_code_metadata = CheatCode::Metadata(); + code_body_start.reset(); + + // overwrite existing codes with the same name. + const auto iter = std::find_if(dst_list->begin(), dst_list->end(), [&code](const std::unique_ptr& rhs) { + return code->GetName() == rhs->GetName(); + }); + if (iter != dst_list->end()) + *iter = std::move(code); + else + dst_list->push_back(std::move(code)); + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty()) + continue; + + // legacy metadata parsing + if (linev.starts_with("#group=")) + { + next_code_group = StringUtil::StripWhitespace(linev.substr(7)); + continue; + } + else if (linev.starts_with("#type=")) + { + const std::optional type = ParseTypeName(StringUtil::StripWhitespace(linev.substr(6))); + if (!type.has_value()) + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.type = type.value(); + + continue; + } + else if (linev.starts_with("#activation=")) + { + const std::optional activation = + ParseActivationName(StringUtil::StripWhitespace(linev.substr(12))); + if (!activation.has_value()) + WARNING_LOG("Unknown type at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.activation = activation.value(); + + continue; + } + + // skip comments + if (linev[0] == '#' || linev[0] == ';') + continue; + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (!next_code_metadata.name.empty()) + finish_code(); + + // new code. + const std::string_view name = linev.substr(1, linev.length() - 2); + next_code_metadata.name = + next_code_group.empty() ? std::string(name) : fmt::format("{}\\{}", next_code_group, name); + continue; + } + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + // metadata? + if (linev.find('=') != std::string_view::npos) + { + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + WARNING_LOG("Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line); + continue; + } + + if (key == "Type") + { + const std::optional type = ParseTypeName(value); + if (!type.has_value()) + WARNING_LOG("Unknown code type at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.type = type.value(); + } + else if (key == "Activation") + { + const std::optional activation = ParseActivationName(value); + if (!activation.has_value()) + WARNING_LOG("Unknown code activation at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.activation = activation.value(); + } + else if (key == "OverrideAspectRatio") + { + const std::optional aspect_ratio = + Settings::ParseDisplayAspectRatio(TinyString(value).c_str()); + if (!aspect_ratio.has_value()) + WARNING_LOG("Unknown aspect ratio at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.override_aspect_ratio = aspect_ratio; + } + else if (key == "OverrideCPUOverclock") + { + const std::optional ocvalue = StringUtil::FromChars(value); + if (!ocvalue.has_value() || ocvalue.value() == 0) + WARNING_LOG("Invalid CPU overclock at line {}: {}", reader.GetCurrentLineNumber(), line); + else + next_code_metadata.override_cpu_overclock = ocvalue.value(); + } + else if (key == "DisableWidescreenRendering") + { + next_code_metadata.disable_widescreen_rendering = StringUtil::FromChars(value).value_or(false); + } + else if (key == "DisallowForAchievements") + { + next_code_metadata.disallow_for_achievements = StringUtil::FromChars(value).value_or(false); + } + else if (key == "Option" || key == "OptionRange") + { + // we don't care about the actual values, we load them from the config + next_code_metadata.has_options = true; + } + else if (key == "Author" || key == "Description") + { + // ignored when loading + } + else + { + WARNING_LOG("Unknown parameter {} at line {}", key, reader.GetCurrentLineNumber()); + } + + continue; + } + + if (!code_body_start.has_value()) + code_body_start = reader.GetCurrentLineOffset(); + } + + finish_code(); +} + +std::optional Cheats::ParseOption(const std::string_view value) +{ + // Option = Value1:0x1 + std::optional ret; + if (const std::string_view::size_type pos = value.rfind(':'); pos != std::string_view::npos) + { + const std::string_view opt_name = StringUtil::StripWhitespace(value.substr(0, pos)); + const std::optional opt_value = + StringUtil::FromCharsWithOptionalBase(StringUtil::StripWhitespace(value.substr(pos + 1))); + if (opt_value.has_value()) + ret = CodeOption(opt_name, opt_value.value()); + } + return ret; +} + +bool Cheats::ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end) +{ + // OptionRange = 0:255 + if (const std::string_view::size_type pos = value.rfind(':'); pos != std::string_view::npos) + { + const std::optional start = StringUtil::FromChars(StringUtil::StripWhitespace(value.substr(0, pos))); + const std::optional end = StringUtil::FromChars(StringUtil::StripWhitespace(value.substr(pos + 1))); + if (start.has_value() && end.has_value() && start.value() <= std::numeric_limits::max() && + end.value() <= std::numeric_limits::max() && end.value() > start.value()) + { + *out_range_start = static_cast(start.value()); + *out_range_end = static_cast(end.value()); + return true; + } + } + + return false; +} + +////////////////////////////////////////////////////////////////////////// +// File Importing +////////////////////////////////////////////////////////////////////////// + +bool Cheats::ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error) +{ + if (codes.empty()) + { + Error::SetStringView(error, "Code list is empty."); + return false; + } + + auto fp = FileSystem::CreateAtomicRenamedFile(std::move(path), error); + if (!fp) + return false; + + for (const CodeInfo& code : codes) + { + std::string code_body = FormatCodeForFile(code); + + // ensure there's at least two newlines of space between each code + const size_t newline_len = code_body.ends_with("\n\n") ? 0 : (code_body.ends_with("\n") ? 1 : 2); + for (size_t i = 0; i < newline_len; i++) + code_body.push_back('\n'); + + if (std::fwrite(code_body.data(), code_body.length(), 1, fp.get()) != 1) + { + Error::SetErrno(error, "fwrite() failed: ", errno); + FileSystem::DiscardAtomicRenamedFile(fp); + return false; + } + } + + return FileSystem::CommitAtomicRenamedFile(fp, error); +} + +bool Cheats::ImportCodesFromString(CodeInfoList* dst, const std::string_view file_contents, FileFormat file_format, + bool stop_on_error, Error* error) +{ + if (file_format == FileFormat::Unknown) + file_format = DetectFileFormat(file_contents); + + if (file_format == FileFormat::PCSX) + { + if (!ImportPCSXFile(dst, file_contents, stop_on_error, error)) + return false; + } + else if (file_format == FileFormat::Libretro) + { + if (!ImportLibretroFile(dst, file_contents, stop_on_error, error)) + return false; + } + else if (file_format == FileFormat::EPSXe) + { + if (!ImportEPSXeFile(dst, file_contents, stop_on_error, error)) + return false; + } + else + { + Error::SetStringView(error, "Unknown file format."); + return false; + } + + if (dst->empty()) + { + Error::SetStringView(error, "No codes found in file."); + return false; + } + + return true; +} + +Cheats::FileFormat Cheats::DetectFileFormat(const std::string_view file_contents) +{ + CheatFileReader reader(file_contents); + std::string_view line; + while (reader.GetLine(&line)) + { + // skip comments/empty lines + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == ';' || linev[0] == '#') + continue; + + if (linev.starts_with("cheats")) + return FileFormat::Libretro; + + // pcsxr if we see brackets + if (linev[0] == '[') + return FileFormat::PCSX; + + // otherwise if it's a code, it's probably epsxe + if (StringUtil::IsHexDigit(linev[0])) + return FileFormat::EPSXe; + } + + return FileFormat::Unknown; +} + +bool Cheats::ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) +{ + CheatFileReader reader(file_contents); + CodeInfo current_code; + + const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { + if (current_code.file_offset_end <= current_code.file_offset_body_start) + { + if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) + return false; + } + + current_code.body = std::string_view(file_contents) + .substr(current_code.file_offset_body_start, + current_code.file_offset_end - current_code.file_offset_body_start); + + AppendCheatToList(dst, std::move(current_code)); + return true; + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == '#' || linev[0] == ';') + continue; + + if (linev.front() == '[') + { + if (linev.size() < 3 || linev.back() != ']') + { + 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; + } + + // strip comments off end of lines + const std::string_view::size_type comment_pos = linev.find_last_of("#;"); + if (comment_pos != std::string_view::npos) + { + linev = StringUtil::StripWhitespace(linev.substr(0, comment_pos)); + if (linev.empty()) + continue; + } + + if (current_code.name.empty()) + { + if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", + reader.GetCurrentLineNumber(), line)) + { + return false; + } + + continue; + } + + if (current_code.file_offset_body_start == current_code.file_offset_start) + current_code.file_offset_body_start = static_cast(reader.GetCurrentLineOffset()); + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty() && !finish_code()) + return false; + + return true; +} + +bool Cheats::ImportLibretroFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, + Error* error) +{ + std::vector> kvp; + + static constexpr auto FindKey = [](const std::vector>& kvp, + const std::string_view search) -> const std::string_view* { + for (const auto& it : kvp) + { + if (StringUtil::EqualNoCase(search, it.first)) + return &it.second; + } + + return nullptr; + }; + + CheatFileReader reader(file_contents); + std::string_view line; + while (reader.GetLine(&line)) + { + const std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty()) + continue; + + // skip comments + if (linev[0] == '#' || linev[0] == ';') + continue; + + std::string_view key, value; + if (!StringUtil::ParseAssignmentString(linev, &key, &value)) + { + if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), line)) + return false; + + continue; + } + + kvp.emplace_back(key, value); + } + + if (kvp.empty()) + { + reader.LogError(error, stop_on_error, "No key/values found."); + return false; + } + + const std::string_view* num_cheats_value = FindKey(kvp, "cheats"); + const u32 num_cheats = num_cheats_value ? StringUtil::FromChars(*num_cheats_value).value_or(0) : 0; + if (num_cheats == 0) + return false; + + for (u32 i = 0; i < num_cheats; i++) + { + const std::string_view* desc = FindKey(kvp, TinyString::from_format("cheat{}_desc", i)); + const std::string_view* code = FindKey(kvp, TinyString::from_format("cheat{}_code", i)); + if (!desc || desc->empty() || !code || code->empty()) + { + if (!reader.LogError(error, stop_on_error, "Missing desc/code for cheat {}", i)) + return false; + + continue; + } + + // Need to convert + to newlines. + CodeInfo info; + info.name = *desc; + info.body = StringUtil::ReplaceAll(*code, '+', '\n'); + info.file_offset_start = 0; + info.file_offset_end = 0; + info.file_offset_body_start = 0; + info.type = CodeType::Gameshark; + info.activation = CodeActivation::EndFrame; + info.from_database = false; + AppendCheatToList(dst, std::move(info)); + } + + return true; +} + +bool Cheats::ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error) +{ + CheatFileReader reader(file_contents); + CodeInfo current_code; + + const auto finish_code = [&dst, &file_contents, &stop_on_error, &error, ¤t_code, &reader]() { + if (current_code.file_offset_end <= current_code.file_offset_body_start) + { + if (!reader.LogError(error, stop_on_error, "Empty body for cheat '{}'", current_code.name)) + return false; + } + + current_code.body = + std::string_view(file_contents).substr(current_code.file_offset_body_start, current_code.file_offset_end); + + AppendCheatToList(dst, std::move(current_code)); + return true; + }; + + std::string_view line; + while (reader.GetLine(&line)) + { + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || linev[0] == ';') + continue; + + if (linev.front() == '#') + { + if (linev.size() < 2) + { + if (!reader.LogError(error, stop_on_error, "Malformed code at line {}: {}", reader.GetCurrentLineNumber(), + line)) + { + return false; + } + + continue; + } + + if (!current_code.name.empty() && !finish_code()) + return false; + + // new code. + current_code = CodeInfo(); + current_code.name = linev.substr(1); + current_code.file_offset_start = static_cast(reader.GetCurrentLineOffset()); + current_code.file_offset_end = current_code.file_offset_start; + current_code.file_offset_body_start = current_code.file_offset_start; + current_code.type = CodeType::Gameshark; + current_code.activation = CodeActivation::EndFrame; + current_code.from_database = false; + continue; + } + + if (current_code.name.empty()) + { + if (!reader.LogError(error, stop_on_error, "Code data specified without name at line {}: {}", + reader.GetCurrentLineNumber(), line)) + { + return false; + } + + continue; + } + + // if it's a code line, update the ending point + current_code.file_offset_end = static_cast(reader.GetCurrentOffset()); + } + + // last code. + if (!current_code.name.empty() && !finish_code()) + return false; + + return true; +} + +bool Cheats::ImportOldChtFile(const std::string_view serial) +{ + const GameDatabase::Entry* dbentry = GameDatabase::GetEntryForSerial(serial); + if (!dbentry || dbentry->title.empty()) + return false; + + const std::string old_path = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}.cht", EmuFolders::Cheats, dbentry->title); + if (!FileSystem::FileExists(old_path.c_str())) + return false; + + Error error; + std::optional old_data = FileSystem::ReadFileToString(old_path.c_str(), &error); + if (!old_data.has_value()) + { + ERROR_LOG("Failed to open old cht file '{}' for importing: {}", Path::GetFileName(old_path), + error.GetDescription()); + return false; + } + + CodeInfoList new_codes; + if (!ImportCodesFromString(&new_codes, old_data.value(), FileFormat::Unknown, false, &error) || new_codes.empty()) + { + ERROR_LOG("Failed to import old cht file '{}': {}", Path::GetFileName(old_path), error.GetDescription()); + return false; + } + + const std::string new_path = GetChtFilename(serial, std::nullopt, true); + if (!SaveCodesToFile(new_path.c_str(), new_codes, &error)) + { + ERROR_LOG("Failed to write new cht file '{}': {}", Path::GetFileName(new_path), error.GetDescription()); + return false; + } + + INFO_LOG("Imported {} codes from {}.", new_codes.size(), Path::GetFileName(old_path)); + return true; +} + +////////////////////////////////////////////////////////////////////////// +// Gameshark codes +////////////////////////////////////////////////////////////////////////// + +namespace Cheats { +namespace { + +class GamesharkCheatCode final : public CheatCode +{ +public: + GamesharkCheatCode(Metadata metadata); + ~GamesharkCheatCode() override; + + static std::unique_ptr Parse(Metadata metadata, const std::string_view data, Error* error); + + void SetOptionValue(u32 value) override; + + void Apply() const override; + void ApplyOnDisable() const override; + +private: + enum class InstructionCode : u8 + { + Nop = 0x00, + ConstantWrite8 = 0x30, + ConstantWrite16 = 0x80, + ScratchpadWrite16 = 0x1F, + Increment16 = 0x10, + Decrement16 = 0x11, + Increment8 = 0x20, + Decrement8 = 0x21, + DelayActivation = 0xC1, + SkipIfNotEqual16 = 0xC0, + SkipIfButtonsNotEqual = 0xD5, + SkipIfButtonsEqual = 0xD6, + CompareButtons = 0xD4, + CompareEqual16 = 0xD0, + CompareNotEqual16 = 0xD1, + CompareLess16 = 0xD2, + CompareGreater16 = 0xD3, + CompareEqual8 = 0xE0, + CompareNotEqual8 = 0xE1, + CompareLess8 = 0xE2, + CompareGreater8 = 0xE3, + Slide = 0x50, + MemoryCopy = 0xC2, + ExtImprovedSlide = 0x53, + + // Extension opcodes, not present on original GameShark. + ExtConstantWrite32 = 0x90, + ExtScratchpadWrite32 = 0xA5, + ExtCompareEqual32 = 0xA0, + ExtCompareNotEqual32 = 0xA1, + ExtCompareLess32 = 0xA2, + ExtCompareGreater32 = 0xA3, + ExtSkipIfNotEqual32 = 0xA4, + ExtIncrement32 = 0x60, + ExtDecrement32 = 0x61, + ExtConstantWriteIfMatch16 = 0xA6, + ExtConstantWriteIfMatchWithRestore16 = 0xA7, + ExtConstantForceRange8 = 0xF0, + ExtConstantForceRangeLimits16 = 0xF1, + ExtConstantForceRangeRollRound16 = 0xF2, + ExtConstantForceRange16 = 0xF3, + ExtFindAndReplace = 0xF4, + ExtConstantSwap16 = 0xF5, + + ExtConstantBitSet8 = 0x31, + ExtConstantBitClear8 = 0x32, + ExtConstantBitSet16 = 0x81, + ExtConstantBitClear16 = 0x82, + ExtConstantBitSet32 = 0x91, + ExtConstantBitClear32 = 0x92, + + ExtBitCompareButtons = 0xD7, + ExtSkipIfNotLess8 = 0xC3, + ExtSkipIfNotGreater8 = 0xC4, + ExtSkipIfNotLess16 = 0xC5, + ExtSkipIfNotGreater16 = 0xC6, + ExtMultiConditionals = 0xF6, + + ExtCheatRegisters = 0x51, + ExtCheatRegistersCompare = 0x52, + + ExtCompareBitsSet8 = 0xE4, // Only used inside ExtMultiConditionals + ExtCompareBitsClear8 = 0xE5, // Only used inside ExtMultiConditionals + }; + + union Instruction + { + u64 bits; + + struct + { + u32 second; + u32 first; + }; + + BitField code; + BitField address; + BitField value32; + BitField value16; + BitField value8; + }; + + std::vector instructions; + std::vector> option_instruction_values; + + u32 GetNextNonConditionalInstruction(u32 index) const; + + static bool IsConditionalInstruction(InstructionCode code); +}; + +} // namespace + +} // namespace Cheats + +Cheats::GamesharkCheatCode::GamesharkCheatCode(Metadata metadata) : CheatCode(std::move(metadata)) +{ +} + +Cheats::GamesharkCheatCode::~GamesharkCheatCode() = default; + +static std::optional ParseHexOptionMask(const std::string_view str, u8* out_option_start, u8* out_option_count) +{ + if (str.length() > 8) + return std::nullopt; + + const u32 num_nibbles = static_cast(str.size()); + std::array nibble_values; + u32 option_nibble_start = 0; + u32 option_nibble_count = 0; + bool last_was_option = false; + for (u32 i = 0; i < num_nibbles; i++) + { + if (str[i] == '?') + { + if (option_nibble_count == 0) + { + option_nibble_start = i; + } + else if (!last_was_option) + { + // ? must be consecutive + return false; + } + + option_nibble_count++; + last_was_option = true; + nibble_values[i] = '0'; + } + else if (StringUtil::IsHexDigit(str[i])) + { + last_was_option = false; + nibble_values[i] = str[i]; + } + else + { + // not a valid hex digit + return false; + } + } + + // use stringutil to decode it, it has zeros in the place + const std::optional parsed = StringUtil::FromChars(std::string_view(nibble_values.data(), num_nibbles), 16); + if (!parsed.has_value()) [[unlikely]] + return std::nullopt; + + // LSB comes first, so reverse + *out_option_start = static_cast((num_nibbles - option_nibble_start - option_nibble_count) * 4); + *out_option_count = static_cast(option_nibble_count * 4); + return parsed; +} + +std::unique_ptr Cheats::GamesharkCheatCode::Parse(Metadata metadata, + const std::string_view data, Error* error) +{ + std::unique_ptr code = std::make_unique(std::move(metadata)); + CheatFileReader reader(data); + std::string_view line; + while (reader.GetLine(&line)) + { + // skip comments/empty lines + std::string_view linev = StringUtil::StripWhitespace(line); + if (linev.empty() || !StringUtil::IsHexDigit(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(); + + std::optional second; + if (next.find('?') != std::string_view::npos) + { + u8 option_bitpos, option_bitcount; + second = ParseHexOptionMask(next, &option_bitpos, &option_bitcount); + if (second.has_value()) + { + code->option_instruction_values.emplace_back(static_cast(code->instructions.size()), option_bitpos, + option_bitcount); + } + } + else + { + second = StringUtil::FromChars(next, 16); + } + + if (!second.has_value()) + { + Error::SetStringFmt(error, "Malformed instruction at line {}: {}", reader.GetCurrentLineNumber(), linev); + code.reset(); + break; + } + + Instruction inst; + inst.first = first.value(); + inst.second = second.value(); + code->instructions.push_back(inst); + } + + if (code->instructions.empty()) + { + Error::SetStringFmt(error, "No instructions in code."); + code.reset(); + } + + return code; +} + +static std::array cht_register; // Used for D7 ,51 & 52 cheat types + template NEVER_INLINE static T DoMemoryRead(VirtualMemoryAddress address) { @@ -167,805 +2075,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 +2099,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 +2114,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 +2602,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 +2614,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 +2628,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 +2641,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 +2655,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 +2709,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 +3789,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 +3835,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 +3852,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 +3890,20 @@ 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) +void Cheats::GamesharkCheatCode::SetOptionValue(u32 value) { - 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++) + for (const auto& [index, bitpos_start, bit_count] : option_instruction_values) { - 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; + Instruction& inst = instructions[index]; + const u32 value_mask = ((1u << bit_count) - 1); + ; + const u32 fixed_mask = ~(value_mask << bitpos_start); + inst.second = (inst.second & fixed_mask) | ((value & value_mask) << bitpos_start); } } -void MemoryScan::SearchBytes() +std::unique_ptr Cheats::ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, + Error* error) { - 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(metadata), data, error); } diff --git a/src/core/cheats.h b/src/core/cheats.h index 9f6b03730..927e68b45 100644 --- a/src/core/cheats.h +++ b/src/core/cheats.h @@ -7,316 +7,145 @@ #include "types.h" +#include +#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, +using CodeOption = std::pair; +using CodeOptionList = std::vector; - 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; + CodeOptionList options; + u16 option_range_start = 0; + u16 option_range_end = 0; + u32 file_offset_start = 0; + u32 file_offset_body_start = 0; + u32 file_offset_end = 0; + CodeType type = CodeType::Gameshark; + CodeActivation activation = CodeActivation::EndFrame; + bool from_database = false; - ALWAYS_INLINE bool Valid() const { return !instructions.empty() && !description.empty(); } - ALWAYS_INLINE bool IsManuallyActivated() const { return (activation == Activation::Manual); } + std::string_view GetNamePart() const; + std::string_view GetNameParentPart() const; - 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); + bool HasOptionChoices() const { return (!options.empty()); } + bool HasOptionRange() const { return (option_range_end > option_range_start); } + std::string_view MapOptionValueToName(u32 value) const; + std::string_view MapOptionValueToName(const std::string_view value) const; + u32 MapOptionNameToValue(const std::string_view opt_name) 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 sort_by_name); - bool LoadFromFile(const char* filename, Format format); - bool LoadFromPCSXRFile(const char* filename); - bool LoadFromLibretroFile(const char* filename); +/// Returns a list of all unique prefixes/groups for a cheat list. +extern std::vector GetCodeListUniquePrefixes(const CodeInfoList& list, bool include_empty); - 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 const CodeInfo* FindCodeInInfoList(const CodeInfoList& list, const std::string_view name); - bool SaveToPCSXRFile(const char* filename); +/// Searches for a given code by name. +extern CodeInfo* FindCodeInInfoList(CodeInfoList& list, const std::string_view name); - bool LoadFromPackage(const std::string& serial); +/// 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); - void Apply(); +/// Exports codes to the given file, in DuckStation format. +extern bool ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error); - void ApplyCode(u32 index); +/// Adds, updates, or removes the specified code from the file, rewriting it. If code is null, it will be removed. +extern bool UpdateCodeInFile(const char* path, const std::string_view name, 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 setting changes based on patches. +extern void ApplySettingOverrides(); - MemoryScan(); - ~MemoryScan(); +/// Applies all currently-registered frame end cheat codes. +extern void ApplyFrameEndCodes(); - 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()); } +/// Returns true if cheats are enabled in the current game's configuration. +extern bool AreCheatsEnabled(); - 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; } +/// Enumerates the names of all manually-activated codes. +extern bool EnumerateManualCodes(std::function callback); - void ResetSearch(); - void Search(); - void SearchAgain(); - void UpdateResultsValues(); +/// Invokes/applies the specified manually-activated code. +extern bool ApplyManualCode(const std::string_view name); - void SetResultValue(u32 index, u32 value); +// 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; -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..7fbb29d2a 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -65,6 +65,7 @@ + @@ -145,6 +146,7 @@ + @@ -170,6 +172,9 @@ {bb08260f-6fbc-46af-8924-090ee71360c6} + + {8bda439c-6358-45fb-9994-2ff083babe06} + {e4357877-d459-45c7-b8f6-dcbb587bb528} @@ -206,7 +211,8 @@ ZYDIS_DISABLE_ENCODER;ZYDIS_DISABLE_AVX512;ZYDIS_DISABLE_KNC;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions) - $(SolutionDir)dep\zydis\include;$(SolutionDir)dep\zydis\dependencies\zycore\include;%(AdditionalIncludeDirectories) + %(AdditionalIncludeDirectories);$(SolutionDir)dep\zydis\include;$(SolutionDir)dep\zydis\dependencies\zycore\include + %(AdditionalIncludeDirectories);$(SolutionDir)dep\minizip\include $(IntDir)/%(RelativeDir)/ Use pch.h 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 05de29302..e83281a73 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -197,6 +197,8 @@ enum class SettingsPage Audio, Achievements, Advanced, + Patches, + Cheats, Count }; @@ -276,7 +278,6 @@ static void DoChangeDisc(); static void DoRequestExit(); static void DoDesktopMode(); static void DoToggleFullscreen(); -static void DoCheatsMenu(); static void DoToggleAnalogMode(); ////////////////////////////////////////////////////////////////////////// @@ -304,6 +305,7 @@ static void DrawControllerSettingsPage(); static void DrawHotkeySettingsPage(); static void DrawAchievementsSettingsPage(); static void DrawAdvancedSettingsPage(); +static void DrawPatchesOrCheatsSettingsPage(bool cheats); static bool IsEditingGameSettings(SettingsInterface* bsi); static SettingsInterface* GetEditingSettingsInterface(); @@ -386,6 +388,7 @@ static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const c static void PopulateGraphicsAdapterList(); static void PopulateGameListDirectoryCache(SettingsInterface* si); +static void PopulatePatchesAndCheatsList(const std::string_view serial); static void PopulatePostProcessingChain(SettingsInterface* si, const char* section); static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section, std::string_view key, std::string_view display_name); @@ -402,6 +405,11 @@ static std::unique_ptr s_game_settings_entry; static std::vector> s_game_list_directories_cache; static GPUDevice::AdapterInfoList s_graphics_adapter_list_cache; static std::vector s_fullscreen_mode_list_cache; +static Cheats::CodeInfoList s_game_patch_list; +static std::vector s_enabled_game_patch_cache; +static Cheats::CodeInfoList s_game_cheats_list; +static std::vector s_enabled_game_cheat_cache; +static std::vector s_game_cheat_groups; static std::vector s_postprocessing_stages; static std::vector s_hotkey_list_cache; static std::atomic_bool s_settings_changed{false}; @@ -754,6 +762,11 @@ void FullscreenUI::Shutdown() std::memset(s_controller_macro_expanded, 0, sizeof(s_controller_macro_expanded)); s_game_list_sorted_entries = {}; s_game_list_directories_cache = {}; + s_game_patch_list = {}; + s_enabled_game_patch_cache = {}; + s_game_cheats_list = {}; + s_enabled_game_cheat_cache = {}; + s_game_cheat_groups = {}; s_postprocessing_stages = {}; s_fullscreen_mode_list_cache = {}; s_graphics_adapter_list_cache = {}; @@ -1211,48 +1224,6 @@ void FullscreenUI::DoChangeDisc() DoChangeDiscFromFile(); } -void FullscreenUI::DoCheatsMenu() -{ - CheatList* cl = System::GetCheatList(); - if (!cl) - { - if (!System::LoadCheatListFromDatabase() || ((cl = System::GetCheatList()) == nullptr)) - { - Host::AddKeyedOSDMessage("load_cheat_list", - fmt::format(FSUI_FSTR("No cheats found for {}."), System::GetGameTitle()), 10.0f); - ReturnToPreviousWindow(); - return; - } - } - - ImGuiFullscreen::ChoiceDialogOptions options; - options.reserve(cl->GetCodeCount()); - for (u32 i = 0; i < cl->GetCodeCount(); i++) - { - const CheatCode& cc = cl->GetCode(i); - options.emplace_back(cc.description.c_str(), cc.enabled); - } - - auto callback = [](s32 index, const std::string& title, bool checked) { - if (index < 0) - { - ReturnToPreviousWindow(); - return; - } - - CheatList* cl = System::GetCheatList(); - if (!cl) - return; - - const CheatCode& cc = cl->GetCode(static_cast(index)); - if (cc.activation == CheatCode::Activation::Manual) - cl->ApplyCode(static_cast(index)); - else - System::SetCheatCodeState(static_cast(index), checked); - }; - OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_FROWN, "Cheat List"), true, std::move(options), std::move(callback)); -} - void FullscreenUI::DoToggleAnalogMode() { // hacky way to toggle analog mode @@ -2728,6 +2699,11 @@ void FullscreenUI::SwitchToSettings() { s_game_settings_entry.reset(); s_game_settings_interface.reset(); + s_game_patch_list = {}; + s_enabled_game_patch_cache = {}; + s_game_cheats_list = {}; + s_enabled_game_cheat_cache = {}; + s_game_cheat_groups = {}; PopulateGraphicsAdapterList(); PopulatePostProcessingChain(GetEditingSettingsInterface(), PostProcessing::Config::DISPLAY_CHAIN_SECTION); @@ -2741,6 +2717,7 @@ void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial) s_game_settings_entry.reset(); s_game_settings_interface = std::make_unique(System::GetGameSettingsPath(serial)); s_game_settings_interface->Load(); + PopulatePatchesAndCheatsList(serial); s_current_main_window = MainWindowType::Settings; s_settings_page = SettingsPage::Summary; QueueResetFocus(FocusResetType::ViewChanged); @@ -2796,6 +2773,19 @@ void FullscreenUI::PopulateGameListDirectoryCache(SettingsInterface* si) s_game_list_directories_cache.emplace_back(std::move(dir), true); } +void FullscreenUI::PopulatePatchesAndCheatsList(const std::string_view serial) +{ + s_game_patch_list = Cheats::GetCodeInfoList(serial, std::nullopt, false, true, true); + s_game_cheats_list = + Cheats::GetCodeInfoList(serial, std::nullopt, true, + s_game_settings_interface->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true), true); + s_game_cheat_groups = Cheats::GetCodeListUniquePrefixes(s_game_cheats_list, true); + s_enabled_game_patch_cache = + s_game_settings_interface->GetStringList(Cheats::PATCHES_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY); + s_enabled_game_cheat_cache = + s_game_settings_interface->GetStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY); +} + void FullscreenUI::DoCopyGameSettings() { if (!s_game_settings_interface) @@ -2839,32 +2829,34 @@ void FullscreenUI::DrawSettingsWindow() { static constexpr float ITEM_WIDTH = 25.0f; - static constexpr const char* global_icons[] = { - ICON_FA_TV, ICON_FA_DICE_D20, ICON_FA_COGS, ICON_PF_MICROCHIP, - ICON_PF_PICTURE, ICON_FA_MAGIC, ICON_PF_SOUND, ICON_PF_GAMEPAD_ALT, - ICON_PF_KEYBOARD_ALT, ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE}; - static constexpr const char* per_game_icons[] = {ICON_FA_PARAGRAPH, ICON_FA_HDD, ICON_FA_COGS, - ICON_PF_PICTURE, ICON_PF_SOUND, ICON_PF_GAMEPAD_ALT, - ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE}; - static constexpr SettingsPage global_pages[] = { + static constexpr const SettingsPage global_pages[] = { SettingsPage::Interface, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::BIOS, SettingsPage::Display, SettingsPage::PostProcessing, SettingsPage::Audio, SettingsPage::Controller, SettingsPage::Hotkey, SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced}; - static constexpr SettingsPage per_game_pages[] = { - SettingsPage::Summary, SettingsPage::Console, SettingsPage::Emulation, - SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller, + static constexpr const SettingsPage per_game_pages[] = { + SettingsPage::Summary, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::Patches, + SettingsPage::Cheats, SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller, SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced}; - static constexpr std::array(SettingsPage::Count)> titles = { - {FSUI_NSTR("Summary"), FSUI_NSTR("Interface Settings"), FSUI_NSTR("Console Settings"), - FSUI_NSTR("Emulation Settings"), FSUI_NSTR("BIOS Settings"), FSUI_NSTR("Controller Settings"), - FSUI_NSTR("Hotkey Settings"), FSUI_NSTR("Memory Card Settings"), FSUI_NSTR("Graphics Settings"), - FSUI_NSTR("Post-Processing Settings"), FSUI_NSTR("Audio Settings"), FSUI_NSTR("Achievements Settings"), - FSUI_NSTR("Advanced Settings")}}; + static constexpr std::array, static_cast(SettingsPage::Count)> titles = { + {{FSUI_NSTR("Summary"), ICON_FA_PARAGRAPH}, + {FSUI_NSTR("Interface Settings"), ICON_FA_TV}, + {FSUI_NSTR("Console Settings"), ICON_FA_DICE_D20}, + {FSUI_NSTR("Emulation Settings"), ICON_FA_COGS}, + {FSUI_NSTR("BIOS Settings"), ICON_PF_MICROCHIP}, + {FSUI_NSTR("Controller Settings"), ICON_PF_GAMEPAD_ALT}, + {FSUI_NSTR("Hotkey Settings"), ICON_PF_KEYBOARD_ALT}, + {FSUI_NSTR("Memory Card Settings"), ICON_PF_MEMORY_CARD}, + {FSUI_NSTR("Graphics Settings"), ICON_PF_PICTURE}, + {FSUI_NSTR("Post-Processing Settings"), ICON_FA_MAGIC}, + {FSUI_NSTR("Audio Settings"), ICON_PF_SOUND}, + {FSUI_NSTR("Achievements Settings"), ICON_FA_TROPHY}, + {FSUI_NSTR("Advanced Settings"), ICON_FA_EXCLAMATION_TRIANGLE}, + {FSUI_NSTR("Patches"), ICON_FA_BAND_AID}, + {FSUI_NSTR("Cheats"), ICON_FA_FLASK}}}; const bool game_settings = IsEditingGameSettings(GetEditingSettingsInterface()); const u32 count = game_settings ? static_cast(std::size(per_game_pages)) : static_cast(std::size(global_pages)); - const char* const* icons = game_settings ? per_game_icons : global_icons; const SettingsPage* pages = game_settings ? per_game_pages : global_pages; u32 index = 0; for (u32 i = 0; i < count; i++) @@ -2903,13 +2895,14 @@ void FullscreenUI::DrawSettingsWindow() if (s_game_settings_entry) NavTitle(s_game_settings_entry->title.c_str()); else - NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast(pages[index])])); + NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast(pages[index])].first)); RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); for (u32 i = 0; i < count; i++) { - if (NavButton(icons[i], i == index, true, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + if (NavButton(titles[static_cast(pages[i])].second, i == index, true, ITEM_WIDTH, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) { s_settings_page = pages[i]; QueueResetFocus(FocusResetType::Other); @@ -2994,6 +2987,14 @@ void FullscreenUI::DrawSettingsWindow() DrawAdvancedSettingsPage(); break; + case SettingsPage::Patches: + DrawPatchesOrCheatsSettingsPage(false); + break; + + case SettingsPage::Cheats: + DrawPatchesOrCheatsSettingsPage(true); + break; + default: break; } @@ -5220,6 +5221,186 @@ void FullscreenUI::DrawAdvancedSettingsPage() EndMenuButtons(); } +void FullscreenUI::DrawPatchesOrCheatsSettingsPage(bool cheats) +{ + SettingsInterface* bsi = GetEditingSettingsInterface(); + + const Cheats::CodeInfoList& code_list = cheats ? s_game_cheats_list : s_game_patch_list; + std::vector& enable_list = cheats ? s_enabled_game_cheat_cache : s_enabled_game_patch_cache; + const char* section = cheats ? Cheats::CHEATS_CONFIG_SECTION : Cheats::PATCHES_CONFIG_SECTION; + + BeginMenuButtons(); + + static constexpr auto draw_code = [](SettingsInterface* bsi, const char* section, const Cheats::CodeInfo& ci, + std::vector& enable_list, bool cheats) { + const auto enable_it = std::find(enable_list.begin(), enable_list.end(), ci.name); + + SmallString title; + if (!cheats) + title = std::string_view(ci.name); + else + title = ci.GetNamePart(); + + // TODO: Handle ranges. + bool state = (enable_it != enable_list.end()); + + if (ci.HasOptionChoices()) + { + TinyString visible_value(FSUI_VSTR("Disabled")); + bool has_option = false; + if (state) + { + // Need to map the value to an option. + visible_value = ci.MapOptionValueToName(bsi->GetTinyStringValue(section, ci.name.c_str())); + has_option = true; + } + + if (MenuButtonWithValue(title.c_str(), ci.description.c_str(), visible_value)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(ci.options.size() + 1); + options.emplace_back(FSUI_VSTR("Disabled"), !has_option); + + for (const Cheats::CodeOption& opt : ci.options) + options.emplace_back(opt.first, has_option && (visible_value.view() == opt.first)); + + OpenChoiceDialog(ci.name, false, std::move(options), + [cheat_name = ci.name, cheats, section](s32 index, const std::string& title, bool checked) { + if (index >= 0) + { + const Cheats::CodeInfo* ci = + Cheats::FindCodeInInfoList(cheats ? s_game_cheats_list : s_game_patch_list, cheat_name); + if (ci) + { + SettingsInterface* bsi = GetEditingSettingsInterface(); + std::vector& enable_list = + cheats ? s_enabled_game_cheat_cache : s_enabled_game_patch_cache; + const auto it = std::find(enable_list.begin(), enable_list.end(), ci->name); + if (index == 0) + { + bsi->RemoveFromStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci->name.c_str()); + if (it != enable_list.end()) + enable_list.erase(it); + } + else + { + bsi->AddToStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci->name.c_str()); + bsi->SetUIntValue(section, ci->name.c_str(), ci->MapOptionNameToValue(title)); + if (it == enable_list.end()) + enable_list.push_back(std::move(cheat_name)); + } + + SetSettingsChanged(bsi); + } + } + + CloseChoiceDialog(); + }); + } + } + else + { + const bool changed = ToggleButton(title.c_str(), ci.description.c_str(), &state); + if (changed) + { + if (state) + { + bsi->AddToStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci.name.c_str()); + enable_list.push_back(ci.name); + } + else + { + bsi->RemoveFromStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci.name.c_str()); + enable_list.erase(enable_it); + } + + SetSettingsChanged(bsi); + } + } + }; + + if (cheats) + { + ActiveButton( + FSUI_CSTR( + "WARNING: Activating cheats can cause unpredictable behavior, crashing, soft-locks, or broken saved games."), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + MenuHeading(FSUI_CSTR("Settings")); + + bool enable_cheats = bsi->GetBoolValue("Cheats", "EnableCheats", false); + if (ToggleButton(FSUI_CSTR("Enable Cheats"), FSUI_CSTR("Enables the cheats that are selected below."), + &enable_cheats)) + { + if (enable_cheats) + bsi->SetBoolValue("Cheats", "EnableCheats", true); + else + bsi->DeleteValue("Cheats", "EnableCheats"); + SetSettingsChanged(bsi); + } + + bool load_database_cheats = bsi->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true); + if (ToggleButton(FSUI_CSTR("Load Database Cheats"), + FSUI_CSTR("Enables loading of cheats for this game from DuckStation's database."), + &load_database_cheats)) + { + if (load_database_cheats) + bsi->DeleteValue("Cheats", "LoadCheatsFromDatabase"); + else + bsi->SetBoolValue("Cheats", "LoadCheatsFromDatabase", false); + SetSettingsChanged(bsi); + if (s_game_settings_entry) + PopulatePatchesAndCheatsList(s_game_settings_entry->serial); + } + + if (code_list.empty()) + { + ActiveButton(FSUI_CSTR("No cheats are available for this game."), false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + else + { + for (const std::string_view& group : s_game_cheat_groups) + { + if (group.empty()) + MenuHeading(FSUI_CSTR("Ungrouped")); + else + MenuHeading(SmallString(group).c_str()); + + for (const Cheats::CodeInfo& ci : code_list) + { + if (ci.GetNameParentPart() != group) + continue; + + draw_code(bsi, section, ci, enable_list, true); + } + } + } + } + else + { + ActiveButton( + FSUI_CSTR("WARNING: Activating game patches can cause unpredictable behavior, crashing, soft-locks, or broken " + "saved games."), + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + if (code_list.empty()) + { + ActiveButton(FSUI_CSTR("No patches are available for this game."), false, false, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + else + { + MenuHeading(FSUI_CSTR("Game Patches")); + + for (const Cheats::CodeInfo& ci : code_list) + draw_code(bsi, section, ci, enable_list, false); + } + } + + EndMenuButtons(); +} + void FullscreenUI::DrawPauseMenu() { SmallString buffer; @@ -5327,7 +5508,7 @@ void FullscreenUI::DrawPauseMenu() ImVec2(10.0f, 10.0f), ImGuiWindowFlags_NoBackground)) { static constexpr u32 submenu_item_count[] = { - 12, // None + 11, // None 4, // Exit 3, // Achievements }; @@ -5365,13 +5546,6 @@ void FullscreenUI::DrawPauseMenu() s_current_main_window = MainWindowType::None; } - 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(); - } - if (ActiveButton(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Toggle Analog"), false)) { ClosePauseMenu(); diff --git a/src/core/game_database.cpp b/src/core/game_database.cpp index 2bfa4ca1b..ddcc37a45 100644 --- a/src/core/game_database.cpp +++ b/src/core/game_database.cpp @@ -220,7 +220,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 f045764d9..b3e82da19 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -82,7 +82,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); @@ -193,7 +193,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 e6112790b..a154a0dcc 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); @@ -518,7 +517,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); @@ -938,7 +936,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; @@ -1051,7 +1048,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; @@ -2135,6 +2131,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; @@ -2154,6 +2151,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"); @@ -2185,6 +2183,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"); @@ -2201,6 +2200,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); @@ -2222,6 +2222,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()); @@ -2262,6 +2263,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 a6f44650d..b3b664e71 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; @@ -578,6 +577,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..5e3f3f0b7 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); @@ -1288,6 +1287,9 @@ void System::LoadSettings(bool display_osd_messages) entry->ApplySettings(g_settings, display_osd_messages); } + // patch overrides take precedence over compat settings + Cheats::ApplySettingOverrides(); + g_settings.FixIncompatibleSettings(display_osd_messages); } @@ -1483,6 +1485,9 @@ bool System::UpdateGameSettingsLayer() Host::Internal::SetInputSettingsLayer(input_interface.get(), lock); s_input_settings_interface = std::move(input_interface); s_input_profile_name = std::move(input_profile_name); + + Cheats::ReloadCheats(false, true, false, true); + return true; } @@ -1496,9 +1501,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 +1716,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 +1732,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 +1770,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (cancelled) { s_state = State::Shutdown; + Cheats::UnloadAll(); ClearRunningGame(); Host::OnSystemDestroyed(); Host::OnIdleStateChanged(); @@ -1776,6 +1784,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 +1796,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 +1991,7 @@ void System::DestroySystem() ClearMemorySaveStates(); + Cheats::UnloadAll(); PCDrv::Shutdown(); SIO::Shutdown(); MDEC::Shutdown(); @@ -2013,7 +2024,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 +2095,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 +3607,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 @@ -4114,12 +4096,11 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool Achievements::GameChanged(s_running_game_path, image); + // game layer reloads cheats, but only the active list, we need new files + Cheats::ReloadCheats(true, false, false, true); UpdateGameSettingsLayer(); - ApplySettings(true); - s_cheat_list.reset(); - if (g_settings.enable_cheats) - LoadCheatList(); + ApplySettings(true); if (s_running_game_serial != prev_serial) UpdateSessionTime(prev_serial); @@ -4248,40 +4229,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 +4337,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 +4627,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 +5526,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..291942f50 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -29,12 +29,6 @@ set(SRCS biossettingswidget.cpp biossettingswidget.h biossettingswidget.ui - cheatcodeeditordialog.cpp - cheatcodeeditordialog.h - cheatcodeeditordialog.ui - cheatmanagerwindow.cpp - cheatmanagerwindow.h - cheatmanagerwindow.ui colorpickerbutton.cpp colorpickerbutton.h consolesettingswidget.cpp @@ -79,6 +73,15 @@ set(SRCS foldersettingswidget.cpp foldersettingswidget.h foldersettingswidget.ui + gamecheatcodechoiceeditordialog.ui + gamecheatcodeeditordialog.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 deleted file mode 100644 index 0516f327c..000000000 --- a/src/duckstation-qt/cheatcodeeditordialog.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin -// SPDX-License-Identifier: CC-BY-NC-ND-4.0 - -#include "cheatcodeeditordialog.h" -#include - -CheatCodeEditorDialog::CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent) - : QDialog(parent), m_code(code) -{ - m_ui.setupUi(this); - setupAdditionalUi(group_names); - fillUi(); - connectUi(); -} - -CheatCodeEditorDialog::~CheatCodeEditorDialog() = default; - -void CheatCodeEditorDialog::saveClicked() -{ - std::string new_description = m_ui.description->text().toStdString(); - if (new_description.empty()) - { - QMessageBox::critical(this, tr("Error"), tr("Description cannot be empty.")); - return; - } - - if (!m_code->SetInstructionsFromString(m_ui.instructions->toPlainText().toStdString())) - { - QMessageBox::critical(this, tr("Error"), tr("Instructions are invalid.")); - 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(); - - done(1); -} - -void CheatCodeEditorDialog::cancelClicked() -{ - done(0); -} - -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(CheatCode::Activation::Count); i++) - { - m_ui.activation->addItem( - qApp->translate("Cheats", CheatCode::GetActivationDisplayName(static_cast(i)))); - } - - if (!group_names.isEmpty()) - m_ui.group->addItems(group_names); - else - m_ui.group->addItem(QStringLiteral("Ungrouped")); -} - -void CheatCodeEditorDialog::fillUi() -{ - m_ui.description->setText(QString::fromStdString(m_code->description)); - - const QString group_qstr(QString::fromStdString(m_code->group)); - int index = m_ui.group->findText(group_qstr); - if (index >= 0) - { - m_ui.group->setCurrentIndex(index); - } - else - { - index = m_ui.group->count(); - m_ui.group->addItem(group_qstr); - 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.instructions->setPlainText(QString::fromStdString(m_code->GetInstructionsAsString())); -} - -void CheatCodeEditorDialog::connectUi() -{ - connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &CheatCodeEditorDialog::saveClicked); - connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::cancelClicked); -} diff --git a/src/duckstation-qt/cheatcodeeditordialog.h b/src/duckstation-qt/cheatcodeeditordialog.h deleted file mode 100644 index 954f6b878..000000000 --- a/src/duckstation-qt/cheatcodeeditordialog.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin -// SPDX-License-Identifier: CC-BY-NC-ND-4.0 - -#pragma once -#include "core/cheats.h" -#include "ui_cheatcodeeditordialog.h" - -class CheatCodeEditorDialog : public QDialog -{ - Q_OBJECT - -public: - CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent); - ~CheatCodeEditorDialog(); - -private Q_SLOTS: - void saveClicked(); - void cancelClicked(); - -private: - void setupAdditionalUi(const QStringList& group_names); - void fillUi(); - void connectUi(); - - CheatCode* m_code; - - Ui::CheatCodeEditorDialog m_ui; -}; diff --git a/src/duckstation-qt/cheatcodeeditordialog.ui b/src/duckstation-qt/cheatcodeeditordialog.ui deleted file mode 100644 index f6d6ab193..000000000 --- a/src/duckstation-qt/cheatcodeeditordialog.ui +++ /dev/null @@ -1,74 +0,0 @@ - - - CheatCodeEditorDialog - - - - 0 - 0 - 491 - 284 - - - - Cheat Code Editor - - - true - - - - - - Description: - - - - - - - - - - Group: - - - - - - - - - - Type: - - - - - - - - - - Activation: - - - - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - - - - - - 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..30a86b6af 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -9,8 +9,6 @@ - - @@ -21,7 +19,9 @@ + + @@ -58,8 +58,6 @@ - - @@ -85,6 +83,8 @@ + + @@ -139,10 +139,7 @@ Document - - Document - - + Document @@ -222,8 +219,6 @@ - - @@ -233,10 +228,12 @@ + + @@ -342,6 +339,18 @@ Document + + 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..d6cf8760a 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -23,8 +23,6 @@ - - @@ -65,12 +63,6 @@ moc - - moc - - - moc - moc @@ -175,6 +167,14 @@ + + + + moc + + + moc + @@ -216,8 +216,6 @@ - - @@ -237,6 +235,8 @@ + + @@ -252,8 +252,7 @@ - - + @@ -285,6 +284,10 @@ + + + + diff --git a/src/duckstation-qt/gamecheatcodechoiceeditordialog.ui b/src/duckstation-qt/gamecheatcodechoiceeditordialog.ui new file mode 100644 index 000000000..4db4a4631 --- /dev/null +++ b/src/duckstation-qt/gamecheatcodechoiceeditordialog.ui @@ -0,0 +1,84 @@ + + + GameCheatCodeChoiceEditorDialog + + + + 0 + 0 + 400 + 231 + + + + Cheat Choice Editor + + + + + + + Name + + + + + Value + + + + + + + + + + + 0 + 0 + + + + Add Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + + 0 + 0 + + + + Remove Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save + + + + + + + + + + diff --git a/src/duckstation-qt/gamecheatcodeeditordialog.ui b/src/duckstation-qt/gamecheatcodeeditordialog.ui new file mode 100644 index 000000000..19647bc2d --- /dev/null +++ b/src/duckstation-qt/gamecheatcodeeditordialog.ui @@ -0,0 +1,142 @@ + + + GameCheatCodeEditorDialog + + + + 0 + 0 + 491 + 318 + + + + Cheat Code Editor + + + true + + + + + + Name: + + + + + + + + + + Description: + + + + + + + + 16777215 + 48 + + + + + + + + Group: + + + + + + + + + + Type: + + + + + + + + + + Activation: + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save + + + + + + + Options: + + + + + + + + + + None + + + + + Choice + + + + + Range + + + + + + + + 65535 + + + + + + + 65535 + + + + + + + Edit Choices... + + + + + + + + + + diff --git a/src/duckstation-qt/gamecheatsettingswidget.cpp b/src/duckstation-qt/gamecheatsettingswidget.cpp new file mode 100644 index 000000000..0e5444804 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.cpp @@ -0,0 +1,948 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "gamecheatsettingswidget.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 "common/string_util.h" + +#include "fmt/format.h" + +#include +#include +#include +#include + +namespace { +class CheatListOptionDelegate : public QStyledItemDelegate +{ +public: + CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeWidget* treeview); + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + +private: + std::string getCodeNameForRow(const QModelIndex& index) const; + const Cheats::CodeInfo* getCodeInfoForRow(const QModelIndex& index) const; + + GameCheatSettingsWidget* m_parent; + QTreeWidget* m_treeview; +}; +}; // namespace + +CheatListOptionDelegate::CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeWidget* treeview) + : QStyledItemDelegate(parent), m_parent(parent), m_treeview(treeview) +{ +} + +std::string CheatListOptionDelegate::getCodeNameForRow(const QModelIndex& index) const +{ + return index.siblingAtColumn(0).data(Qt::UserRole).toString().toStdString(); +} + +const Cheats::CodeInfo* CheatListOptionDelegate::getCodeInfoForRow(const QModelIndex& index) const +{ + return m_parent->getCodeInfo(getCodeNameForRow(index)); +} + +QWidget* CheatListOptionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + // only edit the value, don't want the title becoming editable + if (index.column() != 1) + return nullptr; + + const QVariant data = index.data(Qt::UserRole); + if (data.isNull()) + return nullptr; + + // if it's a uint, it's a range, otherwise string => combobox + if (data.typeId() == QMetaType::QString) + return new QComboBox(parent); + else if (data.typeId() == QMetaType::UInt) + return new QSpinBox(parent); + else + return QStyledItemDelegate::createEditor(parent, option, index); +} + +void CheatListOptionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const +{ + if (index.column() != 1) + return; + + if (QComboBox* cb = qobject_cast(editor)) + { + const Cheats::CodeInfo* ci = getCodeInfoForRow(index); + if (ci) + { + int current_index = 0; + const QString selected_name = index.data(Qt::UserRole).toString(); + for (const Cheats::CodeOption& opt : ci->options) + { + const QString name = QString::fromStdString(opt.first); + cb->addItem(name, QVariant(static_cast(opt.second))); + if (name == selected_name) + cb->setCurrentIndex(current_index); + current_index++; + } + } + } + else if (QSpinBox* sb = qobject_cast(editor)) + { + const Cheats::CodeInfo* ci = getCodeInfoForRow(index); + if (ci) + { + sb->setMinimum(ci->option_range_start); + sb->setMaximum(ci->option_range_end); + sb->setValue(index.data(Qt::UserRole).toUInt()); + } + } + else + { + return QStyledItemDelegate::setEditorData(editor, index); + } +} + +void CheatListOptionDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +{ + if (index.column() != 1) + return; + + if (QComboBox* cb = qobject_cast(editor)) + { + const QString value = cb->currentText(); + const Cheats::CodeInfo* ci = getCodeInfoForRow(index); + if (ci) + { + m_parent->setCodeOption(ci->name, ci->MapOptionNameToValue(value.toStdString())); + model->setData(index, value, Qt::UserRole); + } + } + else if (QSpinBox* sb = qobject_cast(editor)) + { + const u32 value = static_cast(sb->value()); + m_parent->setCodeOption(getCodeNameForRow(index), value); + model->setData(index, static_cast(value), Qt::UserRole); + } + else + { + return QStyledItemDelegate::setModelData(editor, model, index); + } +} + +void CheatListOptionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + if (index.column() == 0) + { + // skip for editable rows + if (index.flags() & Qt::ItemIsEditable) + return QStyledItemDelegate::paint(painter, option, index); + + // expand the width to full for those without options + QStyleOptionViewItem option_copy(option); + option_copy.rect.setWidth(option_copy.rect.width() + m_treeview->columnWidth(1)); + return QStyledItemDelegate::paint(painter, option_copy, index); + } + else + { + if (!(index.flags() & Qt::ItemIsEditable)) + { + // disable painting this column, so we can expand it (see above) + return; + } + + // just draw the number or label as a string + const QVariant data = index.data(Qt::UserRole); + painter->drawText(option.rect, 0, data.toString()); + } +} + +GameCheatSettingsWidget::GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog) +{ + m_ui.setupUi(this); + m_ui.cheatList->setItemDelegate(new CheatListOptionDelegate(this, m_ui.cheatList)); + + 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)); + + 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::customContextMenuRequested, this, + &GameCheatSettingsWidget::onCheatListContextMenuRequested); + connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged); + connect(m_ui.add, &QToolButton::clicked, this, &GameCheatSettingsWidget::newCode); + connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatSettingsWidget::onRemoveCodeClicked); + connect(m_ui.disableAll, &QToolButton::clicked, this, &GameCheatSettingsWidget::disableAllCheats); + connect(m_ui.reloadCheats, &QToolButton::clicked, this, &GameCheatSettingsWidget::onReloadClicked); + connect(m_ui.importCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onImportClicked); + connect(m_ui.exportCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onExportClicked); +} + +GameCheatSettingsWidget::~GameCheatSettingsWidget() = default; + +const Cheats::CodeInfo* GameCheatSettingsWidget::getCodeInfo(const std::string_view name) const +{ + return Cheats::FindCodeInInfoList(m_codes, name); +} + +void GameCheatSettingsWidget::setCodeOption(const std::string_view name, u32 value) +{ + const Cheats::CodeInfo* info = getCodeInfo(name); + if (!info) + return; + + m_dialog->getSettingsInterface()->SetUIntValue("Cheats", info->name.c_str(), value); + m_dialog->saveAndReloadGameSettings(); +} + +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 +{ + std::vector unique_prefixes = Cheats::GetCodeListUniquePrefixes(m_codes, false); + + QStringList ret; + if (!unique_prefixes.empty()) + { + ret.reserve(unique_prefixes.size()); + for (const std::string_view& prefix : unique_prefixes) + ret.push_back(QtUtils::StringViewToQString(prefix)); + } + return ret; +} + +bool GameCheatSettingsWidget::hasCodeWithName(const std::string_view name) const +{ + return (Cheats::FindCodeInInfoList(m_codes, name) != nullptr); +} + +void GameCheatSettingsWidget::onEnableCheatsChanged(Qt::CheckState state) +{ + if (state == Qt::Checked) + m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "EnableCheats", true); + else + m_dialog->getSettingsInterface()->DeleteValue("Cheats", "EnableCheats"); + m_dialog->saveAndReloadGameSettings(); +} + +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); + m_dialog->saveAndReloadGameSettings(); + reloadList(); +} + +void GameCheatSettingsWidget::onCheatListItemDoubleClicked(QTreeWidgetItem* item, int column) +{ + const QVariant item_data = item->data(0, Qt::UserRole); + if (!item_data.isValid()) + return; + + editCode(item_data.toString().toStdString()); +} + +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::onCheatListContextMenuRequested(const QPoint& pos) +{ + Cheats::CodeInfo* selected = getSelectedCode(); + const std::string selected_code = selected ? selected->name : std::string(); + + QMenu context_menu(m_ui.cheatList); + + QAction* add = context_menu.addAction(QIcon::fromTheme("add-line"), tr("Add Cheat...")); + connect(add, &QAction::triggered, this, &GameCheatSettingsWidget::newCode); + QAction* edit = context_menu.addAction(QIcon::fromTheme("mag-line"), tr("Edit Cheat...")); + edit->setEnabled(selected != nullptr); + connect(edit, &QAction::triggered, this, [this, &selected_code]() { editCode(selected_code); }); + QAction* remove = context_menu.addAction(QIcon::fromTheme("minus-line"), tr("Remove Cheat")); + remove->setEnabled(selected != nullptr); + connect(remove, &QAction::triggered, this, [this, &selected_code]() { removeCode(selected_code, true); }); + context_menu.addSeparator(); + + QAction* disable_all = context_menu.addAction(QIcon::fromTheme("chat-off-line"), tr("Disable All Cheats")); + connect(disable_all, &QAction::triggered, this, &GameCheatSettingsWidget::disableAllCheats); + + QAction* reload = context_menu.addAction(QIcon::fromTheme("refresh-line"), tr("Reload Cheats")); + connect(reload, &QAction::triggered, this, &GameCheatSettingsWidget::onReloadClicked); + + context_menu.exec(m_ui.cheatList->mapToGlobal(pos)); +} + +void GameCheatSettingsWidget::onRemoveCodeClicked() +{ + Cheats::CodeInfo* selected = getSelectedCode(); + if (!selected) + return; + + removeCode(selected->name, true); +} + +void GameCheatSettingsWidget::onReloadClicked() +{ + reloadList(); + g_emu_thread->reloadCheats(true, false, true, true); +} + +bool GameCheatSettingsWidget::shouldLoadFromDatabase() const +{ + return m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true); +} + +Cheats::CodeInfo* GameCheatSettingsWidget::getSelectedCode() +{ + const QList selected = m_ui.cheatList->selectedItems(); + if (selected.size() != 1) + return nullptr; + + const QVariant item_data = selected[0]->data(0, Qt::UserRole); + if (!item_data.isValid()) + return nullptr; + + return Cheats::FindCodeInInfoList(m_codes, item_data.toString().toStdString()); +} + +void GameCheatSettingsWidget::disableAllCheats() +{ + setStateForAll(false); +} + +void GameCheatSettingsWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 150}); +} + +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) + m_dialog->saveAndReloadGameSettings(); +} + +void GameCheatSettingsWidget::setStateForAll(bool enabled) +{ + QSignalBlocker sb(m_ui.cheatList); + setStateRecursively(nullptr, enabled); + m_dialog->saveAndReloadGameSettings(); +} + +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(), true); + 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::newCode() +{ + Cheats::CodeInfo new_code; + CheatCodeEditorDialog dlg(this, &new_code, getGroupNames()); + if (!dlg.exec()) + { + // cancelled + return; + } + + // no need to reload cheats yet, it's not active. just refresh the list + reloadList(); +} + +void GameCheatSettingsWidget::editCode(const std::string_view code_name) +{ + Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name); + if (!code) + return; + + CheatCodeEditorDialog dlg(this, code, getGroupNames()); + if (!dlg.exec()) + { + // no changes + return; + } + + reloadList(); + g_emu_thread->reloadCheats(true, true, false, true); +} + +void GameCheatSettingsWidget::removeCode(const std::string_view code_name, bool confirm) +{ + Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name); + if (!code) + return; + + if (code->from_database) + { + QMessageBox::critical(this, tr("Error"), + tr("This code is from the built-in cheat database, and cannot be removed. To hide this code, " + "uncheck the \"Load Database Cheats\" option.")); + return; + } + + if (QMessageBox::question(this, tr("Confirm Removal"), + tr("You are removing the code named '%1'. You cannot undo this action, are you sure you " + "wish to delete this code?")) != QMessageBox::Yes) + { + return; + } + + Error error; + if (!Cheats::UpdateCodeInFile(getPathForSavingCheats().c_str(), code->name, nullptr, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription()))); + return; + } + + reloadList(); + g_emu_thread->reloadCheats(true, true, false, true); +} + +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 (!pi.description.empty()) + item->setToolTip(0, QString::fromStdString(pi.description)); + if (!name_part.empty()) + item->setText(0, QtUtils::StringViewToQString(name_part)); + + if (pi.HasOptionChoices()) + { + // need to resolve the value back to a name + const std::string_view option_name = + pi.MapOptionValueToName(m_dialog->getSettingsInterface()->GetTinyStringValue("Cheats", pi.name.c_str())); + item->setData(1, Qt::UserRole, QtUtils::StringViewToQString(option_name)); + item->setFlags(item->flags() | Qt::ItemIsEditable); + } + else if (pi.HasOptionRange()) + { + const u32 value = m_dialog->getSettingsInterface()->GetUIntValue("Cheats", pi.name.c_str(), pi.option_range_start); + item->setData(1, Qt::UserRole, static_cast(value)); + item->setFlags(item->flags() | Qt::ItemIsEditable); + } +} + +CheatCodeEditorDialog::CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo* code, + const QStringList& group_names) + : QDialog(parent), m_parent(parent), m_code(code) +{ + m_ui.setupUi(this); + setupAdditionalUi(group_names); + fillUi(); + + connect(m_ui.group, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onGroupSelectedIndexChanged); + connect(m_ui.optionsType, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onOptionTypeChanged); + connect(m_ui.rangeMin, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMinChanged); + connect(m_ui.rangeMax, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMaxChanged); + connect(m_ui.editChoice, &QPushButton::clicked, this, &CheatCodeEditorDialog::onEditChoiceClicked); + connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &CheatCodeEditorDialog::saveClicked); + connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::cancelClicked); +} + +CheatCodeEditorDialog::~CheatCodeEditorDialog() = default; + +void CheatCodeEditorDialog::onGroupSelectedIndexChanged(int index) +{ + if (index != (m_ui.group->count() - 1)) + return; + + // new item... + const QString text = QInputDialog::getText( + this, tr("Enter Group Name"), tr("Enter name for the code group. Using backslashes (\\) will create sub-trees.")); + + // don't want this re-triggering + QSignalBlocker sb(m_ui.group); + + if (text.isEmpty()) + { + // cancelled... + m_ui.group->setCurrentIndex(0); + return; + } + + const int existing_index = m_ui.group->findText(text); + if (existing_index >= 0) + { + m_ui.group->setCurrentIndex(existing_index); + return; + } + + m_ui.group->insertItem(index, text); + m_ui.group->setCurrentIndex(index); +} + +void CheatCodeEditorDialog::saveClicked() +{ + std::string new_name = m_ui.name->text().toStdString(); + if (new_name.empty()) + { + QMessageBox::critical(this, tr("Error"), tr("Name cannot be empty.")); + return; + } + + std::string new_body = m_ui.instructions->toPlainText().toStdString(); + if (new_body.empty()) + { + QMessageBox::critical(this, tr("Error"), tr("Instructions cannot be empty.")); + return; + } + + // name actually includes the prefix + if (const int index = m_ui.group->currentIndex(); index != 0) + { + const std::string prefix = m_ui.group->currentText().toStdString(); + if (!prefix.empty()) + new_name = fmt::format("{}\\{}", prefix, new_name); + } + + // if the name has changed, then we need to make sure it hasn't already been used + if (new_name != m_code->name && m_parent->hasCodeWithName(new_name)) + { + QMessageBox::critical(this, tr("Error"), + tr("A code with the name '%1' already exists.").arg(QString::fromStdString(new_name))); + return; + } + + std::string old_name = std::move(m_code->name); + + // cheats coming from the database need to be copied into the user's file + if (m_code->from_database) + { + m_code->from_database = false; + old_name.clear(); + } + + m_code->name = std::move(new_name); + m_code->description = m_ui.description->toPlainText().replace('\n', ' ').toStdString(); + 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); + + m_code->option_range_start = 0; + m_code->option_range_end = 0; + m_code->options = {}; + if (m_ui.optionsType->currentIndex() == 1) + { + // choices + m_code->options = std::move(m_new_options); + } + else if (m_ui.optionsType->currentIndex() == 2) + { + // range + m_code->option_range_start = static_cast(m_ui.rangeMin->value()); + m_code->option_range_end = static_cast(m_ui.rangeMax->value()); + } + + std::string path = m_parent->getPathForSavingCheats(); + Error error; + if (!Cheats::UpdateCodeInFile(path.c_str(), old_name, m_code, &error)) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to save cheat code:\n%1").arg(QString::fromStdString(error.GetDescription()))); + } + + done(1); +} + +void CheatCodeEditorDialog::cancelClicked() +{ + done(0); +} + +void CheatCodeEditorDialog::onOptionTypeChanged(int index) +{ + m_ui.editChoice->setVisible(index == 1); + m_ui.rangeMin->setVisible(index == 2); + m_ui.rangeMax->setVisible(index == 2); +} + +void CheatCodeEditorDialog::onRangeMinChanged(int value) +{ + m_ui.rangeMax->setValue(std::max(m_ui.rangeMax->value(), value)); +} + +void CheatCodeEditorDialog::onRangeMaxChanged(int value) +{ + m_ui.rangeMin->setValue(std::min(m_ui.rangeMin->value(), value)); +} + +void CheatCodeEditorDialog::onEditChoiceClicked() +{ + GameCheatCodeChoiceEditorDialog dlg(this, m_new_options); + if (dlg.exec()) + m_new_options = dlg.getNewOptions(); +} + +void CheatCodeEditorDialog::setupAdditionalUi(const QStringList& group_names) +{ + 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(Cheats::CodeActivation::Count); i++) + m_ui.activation->addItem(Cheats::GetActivationDisplayName(static_cast(i))); + + m_ui.group->addItem(tr("Ungrouped")); + + if (!group_names.isEmpty()) + m_ui.group->addItems(group_names); + + m_ui.group->addItem(tr("New...")); +} + +void CheatCodeEditorDialog::fillUi() +{ + m_ui.name->setText(QtUtils::StringViewToQString(m_code->GetNamePart())); + m_ui.description->setPlainText(QString::fromStdString(m_code->description)); + + const std::string_view group = m_code->GetNameParentPart(); + if (group.empty()) + { + // ungrouped is always first + m_ui.group->setCurrentIndex(0); + } + else + { + const QString group_qstr(QtUtils::StringViewToQString(group)); + int index = m_ui.group->findText(group_qstr); + if (index < 0) + { + // shouldn't happen... + index = m_ui.group->count() - 1; + m_ui.group->insertItem(index, group_qstr); + } + + 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.instructions->setPlainText(QString::fromStdString(m_code->body)); + + m_ui.rangeMin->setValue(static_cast(m_code->option_range_start)); + m_ui.rangeMax->setValue(static_cast(m_code->option_range_end)); + m_new_options = m_code->options; + + m_ui.optionsType->setCurrentIndex(m_code->HasOptionRange() ? 2 : (m_code->HasOptionChoices() ? 1 : 0)); + onOptionTypeChanged(m_ui.optionsType->currentIndex()); +} + +GameCheatCodeChoiceEditorDialog::GameCheatCodeChoiceEditorDialog(QWidget* parent, const Cheats::CodeOptionList& options) + : QDialog(parent) +{ + m_ui.setupUi(this); + + connect(m_ui.add, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onAddClicked); + connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onRemoveClicked); + connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &GameCheatCodeChoiceEditorDialog::onSaveClicked); + connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::reject); + + m_ui.optionList->setRootIsDecorated(false); + for (const Cheats::CodeOption& opt : options) + { + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setText(0, QString::fromStdString(opt.first)); + item->setText(1, QString::number(opt.second)); + m_ui.optionList->addTopLevelItem(item); + } +} + +GameCheatCodeChoiceEditorDialog::~GameCheatCodeChoiceEditorDialog() = default; + +void GameCheatCodeChoiceEditorDialog::resizeEvent(QResizeEvent* event) +{ + QDialog::resizeEvent(event); + QtUtils::ResizeColumnsForTreeView(m_ui.optionList, {-1, 150}); +} + +void GameCheatCodeChoiceEditorDialog::onAddClicked() +{ + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setText(0, QStringLiteral("Option %1").arg(m_ui.optionList->topLevelItemCount())); + item->setText(1, QStringLiteral("0")); + m_ui.optionList->addTopLevelItem(item); +} + +void GameCheatCodeChoiceEditorDialog::onRemoveClicked() +{ + const QList items = m_ui.optionList->selectedItems(); + for (QTreeWidgetItem* item : items) + { + const int index = m_ui.optionList->indexOfTopLevelItem(item); + if (index >= 0) + delete m_ui.optionList->takeTopLevelItem(index); + } +} + +void GameCheatCodeChoiceEditorDialog::onSaveClicked() +{ + // validate the data + const int count = m_ui.optionList->topLevelItemCount(); + if (count == 0) + { + QMessageBox::critical(this, tr("Error"), tr("At least one option must be defined.")); + return; + } + + for (int i = 0; i < count; i++) + { + const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i); + const QString this_name = it->text(0); + for (int j = 0; j < count; j++) + { + if (i == j) + continue; + + if (m_ui.optionList->topLevelItem(j)->text(0) == this_name) + { + QMessageBox::critical(this, tr("Error"), tr("The option '%1' is defined twice.").arg(this_name)); + return; + } + } + + // should be a parseable number + const QString this_value = it->text(1); + if (bool ok; this_value.toUInt(&ok), !ok) + { + QMessageBox::critical(this, tr("Error"), + tr("The option '%1' does not have a valid value. It must be a number.").arg(this_name)); + return; + } + } + + accept(); +} + +Cheats::CodeOptionList GameCheatCodeChoiceEditorDialog::getNewOptions() const +{ + Cheats::CodeOptionList ret; + + const int count = m_ui.optionList->topLevelItemCount(); + ret.reserve(static_cast(count)); + + for (int i = 0; i < count; i++) + { + const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i); + ret.emplace_back(it->text(0).toStdString(), it->text(1).toUInt()); + } + + return ret; +} diff --git a/src/duckstation-qt/gamecheatsettingswidget.h b/src/duckstation-qt/gamecheatsettingswidget.h new file mode 100644 index 000000000..83834fdd8 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "ui_gamecheatcodechoiceeditordialog.h" +#include "ui_gamecheatcodeeditordialog.h" +#include "ui_gamecheatsettingswidget.h" + +#include "core/cheats.h" + +#include "common/heterogeneous_containers.h" + +#include +#include +#include + +#include +#include +#include + +namespace GameList { +struct Entry; +} + +class SettingsWindow; + +class GameCheatSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent); + ~GameCheatSettingsWidget() override; + + const Cheats::CodeInfo* getCodeInfo(const std::string_view name) const; + void setCodeOption(const std::string_view name, u32 value); + std::string getPathForSavingCheats() const; + QStringList getGroupNames() const; + bool hasCodeWithName(const std::string_view name) 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 onCheatListContextMenuRequested(const QPoint& pos); + void onRemoveCodeClicked(); + void onReloadClicked(); + void onImportClicked(); + void onImportFromFileTriggered(); + void onImportFromTextTriggered(); + void onExportClicked(); + void reloadList(); + +private: + bool shouldLoadFromDatabase() const; + + Cheats::CodeInfo* getSelectedCode(); + 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 importCodes(const std::string& file_contents); + void newCode(); + void editCode(const std::string_view code_name); + void removeCode(const std::string_view code_name, bool confirm); + + Ui::GameCheatSettingsWidget m_ui; + SettingsWindow* m_dialog; + + UnorderedStringMap m_parent_map; + Cheats::CodeInfoList m_codes; + std::vector m_enabled_codes; +}; + +class CheatCodeEditorDialog : public QDialog +{ + Q_OBJECT + +public: + CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo* code, const QStringList& group_names); + ~CheatCodeEditorDialog() override; + +private Q_SLOTS: + void onGroupSelectedIndexChanged(int index); + void saveClicked(); + void cancelClicked(); + + void onOptionTypeChanged(int index); + void onRangeMinChanged(int value); + void onRangeMaxChanged(int value); + void onEditChoiceClicked(); + +private: + void setupAdditionalUi(const QStringList& group_names); + void fillUi(); + + GameCheatSettingsWidget* m_parent; + Ui::GameCheatCodeEditorDialog m_ui; + + Cheats::CodeInfo* m_code; + Cheats::CodeOptionList m_new_options; +}; + +class GameCheatCodeChoiceEditorDialog : public QDialog +{ + Q_OBJECT + +public: + GameCheatCodeChoiceEditorDialog(QWidget* parent, const Cheats::CodeOptionList& options); + ~GameCheatCodeChoiceEditorDialog() override; + + Cheats::CodeOptionList getNewOptions() const; + +protected: + void resizeEvent(QResizeEvent* event) override; + +private Q_SLOTS: + void onAddClicked(); + void onRemoveClicked(); + void onSaveClicked(); + +private: + Ui::GameCheatCodeChoiceEditorDialog m_ui; +}; diff --git a/src/duckstation-qt/gamecheatsettingswidget.ui b/src/duckstation-qt/gamecheatsettingswidget.ui new file mode 100644 index 000000000..33fed1365 --- /dev/null +++ b/src/duckstation-qt/gamecheatsettingswidget.ui @@ -0,0 +1,175 @@ + + + GameCheatSettingsWidget + + + + 0 + 0 + 821 + 401 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Enable Cheats + + + + + + + + 0 + 0 + + + + Add Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + + 0 + 0 + + + + Remove Cheat + + + + + + Qt::ToolButtonStyle::ToolButtonIconOnly + + + + + + + Disable All Cheats + + + + + + + + + + Reload Cheats + + + + + + + + + + + + Qt::ContextMenuPolicy::CustomContextMenu + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOn + + + QAbstractItemView::EditTrigger::AllEditTriggers + + + QAbstractItemView::SelectionMode::SingleSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + true + + + + Name + + + + + Value + + + + + + + + + + Load Database 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..6d62fa563 --- /dev/null +++ b/src/duckstation-qt/gamepatchsettingswidget.cpp @@ -0,0 +1,125 @@ +// 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->reloadCheats(true, false, true, true); +} + +void GamePatchSettingsWidget::disableAllPatches() +{ + SettingsInterface* sif = m_dialog->getSettingsInterface(); + sif->RemoveSection(Cheats::PATCHES_CONFIG_SECTION); + m_dialog->saveAndReloadGameSettings(); + reloadList(); +} + +void GamePatchSettingsWidget::reloadList() +{ + const std::vector patches = + Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, false, true, 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..134d20555 --- /dev/null +++ b/src/duckstation-qt/gamepatchsettingswidget.ui @@ -0,0 +1,71 @@ + + + GamePatchSettingsWidget + + + + 0 + 0 + 766 + 392 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 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 da14d9c1e..a59bb0c68 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); @@ -998,125 +997,55 @@ void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* actio } } -void MainWindow::updateCheatActionsVisibility() -{ - // If the cheat system is disabled, put an action to enable it in place of the menu under System. - const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false); - m_ui.actionCheats->setVisible(!cheats_enabled); - m_ui.menuCheats->menuAction()->setVisible(cheats_enabled); -} - void MainWindow::onCheatsActionTriggered() { - const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false); - if (cheats_enabled) - { - m_ui.menuCheats->exec(QCursor::pos()); - return; - } - - SystemLock lock(pauseAndLockSystem()); - QMessageBox mb(this); - mb.setWindowTitle(tr("Enable Cheats")); - mb.setText( - tr("Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted " - "saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not " - "provide you with any assistance when games break.\n\nCheats persist through save states even after being " - "disabled, please remember to reset/reboot the game after turning off any codes.\n\nAre you sure you want " - "to continue?")); - mb.setIcon(QMessageBox::Warning); - QPushButton* global = mb.addButton(tr("Enable For All Games"), QMessageBox::DestructiveRole); - QPushButton* game = mb.addButton(tr("Enable For This Game"), QMessageBox::AcceptRole); - game->setEnabled(s_system_valid && !s_current_game_serial.isEmpty()); - QPushButton* cancel = mb.addButton(tr("Cancel"), QMessageBox::RejectRole); - mb.setDefaultButton(cancel); - mb.setEscapeButton(cancel); - mb.exec(); - - if (mb.clickedButton() == global) - { - // enable globally - Host::SetBaseBoolSettingValue("Console", "EnableCheats", true); - Host::CommitBaseSettingChanges(); - g_emu_thread->applySettings(false); - } - else if (mb.clickedButton() == game) - { - if (!SettingsWindow::setGameSettingsBoolForSerial(s_current_game_serial.toStdString(), "Console", "EnableCheats", - true)) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to enable cheats for %1.").arg(s_current_game_serial)); - return; - } - - g_emu_thread->reloadGameSettings(false); - } - else - { - // do nothing - return; - } + m_ui.menuCheats->exec(QCursor::pos()); } 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 +1304,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 +1398,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 +1468,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); } }); @@ -1709,7 +1621,6 @@ void MainWindow::setupAdditionalUi() m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles()); updateDebugMenuVisibility(); - updateCheatActionsVisibility(); for (u32 i = 0; i < static_cast(CPUExecutionMode::Count); i++) { @@ -1795,37 +1706,38 @@ void MainWindow::setupAdditionalUi() void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) { - m_ui.actionStartFile->setDisabled(starting || running); - m_ui.actionStartDisc->setDisabled(starting || running); - m_ui.actionStartBios->setDisabled(starting || running); - m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode); - m_ui.actionStartFullscreenUI->setDisabled(starting || running); - m_ui.actionStartFullscreenUI2->setDisabled(starting || running); + const bool starting_or_running = (starting || running); + const bool starting_or_not_running = (starting || !running); + m_ui.actionStartFile->setDisabled(starting_or_running); + m_ui.actionStartDisc->setDisabled(starting_or_running); + m_ui.actionStartBios->setDisabled(starting_or_running); + m_ui.actionResumeLastState->setDisabled(starting_or_running || cheevos_challenge_mode); + m_ui.actionStartFullscreenUI->setDisabled(starting_or_running); + m_ui.actionStartFullscreenUI2->setDisabled(starting_or_running); - m_ui.actionPowerOff->setDisabled(starting || !running); - m_ui.actionPowerOffWithoutSaving->setDisabled(starting || !running); - m_ui.actionReset->setDisabled(starting || !running); - m_ui.actionPause->setDisabled(starting || !running); - m_ui.actionChangeDisc->setDisabled(starting || !running); - m_ui.actionCheats->setDisabled(cheevos_challenge_mode); - m_ui.actionCheatsToolbar->setDisabled(cheevos_challenge_mode); - m_ui.actionScreenshot->setDisabled(starting || !running); - m_ui.menuChangeDisc->setDisabled(starting || !running); - m_ui.menuCheats->setDisabled(cheevos_challenge_mode); + m_ui.actionPowerOff->setDisabled(starting_or_not_running); + m_ui.actionPowerOffWithoutSaving->setDisabled(starting_or_not_running); + m_ui.actionReset->setDisabled(starting_or_not_running); + m_ui.actionPause->setDisabled(starting_or_not_running); + m_ui.actionChangeDisc->setDisabled(starting_or_not_running); + m_ui.actionCheatsToolbar->setDisabled(starting_or_not_running || cheevos_challenge_mode); + m_ui.actionScreenshot->setDisabled(starting_or_not_running); + m_ui.menuChangeDisc->setDisabled(starting_or_not_running); + m_ui.menuCheats->setDisabled(starting_or_not_running || cheevos_challenge_mode); m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode); m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode); - m_ui.actionReloadTextureReplacements->setDisabled(starting || !running); - m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode); - m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode); - m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode); + m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running); + m_ui.actionDumpRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); + m_ui.actionDumpVRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); + m_ui.actionDumpSPURAM->setDisabled(starting_or_not_running || cheevos_challenge_mode); - m_ui.actionSaveState->setDisabled(starting || !running); - m_ui.menuSaveState->setDisabled(starting || !running); - m_ui.menuWindowSize->setDisabled(starting || !running); + m_ui.actionSaveState->setDisabled(starting_or_not_running); + m_ui.menuSaveState->setDisabled(starting_or_not_running); + m_ui.menuWindowSize->setDisabled(starting_or_not_running); - m_ui.actionViewGameProperties->setDisabled(starting || !running); + m_ui.actionViewGameProperties->setDisabled(starting_or_not_running); - if (starting || running) + if (starting_or_running) { if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff)) { @@ -2037,7 +1949,6 @@ void MainWindow::connectSignals() connect(m_ui.menuLoadState, &QMenu::aboutToShow, this, &MainWindow::onLoadStateMenuAboutToShow); connect(m_ui.menuSaveState, &QMenu::aboutToShow, this, &MainWindow::onSaveStateMenuAboutToShow); connect(m_ui.menuCheats, &QMenu::aboutToShow, this, &MainWindow::onCheatsMenuAboutToShow); - connect(m_ui.actionCheats, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered); connect(m_ui.actionCheatsToolbar, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered); connect(m_ui.actionStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); @@ -2081,7 +1992,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, @@ -2095,8 +2006,10 @@ void MainWindow::connectSignals() connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled); connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); 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); @@ -2335,6 +2248,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) @@ -2628,7 +2560,6 @@ void MainWindow::checkForSettingChanges() { LogWindow::updateSettings(); updateWindowState(); - updateCheatActionsVisibility(); } std::optional MainWindow::getWindowInfo() @@ -2709,7 +2640,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); } @@ -2781,23 +2711,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 d3afec21d..099b22cc8 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); @@ -171,7 +167,6 @@ private Q_SLOTS: void onViewGameListActionTriggered(); void onViewGameGridActionTriggered(); void onViewSystemDisplayTriggered(); - void onViewGamePropertiesActionTriggered(); void onGitHubRepositoryActionTriggered(); void onIssueTrackerActionTriggered(); void onDiscordServerActionTriggered(); @@ -193,7 +188,6 @@ private Q_SLOTS: void onUpdateCheckComplete(); - void openCheatManager(); void openCPUDebugger(); protected: @@ -220,7 +214,6 @@ private: void updateStatusBarWidgetVisibility(); void updateWindowTitle(); void updateWindowState(bool force_visible = false); - void updateCheatActionsVisibility(); void setProgressBar(int current, int total); void clearProgressBar(); @@ -245,6 +238,7 @@ private: void updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen); void doSettings(const char* category = nullptr); + void openGamePropertiesForCurrentGame(const char* category = nullptr); void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count); void updateDebugMenuCPUExecutionMode(); @@ -301,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/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index cad3559c0..b917ac204 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -87,7 +87,6 @@ - @@ -481,14 +480,6 @@ Change Disc... - - - - - - Cheats... - - diff --git a/src/duckstation-qt/memoryscannerwindow.cpp b/src/duckstation-qt/memoryscannerwindow.cpp index 38e788ec2..1d789cb47 100644 --- a/src/duckstation-qt/memoryscannerwindow.cpp +++ b/src/duckstation-qt/memoryscannerwindow.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "memoryscannerwindow.h" -#include "cheatcodeeditordialog.h" #include "qthost.h" #include "qtutils.h" 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 5660de4e4..241ecb518 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1292,28 +1292,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::reloadCheats(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 5ca702c50..6a6c7ceea 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 reloadCheats(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 966abb7f6..4ce905eda 100644 --- a/src/duckstation-qt/resources/duckstation-qt.qrc +++ b/src/duckstation-qt/resources/duckstation-qt.qrc @@ -31,6 +31,7 @@ icons/black/svg/arrow-left-right-line.svg icons/black/svg/arrow-up-line.svg icons/black/svg/artboard-2-line.svg + icons/black/svg/chat-off-line.svg icons/black/svg/cheats-line.svg icons/black/svg/checkbox-multiple-blank-line.svg icons/black/svg/chip-2-line.svg @@ -55,6 +56,7 @@ icons/black/svg/download-2-line.svg icons/black/svg/eject-line.svg icons/black/svg/emulation-line.svg + icons/black/svg/export-line.svg icons/black/svg/file-add-line.svg icons/black/svg/file-line.svg icons/black/svg/file-list-line.svg @@ -74,6 +76,7 @@ icons/black/svg/global-line.svg icons/black/svg/guncon-line.svg icons/black/svg/image-fill.svg + icons/black/svg/import-line.svg icons/black/svg/information-line.svg icons/black/svg/joystick-line.svg icons/black/svg/keyboard-line.svg @@ -98,6 +101,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 @@ -249,6 +253,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 @@ -271,6 +276,7 @@ icons/white/svg/download-2-line.svg icons/white/svg/eject-line.svg icons/white/svg/emulation-line.svg + icons/white/svg/export-line.svg icons/white/svg/file-add-line.svg icons/white/svg/file-line.svg icons/white/svg/file-list-line.svg @@ -290,6 +296,7 @@ icons/white/svg/global-line.svg icons/white/svg/guncon-line.svg icons/white/svg/image-fill.svg + icons/white/svg/import-line.svg icons/white/svg/information-line.svg icons/white/svg/joystick-line.svg icons/white/svg/keyboard-line.svg @@ -314,6 +321,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/export-line.svg b/src/duckstation-qt/resources/icons/black/svg/export-line.svg new file mode 100644 index 000000000..a5abf68df --- /dev/null +++ b/src/duckstation-qt/resources/icons/black/svg/export-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/black/svg/import-line.svg b/src/duckstation-qt/resources/icons/black/svg/import-line.svg new file mode 100644 index 000000000..8b1d1b151 --- /dev/null +++ b/src/duckstation-qt/resources/icons/black/svg/import-line.svg @@ -0,0 +1 @@ + \ 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/export-line.svg b/src/duckstation-qt/resources/icons/white/svg/export-line.svg new file mode 100644 index 000000000..54d76f4dd --- /dev/null +++ b/src/duckstation-qt/resources/icons/white/svg/export-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/white/svg/import-line.svg b/src/duckstation-qt/resources/icons/white/svg/import-line.svg new file mode 100644 index 000000000..536709972 --- /dev/null +++ b/src/duckstation-qt/resources/icons/white/svg/import-line.svg @@ -0,0 +1 @@ + \ 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..ce847773d 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,23 @@ 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. 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.")); + 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. " + "Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, " + "and corrupted saves. Cheats also persist through save states even after being disabled, " + "please remember to reset/reboot the game after turning off any codes.")); + } + addWidget( m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"), QStringLiteral("memcard-line"), @@ -635,8 +654,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 +689,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 +699,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 89aad7eba..934c67533 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,0xf0ac,0xf0ad,0xf0c3,0xf0c3,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,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x278a,0x278e,0x27fc,0x27fc,0xe001,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 };