GBA Savedata: Add GSV importing

This commit is contained in:
Vicki Pfau 2021-12-21 20:36:18 -08:00
parent b5e94b0abb
commit a1641f7fae
7 changed files with 180 additions and 47 deletions

View File

@ -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:

View File

@ -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

View File

@ -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 <mgba-util/vfs.h>
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;
}
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 false;
}
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);
}

View File

@ -852,6 +852,7 @@ void CoreController::importSharkport(const QString& path) {
}
Interrupter interrupter(this);
GBASavedataImportSharkPort(static_cast<GBA*>(m_threadContext.core->board), vf, false);
GBASavedataImportGSV(static_cast<GBA*>(m_threadContext.core->board), vf, false);
vf->close(vf);
#endif
}

View File

@ -35,7 +35,7 @@ SaveConverter::SaveConverter(std::shared_ptr<CoreController> 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<VFileDevice> vf) {
void SaveConverter::detectFromHeaders(std::shared_ptr<VFileDevice> 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<VFileDevice> 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<const char*>(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<VFileDevice>(bytes), Endian::NONE, Container::GSV});
break;
case SIZE_CART_FLASH512:
m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH512, std::make_shared<VFileDevice>(bytes), Endian::NONE, Container::GSV});
m_validSaves.append(AnnotatedSave{SAVEDATA_FLASH1M, std::make_shared<VFileDevice>(bytes), Endian::NONE, Container::GSV});
break;
case SIZE_CART_SRAM:
m_validSaves.append(AnnotatedSave{SAVEDATA_SRAM, std::make_shared<VFileDevice>(bytes.left(SIZE_CART_SRAM)), Endian::NONE, Container::GSV});
break;
case SIZE_CART_EEPROM:
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM, std::make_shared<VFileDevice>(bytes.left(SIZE_CART_EEPROM)), Endian::BIG, Container::GSV});
break;
case SIZE_CART_EEPROM512:
m_validSaves.append(AnnotatedSave{SAVEDATA_EEPROM512, std::make_shared<VFileDevice>(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;
}

View File

@ -59,7 +59,8 @@ private:
enum class Container {
NONE = 0,
SAVESTATE,
SHARKPORT
SHARKPORT,
GSV
};
struct AnnotatedSave {
AnnotatedSave();

View File

@ -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);
}