From a1641f7fae9d53829ef3084be9da3387d6320c0d Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 21 Dec 2021 20:36:18 -0800 Subject: [PATCH] GBA Savedata: Add GSV importing --- CHANGES | 3 +- include/mgba/internal/gba/sharkport.h | 6 +- src/gba/sharkport.c | 180 ++++++++++++++++++++------ src/platform/qt/CoreController.cpp | 1 + src/platform/qt/SaveConverter.cpp | 32 ++++- src/platform/qt/SaveConverter.h | 3 +- src/platform/qt/Window.cpp | 2 +- 7 files changed, 180 insertions(+), 47 deletions(-) diff --git a/CHANGES b/CHANGES index 2d131b40b..398df1445 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,7 @@ Features: - Support for 64 kiB SRAM saves used in some bootlegs - Discord Rich Presence now supports time elapsed - Additional scaling shaders + - Support for GameShark Advance SP (.gsv) save file importing Emulation fixes: - ARM7: Fix unsigned multiply timing - GB Memory: Add cursory cartridge open bus emulation (fixes mgba.io/i/2032) @@ -25,7 +26,7 @@ Misc: - Qt: Rearrange menus some - Qt: Clean up cheats dialog - Qt: Only set default controller bindings if loading fails (fixes mgba.io/i/799) - - Qt: Save converter now supports importing SharkPort saves + - Qt: Save converter now supports importing GameShark Advance saves 0.9.3: (2021-12-17) Emulation fixes: diff --git a/include/mgba/internal/gba/sharkport.h b/include/mgba/internal/gba/sharkport.h index 7c689d7b7..c6b986707 100644 --- a/include/mgba/internal/gba/sharkport.h +++ b/include/mgba/internal/gba/sharkport.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2013-2015 Jeffrey Pfau +/* 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 @@ -18,6 +18,10 @@ void* GBASavedataSharkPortGetPayload(struct VFile* vf, size_t* size, uint8_t* he bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChecksum); bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf); +int GBASavedataGSVPayloadSize(struct VFile* vf); +void* GBASavedataGSVGetPayload(struct VFile* vf, size_t* size, uint8_t* ident, bool testChecksum); +bool GBASavedataImportGSV(struct GBA* gba, struct VFile* vf, bool testChecksum); + CXX_GUARD_END #endif diff --git a/src/gba/sharkport.c b/src/gba/sharkport.c index 3de45e1c5..9922f425f 100644 --- a/src/gba/sharkport.c +++ b/src/gba/sharkport.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2013-2015 Jeffrey Pfau +/* 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 @@ -10,6 +10,50 @@ #include static const char* const SHARKPORT_HEADER = "SharkPortSave"; +static const char* const GSV_HEADER = "ADVSAVEG"; +static const char* const GSV_FOOTER = "xV4\x12"; +static const int GSV_IDENT_OFFSET = 0xC; +static const int GSV_PAYLOAD_OFFSET = 0x430; + +static bool _importSavedata(struct GBA* gba, void* payload, size_t size) { + bool success = false; + switch (gba->memory.savedata.type) { + case SAVEDATA_FLASH512: + if (size > SIZE_CART_FLASH512) { + GBASavedataForceType(&gba->memory.savedata, SAVEDATA_FLASH1M); + } + // Fall through + default: + if (size > GBASavedataSize(&gba->memory.savedata)) { + size = GBASavedataSize(&gba->memory.savedata); + } + break; + case SAVEDATA_FORCE_NONE: + case SAVEDATA_AUTODETECT: + goto cleanup; + } + + if (size == SIZE_CART_EEPROM || size == SIZE_CART_EEPROM512) { + size_t i; + for (i = 0; i < size; i += 8) { + uint32_t lo, hi; + LOAD_32BE(lo, i, payload); + LOAD_32BE(hi, i + 4, payload); + STORE_32LE(hi, i, gba->memory.savedata.data); + STORE_32LE(lo, i + 4, gba->memory.savedata.data); + } + } else { + memcpy(gba->memory.savedata.data, payload, size); + } + if (gba->memory.savedata.vf) { + gba->memory.savedata.vf->sync(gba->memory.savedata.vf, gba->memory.savedata.data, size); + } + success = true; + +cleanup: + free(payload); + return success; +} int GBASavedataSharkPortPayloadSize(struct VFile* vf) { union { @@ -121,7 +165,6 @@ cleanup: return NULL; } - bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChecksum) { uint8_t buffer[0x1C]; uint8_t header[0x1C]; @@ -132,7 +175,6 @@ bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChec return false; } - bool success = false; struct GBACartridge* cart = (struct GBACartridge*) gba->memory.rom; memcpy(buffer, &cart->title, 16); buffer[0x10] = 0; @@ -148,46 +190,11 @@ bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChec buffer[0x1A] = 0; buffer[0x1B] = 0; if (memcmp(buffer, header, testChecksum ? 0x1C : 0xF) != 0) { - goto cleanup; + free(payload); + return false; } - switch (gba->memory.savedata.type) { - case SAVEDATA_FLASH512: - if (size > SIZE_CART_FLASH512) { - GBASavedataForceType(&gba->memory.savedata, SAVEDATA_FLASH1M); - } - // Fall through - default: - if (size > GBASavedataSize(&gba->memory.savedata)) { - size = GBASavedataSize(&gba->memory.savedata); - } - break; - case SAVEDATA_FORCE_NONE: - case SAVEDATA_AUTODETECT: - goto cleanup; - } - - - if (size == SIZE_CART_EEPROM || size == SIZE_CART_EEPROM512) { - size_t i; - for (i = 0; i < size; i += 8) { - uint32_t lo, hi; - LOAD_32BE(lo, i, payload); - LOAD_32BE(hi, i + 4, payload); - STORE_32LE(hi, i, gba->memory.savedata.data); - STORE_32LE(lo, i + 4, gba->memory.savedata.data); - } - } else { - memcpy(gba->memory.savedata.data, payload, size); - } - if (gba->memory.savedata.vf) { - gba->memory.savedata.vf->sync(gba->memory.savedata.vf, gba->memory.savedata.data, size); - } - success = true; - -cleanup: - free(payload); - return success; + return _importSavedata(gba, payload, size); } bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf) { @@ -269,7 +276,6 @@ bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf) { checksum += buffer.c[i] << (checksum % 24); } - if (gba->memory.savedata.type == SAVEDATA_EEPROM) { for (i = 0; i < size; ++i) { char byte = gba->memory.savedata.data[i ^ 7]; @@ -291,3 +297,93 @@ bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf) { return true; } + +int GBASavedataGSVPayloadSize(struct VFile* vf) { + union { + char c[8]; + int32_t i; + } buffer; + vf->seek(vf, 0, SEEK_SET); + if (vf->read(vf, &buffer.c, 8) < 8) { + return 0; + } + if (memcmp(GSV_HEADER, buffer.c, 8) != 0) { + return 0; + } + + // Skip the checksum + if (vf->read(vf, &buffer.i, 4) < 4) { + return 0; + } + + struct { + char name[12]; + int padding; + int type; + int unk[3]; + char description[0x400]; + char footer[4]; + } header; + + if (vf->read(vf, &header, sizeof(header)) < (ssize_t) sizeof(header)) { + return 0; + } + if (memcmp(GSV_FOOTER, header.footer, 4) != 0) { + return 0; + } + + int type; + LOAD_32(type, 0, &header.type); + switch (type) { + case 2: + return SIZE_CART_SRAM; + case 3: + return SIZE_CART_EEPROM512; + case 4: + return SIZE_CART_EEPROM; + case 5: + return SIZE_CART_FLASH512; + case 6: + return SIZE_CART_FLASH1M; // Unconfirmed + default: + return vf->size(vf) - GSV_PAYLOAD_OFFSET; + } +} + +void* GBASavedataGSVGetPayload(struct VFile* vf, size_t* osize, uint8_t* ident, bool testChecksum) { + int32_t size = GBASavedataGSVPayloadSize(vf); + if (!size || size > SIZE_CART_FLASH1M) { + return NULL; + } + + vf->seek(vf, GSV_IDENT_OFFSET, SEEK_SET); + if (ident && vf->read(vf, ident, 12) != 12) { + return NULL; + } + vf->seek(vf, GSV_PAYLOAD_OFFSET, SEEK_SET); + + int8_t* payload = malloc(size); + if (vf->read(vf, payload, size) != size) { + free(payload); + return NULL; + } + UNUSED(testChecksum); // The checksum format is currently unknown + *osize = size; + return payload; +} + +bool GBASavedataImportGSV(struct GBA* gba, struct VFile* vf, bool testChecksum) { + size_t size; + uint8_t ident[12]; + void* payload = GBASavedataGSVGetPayload(vf, &size, ident, testChecksum); + if (!payload) { + return false; + } + struct GBACartridge* cart = (struct GBACartridge*) gba->memory.rom; + if (memcmp(ident, cart->title, sizeof(ident)) != 0) { + free(payload); + return false; + } + + return _importSavedata(gba, payload, size); +} diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp index fd99cf4ee..d4715620d 100644 --- a/src/platform/qt/CoreController.cpp +++ b/src/platform/qt/CoreController.cpp @@ -852,6 +852,7 @@ void CoreController::importSharkport(const QString& path) { } Interrupter interrupter(this); GBASavedataImportSharkPort(static_cast(m_threadContext.core->board), vf, false); + GBASavedataImportGSV(static_cast(m_threadContext.core->board), vf, false); vf->close(vf); #endif } diff --git a/src/platform/qt/SaveConverter.cpp b/src/platform/qt/SaveConverter.cpp index 050da9be9..d904ed957 100644 --- a/src/platform/qt/SaveConverter.cpp +++ b/src/platform/qt/SaveConverter.cpp @@ -35,7 +35,7 @@ SaveConverter::SaveConverter(std::shared_ptr controller, QWidget connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes); connect(m_ui.inputBrowse, &QAbstractButton::clicked, this, [this]() { - QStringList formats{"*.sav", "*.sgm", "*.sps", "*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9", "*.xps"}; + QStringList formats{"*.gsv", "*.sav", "*.sgm", "*.sps", "*.ss0", "*.ss1", "*.ss2", "*.ss3", "*.ss4", "*.ss5", "*.ss6", "*.ss7", "*.ss8", "*.ss9", "*.xps"}; 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()) { @@ -256,6 +256,7 @@ void SaveConverter::detectFromSize(std::shared_ptr vf) { void SaveConverter::detectFromHeaders(std::shared_ptr vf) { const QByteArray sharkport("\xd\0\0\0SharkPortSave", 0x11); + const QByteArray gsv("ADVSAVEG", 8); QByteArray buffer; vf->seek(0); @@ -276,6 +277,32 @@ void SaveConverter::detectFromHeaders(std::shared_ptr vf) { } free(data); } + } else if (buffer.left(gsv.count()) == gsv) { + size_t size; + void* data = GBASavedataGSVGetPayload(*vf, &size, nullptr, false); + if (data) { + QByteArray bytes = QByteArray::fromRawData(static_cast(data), size); + bytes.data(); // Trigger a deep copy before we delete the backing + switch (size) { + case SIZE_CART_FLASH1M: + m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, std::make_shared(bytes), Endian::NONE, Container::GSV}); + break; + case SIZE_CART_FLASH512: + m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH512, std::make_shared(bytes), Endian::NONE, Container::GSV}); + m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, std::make_shared(bytes), Endian::NONE, Container::GSV}); + break; + case SIZE_CART_SRAM: + m_validSaves.append(AnnotatedSave{SAVEDATA_SRAM, std::make_shared(bytes.left(SIZE_CART_SRAM)), Endian::NONE, Container::GSV}); + break; + case SIZE_CART_EEPROM: + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, std::make_shared(bytes.left(SIZE_CART_EEPROM)), Endian::BIG, Container::GSV}); + break; + case SIZE_CART_EEPROM512: + m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, std::make_shared(bytes.left(SIZE_CART_EEPROM512)), Endian::BIG, Container::GSV}); + break; + } + free(data); + } } } @@ -501,6 +528,9 @@ SaveConverter::AnnotatedSave::operator QString() const { case Container::SHARKPORT: format = QCoreApplication::translate("SaveConverter", "%1 SharkPort %2 save game"); break; + case Container::GSV: + format = QCoreApplication::translate("SaveConverter", "%1 GameShark Advance SP %2 save game"); + break; case Container::NONE: break; } diff --git a/src/platform/qt/SaveConverter.h b/src/platform/qt/SaveConverter.h index 5a94c4804..6b9f3df62 100644 --- a/src/platform/qt/SaveConverter.h +++ b/src/platform/qt/SaveConverter.h @@ -59,7 +59,8 @@ private: enum class Container { NONE = 0, SAVESTATE, - SHARKPORT + SHARKPORT, + GSV }; struct AnnotatedSave { AnnotatedSave(); diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index fb85c8990..a57106152 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -492,7 +492,7 @@ void Window::loadCamImage() { } void Window::importSharkport() { - QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save"), tr("GameShark saves (*.sps *.xps)")); + QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save"), tr("GameShark saves (*.gsv *.sps *.xps)")); if (!filename.isEmpty()) { m_controller->importSharkport(filename); }