From b694577c3848bf3a2da0c8c778c49a903f8e321a Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 20 Oct 2020 01:14:49 +1000 Subject: [PATCH] Qt: Add new cheat manager --- README.md | 1 + src/core/cheats.cpp | 565 +++++++++++++- src/core/cheats.h | 149 +++- src/duckstation-qt/CMakeLists.txt | 6 + src/duckstation-qt/cheatcodeeditordialog.cpp | 88 +++ src/duckstation-qt/cheatcodeeditordialog.h | 25 + src/duckstation-qt/cheatcodeeditordialog.ui | 101 +++ src/duckstation-qt/cheatmanagerdialog.cpp | 689 ++++++++++++++++++ src/duckstation-qt/cheatmanagerdialog.h | 71 ++ src/duckstation-qt/cheatmanagerdialog.ui | 550 ++++++++++++++ src/duckstation-qt/duckstation-qt.vcxproj | 12 + src/duckstation-qt/main.cpp | 1 + src/duckstation-qt/mainwindow.cpp | 20 + src/duckstation-qt/mainwindow.h | 3 + src/duckstation-qt/mainwindow.ui | 6 + src/duckstation-qt/qthostinterface.cpp | 26 +- src/duckstation-qt/qthostinterface.h | 2 + src/duckstation-qt/qtutils.cpp | 24 +- src/duckstation-qt/qtutils.h | 4 + src/frontend-common/common_host_interface.cpp | 29 +- src/frontend-common/common_host_interface.h | 3 + 21 files changed, 2356 insertions(+), 19 deletions(-) create mode 100644 src/duckstation-qt/cheatcodeeditordialog.cpp create mode 100644 src/duckstation-qt/cheatcodeeditordialog.h create mode 100644 src/duckstation-qt/cheatcodeeditordialog.ui create mode 100644 src/duckstation-qt/cheatmanagerdialog.cpp create mode 100644 src/duckstation-qt/cheatmanagerdialog.h create mode 100644 src/duckstation-qt/cheatmanagerdialog.ui diff --git a/README.md b/README.md index dd5166341..f128b429e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c ## Latest News +- 2020/10/20: New cheat manager with memory scanning added. More features will be added over time. - 2020/10/05: CD-ROM read speedup enhancement added. - 2020/09/30: CPU overclocking is now supported. Use with caution as it will break games and increase system requirements. It can be set globally or per-game. - 2020/09/25: Cheat support added for libretro core. diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index eeac73120..1b5ad7c69 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -1,10 +1,14 @@ #include "cheats.h" +#include "common/assert.h" #include "common/file_system.h" #include "common/log.h" #include "common/string.h" #include "common/string_util.h" #include "cpu_core.h" +#include "host_interface.h" #include +#include +#include Log_SetChannel(Cheats); using KeyValuePairVector = std::vector>; @@ -37,6 +41,7 @@ bool CheatList::LoadFromPCSXRFile(const char* filename) char line[1024]; CheatCode current_code; + current_code.group = "Ungrouped"; while (std::fgets(line, sizeof(line), fp.get())) { char* start = line; @@ -67,8 +72,9 @@ bool CheatList::LoadFromPCSXRFile(const char* filename) if (current_code.Valid()) m_codes.push_back(std::move(current_code)); - current_code = {}; - current_code.enabled = false; + current_code = CheatCode(); + current_code.group = "Ungrouped"; + if (*start == '*') { current_code.enabled = true; @@ -191,6 +197,7 @@ bool CheatList::LoadFromLibretroFile(const char* filename) } CheatCode cc; + cc.group = "Ungrouped"; cc.description = *desc; cc.enabled = StringUtil::FromChars(*enable).value_or(false); if (ParseLibretroCheat(&cc, code->c_str())) @@ -357,6 +364,20 @@ u32 CheatList::GetEnabledCodeCount() const 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()) @@ -383,6 +404,73 @@ void CheatList::ApplyCode(u32 index) m_codes[index].Apply(); } +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(*start)) + start++; + + // skip empty lines + if (*start == '\0') + continue; + + char* end = start + std::strlen(start) - 1; + while (end > start && std::isspace(*end)) + { + *end = '\0'; + end--; + } + + // skip comments and empty line + if (*start == '#' || *start == ';' || *start == '/' || *start == '\"') + continue; + + while (!IsHexCharacter(*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 (!IsHexCharacter(*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; +} + void CheatCode::Apply() const { const u32 count = static_cast(instructions.size()); @@ -622,3 +710,476 @@ void CheatCode::Apply() const } } } + +static std::array s_cheat_code_type_names = {{"Gameshark"}}; +static std::array s_cheat_code_type_display_names{{TRANSLATABLE("Cheats", "Gameshark")}}; + +const char* CheatCode::GetTypeName(Type type) +{ + 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 (u32 i = 0; i < static_cast(s_cheat_code_type_names.size()); i++) + { + if (std::strcmp(s_cheat_code_type_names[i], str) == 0) + return static_cast(i); + } + + return std::nullopt; +} + +static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; +static std::array s_cheat_code_activation_display_names{ + {TRANSLATABLE("Cheats", "Manual"), TRANSLATABLE("Cheats", "Automatic (Frame End)")}}; + +const char* CheatCode::GetActivationName(Activation activation) +{ + return s_cheat_code_activation_names[static_cast(activation)]; +} + +const char* CheatCode::GetActivationDisplayName(Activation activation) +{ + return s_cheat_code_activation_display_names[static_cast(activation)]; +} + +std::optional CheatCode::ParseActivationName(const char* str) +{ + for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) + { + if (std::strcmp(s_cheat_code_activation_names[i], str) == 0) + return static_cast(i); + } + + return std::nullopt; +} + +MemoryScan::MemoryScan() = default; + +MemoryScan::~MemoryScan() = default; + +void MemoryScan::ResetSearch() +{ + m_results.clear(); +} + +void MemoryScan::Search() +{ + m_results.clear(); + + switch (m_size) + { + case MemoryAccessSize::Byte: + SearchBytes(); + break; + + case MemoryAccessSize::HalfWord: + SearchHalfwords(); + break; + + case MemoryAccessSize::Word: + SearchWords(); + break; + + default: + break; + } +} + +void MemoryScan::SearchBytes() +{ + for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address++) + { + u8 bvalue = 0; + CPU::SafeReadMemoryByte(address, &bvalue); + + 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) + { + u16 bvalue = 0; + CPU::SafeReadMemoryHalfWord(address, &bvalue); + + 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) + { + Result res; + res.address = address; + CPU::SafeReadMemoryWord(address, &res.value); + 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); + } + + 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; + CPU::SafeReadMemoryByte(address, &bvalue); + value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::HalfWord: + { + u16 bvalue = 0; + CPU::SafeReadMemoryHalfWord(address, &bvalue); + 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; + CPU::SafeReadMemoryByte(entry->address, &bvalue); + entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); + } + break; + + case MemoryAccessSize::HalfWord: + { + u16 bvalue = 0; + CPU::SafeReadMemoryHalfWord(entry->address, &bvalue); + 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/cheats.h b/src/core/cheats.h index 8c2f86d58..f4d79e5ac 100644 --- a/src/core/cheats.h +++ b/src/core/cheats.h @@ -7,6 +7,19 @@ struct CheatCode { + enum class Type : u8 + { + Gameshark, + Count + }; + + enum class Activation : u8 + { + Manual, + EndFrame, + Count, + }; + enum class InstructionCode : u8 { Nop = 0x00, @@ -45,13 +58,28 @@ struct CheatCode BitField value8; }; + std::string group; std::string description; std::vector instructions; - bool enabled; + Type type = Type::Gameshark; + Activation activation = Activation::EndFrame; + bool enabled = false; ALWAYS_INLINE bool Valid() const { return !instructions.empty() && !description.empty(); } + ALWAYS_INLINE bool IsManuallyActivated() const { return (activation == Activation::Manual); } + + std::string GetInstructionsAsString() const; + bool SetInstructionsFromString(const std::string& str); void Apply() 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); }; class CheatList final @@ -78,6 +106,7 @@ public: void RemoveCode(u32 i); u32 GetEnabledCodeCount() const; + std::vector GetCodeGroups() const; void EnableCode(u32 index); void DisableCode(u32 index); void SetCodeEnabled(u32 index, bool state); @@ -98,3 +127,121 @@ public: private: std::vector m_codes; }; + +class MemoryScan +{ +public: + enum class Operator + { + Equal, + NotEqual, + GreaterThan, + GreaterEqual, + LessThan, + LessEqual, + IncreasedBy, + DecreasedBy, + ChangedBy, + EqualLast, + NotEqualLast, + GreaterThanLast, + GreaterEqualLast, + LessThanLast, + LessEqualLast + }; + + 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::Word; + Operator m_operator = Operator::Equal; + PhysicalMemoryAddress m_start_address = 0; + PhysicalMemoryAddress m_end_address = 0x200000; + ResultVector m_results; + bool m_signed = true; +}; + +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/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 92871d4cb..b83be92fe 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -18,6 +18,12 @@ set(SRCS biossettingswidget.cpp biossettingswidget.h biossettingswidget.ui + cheatmanagerdialog.cpp + cheatmanagerdialog.h + cheatmanagerdialog.ui + cheatcodeeditordialog.cpp + cheatcodeeditordialog.h + cheatcodeeditordialog.ui consolesettingswidget.cpp consolesettingswidget.h consolesettingswidget.ui diff --git a/src/duckstation-qt/cheatcodeeditordialog.cpp b/src/duckstation-qt/cheatcodeeditordialog.cpp new file mode 100644 index 000000000..fc6adf6ec --- /dev/null +++ b/src/duckstation-qt/cheatcodeeditordialog.cpp @@ -0,0 +1,88 @@ +#include "cheatcodeeditordialog.h" +#include + +CheatCodeEditorDialog::CheatCodeEditorDialog(CheatList* list, CheatCode* code, QWidget* parent) + : m_code(code), QDialog(parent) +{ + m_ui.setupUi(this); + setupAdditionalUi(list); + 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(CheatList* list) +{ + 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)))); + } + + const auto groups = list->GetCodeGroups(); + if (!groups.empty()) + { + for (const std::string& group_name : groups) + m_ui.group->addItem(QString::fromStdString(group_name)); + } + else + { + m_ui.group->addItem(QStringLiteral("Ungrouped")); + } +} + +void CheatCodeEditorDialog::fillUi() +{ + m_ui.description->setText(QString::fromStdString(m_code->description)); + + int index = m_ui.group->findText(QString::fromStdString(m_code->group)); + if (index >= 0) + m_ui.group->setCurrentIndex(index); + else + m_ui.group->setCurrentIndex(0); + + 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.save, &QPushButton::clicked, this, &CheatCodeEditorDialog::saveClicked); + connect(m_ui.cancel, &QPushButton::clicked, this, &CheatCodeEditorDialog::cancelClicked); +} diff --git a/src/duckstation-qt/cheatcodeeditordialog.h b/src/duckstation-qt/cheatcodeeditordialog.h new file mode 100644 index 000000000..0487d043c --- /dev/null +++ b/src/duckstation-qt/cheatcodeeditordialog.h @@ -0,0 +1,25 @@ +#pragma once +#include "core/cheats.h" +#include "ui_cheatcodeeditordialog.h" + +class CheatCodeEditorDialog : public QDialog +{ + Q_OBJECT + +public: + CheatCodeEditorDialog(CheatList* list, CheatCode* code, QWidget* parent); + ~CheatCodeEditorDialog(); + +private Q_SLOTS: + void saveClicked(); + void cancelClicked(); + +private: + void setupAdditionalUi(CheatList* list); + 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 new file mode 100644 index 000000000..a9fdb75c9 --- /dev/null +++ b/src/duckstation-qt/cheatcodeeditordialog.ui @@ -0,0 +1,101 @@ + + + CheatCodeEditorDialog + + + + 0 + 0 + 491 + 284 + + + + Cheat Code Editor + + + true + + + + + + Description: + + + + + + + + + + Group: + + + + + + + + + + Type: + + + + + + + + + + Activation: + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/src/duckstation-qt/cheatmanagerdialog.cpp b/src/duckstation-qt/cheatmanagerdialog.cpp new file mode 100644 index 000000000..85c424eb1 --- /dev/null +++ b/src/duckstation-qt/cheatmanagerdialog.cpp @@ -0,0 +1,689 @@ +#include "cheatmanagerdialog.h" +#include "cheatcodeeditordialog.h" +#include "common/assert.h" +#include "common/string_util.h" +#include "core/system.h" +#include "qthostinterface.h" +#include "qtutils.h" +#include +#include +#include +#include +#include +#include + +static QString formatHexValue(u32 value) +{ + return QStringLiteral("0x%1").arg(static_cast(value), 8, 16, QChar('0')); +} + +static QString formatValue(u32 value, bool is_signed) +{ + if (is_signed) + return QStringLiteral("%1").arg(static_cast(value)); + else + return QStringLiteral("%1").arg(static_cast(value)); +} + +CheatManagerDialog::CheatManagerDialog(QWidget* parent) : QDialog(parent) +{ + m_ui.setupUi(this); + + setupAdditionalUi(); + connectUi(); + + updateCheatList(); +} + +CheatManagerDialog::~CheatManagerDialog() = default; + +void CheatManagerDialog::setupAdditionalUi() +{ + m_ui.scanStartAddress->setText(formatHexValue(m_scanner.GetStartAddress())); + m_ui.scanEndAddress->setText(formatHexValue(m_scanner.GetEndAddress())); +} + +void CheatManagerDialog::connectUi() +{ + connect(m_ui.tabWidget, &QTabWidget::currentChanged, [this](int index) { + resizeColumns(); + setUpdateTimerEnabled(index == 1); + }); + connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerDialog::cheatListCurrentItemChanged); + connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerDialog::cheatListItemActivated); + connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerDialog::cheatListItemChanged); + connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerDialog::newCategoryClicked); + connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerDialog::addCodeClicked); + connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerDialog::editCodeClicked); + connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerDialog::deleteCodeClicked); + connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerDialog::activateCodeClicked); + connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerDialog::importClicked); + connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerDialog::exportClicked); + + connect(m_ui.scanValue, &QLineEdit::textChanged, this, &CheatManagerDialog::updateScanValue); + connect(m_ui.scanValueBase, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { updateScanValue(); }); + connect(m_ui.scanSize, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + m_scanner.SetSize(static_cast(index)); + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanValueSigned, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + m_scanner.SetValueSigned(index == 0); + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanOperator, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { m_scanner.SetOperator(static_cast(index)); }); + connect(m_ui.scanNewSearch, &QPushButton::clicked, [this]() { + m_scanner.Search(); + updateResults(); + }); + connect(m_ui.scanSearchAgain, &QPushButton::clicked, [this]() { + m_scanner.SearchAgain(); + updateResults(); + }); + connect(m_ui.scanResetSearch, &QPushButton::clicked, [this]() { + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanAddWatch, &QPushButton::clicked, this, &CheatManagerDialog::addToWatchClicked); + connect(m_ui.scanRemoveWatch, &QPushButton::clicked, this, &CheatManagerDialog::removeWatchClicked); + connect(m_ui.scanTable, &QTableWidget::currentItemChanged, this, &CheatManagerDialog::scanCurrentItemChanged); + connect(m_ui.watchTable, &QTableWidget::currentItemChanged, this, &CheatManagerDialog::watchCurrentItemChanged); + connect(m_ui.scanTable, &QTableWidget::itemChanged, this, &CheatManagerDialog::scanItemChanged); + connect(m_ui.watchTable, &QTableWidget::itemChanged, this, &CheatManagerDialog::watchItemChanged); +} + +void CheatManagerDialog::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + resizeColumns(); +} + +void CheatManagerDialog::resizeEvent(QResizeEvent* event) +{ + QDialog::resizeEvent(event); + resizeColumns(); +} + +void CheatManagerDialog::resizeColumns() +{ + QtUtils::ResizeColumnsForTableView(m_ui.scanTable, {-1, 100, 100}); + QtUtils::ResizeColumnsForTableView(m_ui.watchTable, {50, -1, 100, 150, 100}); + QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100}); +} + +void CheatManagerDialog::setUpdateTimerEnabled(bool enabled) +{ + if ((!m_update_timer && !enabled) && m_update_timer->isActive() == enabled) + return; + + if (!m_update_timer) + { + m_update_timer = new QTimer(this); + connect(m_update_timer, &QTimer::timeout, this, &CheatManagerDialog::updateScanUi); + } + + if (enabled) + m_update_timer->start(100); + else + m_update_timer->stop(); +} + +int CheatManagerDialog::getSelectedResultIndex() const +{ + QList sel = m_ui.scanTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().topRow(); +} + +int CheatManagerDialog::getSelectedWatchIndex() const +{ + QList sel = m_ui.watchTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().topRow(); +} + +QTreeWidgetItem* CheatManagerDialog::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; +} + +static u32 getCheatIndexFromItem(QTreeWidgetItem* item) +{ + return item->data(0, Qt::UserRole).toUInt(); +} + +int CheatManagerDialog::getSelectedCheatIndex() const +{ + QList sel = m_ui.cheatList->selectedItems(); + if (sel.isEmpty()) + return -1; + + return static_cast(getCheatIndexFromItem(sel.first())); +} + +CheatList* CheatManagerDialog::getCheatList() const +{ + Assert(System::IsValid()); + + CheatList* list = System::GetCheatList(); + if (!list) + QtHostInterface::GetInstance()->LoadCheatListFromGameTitle(); + if (!list) + { + QtHostInterface::GetInstance()->executeOnEmulationThread( + []() { System::SetCheatList(std::make_unique()); }, true); + list = System::GetCheatList(); + } + + return list; +} + +void CheatManagerDialog::updateCheatList() +{ + QSignalBlocker sb(m_ui.cheatList); + + CheatList* list = getCheatList(); + while (m_ui.cheatList->topLevelItemCount() > 0) + delete m_ui.cheatList->takeTopLevelItem(0); + + const std::vector groups = list->GetCodeGroups(); + for (const std::string& group_name : groups) + { + QTreeWidgetItem* group = new QTreeWidgetItem(); + group->setFlags(group->flags() | Qt::ItemIsUserCheckable); + group->setText(0, QString::fromStdString(group_name)); + m_ui.cheatList->addTopLevelItem(group); + + 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); + item->setData(0, Qt::UserRole, QVariant(static_cast(i))); + 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()))); + + all_enabled &= code.enabled; + } + + group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked); + group->setExpanded(true); + } + + m_ui.cheatListEdit->setEnabled(false); + m_ui.cheatListRemove->setEnabled(false); + m_ui.cheatListActivate->setText(tr("Activate")); + m_ui.cheatListActivate->setEnabled(false); + m_ui.cheatListExport->setEnabled(list->GetCodeCount() > 0); +} + +void CheatManagerDialog::saveCheatList() +{ + QtHostInterface::GetInstance()->executeOnEmulationThread([]() { QtHostInterface::GetInstance()->SaveCheatList(); }); +} + +void CheatManagerDialog::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + const bool has_current = (current != nullptr); + m_ui.cheatListEdit->setEnabled(has_current); + m_ui.cheatListRemove->setEnabled(has_current); + m_ui.cheatListActivate->setEnabled(has_current); + + if (!current) + { + m_ui.cheatListActivate->setText(tr("Activate")); + } + else + { + const bool manual_activation = getCheatList()->GetCode(getCheatIndexFromItem(current)).IsManuallyActivated(); + m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle")); + } +} + +void CheatManagerDialog::cheatListItemActivated(QTreeWidgetItem* item) +{ + if (!item) + return; + + const u32 index = getCheatIndexFromItem(item); + activateCheat(index); +} + +void CheatManagerDialog::cheatListItemChanged(QTreeWidgetItem* item, int column) +{ + if (!item || column != 0) + return; + + const u32 index = getCheatIndexFromItem(item); + CheatList* list = getCheatList(); + if (index >= list->GetCodeCount()) + return; + + CheatCode& cc = list->GetCode(index); + if (cc.IsManuallyActivated()) + return; + + const bool new_enabled = (item->checkState(0) == Qt::Checked); + if (cc.enabled == new_enabled) + return; + + QtHostInterface::GetInstance()->executeOnEmulationThread([index, new_enabled]() { + System::GetCheatList()->SetCodeEnabled(index, new_enabled); + QtHostInterface::GetInstance()->SaveCheatList(); + }); +} + +void CheatManagerDialog::activateCheat(u32 index) +{ + CheatList* list = getCheatList(); + if (index >= list->GetCodeCount()) + return; + + CheatCode& cc = list->GetCode(index); + if (cc.IsManuallyActivated()) + { + QtHostInterface::GetInstance()->applyCheat(index); + return; + } + + const bool new_enabled = !cc.enabled; + QTreeWidgetItem* item = getItemForCheatIndex(index); + if (item) + { + QSignalBlocker sb(m_ui.cheatList); + item->setCheckState(0, new_enabled ? Qt::Checked : Qt::Unchecked); + } + + QtHostInterface::GetInstance()->executeOnEmulationThread([index, new_enabled]() { + System::GetCheatList()->SetCodeEnabled(index, new_enabled); + QtHostInterface::GetInstance()->SaveCheatList(); + }); +} + +void CheatManagerDialog::newCategoryClicked() +{ + // +} + +void CheatManagerDialog::addCodeClicked() +{ + CheatList* list = getCheatList(); + + CheatCode new_code; + CheatCodeEditorDialog editor(list, &new_code, this); + if (editor.exec() > 0) + { + QtHostInterface::GetInstance()->executeOnEmulationThread( + [this, &new_code]() { + System::GetCheatList()->AddCode(std::move(new_code)); + QtHostInterface::GetInstance()->SaveCheatList(); + }, + true); + updateCheatList(); + } +} + +void CheatManagerDialog::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(list, &new_code, this); + if (editor.exec() > 0) + { + QtHostInterface::GetInstance()->executeOnEmulationThread( + [index, &new_code]() { + System::GetCheatList()->SetCode(static_cast(index), std::move(new_code)); + QtHostInterface::GetInstance()->SaveCheatList(); + }, + true); + updateCheatList(); + } +} + +void CheatManagerDialog::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; + } + + QtHostInterface::GetInstance()->executeOnEmulationThread( + [index]() { + System::GetCheatList()->RemoveCode(static_cast(index)); + QtHostInterface::GetInstance()->SaveCheatList(); + }, + true); + updateCheatList(); +} + +void CheatManagerDialog::activateCodeClicked() +{ + int index = getSelectedCheatIndex(); + if (index < 0) + return; + + activateCheat(static_cast(index)); +} + +void CheatManagerDialog::importClicked() +{ + const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)")); + const QString filename(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; + } + + QtHostInterface::GetInstance()->executeOnEmulationThread( + [&new_cheats]() { + DebugAssert(System::HasCheatList()); + CheatList* list = System::GetCheatList(); + for (u32 i = 0; i < new_cheats.GetCodeCount(); i++) + list->AddCode(new_cheats.GetCode(i)); + + QtHostInterface::GetInstance()->SaveCheatList(); + }, + true); + updateCheatList(); +} + +void CheatManagerDialog::exportClicked() +{ + const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)")); + const QString filename(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 CheatManagerDialog::addToWatchClicked() +{ + const int index = getSelectedResultIndex(); + if (index < 0) + return; + + const MemoryScan::Result& res = m_scanner.GetResults()[static_cast(index)]; + m_watch.AddEntry(StringUtil::StdStringFromFormat("0x%08x", res.address), res.address, m_scanner.GetSize(), + m_scanner.GetValueSigned(), false); + updateWatch(); +} + +void CheatManagerDialog::removeWatchClicked() +{ + const int index = getSelectedWatchIndex(); + if (index < 0) + return; + + m_watch.RemoveEntry(static_cast(index)); + updateWatch(); +} + +void CheatManagerDialog::scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) +{ + m_ui.scanAddWatch->setEnabled((current != nullptr)); +} + +void CheatManagerDialog::watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) +{ + m_ui.scanRemoveWatch->setEnabled((current != nullptr)); +} + +void CheatManagerDialog::scanItemChanged(QTableWidgetItem* item) +{ + const u32 index = static_cast(item->row()); + switch (item->column()) + { + case 1: + { + bool value_ok = false; + if (m_scanner.GetValueSigned()) + { + int value = item->text().toInt(&value_ok); + if (value_ok) + m_scanner.SetResultValue(index, static_cast(value)); + } + else + { + uint value = item->text().toUInt(&value_ok); + if (value_ok) + m_scanner.SetResultValue(index, static_cast(value)); + } + } + break; + + default: + break; + } +} + +void CheatManagerDialog::watchItemChanged(QTableWidgetItem* item) +{ + const u32 index = static_cast(item->row()); + if (index >= m_watch.GetEntryCount()) + return; + + switch (item->column()) + { + case 0: + { + m_watch.SetEntryFreeze(index, (item->checkState() == Qt::Checked)); + } + break; + + case 1: + { + m_watch.SetEntryDescription(index, item->text().toStdString()); + } + break; + + case 4: + { + const MemoryWatchList::Entry& entry = m_watch.GetEntry(index); + bool value_ok = false; + if (entry.is_signed) + { + int value = item->text().toInt(&value_ok); + if (value_ok) + m_watch.SetEntryValue(index, static_cast(value)); + } + else + { + uint value = item->text().toUInt(&value_ok); + if (value_ok) + m_watch.SetEntryValue(index, static_cast(value)); + } + } + break; + + default: + break; + } +} + +void CheatManagerDialog::updateScanValue() +{ + QString value = m_ui.scanValue->text(); + if (value.startsWith(QStringLiteral("0x"))) + value.remove(0, 2); + + bool ok = false; + uint uint_value = value.toUInt(&ok, (m_ui.scanValueBase->currentIndex() > 0) ? 16 : 10); + if (ok) + m_scanner.SetValue(uint_value); +} + +void CheatManagerDialog::updateResults() +{ + QSignalBlocker sb(m_ui.scanTable); + m_ui.scanTable->setRowCount(0); + + const MemoryScan::ResultVector& results = m_scanner.GetResults(); + if (!results.empty()) + { + int row = 0; + for (const MemoryScan::Result& res : m_scanner.GetResults()) + { + m_ui.scanTable->insertRow(row); + + QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address)); + address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.scanTable->setItem(row, 0, address_item); + + QTableWidgetItem* value_item = new QTableWidgetItem(formatValue(res.value, m_scanner.GetValueSigned())); + m_ui.scanTable->setItem(row, 1, value_item); + + QTableWidgetItem* previous_item = new QTableWidgetItem(formatValue(res.last_value, m_scanner.GetValueSigned())); + previous_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.scanTable->setItem(row, 2, previous_item); + row++; + } + } + + m_ui.scanResetSearch->setEnabled(!results.empty()); + m_ui.scanSearchAgain->setEnabled(!results.empty()); + m_ui.scanAddWatch->setEnabled(false); +} + +void CheatManagerDialog::updateResultsValues() +{ + QSignalBlocker sb(m_ui.scanTable); + + int row = 0; + for (const MemoryScan::Result& res : m_scanner.GetResults()) + { + if (res.value_changed) + { + QTableWidgetItem* item = m_ui.scanTable->item(row, 1); + item->setText(formatValue(res.value, m_scanner.GetValueSigned())); + item->setForeground(Qt::red); + } + + row++; + } +} + +void CheatManagerDialog::updateWatch() +{ + static constexpr std::array size_strings = { + {QT_TR_NOOP("Byte"), QT_TR_NOOP("Halfword"), QT_TR_NOOP("Word"), QT_TR_NOOP("Signed Byte"), + QT_TR_NOOP("Signed Halfword"), QT_TR_NOOP("Signed Word")}}; + + m_watch.UpdateValues(); + + QSignalBlocker sb(m_ui.watchTable); + m_ui.watchTable->setRowCount(0); + + const MemoryWatchList::EntryVector& entries = m_watch.GetEntries(); + if (!entries.empty()) + { + int row = 0; + for (const MemoryWatchList::Entry& res : entries) + { + m_ui.watchTable->insertRow(row); + + QTableWidgetItem* freeze_item = new QTableWidgetItem(); + freeze_item->setFlags(freeze_item->flags() | (Qt::ItemIsEditable | Qt::ItemIsUserCheckable)); + freeze_item->setCheckState(res.freeze ? Qt::Checked : Qt::Unchecked); + m_ui.watchTable->setItem(row, 0, freeze_item); + + QTableWidgetItem* description_item = new QTableWidgetItem(QString::fromStdString(res.description)); + m_ui.watchTable->setItem(row, 1, description_item); + + QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address)); + address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.watchTable->setItem(row, 2, address_item); + + QTableWidgetItem* size_item = + new QTableWidgetItem(tr(size_strings[static_cast(res.size) + (res.is_signed ? 3 : 0)])); + size_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.watchTable->setItem(row, 3, size_item); + + QTableWidgetItem* value_item = new QTableWidgetItem(formatValue(res.value, res.is_signed)); + m_ui.watchTable->setItem(row, 4, value_item); + + row++; + } + } + + m_ui.scanSaveWatch->setEnabled(!entries.empty()); + m_ui.scanRemoveWatch->setEnabled(false); +} + +void CheatManagerDialog::updateWatchValues() +{ + QSignalBlocker sb(m_ui.watchTable); + int row = 0; + for (const MemoryWatchList::Entry& res : m_watch.GetEntries()) + { + if (res.changed) + m_ui.watchTable->item(row, 4)->setText(formatValue(res.value, res.is_signed)); + + row++; + } +} + +void CheatManagerDialog::updateScanUi() +{ + m_scanner.UpdateResultsValues(); + m_watch.UpdateValues(); + + updateResultsValues(); + updateWatchValues(); +} diff --git a/src/duckstation-qt/cheatmanagerdialog.h b/src/duckstation-qt/cheatmanagerdialog.h new file mode 100644 index 000000000..aaddce95f --- /dev/null +++ b/src/duckstation-qt/cheatmanagerdialog.h @@ -0,0 +1,71 @@ +#pragma once +#include "core/cheats.h" +#include "ui_cheatmanagerdialog.h" +#include +#include +#include +#include +#include +#include +#include + +class CheatManagerDialog : public QDialog +{ + Q_OBJECT + +public: + CheatManagerDialog(QWidget* parent); + ~CheatManagerDialog(); + +protected: + void showEvent(QShowEvent* event); + void resizeEvent(QResizeEvent* event); + +private Q_SLOTS: + void resizeColumns(); + + 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 newCategoryClicked(); + void addCodeClicked(); + void editCodeClicked(); + void deleteCodeClicked(); + void activateCodeClicked(); + void importClicked(); + void exportClicked(); + + void addToWatchClicked(); + void removeWatchClicked(); + void scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); + void watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); + void scanItemChanged(QTableWidgetItem* item); + void watchItemChanged(QTableWidgetItem* item); + void updateScanValue(); + void updateScanUi(); + +private: + void setupAdditionalUi(); + void connectUi(); + void setUpdateTimerEnabled(bool enabled); + void updateResults(); + void updateResultsValues(); + void updateWatch(); + void updateWatchValues(); + + QTreeWidgetItem* getItemForCheatIndex(u32 index) const; + int getSelectedCheatIndex() const; + int getSelectedResultIndex() const; + int getSelectedWatchIndex() const; + + Ui::CheatManagerDialog m_ui; + + MemoryScan m_scanner; + MemoryWatchList m_watch; + + QTimer* m_update_timer = nullptr; +}; diff --git a/src/duckstation-qt/cheatmanagerdialog.ui b/src/duckstation-qt/cheatmanagerdialog.ui new file mode 100644 index 000000000..0017a7b49 --- /dev/null +++ b/src/duckstation-qt/cheatmanagerdialog.ui @@ -0,0 +1,550 @@ + + + CheatManagerDialog + + + + 0 + 0 + 864 + 599 + + + + Cheat Manager + + + + + + 0 + + + + Cheat List + + + + + + + + false + + + &New Category... + + + + + + + &Add Code... + + + + + + + &Edit Code... + + + + + + + false + + + &Delete Code + + + + + + + false + + + Activate + + + + + + + Import... + + + + + + + false + + + Export... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Name + + + + + Type + + + + + Activation + + + + + Instructions + + + + + + + + + Memory Scanner + + + + + + Qt::Vertical + + + + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + Address + + + + + Value + + + + + Previous Value + + + + + + + + + + Search Parameters + + + + + + Value: + + + + + + + + + + + + + Signed + + + + + Unsigned + + + + + + + + + Decimal + + + + + Hex + + + + + + + + + + Data Size: + + + + + + + 2 + + + + Byte (1 byte) + + + + + Halfword (2 bytes) + + + + + Word (4 bytes) + + + + + + + + Operator: + + + + + + + + Equal to... + + + + + Not Equal to... + + + + + Greater Than... + + + + + Greater or Equal... + + + + + Less Than... + + + + + Less or Equal... + + + + + Increased By... + + + + + Decreased By... + + + + + Changed By... + + + + + Equal to Previous + + + + + Not Equal to Previous + + + + + Greater Than Previous + + + + + Greater or Equal to Previous + + + + + Less Than Previous + + + + + Less or Equal to Previous + + + + + + + + Start Address: + + + + + + + + + + End Address: + + + + + + + + + + + + + + + New Search + + + + + + + false + + + Search Again + + + + + + + false + + + Clear Results + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + Freeze + + + + + Description + + + + + Address + + + + + Type + + + + + Value + + + + + + + + + + false + + + Add To Watch + + + + + + + false + + + Add Manual Address + + + + + + + false + + + Remove Watch + + + + + + + false + + + Load Watch + + + + + + + false + + + Save Watch + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index e7b54ce81..9ef490d3c 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -56,6 +56,8 @@ + + @@ -86,6 +88,8 @@ + + @@ -181,6 +185,12 @@ Document + + Document + + + Document + @@ -193,6 +203,8 @@ + + diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp index 66bf4bf51..023f0502a 100644 --- a/src/duckstation-qt/main.cpp +++ b/src/duckstation-qt/main.cpp @@ -11,6 +11,7 @@ int main(int argc, char* argv[]) { // Register any standard types we need elsewhere qRegisterMetaType>(); + qRegisterMetaType>(); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 571419ba5..ac0959019 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1,6 +1,7 @@ #include "mainwindow.h" #include "aboutdialog.h" #include "autoupdaterdialog.h" +#include "cheatmanagerdialog.h" #include "common/assert.h" #include "core/host_display.h" #include "core/settings.h" @@ -233,6 +234,12 @@ void MainWindow::onEmulationStopped() m_emulation_running = false; updateEmulationActions(false, false); switchToGameListView(); + + if (m_cheat_manager_dialog) + { + delete m_cheat_manager_dialog; + m_cheat_manager_dialog = nullptr; + } } void MainWindow::onEmulationPaused(bool paused) @@ -316,6 +323,8 @@ void MainWindow::onChangeDiscFromPlaylistMenuAboutToHide() void MainWindow::onCheatsMenuAboutToShow() { m_ui.menuCheats->clear(); + connect(m_ui.menuCheats->addAction(tr("Cheat Manager")), &QAction::triggered, this, &MainWindow::onToolsCheatManagerTriggered); + m_ui.menuCheats->addSeparator(); m_host_interface->populateCheatsMenu(m_ui.menuCheats); } @@ -645,6 +654,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running) m_ui.actionViewSystemDisplay->setEnabled(starting || running); m_ui.menuChangeDisc->setDisabled(starting || !running); m_ui.menuCheats->setDisabled(starting || !running); + m_ui.actionCheatManager->setDisabled(starting || !running); m_ui.actionSaveState->setDisabled(starting || !running); m_ui.menuSaveState->setDisabled(starting || !running); @@ -779,6 +789,7 @@ void MainWindow::connectSignals() connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); + connect(m_ui.actionCheatManager, &QAction::triggered, this, &MainWindow::onToolsCheatManagerTriggered); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { @@ -1167,6 +1178,15 @@ void MainWindow::onToolsMemoryCardEditorTriggered() m_memory_card_editor_dialog->show(); } +void MainWindow::onToolsCheatManagerTriggered() +{ + if (!m_cheat_manager_dialog) + m_cheat_manager_dialog = new CheatManagerDialog(this); + + m_cheat_manager_dialog->setModal(false); + m_cheat_manager_dialog->show(); +} + void MainWindow::onToolsOpenDataDirectoryTriggered() { QtUtils::OpenURL(this, QUrl::fromLocalFile(m_host_interface->getUserDirectoryRelativePath(QString()))); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index d4607abff..ca2f4bde2 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -15,6 +15,7 @@ class QtHostInterface; class QtDisplayWidget; class AutoUpdaterDialog; class MemoryCardEditorDialog; +class CheatManagerDialog; class HostDisplay; struct GameListEntry; @@ -76,6 +77,7 @@ private Q_SLOTS: void onAboutActionTriggered(); void onCheckForUpdatesActionTriggered(); void onToolsMemoryCardEditorTriggered(); + void onToolsCheatManagerTriggered(); void onToolsOpenDataDirectoryTriggered(); void onGameListEntrySelected(const GameListEntry* entry); @@ -127,6 +129,7 @@ private: SettingsDialog* m_settings_dialog = nullptr; AutoUpdaterDialog* m_auto_updater_dialog = nullptr; MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr; + CheatManagerDialog* m_cheat_manager_dialog = nullptr; bool m_emulation_running = false; }; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 2fbe9eb8d..51ff2110e 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -205,6 +205,7 @@ &Tools + @@ -712,6 +713,11 @@ Memory &Card Editor + + + C&heat Manager + + Game &Grid diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index 83d14ab53..507e051c5 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -971,7 +971,7 @@ void QtHostInterface::populateCheatsMenu(QMenu* menu) QAction* action = menu->addAction(tr("&Load Cheats...")); connect(action, &QAction::triggered, [this]() { QString filename = QFileDialog::getOpenFileName(m_main_window, tr("Select Cheat File"), QString(), - tr("PCSXR/Libretro Cheat Files (*.cht);;All Files (*.*)")); + tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)")); if (!filename.isEmpty()) loadCheatList(filename); }); @@ -980,7 +980,7 @@ void QtHostInterface::populateCheatsMenu(QMenu* menu) action->setEnabled(has_cheat_list); connect(action, &QAction::triggered, [this]() { QString filename = QFileDialog::getSaveFileName(m_main_window, tr("Select Cheat File"), QString(), - tr("PCSXR/Libretro Cheat Files (*.cht);;All Files (*.*)")); + tr("PCSXR Cheat Files (*.cht);;All Files (*.*)")); if (!filename.isEmpty()) SaveCheatList(filename.toUtf8().constData()); }); @@ -1052,6 +1052,28 @@ void QtHostInterface::reloadPostProcessingShaders() ReloadPostProcessingShaders(); } +void QtHostInterface::executeOnEmulationThread(std::function callback, bool wait) +{ + if (isOnWorkerThread()) + { + callback(); + if (wait) + m_worker_thread_sync_execute_done.Signal(); + + return; + } + + QMetaObject::invokeMethod(this, "executeOnEmulationThread", Qt::QueuedConnection, + Q_ARG(std::function, callback), Q_ARG(bool, wait)); + if (wait) + { + // don't deadlock + while (!m_worker_thread_sync_execute_done.TryWait(10)) + qApp->processEvents(QEventLoop::ExcludeSocketNotifiers); + m_worker_thread_sync_execute_done.Reset(); + } +} + void QtHostInterface::loadState(const QString& filename) { if (!isOnWorkerThread()) diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index 6916c99e6..13d57dd7b 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -167,6 +167,7 @@ public Q_SLOTS: void setCheatEnabled(quint32 index, bool enabled); void applyCheat(quint32 index); void reloadPostProcessingShaders(); + void executeOnEmulationThread(std::function callback, bool wait = false); private Q_SLOTS: void doStopThread(); @@ -254,6 +255,7 @@ private: QThread* m_original_thread = nullptr; Thread* m_worker_thread = nullptr; QEventLoop* m_worker_thread_event_loop = nullptr; + Common::Event m_worker_thread_sync_execute_done; std::atomic_bool m_shutdown_flag{false}; diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 1122cdf00..9d7791586 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -44,10 +45,17 @@ QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog) return widget; } -void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths) +template +ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initializer_list& widths) { - const int min_column_width = view->horizontalHeader()->minimumSectionSize(); - const int max_column_width = view->horizontalHeader()->maximumSectionSize(); + QHeaderView* header; + if constexpr (std::is_same_v) + header = view->horizontalHeader(); + else + header = view->header(); + + const int min_column_width = header->minimumSectionSize(); + const int max_column_width = header->maximumSectionSize(); const int total_width = std::accumulate(widths.begin(), widths.end(), 0, [&min_column_width, &max_column_width](int a, int b) { return a + ((b < 0) ? 0 : std::clamp(b, min_column_width, max_column_width)); @@ -74,6 +82,16 @@ void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths) +{ + ResizeColumnsForView(view, widths); +} + +void ResizeColumnsForTreeView(QTreeView* view, const std::initializer_list& widths) +{ + ResizeColumnsForView(view, widths); +} + static const std::map s_qt_key_names = { {Qt::Key_Escape, QStringLiteral("Escape")}, {Qt::Key_Tab, QStringLiteral("Tab")}, diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index b0df67fe9..32025738e 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -2,10 +2,12 @@ #include #include #include +#include #include #include Q_DECLARE_METATYPE(std::optional); +Q_DECLARE_METATYPE(std::function); class ByteStream; @@ -13,6 +15,7 @@ class QComboBox; class QFrame; class QKeyEvent; class QTableView; +class QTreeView; class QWidget; class QUrl; @@ -27,6 +30,7 @@ QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog = true); /// Resizes columns of the table view to at the specified widths. A negative width will stretch the column to use the /// remaining space. void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths); +void ResizeColumnsForTreeView(QTreeView* view, const std::initializer_list& widths); /// Returns a string identifier for a Qt key ID. QString GetKeyIdentifier(int key); diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index fcbc12ce7..0300c42fa 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -2366,6 +2366,23 @@ bool CommonHostInterface::LoadCheatListFromGameTitle() return LoadCheatList(filename.c_str()); } +bool CommonHostInterface::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())) + { + AddFormattedOSDMessage(15.0f, TranslateString("OSDMessage", "Failed to save cheat list to '%s'"), filename.c_str()); + } + + return true; +} + bool CommonHostInterface::SaveCheatList(const char* filename) { if (!System::IsValid() || !System::HasCheatList()) @@ -2404,17 +2421,7 @@ void CommonHostInterface::SetCheatCodeState(u32 index, bool enabled, bool save_t } if (save_to_file) - { - const std::string filename(GetCheatFileName()); - if (!filename.empty()) - { - if (!cl->SaveToPCSXRFile(filename.c_str())) - { - AddFormattedOSDMessage(15.0f, TranslateString("OSDMessage", "Failed to save cheat list to '%s'"), - filename.c_str()); - } - } - } + SaveCheatList(); } void CommonHostInterface::ApplyCheatCode(u32 index) diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 9d638466d..a48b13d3c 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -159,6 +159,9 @@ public: /// Loads the cheat list for the current game title from the user directory. bool LoadCheatListFromGameTitle(); + /// Saves the current cheat list to the game title's file. + bool SaveCheatList(); + /// Saves the current cheat list to the specified file. bool SaveCheatList(const char* filename);