Qt: Add new cheat manager

This commit is contained in:
Connor McLaughlin 2020-10-20 01:14:49 +10:00
parent 9f0f24a5e5
commit b694577c38
21 changed files with 2356 additions and 19 deletions

View File

@ -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.

View File

@ -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 <cctype>
#include <iomanip>
#include <sstream>
Log_SetChannel(Cheats);
using KeyValuePairVector = std::vector<std::pair<std::string, std::string>>;
@ -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<bool>(*enable).value_or(false);
if (ParseLibretroCheat(&cc, code->c_str()))
@ -357,6 +364,20 @@ u32 CheatList::GetEnabledCodeCount() const
return count;
}
std::vector<std::string> CheatList::GetCodeGroups() const
{
std::vector<std::string> 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<Instruction> 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<u32>(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<u32>(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<u32>(instructions.size());
@ -622,3 +710,476 @@ void CheatCode::Apply() const
}
}
}
static std::array<const char*, 1> s_cheat_code_type_names = {{"Gameshark"}};
static std::array<const char*, 1> s_cheat_code_type_display_names{{TRANSLATABLE("Cheats", "Gameshark")}};
const char* CheatCode::GetTypeName(Type type)
{
return s_cheat_code_type_names[static_cast<u32>(type)];
}
const char* CheatCode::GetTypeDisplayName(Type type)
{
return s_cheat_code_type_display_names[static_cast<u32>(type)];
}
std::optional<CheatCode::Type> CheatCode::ParseTypeName(const char* str)
{
for (u32 i = 0; i < static_cast<u32>(s_cheat_code_type_names.size()); i++)
{
if (std::strcmp(s_cheat_code_type_names[i], str) == 0)
return static_cast<Type>(i);
}
return std::nullopt;
}
static std::array<const char*, 2> s_cheat_code_activation_names = {{"Manual", "EndFrame"}};
static std::array<const char*, 2> 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<u32>(activation)];
}
const char* CheatCode::GetActivationDisplayName(Activation activation)
{
return s_cheat_code_activation_display_names[static_cast<u32>(activation)];
}
std::optional<CheatCode::Activation> CheatCode::ParseActivationName(const char* str)
{
for (u32 i = 0; i < static_cast<u32>(s_cheat_code_activation_names.size()); i++)
{
if (std::strcmp(s_cheat_code_activation_names[i], str) == 0)
return static_cast<Activation>(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<s32>(value) > static_cast<s32>(comp_value)) : (value > comp_value);
}
case Operator::GreaterEqual:
{
return is_signed ? (static_cast<s32>(value) >= static_cast<s32>(comp_value)) : (value >= comp_value);
}
case Operator::LessThan:
{
return is_signed ? (static_cast<s32>(value) < static_cast<s32>(comp_value)) : (value < comp_value);
}
case Operator::LessEqual:
{
return is_signed ? (static_cast<s32>(value) <= static_cast<s32>(comp_value)) : (value <= comp_value);
}
case Operator::IncreasedBy:
{
return is_signed ? ((static_cast<s32>(value) - static_cast<s32>(last_value)) == static_cast<s32>(comp_value)) :
((value - last_value) == comp_value);
}
case Operator::DecreasedBy:
{
return is_signed ? ((static_cast<s32>(last_value) - static_cast<s32>(value)) == static_cast<s32>(comp_value)) :
((last_value - value) == comp_value);
}
case Operator::ChangedBy:
{
if (is_signed)
return (std::abs(static_cast<s32>(last_value) - static_cast<s32>(value)) == static_cast<s32>(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<s32>(value) > static_cast<s32>(last_value)) : (value > last_value);
}
case Operator::GreaterEqualLast:
{
return is_signed ? (static_cast<s32>(value) >= static_cast<s32>(last_value)) : (value >= last_value);
}
case Operator::LessThanLast:
{
return is_signed ? (static_cast<s32>(value) < static_cast<s32>(last_value)) : (value < last_value);
}
case Operator::LessEqualLast:
{
return is_signed ? (static_cast<s32>(value) <= static_cast<s32>(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);
}

View File

@ -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<u64, u8, 0, 8> value8;
};
std::string group;
std::string description;
std::vector<Instruction> 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<Type> ParseTypeName(const char* str);
static const char* GetActivationName(Activation activation);
static const char* GetActivationDisplayName(Activation activation);
static std::optional<Activation> ParseActivationName(const char* str);
};
class CheatList final
@ -78,6 +106,7 @@ public:
void RemoveCode(u32 i);
u32 GetEnabledCodeCount() const;
std::vector<std::string> GetCodeGroups() const;
void EnableCode(u32 index);
void DisableCode(u32 index);
void SetCodeEnabled(u32 index, bool state);
@ -98,3 +127,121 @@ public:
private:
std::vector<CheatCode> 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<Result>;
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<u32>(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<Entry>;
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<u32>(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;
};

View File

@ -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

View File

@ -0,0 +1,88 @@
#include "cheatcodeeditordialog.h"
#include <QtWidgets/QMessageBox>
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<CheatCode::Type>(m_ui.type->currentIndex());
m_code->activation = static_cast<CheatCode::Activation>(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<u32>(CheatCode::Type::Count); i++)
{
m_ui.type->addItem(qApp->translate("Cheats", CheatCode::GetTypeDisplayName(static_cast<CheatCode::Type>(i))));
}
for (u32 i = 0; i < static_cast<u32>(CheatCode::Activation::Count); i++)
{
m_ui.activation->addItem(
qApp->translate("Cheats", CheatCode::GetActivationDisplayName(static_cast<CheatCode::Activation>(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<int>(m_code->type));
m_ui.activation->setCurrentIndex(static_cast<int>(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);
}

View File

@ -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;
};

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheatCodeEditorDialog</class>
<widget class="QDialog" name="CheatCodeEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>491</width>
<height>284</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Code Editor</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="description"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Group:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="group"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="type"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Activation:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="activation"/>
</item>
<item row="4" column="0" colspan="2">
<widget class="QPlainTextEdit" name="instructions"/>
</item>
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="save">
<property name="text">
<string>Save</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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 <QtCore/QFileInfo>
#include <QtGui/QColor>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QTreeWidgetItemIterator>
#include <array>
static QString formatHexValue(u32 value)
{
return QStringLiteral("0x%1").arg(static_cast<uint>(value), 8, 16, QChar('0'));
}
static QString formatValue(u32 value, bool is_signed)
{
if (is_signed)
return QStringLiteral("%1").arg(static_cast<int>(value));
else
return QStringLiteral("%1").arg(static_cast<uint>(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<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { updateScanValue(); });
connect(m_ui.scanSize, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
m_scanner.SetSize(static_cast<MemoryAccessSize>(index));
m_scanner.ResetSearch();
updateResults();
});
connect(m_ui.scanValueSigned, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
m_scanner.SetValueSigned(index == 0);
m_scanner.ResetSearch();
updateResults();
});
connect(m_ui.scanOperator, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { m_scanner.SetOperator(static_cast<MemoryScan::Operator>(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<QTableWidgetSelectionRange> sel = m_ui.scanTable->selectedRanges();
if (sel.isEmpty())
return -1;
return sel.front().topRow();
}
int CheatManagerDialog::getSelectedWatchIndex() const
{
QList<QTableWidgetSelectionRange> 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<QTreeWidgetItem*> sel = m_ui.cheatList->selectedItems();
if (sel.isEmpty())
return -1;
return static_cast<int>(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<CheatList>()); }, 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<std::string> 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<uint>(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<uint>(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<u32>(index) >= list->GetCodeCount())
return;
CheatCode new_code = list->GetCode(static_cast<u32>(index));
CheatCodeEditorDialog editor(list, &new_code, this);
if (editor.exec() > 0)
{
QtHostInterface::GetInstance()->executeOnEmulationThread(
[index, &new_code]() {
System::GetCheatList()->SetCode(static_cast<u32>(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<u32>(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<u32>(index));
QtHostInterface::GetInstance()->SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerDialog::activateCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
activateCheat(static_cast<u32>(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<u32>(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<u32>(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<u32>(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<u32>(value));
}
else
{
uint value = item->text().toUInt(&value_ok);
if (value_ok)
m_scanner.SetResultValue(index, static_cast<u32>(value));
}
}
break;
default:
break;
}
}
void CheatManagerDialog::watchItemChanged(QTableWidgetItem* item)
{
const u32 index = static_cast<u32>(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<u32>(value));
}
else
{
uint value = item->text().toUInt(&value_ok);
if (value_ok)
m_watch.SetEntryValue(index, static_cast<u32>(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<const char*, 6> 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<u32>(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();
}

View File

@ -0,0 +1,71 @@
#pragma once
#include "core/cheats.h"
#include "ui_cheatmanagerdialog.h"
#include <QtCore/QTimer>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDialog>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTableWidget>
#include <optional>
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;
};

View File

@ -0,0 +1,550 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheatManagerDialog</class>
<widget class="QDialog" name="CheatManagerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>864</width>
<height>599</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Manager</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Cheat List</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="cheatListNewCategory">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;New Category...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListAdd">
<property name="text">
<string>&amp;Add Code...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListEdit">
<property name="text">
<string>&amp;Edit Code...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListRemove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete Code</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListActivate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Activate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListImport">
<property name="text">
<string>Import...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListExport">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Export...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTreeWidget" name="cheatList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>Activation</string>
</property>
</column>
<column>
<property name="text">
<string>Instructions</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Memory Scanner</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="layoutWidget2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QTableWidget" name="scanTable">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Previous Value</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Search Parameters</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Value:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="scanValue"/>
</item>
<item>
<widget class="QComboBox" name="scanValueSigned">
<item>
<property name="text">
<string>Signed</string>
</property>
</item>
<item>
<property name="text">
<string>Unsigned</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="scanValueBase">
<item>
<property name="text">
<string>Decimal</string>
</property>
</item>
<item>
<property name="text">
<string>Hex</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Data Size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="scanSize">
<property name="currentIndex">
<number>2</number>
</property>
<item>
<property name="text">
<string>Byte (1 byte)</string>
</property>
</item>
<item>
<property name="text">
<string>Halfword (2 bytes)</string>
</property>
</item>
<item>
<property name="text">
<string>Word (4 bytes)</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Operator:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="scanOperator">
<item>
<property name="text">
<string>Equal to...</string>
</property>
</item>
<item>
<property name="text">
<string>Not Equal to...</string>
</property>
</item>
<item>
<property name="text">
<string>Greater Than...</string>
</property>
</item>
<item>
<property name="text">
<string>Greater or Equal...</string>
</property>
</item>
<item>
<property name="text">
<string>Less Than...</string>
</property>
</item>
<item>
<property name="text">
<string>Less or Equal...</string>
</property>
</item>
<item>
<property name="text">
<string>Increased By...</string>
</property>
</item>
<item>
<property name="text">
<string>Decreased By...</string>
</property>
</item>
<item>
<property name="text">
<string>Changed By...</string>
</property>
</item>
<item>
<property name="text">
<string>Equal to Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Not Equal to Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Greater Than Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Greater or Equal to Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Less Than Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Less or Equal to Previous</string>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Start Address:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="scanStartAddress"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>End Address:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="scanEndAddress"/>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="scanNewSearch">
<property name="text">
<string>New Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanSearchAgain">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Search Again</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanResetSearch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Clear Results</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableWidget" name="watchTable">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Freeze</string>
</property>
</column>
<column>
<property name="text">
<string>Description</string>
</property>
</column>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="scanAddWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Add To Watch</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanAddManualAddress">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Add Manual Address</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanRemoveWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove Watch</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanLoadWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Load Watch</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanSaveWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save Watch</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -56,6 +56,8 @@
<ClCompile Include="audiosettingswidget.cpp" />
<ClCompile Include="autoupdaterdialog.cpp" />
<ClCompile Include="biossettingswidget.cpp" />
<ClCompile Include="cheatmanagerdialog.cpp" />
<ClCompile Include="cheatcodeeditordialog.cpp" />
<ClCompile Include="consolesettingswidget.cpp" />
<ClCompile Include="enhancementsettingswidget.cpp" />
<ClCompile Include="gamelistmodel.cpp" />
@ -86,6 +88,8 @@
<QtMoc Include="aboutdialog.h" />
<QtMoc Include="audiosettingswidget.h" />
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="cheatmanagerdialog.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="controllersettingswidget.h" />
<QtMoc Include="enhancementsettingswidget.h" />
<QtMoc Include="memorycardsettingswidget.h" />
@ -181,6 +185,12 @@
<QtUi Include="memorycardeditordialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatmanagerdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatcodeeditordialog.ui">
<FileType>Document</FileType>
</QtUi>
</ItemGroup>
<ItemGroup>
<QtResource Include="resources\resources.qrc">
@ -193,6 +203,8 @@
<ClCompile Include="$(IntDir)moc_autoupdaterdialog.cpp" />
<ClCompile Include="$(IntDir)moc_advancedsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_biossettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_cheatmanagerdialog.cpp" />
<ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp" />
<ClCompile Include="$(IntDir)moc_consolesettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_controllersettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_enhancementsettingswidget.cpp" />

View File

@ -11,6 +11,7 @@ int main(int argc, char* argv[])
{
// Register any standard types we need elsewhere
qRegisterMetaType<std::optional<bool>>();
qRegisterMetaType<std::function<void()>>();
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);

View File

@ -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())));

View File

@ -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;
};

View File

@ -205,6 +205,7 @@
<string>&amp;Tools</string>
</property>
<addaction name="actionMemory_Card_Editor"/>
<addaction name="actionCheatManager"/>
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
</widget>
@ -712,6 +713,11 @@
<string>Memory &amp;Card Editor</string>
</property>
</action>
<action name="actionCheatManager">
<property name="text">
<string>C&amp;heat Manager</string>
</property>
</action>
<action name="actionViewGameGrid">
<property name="text">
<string>Game &amp;Grid</string>

View File

@ -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<void()> 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<void()>, 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())

View File

@ -167,6 +167,7 @@ public Q_SLOTS:
void setCheatEnabled(quint32 index, bool enabled);
void applyCheat(quint32 index);
void reloadPostProcessingShaders();
void executeOnEmulationThread(std::function<void()> 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};

View File

@ -12,6 +12,7 @@
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QStyle>
#include <QtWidgets/QTableView>
#include <QtWidgets/QTreeView>
#include <algorithm>
#include <array>
#include <map>
@ -44,10 +45,17 @@ QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog)
return widget;
}
void ResizeColumnsForTableView(QTableView* view, const std::initializer_list<int>& widths)
template<typename T>
ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initializer_list<int>& 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<T, QTableView>)
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<int
}
}
void ResizeColumnsForTableView(QTableView* view, const std::initializer_list<int>& widths)
{
ResizeColumnsForView(view, widths);
}
void ResizeColumnsForTreeView(QTreeView* view, const std::initializer_list<int>& widths)
{
ResizeColumnsForView(view, widths);
}
static const std::map<int, QString> s_qt_key_names = {
{Qt::Key_Escape, QStringLiteral("Escape")},
{Qt::Key_Tab, QStringLiteral("Tab")},

View File

@ -2,10 +2,12 @@
#include <QtCore/QByteArray>
#include <QtCore/QMetaType>
#include <QtCore/QString>
#include <functional>
#include <initializer_list>
#include <optional>
Q_DECLARE_METATYPE(std::optional<bool>);
Q_DECLARE_METATYPE(std::function<void()>);
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<int>& widths);
void ResizeColumnsForTreeView(QTreeView* view, const std::initializer_list<int>& widths);
/// Returns a string identifier for a Qt key ID.
QString GetKeyIdentifier(int key);

View File

@ -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)

View File

@ -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);