mirror of https://github.com/mgba-emu/mgba.git
Qt: Add save converter tool
This commit is contained in:
parent
1089285b45
commit
217d1b238b
1
CHANGES
1
CHANGES
|
@ -1,6 +1,7 @@
|
||||||
0.9.0: (Future)
|
0.9.0: (Future)
|
||||||
Features:
|
Features:
|
||||||
- e-Reader card scanning
|
- e-Reader card scanning
|
||||||
|
- New tool for converting between different save game formats
|
||||||
- WebP and APNG recording
|
- WebP and APNG recording
|
||||||
- Separate overrides for GBC games that can also run on SGB or regular GB
|
- 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
|
- Game Boy Player features can be enabled by default for all compatible games
|
||||||
|
|
|
@ -103,6 +103,7 @@ set(SOURCE_FILES
|
||||||
ReportView.cpp
|
ReportView.cpp
|
||||||
ROMInfo.cpp
|
ROMInfo.cpp
|
||||||
RotatedHeaderView.cpp
|
RotatedHeaderView.cpp
|
||||||
|
SaveConverter.cpp
|
||||||
SavestateButton.cpp
|
SavestateButton.cpp
|
||||||
SensorView.cpp
|
SensorView.cpp
|
||||||
SettingsView.cpp
|
SettingsView.cpp
|
||||||
|
@ -142,6 +143,7 @@ set(UI_FILES
|
||||||
PrinterView.ui
|
PrinterView.ui
|
||||||
ReportView.ui
|
ReportView.ui
|
||||||
ROMInfo.ui
|
ROMInfo.ui
|
||||||
|
SaveConverter.ui
|
||||||
SensorView.ui
|
SensorView.ui
|
||||||
SettingsView.ui
|
SettingsView.ui
|
||||||
ShaderSelector.ui
|
ShaderSelector.ui
|
||||||
|
|
|
@ -14,9 +14,6 @@
|
||||||
#ifdef M_CORE_GBA
|
#ifdef M_CORE_GBA
|
||||||
#include <mgba/gba/core.h>
|
#include <mgba/gba/core.h>
|
||||||
#endif
|
#endif
|
||||||
#ifdef M_CORE_GB
|
|
||||||
#include <mgba/gb/core.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <mgba/core/core.h>
|
#include <mgba/core/core.h>
|
||||||
#include <mgba-util/vfs.h>
|
#include <mgba-util/vfs.h>
|
||||||
|
@ -31,57 +28,6 @@ void CoreManager::setMultiplayerController(MultiplayerController* multiplayer) {
|
||||||
m_multiplayer = 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) {
|
CoreController* CoreManager::loadGame(const QString& path) {
|
||||||
QFileInfo info(path);
|
QFileInfo info(path);
|
||||||
if (!info.isReadable()) {
|
if (!info.isReadable()) {
|
||||||
|
|
|
@ -9,8 +9,6 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include <mgba/core/serialize.h>
|
|
||||||
|
|
||||||
struct mCoreConfig;
|
struct mCoreConfig;
|
||||||
struct VFile;
|
struct VFile;
|
||||||
|
|
||||||
|
@ -27,8 +25,6 @@ public:
|
||||||
void setMultiplayerController(MultiplayerController*);
|
void setMultiplayerController(MultiplayerController*);
|
||||||
void setPreload(bool preload) { m_preload = preload; }
|
void setPreload(bool preload) { m_preload = preload; }
|
||||||
|
|
||||||
static QByteArray getExtdata(const QString& filename, mStateExtdataTag extdataType);
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
CoreController* loadGame(const QString& path);
|
CoreController* loadGame(const QString& path);
|
||||||
CoreController* loadGame(VFile* vf, const QString& path, const QString& base);
|
CoreController* loadGame(VFile* vf, const QString& path, const QString& base);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -49,6 +49,7 @@
|
||||||
#include "PrinterView.h"
|
#include "PrinterView.h"
|
||||||
#include "ReportView.h"
|
#include "ReportView.h"
|
||||||
#include "ROMInfo.h"
|
#include "ROMInfo.h"
|
||||||
|
#include "SaveConverter.h"
|
||||||
#include "SensorView.h"
|
#include "SensorView.h"
|
||||||
#include "ShaderSelector.h"
|
#include "ShaderSelector.h"
|
||||||
#include "ShortcutController.h"
|
#include "ShortcutController.h"
|
||||||
|
@ -1209,6 +1210,8 @@ void Window::setupMenu(QMenuBar* menubar) {
|
||||||
|
|
||||||
#ifdef M_CORE_GBA
|
#ifdef M_CORE_GBA
|
||||||
m_actions.addSeparator("file");
|
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");
|
Action* importShark = addGameAction(tr("Import GameShark Save..."), "importShark", this, &Window::importSharkport, "file");
|
||||||
m_platformActions.insert(mPLATFORM_GBA, importShark);
|
m_platformActions.insert(mPLATFORM_GBA, importShark);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,13 @@
|
||||||
|
|
||||||
namespace QGBA {
|
namespace QGBA {
|
||||||
|
|
||||||
|
enum class Endian {
|
||||||
|
NONE = 0b00,
|
||||||
|
BIG = 0b01,
|
||||||
|
LITTLE = 0b10,
|
||||||
|
UNKNOWN = 0b11
|
||||||
|
};
|
||||||
|
|
||||||
QString niceSizeFormat(size_t filesize);
|
QString niceSizeFormat(size_t filesize);
|
||||||
QString nicePlatformFormat(mPlatform platform);
|
QString nicePlatformFormat(mPlatform platform);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue