all repos — mgba @ b92bb30a722a87a7114461e1ad634367c8520457

mGBA Game Boy Advance Emulator

src/platform/qt/SaveConverter.cpp (view raw)

  1/* Copyright (c) 2013-2021 Jeffrey Pfau
  2 *
  3 * This Source Code Form is subject to the terms of the Mozilla Public
  4 * License, v. 2.0. If a copy of the MPL was not distributed with this
  5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  6#include "SaveConverter.h"
  7
  8#include <QMessageBox>
  9
 10#include "GBAApp.h"
 11#include "LogController.h"
 12#include "VFileDevice.h"
 13#include "utils.h"
 14
 15#ifdef M_CORE_GBA
 16#include <mgba/gba/core.h>
 17#include <mgba/internal/gba/serialize.h>
 18#endif
 19#ifdef M_CORE_GB
 20#include <mgba/gb/core.h>
 21#include <mgba/internal/gb/serialize.h>
 22#endif
 23
 24#include <mgba-util/memory.h>
 25#include <mgba-util/vfs.h>
 26
 27using namespace QGBA;
 28
 29SaveConverter::SaveConverter(std::shared_ptr<CoreController> controller, QWidget* parent)
 30	: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
 31	, m_controller(controller)
 32{
 33	m_ui.setupUi(this);
 34
 35	connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes);
 36	connect(m_ui.inputBrowse, &QAbstractButton::clicked, this, [this]() {
 37		// TODO: Add gameshark saves here too
 38		QStringList formats{"*.sav", "*.sgm", "*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9"};
 39		QString filter = tr("Save games and save states (%1)").arg(formats.join(QChar(' ')));
 40		QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save game or save state"), filter);
 41		if (!filename.isEmpty()) {
 42			m_ui.inputFile->setText(filename);
 43			refreshInputTypes();
 44		}
 45	});
 46	connect(m_ui.inputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::refreshOutputTypes);
 47
 48	connect(m_ui.outputFile, &QLineEdit::textEdited, this, &SaveConverter::checkCanConvert);
 49	connect(m_ui.outputBrowse, &QAbstractButton::clicked, this, [this]() {
 50		// TODO: Add gameshark saves here too
 51		QStringList formats{"*.sav", "*.sgm"};
 52		QString filter = tr("Save games (%1)").arg(formats.join(QChar(' ')));
 53		QString filename = GBAApp::app()->getSaveFileName(this, tr("Select save game"), filter);
 54		if (!filename.isEmpty()) {
 55			m_ui.outputFile->setText(filename);
 56			checkCanConvert();
 57		}
 58	});
 59	connect(m_ui.outputType, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &SaveConverter::checkCanConvert);
 60	connect(this, &QDialog::accepted, this, &SaveConverter::convert);
 61
 62	refreshInputTypes();
 63	m_ui.buttonBox->button(QDialogButtonBox::Save)->setDisabled(true);
 64}
 65
 66void SaveConverter::convert() {
 67	if (m_validSaves.isEmpty() || m_validOutputs.isEmpty()) {
 68		return;
 69	}
 70	const AnnotatedSave& input = m_validSaves[m_ui.inputType->currentIndex()];
 71	const AnnotatedSave& output = m_validOutputs[m_ui.outputType->currentIndex()];
 72	QByteArray converted = input.convertTo(output);
 73	if (converted.isEmpty()) {
 74		QMessageBox* failure = new QMessageBox(QMessageBox::Warning, tr("Conversion failed"), tr("Failed to convert the save game. This is probably a bug."),
 75		                                       QMessageBox::Ok, this, Qt::Sheet);
 76		failure->setAttribute(Qt::WA_DeleteOnClose);
 77		failure->show();
 78		return;
 79	}
 80	QFile out(m_ui.outputFile->text());
 81	out.open(QIODevice::WriteOnly | QIODevice::Truncate);
 82	out.write(converted);
 83	out.close();
 84}
 85
 86void SaveConverter::refreshInputTypes() {
 87	m_validSaves.clear();
 88	m_ui.inputType->clear();
 89	if (m_ui.inputFile->text().isEmpty()) {
 90		m_ui.inputType->addItem(tr("No file selected"));	
 91		m_ui.inputType->setEnabled(false);
 92		return;
 93	}
 94
 95	std::shared_ptr<VFileDevice> vf = std::make_shared<VFileDevice>(m_ui.inputFile->text(), QIODevice::ReadOnly);
 96	if (!vf->isOpen()) {
 97		m_ui.inputType->addItem(tr("Could not open file"));	
 98		m_ui.inputType->setEnabled(false);
 99		return;
100	}
101	
102	detectFromSavestate(*vf);
103	detectFromSize(vf);
104
105	for (const auto& save : m_validSaves) {
106		m_ui.inputType->addItem(save);
107	}
108	if (m_validSaves.count()) {
109		m_ui.inputType->setEnabled(true);
110	} else {
111		m_ui.inputType->addItem(tr("No valid formats found"));	
112		m_ui.inputType->setEnabled(false);
113	}
114}
115
116void SaveConverter::refreshOutputTypes() {
117	m_ui.outputType->clear();
118	if (m_validSaves.isEmpty()) {
119		m_ui.outputType->addItem(tr("Please select a valid input file"));
120		m_ui.outputType->setEnabled(false);
121		return;
122	}
123	m_validOutputs = m_validSaves[m_ui.inputType->currentIndex()].possibleConversions();
124	for (const auto& save : m_validOutputs) {
125		m_ui.outputType->addItem(save);
126	}
127	if (m_validOutputs.count()) {
128		m_ui.outputType->setEnabled(true);
129	} else {
130		m_ui.outputType->addItem(tr("No valid conversions found"));	
131		m_ui.outputType->setEnabled(false);
132	}
133	checkCanConvert();
134}
135
136void SaveConverter::checkCanConvert() {
137	QAbstractButton* button = m_ui.buttonBox->button(QDialogButtonBox::Save);
138	if (m_ui.inputFile->text().isEmpty()) {
139		button->setEnabled(false);
140		return;
141	}
142	if (m_ui.outputFile->text().isEmpty()) {
143		button->setEnabled(false);
144		return;
145	}
146	if (!m_ui.inputType->isEnabled()) {
147		button->setEnabled(false);
148		return;
149	}
150	if (!m_ui.outputType->isEnabled()) {
151		button->setEnabled(false);
152		return;
153	}
154	button->setEnabled(true);
155}
156
157void SaveConverter::detectFromSavestate(VFile* vf) {
158	mPlatform platform = getStatePlatform(vf);
159	if (platform == mPLATFORM_NONE) {
160		return;
161	}
162
163	QByteArray extSavedata = getExtdata(vf, platform, EXTDATA_SAVEDATA);
164	if (!extSavedata.size()) {
165		return;
166	}
167
168	QByteArray state = getState(vf, platform);
169	AnnotatedSave save{platform, std::make_shared<VFileDevice>(extSavedata)};
170	switch (platform) {
171#ifdef M_CORE_GBA
172	case mPLATFORM_GBA:
173		save.gba.type = static_cast<SavedataType>(state.at(offsetof(GBASerializedState, savedata.type)));
174		if (save.gba.type == SAVEDATA_EEPROM || save.gba.type == SAVEDATA_EEPROM512) {
175			save.endianness = Endian::LITTLE;
176		}
177		break;
178#endif
179#ifdef M_CORE_GB
180	case mPLATFORM_GB:
181		// GB savestates don't store the MBC type...should probably fix that
182		save.gb.type = GB_MBC_AUTODETECT;
183		if (state.size() == 0x100) {
184			// MBC2 packed save
185			save.endianness = Endian::LITTLE;
186			save.gb.type = GB_MBC2;
187		}
188		break;
189#endif
190	default:
191		break;
192	}
193	m_validSaves.append(save);
194}
195
196void SaveConverter::detectFromSize(std::shared_ptr<VFileDevice> vf) {
197#ifdef M_CORE_GBA
198	switch (vf->size()) {
199	case SIZE_CART_SRAM:
200		m_validSaves.append(AnnotatedSave{SAVEDATA_SRAM, vf});
201		break;
202	case SIZE_CART_FLASH512:
203		m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH512, vf});
204		break;
205	case SIZE_CART_FLASH1M:
206		m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, vf});
207		break;
208	case SIZE_CART_EEPROM:
209		m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::LITTLE});
210		m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, vf, Endian::BIG});
211		break;
212	case SIZE_CART_EEPROM512:
213		m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::LITTLE});
214		m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, vf, Endian::BIG});
215		break;
216	}
217#endif
218
219#ifdef M_CORE_GB
220	switch (vf->size()) {
221	case 0x800:
222	case 0x82C:
223	case 0x830:
224	case 0x2000:
225	case 0x202C:
226	case 0x2030:
227	case 0x8000:
228	case 0x802C:
229	case 0x8030:
230	case 0x10000:
231	case 0x1002C:
232	case 0x10030:
233	case 0x20000:
234	case 0x2002C:
235	case 0x20030:
236		m_validSaves.append(AnnotatedSave{GB_MBC_AUTODETECT, vf});
237		break;
238	case 0x100:
239		m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::LITTLE});
240		m_validSaves.append(AnnotatedSave{GB_MBC2, vf, Endian::BIG});
241		break;
242	case 0x200:
243		m_validSaves.append(AnnotatedSave{GB_MBC2, vf});
244		break;
245	case GB_SIZE_MBC6_FLASH: // Flash only
246	case GB_SIZE_MBC6_FLASH + 0x8000: // Concatenated SRAM and flash
247		m_validSaves.append(AnnotatedSave{GB_MBC6, vf});
248		break;
249	case 0x20:
250		m_validSaves.append(AnnotatedSave{GB_TAMA5, vf});
251		break;
252	}
253#endif
254}
255
256mPlatform SaveConverter::getStatePlatform(VFile* vf) {
257	uint32_t magic;
258	void* state = nullptr;
259	struct mCore* core = nullptr;
260	mPlatform platform = mPLATFORM_NONE;
261#ifdef M_CORE_GBA
262	if (platform == mPLATFORM_NONE) {
263		core = GBACoreCreate();
264		core->init(core);
265		state = mCoreExtractState(core, vf, nullptr);
266		core->deinit(core);
267		if (state) {
268			LOAD_32LE(magic, 0, state);
269			if (magic - GBA_SAVESTATE_MAGIC <= GBA_SAVESTATE_VERSION) {
270				platform = mPLATFORM_GBA;
271			}
272			mappedMemoryFree(state, core->stateSize(core));
273		}
274	}
275#endif
276#ifdef M_CORE_GB
277	if (platform == mPLATFORM_NONE) {
278		core = GBCoreCreate();
279		core->init(core);
280		state = mCoreExtractState(core, vf, nullptr);
281		core->deinit(core);
282		if (state) {
283			LOAD_32LE(magic, 0, state);
284			if (magic - GB_SAVESTATE_MAGIC <= GB_SAVESTATE_VERSION) {
285				platform = mPLATFORM_GB;
286			}
287			mappedMemoryFree(state, core->stateSize(core));
288		}
289	}
290#endif
291
292	return platform;
293}
294
295QByteArray SaveConverter::getState(VFile* vf, mPlatform platform) {
296	QByteArray bytes;
297	struct mCore* core = mCoreCreate(platform);
298	core->init(core);
299	void* state = mCoreExtractState(core, vf, nullptr);
300	if (state) {
301		size_t size = core->stateSize(core);
302		bytes = QByteArray::fromRawData(static_cast<const char*>(state), size);
303		bytes.data(); // Trigger a deep copy before we delete the backing
304		mappedMemoryFree(state, size);
305	}
306	core->deinit(core);
307	return bytes;
308}
309
310QByteArray SaveConverter::getExtdata(VFile* vf, mPlatform platform, mStateExtdataTag extdataType) {
311	mStateExtdata extdata;
312	mStateExtdataInit(&extdata);
313	QByteArray bytes;
314	struct mCore* core = mCoreCreate(platform);
315	core->init(core);
316	if (mCoreExtractExtdata(core, vf, &extdata)) {
317		mStateExtdataItem extitem;
318		if (mStateExtdataGet(&extdata, extdataType, &extitem) && extitem.size) {
319			bytes = QByteArray::fromRawData(static_cast<const char*>(extitem.data), extitem.size);
320			bytes.data(); // Trigger a deep copy before we delete the backing
321		}
322	}
323	core->deinit(core);
324	mStateExtdataDeinit(&extdata);
325	return bytes;
326}
327
328SaveConverter::AnnotatedSave::AnnotatedSave()
329	: savestate(false)
330	, platform(mPLATFORM_NONE)
331	, size(0)
332	, backing()
333	, endianness(Endian::NONE)
334{
335}
336
337SaveConverter::AnnotatedSave::AnnotatedSave(mPlatform platform, std::shared_ptr<VFileDevice> vf, Endian endianness)
338	: savestate(true)
339	, platform(platform)
340	, size(vf->size())
341	, backing(vf)
342	, endianness(endianness)
343{
344}
345
346#ifdef M_CORE_GBA
347SaveConverter::AnnotatedSave::AnnotatedSave(SavedataType type, std::shared_ptr<VFileDevice> vf, Endian endianness)
348	: savestate(false)
349	, platform(mPLATFORM_GBA)
350	, size(vf->size())
351	, backing(vf)
352	, endianness(endianness)
353	, gba({type})
354{
355}
356#endif
357
358#ifdef M_CORE_GB
359SaveConverter::AnnotatedSave::AnnotatedSave(GBMemoryBankControllerType type, std::shared_ptr<VFileDevice> vf, Endian endianness)
360	: savestate(false)
361	, platform(mPLATFORM_GB)
362	, size(vf->size())
363	, backing(vf)
364	, endianness(endianness)
365	, gb({type})
366{
367}
368#endif
369
370SaveConverter::AnnotatedSave SaveConverter::AnnotatedSave::asRaw() const {
371	AnnotatedSave raw;
372	raw.platform = platform;
373	raw.size = size;
374	raw.endianness = endianness;
375	switch (platform) {
376#ifdef M_CORE_GBA
377	case mPLATFORM_GBA:
378		raw.gba = gba;
379		break;
380#endif
381#ifdef M_CORE_GB
382	case mPLATFORM_GB:
383		raw.gb = gb;
384		break;
385#endif
386	default:
387		break;
388	}
389	return raw;
390}
391
392SaveConverter::AnnotatedSave::operator QString() const {
393	QString sizeStr(niceSizeFormat(size));
394	QString typeFormat("%1");
395	QString endianStr;
396	QString saveType;
397	QString format = QCoreApplication::translate("SaveConverter", "%1 %2 save game");
398
399	switch (endianness) {
400	case Endian::LITTLE:
401		endianStr = QCoreApplication::translate("SaveConverter", "little endian");
402		break;
403	case Endian::BIG:
404		endianStr = QCoreApplication::translate("SaveConverter", "big endian");
405		break;
406	default:
407		break;
408	}
409
410	switch (platform) {
411#ifdef M_CORE_GBA
412	case mPLATFORM_GBA:
413		switch (gba.type) {
414		case SAVEDATA_SRAM:
415			typeFormat = QCoreApplication::translate("SaveConverter", "SRAM");
416			break;
417		case SAVEDATA_FLASH512:
418		case SAVEDATA_FLASH1M:
419			typeFormat = QCoreApplication::translate("SaveConverter", "%1 flash");
420			break;
421		case SAVEDATA_EEPROM:
422		case SAVEDATA_EEPROM512:
423			typeFormat = QCoreApplication::translate("SaveConverter", "%1 EEPROM");
424			break;
425		default:
426			break;
427		}
428		break;
429#endif
430#ifdef M_CORE_GB
431	case mPLATFORM_GB:
432		switch (gb.type) {
433		case GB_MBC_AUTODETECT:
434			if (size & 0xFF) {
435				typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM + RTC");
436			} else {
437				typeFormat = QCoreApplication::translate("SaveConverter", "%1 SRAM");				
438			}
439			break;
440		case GB_MBC2:
441			if (size == 0x100) {
442				typeFormat = QCoreApplication::translate("SaveConverter", "packed MBC2");
443			} else {
444				typeFormat = QCoreApplication::translate("SaveConverter", "unpacked MBC2");				
445			}
446			break;
447		case GB_MBC6:
448			if (size == GB_SIZE_MBC6_FLASH) {
449				typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 flash");
450			} else if (size > GB_SIZE_MBC6_FLASH) {
451				typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 combined SRAM + flash");				
452			} else {
453				typeFormat = QCoreApplication::translate("SaveConverter", "MBC6 SRAM");
454			}
455			break;
456		case GB_TAMA5:
457			typeFormat = QCoreApplication::translate("SaveConverter", "TAMA5");
458			break;
459		default:
460			break;
461		}
462		break;
463#endif
464	default:
465		break;
466	}
467	saveType = typeFormat.arg(sizeStr);
468	if (!endianStr.isEmpty()) {
469		saveType = QCoreApplication::translate("SaveConverter", "%1 (%2)").arg(saveType).arg(endianStr);
470	}
471	if (savestate) {
472		format = QCoreApplication::translate("SaveConverter", "%1 save state with embedded %2 save game");
473	}
474	return format.arg(nicePlatformFormat(platform)).arg(saveType);
475}
476
477bool SaveConverter::AnnotatedSave::operator==(const AnnotatedSave& other) const {
478	if (other.savestate != savestate || other.platform != platform || other.size != size || other.endianness != endianness) {
479		return false;
480	}
481	switch (platform) {
482#ifdef M_CORE_GBA
483	case mPLATFORM_GBA:
484		if (other.gba.type != gba.type) {
485			return false;
486		}
487		break;
488#endif
489#ifdef M_CORE_GB
490	case mPLATFORM_GB:
491		if (other.gb.type != gb.type) {
492			return false;
493		}
494		break;
495#endif
496	default:
497		break;
498	}
499	return true;
500}
501
502QList<SaveConverter::AnnotatedSave> SaveConverter::AnnotatedSave::possibleConversions() const {
503	QList<AnnotatedSave> possible;
504	AnnotatedSave same = asRaw();
505	same.backing.reset();
506	same.savestate = false;
507
508	if (savestate) {
509		possible.append(same);
510	}
511
512
513	AnnotatedSave endianSwapped = same;
514	switch (endianness) {
515	case Endian::LITTLE:
516		endianSwapped.endianness = Endian::BIG;
517		possible.append(endianSwapped);
518		break;
519	case Endian::BIG:
520		endianSwapped.endianness = Endian::LITTLE;
521		possible.append(endianSwapped);
522		break;
523	default:
524		break;
525	}
526
527	switch (platform) {
528#ifdef M_CORE_GB
529	case mPLATFORM_GB:
530		switch (gb.type) {
531		case GB_MBC2:
532			if (size == 0x100) {
533				AnnotatedSave unpacked = same;
534				unpacked.size = 0x200;
535				unpacked.endianness = Endian::NONE;
536				possible.append(unpacked);
537			} else {
538				AnnotatedSave packed = same;
539				packed.size = 0x100;
540				packed.endianness = Endian::LITTLE;
541				possible.append(packed);
542				packed.endianness = Endian::BIG;
543				possible.append(packed);
544			}
545			break;
546		case GB_MBC6:
547			if (size > GB_SIZE_MBC6_FLASH) {
548				AnnotatedSave separated = same;
549				separated.size = size - GB_SIZE_MBC6_FLASH;
550				possible.append(separated);
551				separated.size = GB_SIZE_MBC6_FLASH;
552				possible.append(separated);
553			}
554			break;
555		default:
556			break;
557		}
558		break;
559#endif
560	default:
561		break;
562	}
563
564	return possible;
565}
566
567QByteArray SaveConverter::AnnotatedSave::convertTo(const SaveConverter::AnnotatedSave& target) const {
568	QByteArray converted;
569	QByteArray buffer;
570	backing->seek(0);
571	if (target == asRaw()) {
572		return backing->readAll();
573	}
574
575	if (platform != target.platform) {
576		LOG(QT, ERROR) << tr("Cannot convert save games between platforms");
577		return {};
578	}
579
580	switch (platform) {
581#ifdef M_CORE_GBA
582	case mPLATFORM_GBA:
583		switch (gba.type) {
584		case SAVEDATA_EEPROM:
585		case SAVEDATA_EEPROM512:
586			converted.resize(target.size);
587			buffer = backing->readAll();
588			for (int i = 0; i < size; i += 8) {
589				uint64_t word;
590				const uint64_t* in = reinterpret_cast<const uint64_t*>(buffer.constData());
591				uint64_t* out = reinterpret_cast<uint64_t*>(converted.data());
592				LOAD_64LE(word, i, in);
593				STORE_64BE(word, i, out);
594			}
595			break;
596		default:
597			break;
598		}
599		break;
600#endif
601#ifdef M_CORE_GB
602	case mPLATFORM_GB:
603		switch (gb.type) {
604		case GB_MBC2:
605			converted.reserve(target.size);
606			buffer = backing->readAll();
607			if (size == 0x100 && target.size == 0x200) {
608				if (endianness == Endian::LITTLE) {
609					for (uint8_t byte : buffer) {
610						converted.append(0xF0 | (byte & 0xF));
611						converted.append(0xF0 | (byte >> 4));
612					}
613				} else if (endianness == Endian::BIG) {
614					for (uint8_t byte : buffer) {
615						converted.append(0xF0 | (byte >> 4));
616						converted.append(0xF0 | (byte & 0xF));
617					}
618				}
619			} else if (size == 0x200 && target.size == 0x100) {
620				uint8_t byte;
621				if (target.endianness == Endian::LITTLE) {
622					for (int i = 0; i < target.size; ++i) {
623						byte = buffer[i * 2] & 0xF;
624						byte |= (buffer[i * 2 + 1] & 0xF) << 4;
625						converted.append(byte);
626					}
627				} else if (target.endianness == Endian::BIG) {
628					for (int i = 0; i < target.size; ++i) {
629						byte = (buffer[i * 2] & 0xF) << 4;
630						byte |= buffer[i * 2 + 1] & 0xF;
631						converted.append(byte);
632					}
633				}
634			} else if (size == 0x100 && target.size == 0x100) {
635				for (uint8_t byte : buffer) {
636					converted.append((byte >> 4) | (byte << 4));
637				}
638			}
639			break;
640		case GB_MBC6:
641			if (size == target.size + GB_SIZE_MBC6_FLASH) {
642				converted = backing->read(target.size);
643			} else if (target.size == GB_SIZE_MBC6_FLASH) {
644				backing->seek(size - GB_SIZE_MBC6_FLASH);
645				converted = backing->read(GB_SIZE_MBC6_FLASH);
646			}
647			break;
648		default:
649			break;
650		}
651		break;
652#endif
653	default:
654		break;
655	}
656
657	return converted;
658}