From 217d1b238b5d7c194a20d75cf3e48bc98d1411dd Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Thu, 4 Feb 2021 00:00:31 -0800 Subject: [PATCH] Qt: Add save converter tool --- CHANGES | 1 + src/platform/qt/CMakeLists.txt | 2 + src/platform/qt/CoreManager.cpp | 54 --- src/platform/qt/CoreManager.h | 4 - src/platform/qt/SaveConverter.cpp | 658 ++++++++++++++++++++++++++++++ src/platform/qt/SaveConverter.h | 102 +++++ src/platform/qt/SaveConverter.ui | 124 ++++++ src/platform/qt/Window.cpp | 3 + src/platform/qt/utils.h | 7 + 9 files changed, 897 insertions(+), 58 deletions(-) create mode 100644 src/platform/qt/SaveConverter.cpp create mode 100644 src/platform/qt/SaveConverter.h create mode 100644 src/platform/qt/SaveConverter.ui diff --git a/CHANGES b/CHANGES index e0c8e9eec..7e04db0a7 100644 --- a/CHANGES +++ b/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 diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index eb6d947e2..be9172e51 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -103,6 +103,7 @@ set(SOURCE_FILES ReportView.cpp ROMInfo.cpp RotatedHeaderView.cpp + SaveConverter.cpp SavestateButton.cpp SensorView.cpp SettingsView.cpp @@ -142,6 +143,7 @@ set(UI_FILES PrinterView.ui ReportView.ui ROMInfo.ui + SaveConverter.ui SensorView.ui SettingsView.ui ShaderSelector.ui diff --git a/src/platform/qt/CoreManager.cpp b/src/platform/qt/CoreManager.cpp index 758eaaca1..98403216a 100644 --- a/src/platform/qt/CoreManager.cpp +++ b/src/platform/qt/CoreManager.cpp @@ -14,9 +14,6 @@ #ifdef M_CORE_GBA #include #endif -#ifdef M_CORE_GB -#include -#endif #include #include @@ -31,57 +28,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(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) { QFileInfo info(path); if (!info.isReadable()) { diff --git a/src/platform/qt/CoreManager.h b/src/platform/qt/CoreManager.h index 3a31288b9..da7460f22 100644 --- a/src/platform/qt/CoreManager.h +++ b/src/platform/qt/CoreManager.h @@ -9,8 +9,6 @@ #include #include -#include - struct mCoreConfig; struct VFile; @@ -27,8 +25,6 @@ public: 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); CoreController* loadGame(VFile* vf, const QString& path, const QString& base); diff --git a/src/platform/qt/SaveConverter.cpp b/src/platform/qt/SaveConverter.cpp new file mode 100644 index 000000000..ec8ad08f3 --- /dev/null +++ b/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 + +#include "GBAApp.h" +#include "LogController.h" +#include "VFileDevice.h" +#include "utils.h" + +#ifdef M_CORE_GBA +#include +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +#include +#include + +using namespace QGBA; + +SaveConverter::SaveConverter(std::shared_ptr 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(&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(&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 vf = std::make_shared(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(extSavedata)}; + switch (platform) { +#ifdef M_CORE_GBA + case mPLATFORM_GBA: + save.gba.type = static_cast(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 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(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(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 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 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 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::possibleConversions() const { + QList 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(buffer.constData()); + uint64_t* out = reinterpret_cast(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; +} diff --git a/src/platform/qt/SaveConverter.h b/src/platform/qt/SaveConverter.h new file mode 100644 index 000000000..a9da27952 --- /dev/null +++ b/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 + +#include "CoreController.h" +#include "utils.h" + +#ifdef M_CORE_GBA +#include +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +#include + +#include "ui_SaveConverter.h" + +struct VFile; + +namespace QGBA { + +class SaveConverter : public QDialog { +Q_OBJECT + +public: + SaveConverter(std::shared_ptr 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, Endian = Endian::NONE); +#ifdef M_CORE_GBA + AnnotatedSave(SavedataType, std::shared_ptr, Endian = Endian::NONE); +#endif +#ifdef M_CORE_GB + AnnotatedSave(GBMemoryBankControllerType, std::shared_ptr, Endian = Endian::NONE); +#endif + + AnnotatedSave asRaw() const; + operator QString() const; + bool operator==(const AnnotatedSave&) const; + + QList possibleConversions() const; + QByteArray convertTo(const AnnotatedSave&) const; + + bool savestate; + mPlatform platform; + ssize_t size; + std::shared_ptr 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); + void detectFromHeaders(std::shared_ptr); + + Ui::SaveConverter m_ui; + + std::shared_ptr m_controller; + QList m_validSaves; + QList m_validOutputs; +}; + +} diff --git a/src/platform/qt/SaveConverter.ui b/src/platform/qt/SaveConverter.ui new file mode 100644 index 000000000..758400e25 --- /dev/null +++ b/src/platform/qt/SaveConverter.ui @@ -0,0 +1,124 @@ + + + SaveConverter + + + + 0 + 0 + 546 + 300 + + + + Convert/Extract Save Game + + + + + + Input file + + + + + + + + + Browse + + + + + + + false + + + + + + + + + + Output file + + + + + + + + + Browse + + + + + + + false + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + inputFile + inputBrowse + inputType + outputFile + outputBrowse + outputType + + + + + buttonBox + accepted() + SaveConverter + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SaveConverter + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index bac7f29b0..0b175eb41 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -49,6 +49,7 @@ #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 @@ void Window::setupMenu(QMenuBar* menubar) { #ifdef M_CORE_GBA m_actions.addSeparator("file"); + m_actions.addAction(tr("Convert save game..."), "convertSave", openControllerTView(), "file"); + Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "file"); m_platformActions.insert(mPLATFORM_GBA, importShark); diff --git a/src/platform/qt/utils.h b/src/platform/qt/utils.h index dfab510ed..db0151d67 100644 --- a/src/platform/qt/utils.h +++ b/src/platform/qt/utils.h @@ -15,6 +15,13 @@ namespace QGBA { +enum class Endian { + NONE = 0b00, + BIG = 0b01, + LITTLE = 0b10, + UNKNOWN = 0b11 +}; + QString niceSizeFormat(size_t filesize); QString nicePlatformFormat(mPlatform platform);