Qt: Add save converter tool
Vicki Pfau vi@endrift.com
Thu, 04 Feb 2021 00:00:31 -0800
9 files changed,
897 insertions(+),
58 deletions(-)
M
CHANGES
→
CHANGES
@@ -1,6 +1,7 @@
0.9.0: (Future) Features: - e-Reader card scanning + - New tool for converting between different save game formats - WebP and APNG recording - Separate overrides for GBC games that can also run on SGB or regular GB - Game Boy Player features can be enabled by default for all compatible games
M
src/platform/qt/CMakeLists.txt
→
src/platform/qt/CMakeLists.txt
@@ -103,6 +103,7 @@ RegisterView.cpp
ReportView.cpp ROMInfo.cpp RotatedHeaderView.cpp + SaveConverter.cpp SavestateButton.cpp SensorView.cpp SettingsView.cpp@@ -142,6 +143,7 @@ PlacementControl.ui
PrinterView.ui ReportView.ui ROMInfo.ui + SaveConverter.ui SensorView.ui SettingsView.ui ShaderSelector.ui
M
src/platform/qt/CoreManager.cpp
→
src/platform/qt/CoreManager.cpp
@@ -14,9 +14,6 @@
#ifdef M_CORE_GBA #include <mgba/gba/core.h> #endif -#ifdef M_CORE_GB -#include <mgba/gb/core.h> -#endif #include <mgba/core/core.h> #include <mgba-util/vfs.h>@@ -29,57 +26,6 @@ }
void CoreManager::setMultiplayerController(MultiplayerController* multiplayer) { m_multiplayer = multiplayer; -} - -QByteArray CoreManager::getExtdata(const QString& filename, mStateExtdataTag extdataType) { - VFileDevice vf(filename, QIODevice::ReadOnly); - - if (!vf.isOpen()) { - return {}; - } - - mStateExtdata extdata; - mStateExtdataInit(&extdata); - - QByteArray bytes; - auto extract = [&bytes, &extdata, &vf, extdataType](mCore* core) -> bool { - if (mCoreExtractExtdata(core, vf, &extdata)) { - mStateExtdataItem extitem; - if (!mStateExtdataGet(&extdata, extdataType, &extitem)) { - return false; - } - if (extitem.size) { - bytes = QByteArray::fromRawData(static_cast<const char*>(extitem.data), extitem.size); - } - return true; - } - return false; - }; - - bool done = false; - struct mCore* core = nullptr; -#ifdef USE_PNG - done = extract(nullptr); -#endif -#ifdef M_CORE_GBA - if (!done) { - core = GBACoreCreate(); - core->init(core); - done = extract(core); - core->deinit(core); - } -#endif -#ifdef M_CORE_GB - if (!done) { - core = GBCoreCreate(); - core->init(core); - done = extract(core); - core->deinit(core); - } -#endif - - mStateExtdataDeinit(&extdata); - return bytes; } CoreController* CoreManager::loadGame(const QString& path) {
M
src/platform/qt/CoreManager.h
→
src/platform/qt/CoreManager.h
@@ -9,8 +9,6 @@ #include <QFileInfo>
#include <QObject> #include <QString> -#include <mgba/core/serialize.h> - struct mCoreConfig; struct VFile;@@ -26,8 +24,6 @@ public:
void setConfig(const mCoreConfig*); void setMultiplayerController(MultiplayerController*); void setPreload(bool preload) { m_preload = preload; } - - static QByteArray getExtdata(const QString& filename, mStateExtdataTag extdataType); public slots: CoreController* loadGame(const QString& path);
A
src/platform/qt/SaveConverter.cpp
@@ -0,0 +1,658 @@
+/* Copyright (c) 2013-2021 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "SaveConverter.h" + +#include <QMessageBox> + +#include "GBAApp.h" +#include "LogController.h" +#include "VFileDevice.h" +#include "utils.h" + +#ifdef M_CORE_GBA +#include <mgba/gba/core.h> +#include <mgba/internal/gba/serialize.h> +#endif +#ifdef M_CORE_GB +#include <mgba/gb/core.h> +#include <mgba/internal/gb/serialize.h> +#endif + +#include <mgba-util/memory.h> +#include <mgba-util/vfs.h> + +using namespace QGBA; + +SaveConverter::SaveConverter(std::shared_ptr<CoreController> controller, QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint) + , m_controller(controller) +{ + m_ui.setupUi(this); + + connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes); + connect(m_ui.inputBrowse, &QAbstractButton::clicked, this, [this]() { + // TODO: Add gameshark saves here too + QStringList formats{"*.sav", "*.sgm", "*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9"}; + QString filter = tr("Save games and save states (%1)").arg(formats.join(QChar(' '))); + QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save game or save state"), filter); + if (!filename.isEmpty()) { + m_ui.inputFile->setText(filename); + refreshInputTypes(); + } + }); + connect(m_ui.inputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::refreshOutputTypes); + + connect(m_ui.outputFile, &QLineEdit::textEdited, this, &SaveConverter::checkCanConvert); + connect(m_ui.outputBrowse, &QAbstractButton::clicked, this, [this]() { + // TODO: Add gameshark saves here too + QStringList formats{"*.sav", "*.sgm"}; + QString filter = tr("Save games (%1)").arg(formats.join(QChar(' '))); + QString filename = GBAApp::app()->getSaveFileName(this, tr("Select save game"), filter); + if (!filename.isEmpty()) { + m_ui.outputFile->setText(filename); + checkCanConvert(); + } + }); + connect(m_ui.outputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::checkCanConvert); + connect(this, &QDialog::accepted, this, &SaveConverter::convert); + + refreshInputTypes(); + m_ui.buttonBox->button(QDialogButtonBox::Save)->setDisabled(true); +} + +void SaveConverter::convert() { + if (m_validSaves.isEmpty() || m_validOutputs.isEmpty()) { + return; + } + const AnnotatedSave& input = m_validSaves[m_ui.inputType->currentIndex()]; + const AnnotatedSave& output = m_validOutputs[m_ui.outputType->currentIndex()]; + QByteArray converted = input.convertTo(output); + if (converted.isEmpty()) { + QMessageBox* failure = new QMessageBox(QMessageBox::Warning, tr("Conversion failed"), tr("Failed to convert the save game. This is probably a bug."), + QMessageBox::Ok, this, Qt::Sheet); + failure->setAttribute(Qt::WA_DeleteOnClose); + failure->show(); + return; + } + QFile out(m_ui.outputFile->text()); + out.open(QIODevice::WriteOnly | QIODevice::Truncate); + out.write(converted); + out.close(); +} + +void SaveConverter::refreshInputTypes() { + m_validSaves.clear(); + m_ui.inputType->clear(); + if (m_ui.inputFile->text().isEmpty()) { + m_ui.inputType->addItem(tr("No file selected")); + m_ui.inputType->setEnabled(false); + return; + } + + std::shared_ptr<VFileDevice> vf = std::make_shared<VFileDevice>(m_ui.inputFile->text(), QIODevice::ReadOnly); + if (!vf->isOpen()) { + m_ui.inputType->addItem(tr("Could not open file")); + m_ui.inputType->setEnabled(false); + return; + } + + detectFromSavestate(*vf); + detectFromSize(vf); + + for (const auto& save : m_validSaves) { + m_ui.inputType->addItem(save); + } + if (m_validSaves.count()) { + m_ui.inputType->setEnabled(true); + } else { + m_ui.inputType->addItem(tr("No valid formats found")); + m_ui.inputType->setEnabled(false); + } +} + +void SaveConverter::refreshOutputTypes() { + m_ui.outputType->clear(); + if (m_validSaves.isEmpty()) { + m_ui.outputType->addItem(tr("Please select a valid input file")); + m_ui.outputType->setEnabled(false); + return; + } + m_validOutputs = m_validSaves[m_ui.inputType->currentIndex()].possibleConversions(); + for (const auto& save : m_validOutputs) { + m_ui.outputType->addItem(save); + } + if (m_validOutputs.count()) { + m_ui.outputType->setEnabled(true); + } else { + m_ui.outputType->addItem(tr("No valid conversions found")); + m_ui.outputType->setEnabled(false); + } + checkCanConvert(); +} + +void SaveConverter::checkCanConvert() { + QAbstractButton* button = m_ui.buttonBox->button(QDialogButtonBox::Save); + if (m_ui.inputFile->text().isEmpty()) { + button->setEnabled(false); + return; + } + if (m_ui.outputFile->text().isEmpty()) { + button->setEnabled(false); + return; + } + if (!m_ui.inputType->isEnabled()) { + button->setEnabled(false); + return; + } + if (!m_ui.outputType->isEnabled()) { + button->setEnabled(false); + return; + } + button->setEnabled(true); +} + +void SaveConverter::detectFromSavestate(VFile* vf) { + mPlatform platform = getStatePlatform(vf); + if (platform == mPLATFORM_NONE) { + return; + } + + QByteArray extSavedata = getExtdata(vf, platform, EXTDATA_SAVEDATA); + if (!extSavedata.size()) { + return; + } + + QByteArray state = getState(vf, platform); + AnnotatedSave save{platform, std::make_shared<VFileDevice>(extSavedata)}; + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + save.gba.type = static_cast<SavedataType>(state.at(offsetof(GBASerializedState, savedata.type))); + if (save.gba.type == SAVEDATA_EEPROM || save.gba.type == SAVEDATA_EEPROM512) { + save.endianness = Endian::LITTLE; + } + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + // GB savestates don't store the MBC type...should probably fix that + save.gb.type = GB_MBC_AUTODETECT; + if (state.size() == 0x100) { + // MBC2 packed save + save.endianness = Endian::LITTLE; + save.gb.type = GB_MBC2; + } + break; +#endif + default: + break; + } + m_validSaves.append(save); +} + +void SaveConverter::detectFromSize(std::shared_ptr<VFileDevice> vf) { +#ifdef M_CORE_GBA + switch (vf->size()) { + case SIZE_CART_SRAM: + m_validSaves.append(AnnotatedSave{SAVEDATA_SRAM, vf}); + break; + case SIZE_CART_FLASH512: + m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH512, vf}); + break; + case SIZE_CART_FLASH1M: + m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, vf}); + break; + case SIZE_CART_EEPROM: + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::LITTLE}); + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::BIG}); + break; + case SIZE_CART_EEPROM512: + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::LITTLE}); + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::BIG}); + break; + } +#endif + +#ifdef M_CORE_GB + switch (vf->size()) { + case 0x800: + case 0x82C: + case 0x830: + case 0x2000: + case 0x202C: + case 0x2030: + case 0x8000: + case 0x802C: + case 0x8030: + case 0x10000: + case 0x1002C: + case 0x10030: + case 0x20000: + case 0x2002C: + case 0x20030: + m_validSaves.append(AnnotatedSave{GB_MBC_AUTODETECT, vf}); + break; + case 0x100: + m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::LITTLE}); + m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::BIG}); + break; + case 0x200: + m_validSaves.append(AnnotatedSave{GB_MBC2, vf}); + break; + case GB_SIZE_MBC6_FLASH: // Flash only + case GB_SIZE_MBC6_FLASH + 0x8000: // Concatenated SRAM and flash + m_validSaves.append(AnnotatedSave{GB_MBC6, vf}); + break; + case 0x20: + m_validSaves.append(AnnotatedSave{GB_TAMA5, vf}); + break; + } +#endif +} + +mPlatform SaveConverter::getStatePlatform(VFile* vf) { + uint32_t magic; + void* state = nullptr; + struct mCore* core = nullptr; + mPlatform platform = mPLATFORM_NONE; +#ifdef M_CORE_GBA + if (platform == mPLATFORM_NONE) { + core = GBACoreCreate(); + core->init(core); + state = mCoreExtractState(core, vf, nullptr); + core->deinit(core); + if (state) { + LOAD_32LE(magic, 0, state); + if (magic - GBA_SAVESTATE_MAGIC <= GBA_SAVESTATE_VERSION) { + platform = mPLATFORM_GBA; + } + mappedMemoryFree(state, core->stateSize(core)); + } + } +#endif +#ifdef M_CORE_GB + if (platform == mPLATFORM_NONE) { + core = GBCoreCreate(); + core->init(core); + state = mCoreExtractState(core, vf, nullptr); + core->deinit(core); + if (state) { + LOAD_32LE(magic, 0, state); + if (magic - GB_SAVESTATE_MAGIC <= GB_SAVESTATE_VERSION) { + platform = mPLATFORM_GB; + } + mappedMemoryFree(state, core->stateSize(core)); + } + } +#endif + + return platform; +} + +QByteArray SaveConverter::getState(VFile* vf, mPlatform platform) { + QByteArray bytes; + struct mCore* core = mCoreCreate(platform); + core->init(core); + void* state = mCoreExtractState(core, vf, nullptr); + if (state) { + size_t size = core->stateSize(core); + bytes = QByteArray::fromRawData(static_cast<const char*>(state), size); + bytes.data(); // Trigger a deep copy before we delete the backing + mappedMemoryFree(state, size); + } + core->deinit(core); + return bytes; +} + +QByteArray SaveConverter::getExtdata(VFile* vf, mPlatform platform, mStateExtdataTag extdataType) { + mStateExtdata extdata; + mStateExtdataInit(&extdata); + QByteArray bytes; + struct mCore* core = mCoreCreate(platform); + core->init(core); + if (mCoreExtractExtdata(core, vf, &extdata)) { + mStateExtdataItem extitem; + if (mStateExtdataGet(&extdata, extdataType, &extitem) && extitem.size) { + bytes = QByteArray::fromRawData(static_cast<const char*>(extitem.data), extitem.size); + bytes.data(); // Trigger a deep copy before we delete the backing + } + } + core->deinit(core); + mStateExtdataDeinit(&extdata); + return bytes; +} + +SaveConverter::AnnotatedSave::AnnotatedSave() + : savestate(false) + , platform(mPLATFORM_NONE) + , size(0) + , backing() + , endianness(Endian::NONE) +{ +} + +SaveConverter::AnnotatedSave::AnnotatedSave(mPlatform platform, std::shared_ptr<VFileDevice> vf, Endian endianness) + : savestate(true) + , platform(platform) + , size(vf->size()) + , backing(vf) + , endianness(endianness) +{ +} + +#ifdef M_CORE_GBA +SaveConverter::AnnotatedSave::AnnotatedSave(SavedataType type, std::shared_ptr<VFileDevice> vf, Endian endianness) + : savestate(false) + , platform(mPLATFORM_GBA) + , size(vf->size()) + , backing(vf) + , endianness(endianness) + , gba({type}) +{ +} +#endif + +#ifdef M_CORE_GB +SaveConverter::AnnotatedSave::AnnotatedSave(GBMemoryBankControllerType type, std::shared_ptr<VFileDevice> vf, Endian endianness) + : savestate(false) + , platform(mPLATFORM_GB) + , size(vf->size()) + , backing(vf) + , endianness(endianness) + , gb({type}) +{ +} +#endif + +SaveConverter::AnnotatedSave SaveConverter::AnnotatedSave::asRaw() const { + AnnotatedSave raw; + raw.platform = platform; + raw.size = size; + raw.endianness = endianness; + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + raw.gba = gba; + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + raw.gb = gb; + break; +#endif + default: + break; + } + return raw; +} + +SaveConverter::AnnotatedSave::operator QString() const { + QString sizeStr(niceSizeFormat(size)); + QString typeFormat("%1"); + QString endianStr; + QString saveType; + QString format = QCoreApplication::translate("SaveConverter", "%1 %2 save game"); + + switch (endianness) { + case Endian::LITTLE: + endianStr = QCoreApplication::translate("SaveConverter", "little endian"); + break; + case Endian::BIG: + endianStr = QCoreApplication::translate("SaveConverter", "big endian"); + break; + default: + break; + } + + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + switch (gba.type) { + case SAVEDATA_SRAM: + typeFormat = QCoreApplication::translate("SaveConverter", "SRAM"); + break; + case SAVEDATA_FLASH512: + case SAVEDATA_FLASH1M: + typeFormat = QCoreApplication::translate("SaveConverter", "%1 flash"); + break; + case SAVEDATA_EEPROM: + case SAVEDATA_EEPROM512: + typeFormat = QCoreApplication::translate("SaveConverter", "%1 EEPROM"); + break; + default: + break; + } + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + switch (gb.type) { + case GB_MBC_AUTODETECT: + if (size & 0xFF) { + typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM + RTC"); + } else { + typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM"); + } + break; + case GB_MBC2: + if (size == 0x100) { + typeFormat = QCoreApplication::translate("SaveConverter", "packed MBC2"); + } else { + typeFormat = QCoreApplication::translate("SaveConverter", "unpacked MBC2"); + } + break; + case GB_MBC6: + if (size == GB_SIZE_MBC6_FLASH) { + typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 flash"); + } else if (size > GB_SIZE_MBC6_FLASH) { + typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 combined SRAM + flash"); + } else { + typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 SRAM"); + } + break; + case GB_TAMA5: + typeFormat = QCoreApplication::translate("SaveConverter", "TAMA5"); + break; + default: + break; + } + break; +#endif + default: + break; + } + saveType = typeFormat.arg(sizeStr); + if (!endianStr.isEmpty()) { + saveType = QCoreApplication::translate("SaveConverter", "%1 (%2)").arg(saveType).arg(endianStr); + } + if (savestate) { + format = QCoreApplication::translate("SaveConverter", "%1 save state with embedded %2 save game"); + } + return format.arg(nicePlatformFormat(platform)).arg(saveType); +} + +bool SaveConverter::AnnotatedSave::operator==(const AnnotatedSave& other) const { + if (other.savestate != savestate || other.platform != platform || other.size != size || other.endianness != endianness) { + return false; + } + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + if (other.gba.type != gba.type) { + return false; + } + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + if (other.gb.type != gb.type) { + return false; + } + break; +#endif + default: + break; + } + return true; +} + +QList<SaveConverter::AnnotatedSave> SaveConverter::AnnotatedSave::possibleConversions() const { + QList<AnnotatedSave> possible; + AnnotatedSave same = asRaw(); + same.backing.reset(); + same.savestate = false; + + if (savestate) { + possible.append(same); + } + + + AnnotatedSave endianSwapped = same; + switch (endianness) { + case Endian::LITTLE: + endianSwapped.endianness = Endian::BIG; + possible.append(endianSwapped); + break; + case Endian::BIG: + endianSwapped.endianness = Endian::LITTLE; + possible.append(endianSwapped); + break; + default: + break; + } + + switch (platform) { +#ifdef M_CORE_GB + case mPLATFORM_GB: + switch (gb.type) { + case GB_MBC2: + if (size == 0x100) { + AnnotatedSave unpacked = same; + unpacked.size = 0x200; + unpacked.endianness = Endian::NONE; + possible.append(unpacked); + } else { + AnnotatedSave packed = same; + packed.size = 0x100; + packed.endianness = Endian::LITTLE; + possible.append(packed); + packed.endianness = Endian::BIG; + possible.append(packed); + } + break; + case GB_MBC6: + if (size > GB_SIZE_MBC6_FLASH) { + AnnotatedSave separated = same; + separated.size = size - GB_SIZE_MBC6_FLASH; + possible.append(separated); + separated.size = GB_SIZE_MBC6_FLASH; + possible.append(separated); + } + break; + default: + break; + } + break; +#endif + default: + break; + } + + return possible; +} + +QByteArray SaveConverter::AnnotatedSave::convertTo(const SaveConverter::AnnotatedSave& target) const { + QByteArray converted; + QByteArray buffer; + backing->seek(0); + if (target == asRaw()) { + return backing->readAll(); + } + + if (platform != target.platform) { + LOG(QT, ERROR) << tr("Cannot convert save games between platforms"); + return {}; + } + + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + switch (gba.type) { + case SAVEDATA_EEPROM: + case SAVEDATA_EEPROM512: + converted.resize(target.size); + buffer = backing->readAll(); + for (int i = 0; i < size; i += 8) { + uint64_t word; + const uint64_t* in = reinterpret_cast<const uint64_t*>(buffer.constData()); + uint64_t* out = reinterpret_cast<uint64_t*>(converted.data()); + LOAD_64LE(word, i, in); + STORE_64BE(word, i, out); + } + break; + default: + break; + } + break; +#endif +#ifdef M_CORE_GB + case mPLATFORM_GB: + switch (gb.type) { + case GB_MBC2: + converted.reserve(target.size); + buffer = backing->readAll(); + if (size == 0x100 && target.size == 0x200) { + if (endianness == Endian::LITTLE) { + for (uint8_t byte : buffer) { + converted.append(0xF0 | (byte & 0xF)); + converted.append(0xF0 | (byte >> 4)); + } + } else if (endianness == Endian::BIG) { + for (uint8_t byte : buffer) { + converted.append(0xF0 | (byte >> 4)); + converted.append(0xF0 | (byte & 0xF)); + } + } + } else if (size == 0x200 && target.size == 0x100) { + uint8_t byte; + if (target.endianness == Endian::LITTLE) { + for (int i = 0; i < target.size; ++i) { + byte = buffer[i * 2] & 0xF; + byte |= (buffer[i * 2 + 1] & 0xF) << 4; + converted.append(byte); + } + } else if (target.endianness == Endian::BIG) { + for (int i = 0; i < target.size; ++i) { + byte = (buffer[i * 2] & 0xF) << 4; + byte |= buffer[i * 2 + 1] & 0xF; + converted.append(byte); + } + } + } else if (size == 0x100 && target.size == 0x100) { + for (uint8_t byte : buffer) { + converted.append((byte >> 4) | (byte << 4)); + } + } + break; + case GB_MBC6: + if (size == target.size + GB_SIZE_MBC6_FLASH) { + converted = backing->read(target.size); + } else if (target.size == GB_SIZE_MBC6_FLASH) { + backing->skip(size - GB_SIZE_MBC6_FLASH); + converted = backing->read(GB_SIZE_MBC6_FLASH); + } + break; + default: + break; + } + break; +#endif + default: + break; + } + + return converted; +}
A
src/platform/qt/SaveConverter.h
@@ -0,0 +1,102 @@
+/* Copyright (c) 2013-2021 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#pragma once + +#include <QDialog> + +#include "CoreController.h" +#include "utils.h" + +#ifdef M_CORE_GBA +#include <mgba/gba/core.h> +#include <mgba/internal/gba/savedata.h> +#endif +#ifdef M_CORE_GB +#include <mgba/gb/core.h> +#include <mgba/gb/interface.h> +#endif + +#include <mgba/core/serialize.h> + +#include "ui_SaveConverter.h" + +struct VFile; + +namespace QGBA { + +class SaveConverter : public QDialog { +Q_OBJECT + +public: + SaveConverter(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr); + + static mPlatform getStatePlatform(VFile*); + static QByteArray getState(VFile*, mPlatform); + static QByteArray getExtdata(VFile*, mPlatform, mStateExtdataTag); + +public slots: + void convert(); + +private slots: + void refreshInputTypes(); + void refreshOutputTypes(); + void checkCanConvert(); + +private: +#ifdef M_CORE_GBA + struct GBASave { + SavedataType type; + }; +#endif +#ifdef M_CORE_GB + struct GBSave { + GBMemoryBankControllerType type; + }; +#endif + struct AnnotatedSave { + AnnotatedSave(); + AnnotatedSave(mPlatform, std::shared_ptr<VFileDevice>, Endian = Endian::NONE); +#ifdef M_CORE_GBA + AnnotatedSave(SavedataType, std::shared_ptr<VFileDevice>, Endian = Endian::NONE); +#endif +#ifdef M_CORE_GB + AnnotatedSave(GBMemoryBankControllerType, std::shared_ptr<VFileDevice>, Endian = Endian::NONE); +#endif + + AnnotatedSave asRaw() const; + operator QString() const; + bool operator==(const AnnotatedSave&) const; + + QList<AnnotatedSave> possibleConversions() const; + QByteArray convertTo(const AnnotatedSave&) const; + + bool savestate; + mPlatform platform; + ssize_t size; + std::shared_ptr<VFileDevice> backing; + Endian endianness; + union { +#ifdef M_CORE_GBA + GBASave gba; +#endif +#ifdef M_CORE_GB + GBSave gb; +#endif + }; + }; + + void detectFromSavestate(VFile*); + void detectFromSize(std::shared_ptr<VFileDevice>); + void detectFromHeaders(std::shared_ptr<VFileDevice>); + + Ui::SaveConverter m_ui; + + std::shared_ptr<CoreController> m_controller; + QList<AnnotatedSave> m_validSaves; + QList<AnnotatedSave> m_validOutputs; +}; + +}
A
src/platform/qt/SaveConverter.ui
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SaveConverter</class> + <widget class="QDialog" name="SaveConverter"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>546</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Convert/Extract Save Game</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Input file</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLineEdit" name="inputFile"/> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="inputBrowse"> + <property name="text"> + <string>Browse</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="inputType"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Output file</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLineEdit" name="outputFile"/> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="outputBrowse"> + <property name="text"> + <string>Browse</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="outputType"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>inputFile</tabstop> + <tabstop>inputBrowse</tabstop> + <tabstop>inputType</tabstop> + <tabstop>outputFile</tabstop> + <tabstop>outputBrowse</tabstop> + <tabstop>outputType</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SaveConverter</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SaveConverter</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
M
src/platform/qt/Window.cpp
→
src/platform/qt/Window.cpp
@@ -49,6 +49,7 @@ #include "PlacementControl.h"
#include "PrinterView.h" #include "ReportView.h" #include "ROMInfo.h" +#include "SaveConverter.h" #include "SensorView.h" #include "ShaderSelector.h" #include "ShortcutController.h"@@ -1209,6 +1210,8 @@ m_actions.addAction(tr("Load camera image..."), "loadCamImage", this, &Window::loadCamImage, "file");
#ifdef M_CORE_GBA m_actions.addSeparator("file"); + m_actions.addAction(tr("Convert save game..."), "convertSave", openControllerTView<SaveConverter>(), "file"); + Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "file"); m_platformActions.insert(mPLATFORM_GBA, importShark);
M
src/platform/qt/utils.h
→
src/platform/qt/utils.h
@@ -15,6 +15,13 @@ #include <algorithm>
namespace QGBA { +enum class Endian { + NONE = 0b00, + BIG = 0b01, + LITTLE = 0b10, + UNKNOWN = 0b11 +}; + QString niceSizeFormat(size_t filesize); QString nicePlatformFormat(mPlatform platform);