From 781038adf1985870d25d09d21858455a413a19f5 Mon Sep 17 00:00:00 2001 From: Mr-Auto <36127424+Mr-Auto@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:21:55 +0200 Subject: [PATCH] Replace Threads view with SaveStates (#6) * replace Threads view with SaveStates --- CMakeLists.txt | 4 +- include/Spelunky2.h | 31 ++-- .../Views/{ViewThreads.h => ViewSaveStates.h} | 6 +- include/Views/ViewToolbar.h | 5 +- src/Data/Lookup.cpp | 21 +++ src/Spelunky2.cpp | 22 ++- src/Views/ViewSaveStates.cpp | 167 ++++++++++++++++++ src/Views/ViewThreads.cpp | 153 ---------------- src/Views/ViewToolbar.cpp | 32 ++-- 9 files changed, 249 insertions(+), 192 deletions(-) rename include/Views/{ViewThreads.h => ViewSaveStates.h} (76%) create mode 100644 src/Views/ViewSaveStates.cpp delete mode 100644 src/Views/ViewThreads.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 95e5b65..f876b40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,7 +78,7 @@ x64dbg_plugin(${PROJECT_NAME} include/Views/ViewVirtualFunctions.h include/Views/ViewStdVector.h include/Views/ViewJournalPage.h - include/Views/ViewThreads.h + include/Views/ViewSaveStates.h include/Views/ViewStdMap.h include/Views/ViewStdUnorderedMap.h include/Views/ViewStdList.h @@ -141,7 +141,7 @@ x64dbg_plugin(${PROJECT_NAME} src/Views/ViewStdUnorderedMap.cpp src/Views/ViewStdList.cpp src/Views/ViewJournalPage.cpp - src/Views/ViewThreads.cpp + src/Views/ViewSaveStates.cpp src/Views/ViewEntityList.cpp src/QtHelpers/StyledItemDelegateHTML.cpp src/QtHelpers/TreeViewMemoryFields.cpp diff --git a/include/Spelunky2.h b/include/Spelunky2.h index acfe8f7..f40c96f 100644 --- a/include/Spelunky2.h +++ b/include/Spelunky2.h @@ -29,31 +29,31 @@ namespace S2Plugin uintptr_t get_OnlinePtr(); uintptr_t get_GameAPIPtr(); uintptr_t get_HudPtr(); + uintptr_t get_SaveStatesPtr(); uintptr_t get_StatePtr() const { - if (heapBaseAddr == 0) - return 0; + if (auto base = get_HeapBase(); base != 0) + return base + GAME_OFFSET::STATE; - return heapBaseAddr + GAME_OFFSET::STATE; + return 0; }; uintptr_t get_LevelGenPtr() const { - if (heapBaseAddr == 0) - return 0; - return heapBaseAddr + GAME_OFFSET::LEVEL_GEN; + if (auto base = get_HeapBase(); base != 0) + return base + GAME_OFFSET::LEVEL_GEN; + + return 0; }; uintptr_t get_LiquidEnginePtr() const { - if (heapBaseAddr == 0) - return 0; + if (auto base = get_HeapBase(); base == 0) + return base + GAME_OFFSET::LIQUID_ENGINE; - return heapBaseAddr + GAME_OFFSET::LIQUID_ENGINE; + return 0; } - uintptr_t get_HeapBase() const - { - return heapBaseAddr; - }; + uintptr_t get_HeapBase() const; + const TextureDB& get_TextureDB(); const CharacterDB& get_CharacterDB(); const ParticleDB& get_ParticleDB(); @@ -78,12 +78,13 @@ namespace S2Plugin size_t codeSectionSize{0}; uintptr_t afterBundle{0}; size_t afterBundleSize{0}; - uintptr_t heapBaseAddr{0}; + uintptr_t heapBasePtr{0}; uintptr_t mGameManagerPtr{0}; uintptr_t mOnlinePtr{0}; uintptr_t mGameAPIPtr{0}; uintptr_t mHudPtr{0}; + uintptr_t mSaveStatesPtr{0}; EntityDB mEntityDB; ParticleDB mParticleDB; @@ -103,7 +104,6 @@ namespace S2Plugin { UNKNOWN1 = 0x8, // - ? MALLOC = 0x20, // - malloc base - UNKNOWN2 = 0x3B4, // - ? ILLUMINATION_SYNC = 0x3D0, // - illumination sync timer PRNG = 0x3F0, // - PRNG STATE = 0x4A0, // - State Memory @@ -111,5 +111,6 @@ namespace S2Plugin LIQUID_ENGINE = 0xD8650, // - liquid physics UNKNOWN3 = 0x108420, // - some vector? }; + friend class ViewSaveStates; }; } // namespace S2Plugin diff --git a/include/Views/ViewThreads.h b/include/Views/ViewSaveStates.h similarity index 76% rename from include/Views/ViewThreads.h rename to include/Views/ViewSaveStates.h index 59d2c11..43370f0 100644 --- a/include/Views/ViewThreads.h +++ b/include/Views/ViewSaveStates.h @@ -6,11 +6,11 @@ namespace S2Plugin { - class ViewThreads : public QWidget + class ViewSaveStates : public QWidget { Q_OBJECT public: - ViewThreads(QWidget* parent = nullptr); + ViewSaveStates(QWidget* parent = nullptr); protected: QSize sizeHint() const override; @@ -18,7 +18,7 @@ namespace S2Plugin private slots: void cellClicked(int row, int column); - void refreshThreads(); + void refreshSlots(); private: QTableWidget* mMainTable; diff --git a/include/Views/ViewToolbar.h b/include/Views/ViewToolbar.h index 463a952..e73adc7 100644 --- a/include/Views/ViewToolbar.h +++ b/include/Views/ViewToolbar.h @@ -32,6 +32,7 @@ namespace S2Plugin void showMatrix(uintptr_t address, std::string name, std::string arrayTypeName, size_t rows, size_t columns); void showEntityList(uintptr_t address); void showStdList(uintptr_t address, std::string typeName, bool oldType = false); + void showSaveGame(uintptr_t address); public slots: ViewEntityDB* showEntityDB(); @@ -45,10 +46,10 @@ namespace S2Plugin void showMainThreadLiquidPhysics(); void showEntities(); ViewVirtualTable* showVirtualTableLookup(); - void showSaveGame(); + void showMainThreadSaveGame(); void showLogger(); void showOnline(); - void showThreads(); + void showSaveStates(); void showGameAPI(); void showHud(); void showEntityFactory(); diff --git a/src/Data/Lookup.cpp b/src/Data/Lookup.cpp index 378244d..3061894 100644 --- a/src/Data/Lookup.cpp +++ b/src/Data/Lookup.cpp @@ -264,3 +264,24 @@ uintptr_t S2Plugin::Spelunky2::get_HudPtr() } return mHudPtr; } + +uintptr_t S2Plugin::Spelunky2::get_SaveStatesPtr() +{ + if (mSaveStatesPtr != 0) + return mSaveStatesPtr; + + auto instructionAddress = Script::Pattern::FindMem(afterBundle, afterBundleSize, "90 83 C1 FF"); + if (instructionAddress == 0) + { + displayError("Lookup error: unable to find SaveStates (1)"); + return mSaveStatesPtr; + } + auto relativeOffset = Script::Memory::ReadDword(instructionAddress + 6); + mSaveStatesPtr = instructionAddress + 10 + relativeOffset; + if (!Script::Memory::IsValidPtr(mSaveStatesPtr)) + { + displayError("Lookup error: unable to find SaveStates (2)"); + mSaveStatesPtr = 0; + } + return mSaveStatesPtr; +} diff --git a/src/Spelunky2.cpp b/src/Spelunky2.cpp index c0cd8d8..522d175 100644 --- a/src/Spelunky2.cpp +++ b/src/Spelunky2.cpp @@ -61,7 +61,13 @@ S2Plugin::Spelunky2* S2Plugin::Spelunky2::get() THREADLIST threadList; uintptr_t heapBase{0}; + uintptr_t heapBasePtr{0}; DbgGetThreadList(&threadList); + if (threadList.count == 0) + { + displayError("Could not retrieve thread list\nYou might be too fast, wait for the info in bottom left corner to change to \"Running\""); + return false; + } for (auto x = 0; x < threadList.count; ++x) { auto threadAllInfo = threadList.list[x]; @@ -70,13 +76,14 @@ S2Plugin::Spelunky2* S2Plugin::Spelunky2::get() auto tebAddress = DbgGetTebAddress(threadAllInfo.BasicInfo.ThreadId); auto tebAddress11Ptr = Script::Memory::ReadQword(tebAddress + (11 * sizeof(uintptr_t))); auto tebAddress11Value = Script::Memory::ReadQword(tebAddress11Ptr); - heapBase = Script::Memory::ReadQword(tebAddress11Value + TEB_offset); + heapBasePtr = tebAddress11Value + TEB_offset; + heapBase = Script::Memory::ReadQword(heapBasePtr); break; } } - if (heapBase == 0) + if (!Script::Memory::IsValidPtr(heapBasePtr) || !Script::Memory::IsValidPtr(heapBase)) { - displayError("Could not retrieve heap base of the main thread!"); + displayError("Could not retrieve heap base of the main thread!\nYou might be too fast, wait for the info in bottom left corner to change to \"Running\""); return false; } @@ -85,7 +92,7 @@ S2Plugin::Spelunky2* S2Plugin::Spelunky2::get() addr->codeSectionSize = Spelunky2CodeSectionSize; addr->afterBundle = Spelunky2AfterBundle; addr->afterBundleSize = Spelunky2CodeSectionStart + Spelunky2CodeSectionSize - Spelunky2AfterBundle; - addr->heapBaseAddr = heapBase; + addr->heapBasePtr = heapBasePtr; ptr = addr; } return ptr; @@ -135,7 +142,7 @@ uintptr_t S2Plugin::Spelunky2::get_SaveDataPtr() if (heapOffsetSaveGame == 0) return 0; - return heapBaseAddr + heapOffsetSaveGame; + return get_HeapBase() + heapOffsetSaveGame; } const QString& S2Plugin::Spelunky2::themeNameOfOffset(uintptr_t offset) const @@ -156,3 +163,8 @@ const QString& S2Plugin::Spelunky2::themeNameOfOffset(uintptr_t offset) const static const QString unknown{"UNKNOWN THEME"}; return unknown; } + +uintptr_t S2Plugin::Spelunky2::get_HeapBase() const +{ + return Script::Memory::ReadQword(heapBasePtr); +}; diff --git a/src/Views/ViewSaveStates.cpp b/src/Views/ViewSaveStates.cpp new file mode 100644 index 0000000..2f89ef6 --- /dev/null +++ b/src/Views/ViewSaveStates.cpp @@ -0,0 +1,167 @@ +#include "Views/ViewSaveStates.h" + +#include "QtHelpers/StyledItemDelegateHTML.h" +#include "QtHelpers/WidgetAutorefresh.h" +#include "QtPlugin.h" +#include "Spelunky2.h" +#include "Views/ViewToolbar.h" +#include "pluginmain.h" +#include +#include +#include +#include +#include + +constexpr uint8_t gsSaveStates = 5; +enum Columns +{ + InUse, + HeapBase, + State, + LevelGen, + LiquidPhysics, + SaveGame +}; + +S2Plugin::ViewSaveStates::ViewSaveStates(QWidget* parent) : QWidget(parent) +{ + setWindowIcon(getCavemanIcon()); + setWindowTitle("Save States"); + + auto mainLayout = new QVBoxLayout(this); + auto horLayout = new QHBoxLayout(); + auto autoRefresh = new WidgetAutorefresh(100, this); + horLayout->addWidget(autoRefresh); + QObject::connect(autoRefresh, &WidgetAutorefresh::refresh, this, &ViewSaveStates::refreshSlots); + horLayout->addStretch(); + mainLayout->addLayout(horLayout); + + mMainTable = new QTableWidget(this); + mMainTable->setAlternatingRowColors(true); + mMainTable->verticalHeader()->hide(); + mMainTable->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + mMainTable->verticalHeader()->setDefaultSectionSize(30); + mMainTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + mMainTable->horizontalHeader()->setStretchLastSection(true); + mMainTable->setSelectionBehavior(QAbstractItemView::SelectRows); + mMainTable->setSelectionMode(QAbstractItemView::SingleSelection); + mMainTable->setColumnCount(6); + mMainTable->setRowCount(gsSaveStates); + auto HTMLDelegate = new StyledItemDelegateHTML(this); + mMainTable->setItemDelegate(HTMLDelegate); + mMainTable->setHorizontalHeaderLabels({"In Use", "Heap Base", "State", "LevelGen", "Liquid Phisics", "SaveGame"}); + mMainTable->setColumnWidth(Columns::InUse, 60); + mMainTable->setColumnWidth(Columns::HeapBase, 130); + mMainTable->setColumnWidth(Columns::State, 130); + mMainTable->setColumnWidth(Columns::LevelGen, 130); + mMainTable->setColumnWidth(Columns::LiquidPhysics, 130); + mMainTable->setColumnWidth(Columns::SaveGame, 130); + HTMLDelegate->setCenterVertically(true); + QObject::connect(mMainTable, &QTableWidget::cellClicked, this, &ViewSaveStates::cellClicked); + + mainLayout->addWidget(mMainTable); + refreshSlots(); + autoRefresh->toggleAutoRefresh(true); +} + +constexpr uint32_t gsRoleMemoryAddress = Qt::UserRole + 1; + +void S2Plugin::ViewSaveStates::refreshSlots() +{ + auto updateItem = [&](int row, int column, uintptr_t address) + { + QString text; + if (address != 0) + text = QString::asprintf("0x%016llX", address); + + auto item = mMainTable->item(row, column); + if (item == nullptr) + { + item = new QTableWidgetItem(text); + mMainTable->setItem(row, column, item); + } + else + item->setText(text); + + item->setData(gsRoleMemoryAddress, address); + }; + + uintptr_t heapOffsetSaveGame = 0; + auto gm = Spelunky2::get()->get_GameManagerPtr(); + if (gm != 0) + heapOffsetSaveGame = Script::Memory::ReadQword(Script::Memory::ReadQword(gm + 8)); + + auto saveStatePtr = Spelunky2::get()->get_SaveStatesPtr(); + uint8_t emptySlots = Script::Memory::ReadByte(saveStatePtr); + saveStatePtr += 0x10; + for (uint8_t i = 0; i < gsSaveStates; ++i) + { + auto statePtr = Script::Memory::ReadQword(saveStatePtr + i * sizeof(uintptr_t)); + if (i >= emptySlots) + mMainTable->setItem(i, Columns::InUse, new QTableWidgetItem("Yes")); + else + mMainTable->setItem(i, Columns::InUse, new QTableWidgetItem("No")); + + updateItem(i, Columns::HeapBase, statePtr); + updateItem(i, Columns::State, statePtr + Spelunky2::GAME_OFFSET::STATE); + updateItem(i, Columns::LevelGen, statePtr + Spelunky2::GAME_OFFSET::LEVEL_GEN); + updateItem(i, Columns::LiquidPhysics, statePtr + Spelunky2::GAME_OFFSET::LIQUID_ENGINE); + updateItem(i, Columns::SaveGame, heapOffsetSaveGame == 0 ? 0 : statePtr + heapOffsetSaveGame); + } +} + +QSize S2Plugin::ViewSaveStates::sizeHint() const +{ + return QSize(750, 375); +} + +QSize S2Plugin::ViewSaveStates::minimumSizeHint() const +{ + return QSize(150, 150); +} + +void S2Plugin::ViewSaveStates::cellClicked(int row, int column) +{ + auto clickedItem = mMainTable->item(row, column); + switch (column) + { + case Columns::HeapBase: + { + auto addr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); + if (addr != 0) + { + GuiDumpAt(addr); + GuiShowCpu(); + } + break; + } + case Columns::State: + { + auto statePtr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); + if (statePtr != 0) + getToolbar()->showState(statePtr); + break; + } + case Columns::LevelGen: + { + auto levelGenPtr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); + if (levelGenPtr != 0) + getToolbar()->showLevelGen(levelGenPtr); + break; + } + case Columns::LiquidPhysics: + { + auto liquidPhisicsPtr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); + if (liquidPhisicsPtr != 0) + getToolbar()->showLiquidPhysics(liquidPhisicsPtr); + break; + } + case Columns::SaveGame: + { + auto saveGamePtr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); + if (saveGamePtr != 0) + getToolbar()->showSaveGame(saveGamePtr); + break; + } + } +} diff --git a/src/Views/ViewThreads.cpp b/src/Views/ViewThreads.cpp deleted file mode 100644 index 5033220..0000000 --- a/src/Views/ViewThreads.cpp +++ /dev/null @@ -1,153 +0,0 @@ -#include "Views/ViewThreads.h" - -#include "QtHelpers/StyledItemDelegateHTML.h" -#include "QtPlugin.h" -#include "Spelunky2.h" -#include "Views/ViewToolbar.h" -#include "pluginmain.h" -#include -#include -#include -#include - -static const uint32_t gsColThreadName = 0; -static const uint32_t gsColTEBAddress = 1; -static const uint32_t gsColStateAddress = 2; - -static const uint32_t gsRoleMemoryAddress = Qt::UserRole + 1; - -S2Plugin::ViewThreads::ViewThreads(QWidget* parent) : QWidget(parent) -{ - setWindowIcon(getCavemanIcon()); - setWindowTitle("Threads"); - - auto mainLayout = new QVBoxLayout(this); - auto horLayout = new QHBoxLayout(); - auto refreshButton = new QPushButton("Refresh", this); - horLayout->addWidget(refreshButton); - QObject::connect(refreshButton, &QPushButton::clicked, this, &ViewThreads::refreshThreads); - horLayout->addStretch(); - mainLayout->addLayout(horLayout); - - mMainTable = new QTableWidget(this); - mMainTable->setColumnCount(3); - mMainTable->setAlternatingRowColors(true); - mMainTable->verticalHeader()->hide(); - mMainTable->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); - mMainTable->verticalHeader()->setDefaultSectionSize(30); - mMainTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - mMainTable->horizontalHeader()->setStretchLastSection(true); - mMainTable->setSelectionBehavior(QAbstractItemView::SelectRows); - mMainTable->setSelectionMode(QAbstractItemView::SingleSelection); - auto HTMLDelegate = new StyledItemDelegateHTML(this); - mMainTable->setItemDelegate(HTMLDelegate); - HTMLDelegate->setCenterVertically(true); - QObject::connect(mMainTable, &QTableWidget::cellClicked, this, &ViewThreads::cellClicked); - - mainLayout->addWidget(mMainTable); - refreshThreads(); -} - -void S2Plugin::ViewThreads::refreshThreads() -{ - auto feedCodeOffset = 0x60; - - mMainTable->clear(); - mMainTable->setHorizontalHeaderLabels(QStringList() << "Thread ID" - << "TEB address" - << "State"); - mMainTable->setColumnWidth(gsColThreadName, 150); - mMainTable->setColumnWidth(gsColTEBAddress, 150); - - THREADLIST threadList; - DbgGetThreadList(&threadList); - mMainTable->setRowCount(threadList.count); - for (auto x = 0; x < threadList.count; ++x) - { - auto threadAllInfo = threadList.list[x]; - - std::string threadName(threadAllInfo.BasicInfo.threadName); - QString name; - if (threadName.empty()) - { - name = QString("%1").arg(threadAllInfo.BasicInfo.ThreadId); - } - else - { - name = QString("%1").arg(QString::fromStdString(threadName)); - } - auto item = new QTableWidgetItem(name); - mMainTable->setItem(x, gsColThreadName, item); - - auto tebAddress = DbgGetTebAddress(threadAllInfo.BasicInfo.ThreadId); - auto teb = QString::asprintf("0x%016llX", tebAddress); - item = new QTableWidgetItem(teb); - item->setData(gsRoleMemoryAddress, tebAddress); - mMainTable->setItem(x, gsColTEBAddress, item); - - if (!Script::Memory::IsValidPtr(tebAddress + (11 * sizeof(size_t)))) - { - continue; - } - auto tebAddress11Ptr = Script::Memory::ReadQword(tebAddress + (11 * sizeof(size_t))); - if (!Script::Memory::IsValidPtr(tebAddress11Ptr)) - { - continue; - } - auto tebAddress11Value = Script::Memory::ReadQword(tebAddress11Ptr); - auto heapBase = Script::Memory::ReadQword(tebAddress11Value + TEB_offset); - if (!Script::Memory::IsValidPtr(heapBase)) - { - continue; - } - auto statePtr = heapBase + 0x4A0; // hardcode for now - if (!Script::Memory::IsValidPtr(statePtr)) - { - continue; - } - auto feedCode = statePtr + feedCodeOffset; - if (!Script::Memory::IsValidPtr(statePtr)) - { - continue; - } - if (Script::Memory::ReadDword(feedCode) != 0xFEEDC0DE) - { - continue; - } - - auto s = QString::asprintf("0x%016llX", statePtr); - item = new QTableWidgetItem(s); - item->setData(gsRoleMemoryAddress, statePtr); - mMainTable->setItem(x, gsColStateAddress, item); - } -} - -QSize S2Plugin::ViewThreads::sizeHint() const -{ - return QSize(550, 375); -} - -QSize S2Plugin::ViewThreads::minimumSizeHint() const -{ - return QSize(150, 150); -} - -void S2Plugin::ViewThreads::cellClicked(int row, int column) -{ - auto clickedItem = mMainTable->item(row, column); - if (column == gsColTEBAddress) - { - auto addr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); - if (addr != 0) - { - GuiDumpAt(addr); - GuiShowCpu(); - } - } - else if (column == gsColStateAddress) - { - auto statePtr = clickedItem->data(gsRoleMemoryAddress).toULongLong(); - if (statePtr != 0) - getToolbar()->showState(statePtr); - } -} diff --git a/src/Views/ViewToolbar.cpp b/src/Views/ViewToolbar.cpp index 2c12d3a..9babf03 100644 --- a/src/Views/ViewToolbar.cpp +++ b/src/Views/ViewToolbar.cpp @@ -12,6 +12,7 @@ #include "Views/ViewLevelGen.h" #include "Views/ViewLogger.h" #include "Views/ViewParticleDB.h" +#include "Views/ViewSaveStates.h" #include "Views/ViewStdList.h" #include "Views/ViewStdMap.h" #include "Views/ViewStdUnorderedMap.h" @@ -19,7 +20,6 @@ #include "Views/ViewStringsTable.h" #include "Views/ViewStruct.h" #include "Views/ViewTextureDB.h" -#include "Views/ViewThreads.h" #include "Views/ViewVirtualFunctions.h" #include "Views/ViewVirtualTable.h" #include "pluginmain.h" @@ -86,9 +86,10 @@ S2Plugin::ViewToolbar::ViewToolbar(QMdiArea* mdiArea, QWidget* parent) : QDockWi auto btnHud = new QPushButton("Hud", this); mainLayout->addWidget(btnHud); QObject::connect(btnHud, &QPushButton::clicked, this, &ViewToolbar::showHud); - auto btnThreads = new QPushButton("Threads", this); - mainLayout->addWidget(btnThreads); - QObject::connect(btnThreads, &QPushButton::clicked, this, &ViewToolbar::showThreads); + auto btnSaveStates = new QPushButton("Save States", this); + btnSaveStates->setToolTip("In online, game saves the game state for potential rollback"); + mainLayout->addWidget(btnSaveStates); + QObject::connect(btnSaveStates, &QPushButton::clicked, this, &ViewToolbar::showSaveStates); addDivider(); mainLayout->addWidget(new QLabel("Main Thread heap:", this), 0, Qt::AlignHCenter); @@ -108,7 +109,7 @@ S2Plugin::ViewToolbar::ViewToolbar(QMdiArea* mdiArea, QWidget* parent) : QDockWi QObject::connect(btnLiquid, &QPushButton::clicked, this, &ViewToolbar::showMainThreadLiquidPhysics); auto btnSaveGame = new QPushButton("SaveGame", this); mainLayout->addWidget(btnSaveGame); - QObject::connect(btnSaveGame, &QPushButton::clicked, this, &ViewToolbar::showSaveGame); + QObject::connect(btnSaveGame, &QPushButton::clicked, this, &ViewToolbar::showMainThreadSaveGame); mainLayout->addStretch(); addDivider(); @@ -232,6 +233,14 @@ void S2Plugin::ViewToolbar::showStdList(uintptr_t address, std::string valueType win->setAttribute(Qt::WA_DeleteOnClose); } +void S2Plugin::ViewToolbar::showSaveGame(uintptr_t address) +{ + auto w = new ViewStruct(address, Configuration::get()->typeFields(MemoryFieldType::SaveGame), "SaveGame"); + auto win = mMDIArea->addSubWindow(w); + win->setVisible(true); + win->setAttribute(Qt::WA_DeleteOnClose); +} + // // slots: // @@ -375,14 +384,13 @@ void S2Plugin::ViewToolbar::showEntities() } } -void S2Plugin::ViewToolbar::showSaveGame() +void S2Plugin::ViewToolbar::showMainThreadSaveGame() { if (Spelunky2::is_loaded() && Configuration::is_loaded() && Spelunky2::get()->get_SaveDataPtr() != 0) { - auto w = new ViewStruct(Spelunky2::get()->get_SaveDataPtr(), Configuration::get()->typeFields(MemoryFieldType::SaveGame), "SaveGame"); - auto win = mMDIArea->addSubWindow(w); - win->setVisible(true); - win->setAttribute(Qt::WA_DeleteOnClose); + auto ptr = Spelunky2::get()->get_SaveDataPtr(); + if (ptr != 0) + showSaveGame(Spelunky2::get()->get_SaveDataPtr()); } } @@ -427,11 +435,11 @@ void S2Plugin::ViewToolbar::showLogger() win->setAttribute(Qt::WA_DeleteOnClose); } -void S2Plugin::ViewToolbar::showThreads() +void S2Plugin::ViewToolbar::showSaveStates() { if (Spelunky2::is_loaded() && Configuration::is_loaded()) { - auto w = new ViewThreads(); + auto w = new ViewSaveStates(); auto win = mMDIArea->addSubWindow(w); win->setVisible(true); win->setAttribute(Qt::WA_DeleteOnClose);