duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

cheatmanagerwindow.cpp (16913B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "cheatmanagerwindow.h"
      5 #include "cheatcodeeditordialog.h"
      6 #include "mainwindow.h"
      7 #include "qthost.h"
      8 #include "qtutils.h"
      9 
     10 #include "core/bus.h"
     11 #include "core/cpu_core.h"
     12 #include "core/host.h"
     13 #include "core/system.h"
     14 
     15 #include "common/assert.h"
     16 #include "common/string_util.h"
     17 
     18 #include <QtCore/QFileInfo>
     19 #include <QtGui/QColor>
     20 #include <QtWidgets/QFileDialog>
     21 #include <QtWidgets/QInputDialog>
     22 #include <QtWidgets/QMenu>
     23 #include <QtWidgets/QMessageBox>
     24 #include <QtWidgets/QTreeWidgetItemIterator>
     25 #include <array>
     26 #include <utility>
     27 
     28 CheatManagerWindow::CheatManagerWindow() : QWidget()
     29 {
     30   m_ui.setupUi(this);
     31 
     32   connectUi();
     33 
     34   updateCheatList();
     35 }
     36 
     37 CheatManagerWindow::~CheatManagerWindow() = default;
     38 
     39 void CheatManagerWindow::connectUi()
     40 {
     41   connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged);
     42   connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated);
     43   connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged);
     44   connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked);
     45   connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked);
     46   connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked);
     47   connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked);
     48   connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked);
     49   connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked);
     50   connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked);
     51   connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked);
     52   connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked);
     53 
     54   connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState);
     55   connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList);
     56 }
     57 
     58 void CheatManagerWindow::showEvent(QShowEvent* event)
     59 {
     60   QWidget::showEvent(event);
     61   resizeColumns();
     62 }
     63 
     64 void CheatManagerWindow::closeEvent(QCloseEvent* event)
     65 {
     66   QWidget::closeEvent(event);
     67   emit closed();
     68 }
     69 
     70 void CheatManagerWindow::resizeEvent(QResizeEvent* event)
     71 {
     72   QWidget::resizeEvent(event);
     73   resizeColumns();
     74 }
     75 
     76 void CheatManagerWindow::resizeColumns()
     77 {
     78   QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100});
     79 }
     80 
     81 QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const
     82 {
     83   QTreeWidgetItemIterator iter(m_ui.cheatList);
     84   while (*iter)
     85   {
     86     QTreeWidgetItem* item = *iter;
     87     const QVariant item_data(item->data(0, Qt::UserRole));
     88     if (item_data.isValid() && item_data.toUInt() == index)
     89       return item;
     90 
     91     ++iter;
     92   }
     93 
     94   return nullptr;
     95 }
     96 
     97 QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const
     98 {
     99   const int count = m_ui.cheatList->topLevelItemCount();
    100   for (int i = 0; i < count; i++)
    101   {
    102     QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
    103     if (item->text(0) == group_name)
    104       return item;
    105   }
    106 
    107   return nullptr;
    108 }
    109 
    110 QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const
    111 {
    112   QTreeWidgetItem* group = new QTreeWidgetItem();
    113   group->setFlags(group->flags() | Qt::ItemIsUserCheckable);
    114   group->setText(0, group_name);
    115   m_ui.cheatList->addTopLevelItem(group);
    116   return group;
    117 }
    118 
    119 QStringList CheatManagerWindow::getCheatGroupNames() const
    120 {
    121   QStringList group_names;
    122 
    123   const int count = m_ui.cheatList->topLevelItemCount();
    124   for (int i = 0; i < count; i++)
    125   {
    126     QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
    127     group_names.push_back(item->text(0));
    128   }
    129 
    130   return group_names;
    131 }
    132 
    133 static int getCheatIndexFromItem(QTreeWidgetItem* item)
    134 {
    135   QVariant item_data(item->data(0, Qt::UserRole));
    136   if (!item_data.isValid())
    137     return -1;
    138 
    139   return static_cast<int>(item_data.toUInt());
    140 }
    141 
    142 int CheatManagerWindow::getSelectedCheatIndex() const
    143 {
    144   QList<QTreeWidgetItem*> sel = m_ui.cheatList->selectedItems();
    145   if (sel.isEmpty())
    146     return -1;
    147 
    148   return static_cast<int>(getCheatIndexFromItem(sel.first()));
    149 }
    150 
    151 CheatList* CheatManagerWindow::getCheatList() const
    152 {
    153   return System::IsValid() ? System::GetCheatList() : nullptr;
    154 }
    155 
    156 void CheatManagerWindow::updateCheatList()
    157 {
    158   QSignalBlocker sb(m_ui.cheatList);
    159   while (m_ui.cheatList->topLevelItemCount() > 0)
    160     delete m_ui.cheatList->takeTopLevelItem(0);
    161 
    162   m_ui.cheatList->setEnabled(false);
    163   m_ui.cheatListAdd->setEnabled(false);
    164   m_ui.cheatListNewCategory->setEnabled(false);
    165   m_ui.cheatListEdit->setEnabled(false);
    166   m_ui.cheatListRemove->setEnabled(false);
    167   m_ui.cheatListActivate->setText(tr("Activate"));
    168   m_ui.cheatListActivate->setEnabled(false);
    169   m_ui.cheatListImport->setEnabled(false);
    170   m_ui.cheatListExport->setEnabled(false);
    171   m_ui.cheatListClear->setEnabled(false);
    172   m_ui.cheatListReset->setEnabled(false);
    173 
    174   Host::RunOnCPUThread([]() {
    175     if (!System::IsValid())
    176       return;
    177 
    178     CheatList* list = System::GetCheatList();
    179     if (!list)
    180     {
    181       System::LoadCheatList();
    182       list = System::GetCheatList();
    183     }
    184     if (!list)
    185     {
    186       System::LoadCheatListFromDatabase();
    187       list = System::GetCheatList();
    188     }
    189     if (!list)
    190     {
    191       System::SetCheatList(std::make_unique<CheatList>());
    192       list = System::GetCheatList();
    193     }
    194 
    195     // still racey...
    196     QtHost::RunOnUIThread([list]() {
    197       if (!QtHost::IsSystemValid())
    198         return;
    199 
    200       CheatManagerWindow* cm = g_main_window->getCheatManagerWindow();
    201       if (!cm)
    202         return;
    203 
    204       QSignalBlocker sb(cm->m_ui.cheatList);
    205 
    206       const std::vector<std::string> groups = list->GetCodeGroups();
    207       for (const std::string& group_name : groups)
    208       {
    209         QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name));
    210 
    211         const u32 count = list->GetCodeCount();
    212         bool all_enabled = true;
    213         for (u32 i = 0; i < count; i++)
    214         {
    215           const CheatCode& code = list->GetCode(i);
    216           if (code.group != group_name)
    217             continue;
    218 
    219           QTreeWidgetItem* item = new QTreeWidgetItem(group);
    220           cm->fillItemForCheatCode(item, i, code);
    221 
    222           all_enabled &= code.enabled;
    223         }
    224 
    225         group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked);
    226         group->setExpanded(true);
    227       }
    228 
    229       cm->m_ui.cheatList->setEnabled(true);
    230       cm->m_ui.cheatListAdd->setEnabled(true);
    231       cm->m_ui.cheatListNewCategory->setEnabled(true);
    232       cm->m_ui.cheatListImport->setEnabled(true);
    233       cm->m_ui.cheatListClear->setEnabled(true);
    234       cm->m_ui.cheatListReset->setEnabled(true);
    235       cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0);
    236     });
    237   });
    238 }
    239 
    240 void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code)
    241 {
    242   item->setData(0, Qt::UserRole, QVariant(static_cast<uint>(index)));
    243   if (code.IsManuallyActivated())
    244   {
    245     item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable));
    246   }
    247   else
    248   {
    249     item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
    250     item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked);
    251   }
    252   item->setText(0, QString::fromStdString(code.description));
    253   item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type)));
    254   item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation)));
    255   item->setText(3, QString::number(static_cast<uint>(code.instructions.size())));
    256 }
    257 
    258 void CheatManagerWindow::saveCheatList()
    259 {
    260   Host::RunOnCPUThread([]() { System::SaveCheatList(); });
    261 }
    262 
    263 void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
    264 {
    265   const int cheat_index = current ? getCheatIndexFromItem(current) : -1;
    266   const bool has_current = (cheat_index >= 0);
    267   m_ui.cheatListEdit->setEnabled(has_current);
    268   m_ui.cheatListRemove->setEnabled(has_current);
    269   m_ui.cheatListActivate->setEnabled(has_current);
    270 
    271   if (!has_current)
    272   {
    273     m_ui.cheatListActivate->setText(tr("Activate"));
    274   }
    275   else
    276   {
    277     const bool manual_activation = getCheatList()->GetCode(static_cast<u32>(cheat_index)).IsManuallyActivated();
    278     m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle"));
    279   }
    280 }
    281 
    282 void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item)
    283 {
    284   if (!item)
    285     return;
    286 
    287   const int index = getCheatIndexFromItem(item);
    288   if (index >= 0)
    289     activateCheat(static_cast<u32>(index));
    290 }
    291 
    292 void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column)
    293 {
    294   if (!item || column != 0)
    295     return;
    296 
    297   CheatList* list = getCheatList();
    298 
    299   const int index = getCheatIndexFromItem(item);
    300   if (index < 0)
    301   {
    302     // we're probably a parent/group node
    303     const int child_count = item->childCount();
    304     const Qt::CheckState cs = item->checkState(0);
    305     for (int i = 0; i < child_count; i++)
    306       item->child(i)->setCheckState(0, cs);
    307 
    308     return;
    309   }
    310 
    311   if (static_cast<u32>(index) >= list->GetCodeCount())
    312     return;
    313 
    314   CheatCode& cc = list->GetCode(static_cast<u32>(index));
    315   if (cc.IsManuallyActivated())
    316     return;
    317 
    318   const bool new_enabled = (item->checkState(0) == Qt::Checked);
    319   if (cc.enabled == new_enabled)
    320     return;
    321 
    322   Host::RunOnCPUThread([index, new_enabled]() {
    323     System::GetCheatList()->SetCodeEnabled(static_cast<u32>(index), new_enabled);
    324     System::SaveCheatList();
    325   });
    326 }
    327 
    328 void CheatManagerWindow::activateCheat(u32 index)
    329 {
    330   CheatList* list = getCheatList();
    331   if (index >= list->GetCodeCount())
    332     return;
    333 
    334   CheatCode& cc = list->GetCode(index);
    335   if (cc.IsManuallyActivated())
    336   {
    337     g_emu_thread->applyCheat(index);
    338     return;
    339   }
    340 
    341   const bool new_enabled = !cc.enabled;
    342   setCheatCheckState(index, new_enabled);
    343 
    344   Host::RunOnCPUThread([index, new_enabled]() {
    345     System::GetCheatList()->SetCodeEnabled(index, new_enabled);
    346     System::SaveCheatList();
    347   });
    348 }
    349 
    350 void CheatManagerWindow::setCheatCheckState(u32 index, bool checked)
    351 {
    352   QTreeWidgetItem* item = getItemForCheatIndex(index);
    353   if (item)
    354   {
    355     QSignalBlocker sb(m_ui.cheatList);
    356     item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked);
    357   }
    358 }
    359 
    360 void CheatManagerWindow::newCategoryClicked()
    361 {
    362   QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:"));
    363   if (group_name.isEmpty())
    364     return;
    365 
    366   if (getItemForCheatGroup(group_name) != nullptr)
    367   {
    368     QMessageBox::critical(this, tr("Error"), tr("This group name already exists."));
    369     return;
    370   }
    371 
    372   createItemForCheatGroup(group_name);
    373 }
    374 
    375 void CheatManagerWindow::addCodeClicked()
    376 {
    377   CheatList* list = getCheatList();
    378 
    379   CheatCode new_code;
    380   new_code.group = "Ungrouped";
    381 
    382   CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
    383   if (editor.exec() > 0)
    384   {
    385     const QString group_name_qstr(QString::fromStdString(new_code.group));
    386     QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
    387     if (!group_item)
    388       group_item = createItemForCheatGroup(group_name_qstr);
    389 
    390     QTreeWidgetItem* item = new QTreeWidgetItem(group_item);
    391     fillItemForCheatCode(item, list->GetCodeCount(), new_code);
    392     group_item->setExpanded(true);
    393 
    394     Host::RunOnCPUThread(
    395       [&new_code]() {
    396         System::GetCheatList()->AddCode(std::move(new_code));
    397         System::SaveCheatList();
    398       },
    399       true);
    400   }
    401 }
    402 
    403 void CheatManagerWindow::editCodeClicked()
    404 {
    405   int index = getSelectedCheatIndex();
    406   if (index < 0)
    407     return;
    408 
    409   CheatList* list = getCheatList();
    410   if (static_cast<u32>(index) >= list->GetCodeCount())
    411     return;
    412 
    413   CheatCode new_code = list->GetCode(static_cast<u32>(index));
    414   CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
    415   if (editor.exec() > 0)
    416   {
    417     QTreeWidgetItem* item = getItemForCheatIndex(static_cast<u32>(index));
    418     if (item)
    419     {
    420       if (new_code.group != list->GetCode(static_cast<u32>(index)).group)
    421       {
    422         item = item->parent()->takeChild(item->parent()->indexOfChild(item));
    423 
    424         const QString group_name_qstr(QString::fromStdString(new_code.group));
    425         QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
    426         if (!group_item)
    427           group_item = createItemForCheatGroup(group_name_qstr);
    428         group_item->addChild(item);
    429         group_item->setExpanded(true);
    430       }
    431 
    432       fillItemForCheatCode(item, static_cast<u32>(index), new_code);
    433     }
    434     else
    435     {
    436       // shouldn't happen...
    437       updateCheatList();
    438     }
    439 
    440     Host::RunOnCPUThread(
    441       [index, &new_code]() {
    442         System::GetCheatList()->SetCode(static_cast<u32>(index), std::move(new_code));
    443         System::SaveCheatList();
    444       },
    445       true);
    446   }
    447 }
    448 
    449 void CheatManagerWindow::deleteCodeClicked()
    450 {
    451   int index = getSelectedCheatIndex();
    452   if (index < 0)
    453     return;
    454 
    455   CheatList* list = getCheatList();
    456   if (static_cast<u32>(index) >= list->GetCodeCount())
    457     return;
    458 
    459   if (QMessageBox::question(this, tr("Delete Code"),
    460                             tr("Are you sure you wish to delete the selected code? This action is not reversible."),
    461                             QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
    462   {
    463     return;
    464   }
    465 
    466   Host::RunOnCPUThread(
    467     [index]() {
    468       System::GetCheatList()->RemoveCode(static_cast<u32>(index));
    469       System::SaveCheatList();
    470     },
    471     true);
    472   updateCheatList();
    473 }
    474 
    475 void CheatManagerWindow::activateCodeClicked()
    476 {
    477   int index = getSelectedCheatIndex();
    478   if (index < 0)
    479     return;
    480 
    481   activateCheat(static_cast<u32>(index));
    482 }
    483 
    484 void CheatManagerWindow::importClicked()
    485 {
    486   QMenu menu(this);
    487   connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered);
    488   connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered);
    489   menu.exec(QCursor::pos());
    490 }
    491 
    492 void CheatManagerWindow::importFromFileTriggered()
    493 {
    494   const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)"));
    495   const QString filename =
    496     QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter));
    497   if (filename.isEmpty())
    498     return;
    499 
    500   CheatList new_cheats;
    501   if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect))
    502   {
    503     QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
    504     return;
    505   }
    506 
    507   Host::RunOnCPUThread(
    508     [&new_cheats]() {
    509       DebugAssert(System::HasCheatList());
    510       System::GetCheatList()->MergeList(new_cheats);
    511       System::SaveCheatList();
    512     },
    513     true);
    514   updateCheatList();
    515 }
    516 
    517 void CheatManagerWindow::importFromTextTriggered()
    518 {
    519   const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:"));
    520   if (text.isEmpty())
    521     return;
    522 
    523   CheatList new_cheats;
    524   if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect))
    525   {
    526     QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
    527     return;
    528   }
    529 
    530   Host::RunOnCPUThread(
    531     [&new_cheats]() {
    532       DebugAssert(System::HasCheatList());
    533       System::GetCheatList()->MergeList(new_cheats);
    534       System::SaveCheatList();
    535     },
    536     true);
    537   updateCheatList();
    538 }
    539 
    540 void CheatManagerWindow::exportClicked()
    541 {
    542   const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)"));
    543   const QString filename =
    544     QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter));
    545   if (filename.isEmpty())
    546     return;
    547 
    548   if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData()))
    549     QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information."));
    550 }
    551 
    552 void CheatManagerWindow::clearClicked()
    553 {
    554   if (QMessageBox::question(this, tr("Confirm Clear"),
    555                             tr("Are you sure you want to remove all cheats? This is not reversible.")) !=
    556       QMessageBox::Yes)
    557   {
    558     return;
    559   }
    560 
    561   Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true);
    562   updateCheatList();
    563 }
    564 
    565 void CheatManagerWindow::resetClicked()
    566 {
    567   if (QMessageBox::question(
    568         this, tr("Confirm Reset"),
    569         tr(
    570           "Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) !=
    571       QMessageBox::Yes)
    572   {
    573     return;
    574   }
    575 
    576   Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true);
    577   updateCheatList();
    578 }