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->skip(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}