mirror of https://github.com/mgba-emu/mgba.git
GBA Savedata: Add GSV importing
This commit is contained in:
parent
b5e94b0abb
commit
a1641f7fae
3
CHANGES
3
CHANGES
|
@ -10,6 +10,7 @@ Features:
|
||||||
- Support for 64 kiB SRAM saves used in some bootlegs
|
- Support for 64 kiB SRAM saves used in some bootlegs
|
||||||
- Discord Rich Presence now supports time elapsed
|
- Discord Rich Presence now supports time elapsed
|
||||||
- Additional scaling shaders
|
- Additional scaling shaders
|
||||||
|
- Support for GameShark Advance SP (.gsv) save file importing
|
||||||
Emulation fixes:
|
Emulation fixes:
|
||||||
- ARM7: Fix unsigned multiply timing
|
- ARM7: Fix unsigned multiply timing
|
||||||
- GB Memory: Add cursory cartridge open bus emulation (fixes mgba.io/i/2032)
|
- GB Memory: Add cursory cartridge open bus emulation (fixes mgba.io/i/2032)
|
||||||
|
@ -25,7 +26,7 @@ Misc:
|
||||||
- Qt: Rearrange menus some
|
- Qt: Rearrange menus some
|
||||||
- Qt: Clean up cheats dialog
|
- Qt: Clean up cheats dialog
|
||||||
- Qt: Only set default controller bindings if loading fails (fixes mgba.io/i/799)
|
- 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)
|
0.9.3: (2021-12-17)
|
||||||
Emulation fixes:
|
Emulation fixes:
|
||||||
|
|
|
@ -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
|
* 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
|
* 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 GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChecksum);
|
||||||
bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf);
|
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
|
CXX_GUARD_END
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -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
|
* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
@ -10,6 +10,50 @@
|
||||||
#include <mgba-util/vfs.h>
|
#include <mgba-util/vfs.h>
|
||||||
|
|
||||||
static const char* const SHARKPORT_HEADER = "SharkPortSave";
|
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) {
|
int GBASavedataSharkPortPayloadSize(struct VFile* vf) {
|
||||||
union {
|
union {
|
||||||
|
@ -121,7 +165,6 @@ cleanup:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChecksum) {
|
bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChecksum) {
|
||||||
uint8_t buffer[0x1C];
|
uint8_t buffer[0x1C];
|
||||||
uint8_t header[0x1C];
|
uint8_t header[0x1C];
|
||||||
|
@ -132,7 +175,6 @@ bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChec
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = false;
|
|
||||||
struct GBACartridge* cart = (struct GBACartridge*) gba->memory.rom;
|
struct GBACartridge* cart = (struct GBACartridge*) gba->memory.rom;
|
||||||
memcpy(buffer, &cart->title, 16);
|
memcpy(buffer, &cart->title, 16);
|
||||||
buffer[0x10] = 0;
|
buffer[0x10] = 0;
|
||||||
|
@ -148,46 +190,11 @@ bool GBASavedataImportSharkPort(struct GBA* gba, struct VFile* vf, bool testChec
|
||||||
buffer[0x1A] = 0;
|
buffer[0x1A] = 0;
|
||||||
buffer[0x1B] = 0;
|
buffer[0x1B] = 0;
|
||||||
if (memcmp(buffer, header, testChecksum ? 0x1C : 0xF) != 0) {
|
if (memcmp(buffer, header, testChecksum ? 0x1C : 0xF) != 0) {
|
||||||
goto cleanup;
|
free(payload);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (gba->memory.savedata.type) {
|
return _importSavedata(gba, payload, size);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf) {
|
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);
|
checksum += buffer.c[i] << (checksum % 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (gba->memory.savedata.type == SAVEDATA_EEPROM) {
|
if (gba->memory.savedata.type == SAVEDATA_EEPROM) {
|
||||||
for (i = 0; i < size; ++i) {
|
for (i = 0; i < size; ++i) {
|
||||||
char byte = gba->memory.savedata.data[i ^ 7];
|
char byte = gba->memory.savedata.data[i ^ 7];
|
||||||
|
@ -291,3 +297,93 @@ bool GBASavedataExportSharkPort(const struct GBA* gba, struct VFile* vf) {
|
||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -852,6 +852,7 @@ void CoreController::importSharkport(const QString& path) {
|
||||||
}
|
}
|
||||||
Interrupter interrupter(this);
|
Interrupter interrupter(this);
|
||||||
GBASavedataImportSharkPort(static_cast<GBA*>(m_threadContext.core->board), vf, false);
|
GBASavedataImportSharkPort(static_cast<GBA*>(m_threadContext.core->board), vf, false);
|
||||||
|
GBASavedataImportGSV(static_cast<GBA*>(m_threadContext.core->board), vf, false);
|
||||||
vf->close(vf);
|
vf->close(vf);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ SaveConverter::SaveConverter(std::shared_ptr<CoreController> controller, QWidget
|
||||||
|
|
||||||
connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes);
|
connect(m_ui.inputFile, &QLineEdit::textEdited, this, &SaveConverter::refreshInputTypes);
|
||||||
connect(m_ui.inputBrowse, &QAbstractButton::clicked, this, [this]() {
|
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 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);
|
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save game or save state"), filter);
|
||||||
if (!filename.isEmpty()) {
|
if (!filename.isEmpty()) {
|
||||||
|
@ -256,6 +256,7 @@ void SaveConverter::detectFromSize(std::shared_ptr<VFileDevice> vf) {
|
||||||
|
|
||||||
void SaveConverter::detectFromHeaders(std::shared_ptr<VFileDevice> vf) {
|
void SaveConverter::detectFromHeaders(std::shared_ptr<VFileDevice> vf) {
|
||||||
const QByteArray sharkport("\xd\0\0\0SharkPortSave", 0x11);
|
const QByteArray sharkport("\xd\0\0\0SharkPortSave", 0x11);
|
||||||
|
const QByteArray gsv("ADVSAVEG", 8);
|
||||||
QByteArray buffer;
|
QByteArray buffer;
|
||||||
|
|
||||||
vf->seek(0);
|
vf->seek(0);
|
||||||
|
@ -276,6 +277,32 @@ void SaveConverter::detectFromHeaders(std::shared_ptr<VFileDevice> vf) {
|
||||||
}
|
}
|
||||||
free(data);
|
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:
|
case Container::SHARKPORT:
|
||||||
format = QCoreApplication::translate("SaveConverter", "%1 SharkPort %2 save game");
|
format = QCoreApplication::translate("SaveConverter", "%1 SharkPort %2 save game");
|
||||||
break;
|
break;
|
||||||
|
case Container::GSV:
|
||||||
|
format = QCoreApplication::translate("SaveConverter", "%1 GameShark Advance SP %2 save game");
|
||||||
|
break;
|
||||||
case Container::NONE:
|
case Container::NONE:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,8 @@ private:
|
||||||
enum class Container {
|
enum class Container {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
SAVESTATE,
|
SAVESTATE,
|
||||||
SHARKPORT
|
SHARKPORT,
|
||||||
|
GSV
|
||||||
};
|
};
|
||||||
struct AnnotatedSave {
|
struct AnnotatedSave {
|
||||||
AnnotatedSave();
|
AnnotatedSave();
|
||||||
|
|
|
@ -492,7 +492,7 @@ void Window::loadCamImage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::importSharkport() {
|
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()) {
|
if (!filename.isEmpty()) {
|
||||||
m_controller->importSharkport(filename);
|
m_controller->importSharkport(filename);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue