mirror of https://github.com/mgba-emu/mgba.git
Qt: Support for multiple saves per game using .sa2, .sa3, etc.
This commit is contained in:
parent
16df8fe957
commit
667dffe515
1
CHANGES
1
CHANGES
|
@ -11,6 +11,7 @@ Features:
|
||||||
- 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
|
- Support for GameShark Advance SP (.gsv) save file importing
|
||||||
|
- Support for multiple saves per game using .sa2, .sa3, etc.
|
||||||
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)
|
||||||
|
|
|
@ -223,7 +223,13 @@ bool mCoreAutoloadSave(struct mCore* core) {
|
||||||
if (!core->dirs.save) {
|
if (!core->dirs.save) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return core->loadSave(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.save, ".sav", O_CREAT | O_RDWR));
|
int savePlayerId = 0;
|
||||||
|
char sav[16] = ".sav";
|
||||||
|
mCoreConfigGetIntValue(&core->config, "savePlayerId", &savePlayerId);
|
||||||
|
if (savePlayerId > 1) {
|
||||||
|
snprintf(sav, sizeof(sav), ".sa%i", savePlayerId);
|
||||||
|
}
|
||||||
|
return core->loadSave(core, mDirectorySetOpenSuffix(&core->dirs, core->dirs.save, sav, O_CREAT | O_RDWR));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mCoreAutoloadPatch(struct mCore* core) {
|
bool mCoreAutoloadPatch(struct mCore* core) {
|
||||||
|
@ -365,6 +371,7 @@ void mCoreLoadForeignConfig(struct mCore* core, const struct mCoreConfig* config
|
||||||
|
|
||||||
mCoreConfigCopyValue(&core->config, config, "cheatAutosave");
|
mCoreConfigCopyValue(&core->config, config, "cheatAutosave");
|
||||||
mCoreConfigCopyValue(&core->config, config, "cheatAutoload");
|
mCoreConfigCopyValue(&core->config, config, "cheatAutoload");
|
||||||
|
mCoreConfigCopyValue(&core->config, config, "savePlayerId");
|
||||||
|
|
||||||
core->loadConfig(core, config);
|
core->loadConfig(core, config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
#include "MultiplayerController.h"
|
#include "MultiplayerController.h"
|
||||||
#include "Override.h"
|
#include "Override.h"
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QMutexLocker>
|
#include <QMutexLocker>
|
||||||
|
|
||||||
#include <mgba/core/serialize.h>
|
#include <mgba/core/serialize.h>
|
||||||
|
@ -70,6 +72,7 @@ CoreController::CoreController(mCore* core, QObject* parent)
|
||||||
|
|
||||||
if (controller->m_multiplayer) {
|
if (controller->m_multiplayer) {
|
||||||
controller->m_multiplayer->attachGame(controller);
|
controller->m_multiplayer->attachGame(controller);
|
||||||
|
controller->updatePlayerSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaObject::invokeMethod(controller, "started");
|
QMetaObject::invokeMethod(controller, "started");
|
||||||
|
@ -285,6 +288,13 @@ void CoreController::loadConfig(ConfigController* config) {
|
||||||
mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "volume");
|
mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "volume");
|
||||||
mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "mute");
|
mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "mute");
|
||||||
|
|
||||||
|
int playerId = m_multiplayer->playerId(this) + 1;
|
||||||
|
QVariant savePlayerId = config->getOption("savePlayerId");
|
||||||
|
if (m_multiplayer->attached() < 2 && savePlayerId.canConvert<int>()) {
|
||||||
|
playerId = savePlayerId.toInt();
|
||||||
|
}
|
||||||
|
mCoreConfigSetOverrideIntValue(&m_threadContext.core->config, "savePlayerId", playerId);
|
||||||
|
|
||||||
QSize sizeBefore = screenDimensions();
|
QSize sizeBefore = screenDimensions();
|
||||||
m_activeBuffer.resize(256 * 224 * sizeof(color_t));
|
m_activeBuffer.resize(256 * 224 * sizeof(color_t));
|
||||||
m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast<color_t*>(m_activeBuffer.data()), sizeBefore.width());
|
m_threadContext.core->setVideoBuffer(m_threadContext.core, reinterpret_cast<color_t*>(m_activeBuffer.data()), sizeBefore.width());
|
||||||
|
@ -537,6 +547,41 @@ void CoreController::forceFastForward(bool enable) {
|
||||||
emit fastForwardChanged(enable || m_fastForward);
|
emit fastForwardChanged(enable || m_fastForward);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CoreController::changePlayer(int id) {
|
||||||
|
Interrupter interrupter(this);
|
||||||
|
int playerId = 0;
|
||||||
|
mCoreConfigGetIntValue(&m_threadContext.core->config, "savePlayerId", &playerId);
|
||||||
|
if (id == playerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
interrupter.resume();
|
||||||
|
|
||||||
|
QMessageBox* resetPrompt = new QMessageBox(QMessageBox::Question, tr("Reset the game?"),
|
||||||
|
tr("Most games will require a reset to load the new save. Do you want to reset now?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
||||||
|
connect(resetPrompt, &QMessageBox::buttonClicked, this, [this, resetPrompt, id](QAbstractButton* button) {
|
||||||
|
Interrupter interrupter(this);
|
||||||
|
switch (resetPrompt->standardButton(button)) {
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
case QMessageBox::Yes:
|
||||||
|
mCoreConfigSetOverrideIntValue(&m_threadContext.core->config, "savePlayerId", id);
|
||||||
|
m_resetActions.append([this]() {
|
||||||
|
updatePlayerSave();
|
||||||
|
});
|
||||||
|
interrupter.resume();
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
case QMessageBox::No:
|
||||||
|
mCoreConfigSetOverrideIntValue(&m_threadContext.core->config, "savePlayerId", id);
|
||||||
|
updatePlayerSave();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resetPrompt->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
resetPrompt->show();
|
||||||
|
}
|
||||||
|
|
||||||
void CoreController::overrideMute(bool override) {
|
void CoreController::overrideMute(bool override) {
|
||||||
m_mute = override;
|
m_mute = override;
|
||||||
|
|
||||||
|
@ -734,8 +779,23 @@ void CoreController::loadSave(const QString& path, bool temporary) {
|
||||||
m_threadContext.core->loadSave(m_threadContext.core, vf);
|
m_threadContext.core->loadSave(m_threadContext.core, vf);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (hasStarted()) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CoreController::loadSave(VFile* vf, bool temporary) {
|
||||||
|
m_resetActions.append([this, vf, temporary]() {
|
||||||
|
if (temporary) {
|
||||||
|
m_threadContext.core->loadTemporarySave(m_threadContext.core, vf);
|
||||||
|
} else {
|
||||||
|
m_threadContext.core->loadSave(m_threadContext.core, vf);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasStarted()) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CoreController::loadPatch(const QString& patchPath) {
|
void CoreController::loadPatch(const QString& patchPath) {
|
||||||
Interrupter interrupter(this);
|
Interrupter interrupter(this);
|
||||||
|
@ -1098,6 +1158,26 @@ void CoreController::finishFrame() {
|
||||||
QMetaObject::invokeMethod(this, "frameAvailable");
|
QMetaObject::invokeMethod(this, "frameAvailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CoreController::updatePlayerSave() {
|
||||||
|
int savePlayerId = 0;
|
||||||
|
mCoreConfigGetIntValue(&m_threadContext.core->config, "savePlayerId", &savePlayerId);
|
||||||
|
if (savePlayerId == 0 || m_multiplayer->attached() > 1) {
|
||||||
|
savePlayerId = m_multiplayer->playerId(this) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString saveSuffix;
|
||||||
|
if (savePlayerId < 2) {
|
||||||
|
saveSuffix = QLatin1String(".sav");
|
||||||
|
} else {
|
||||||
|
saveSuffix = QString(".sa%1").arg(savePlayerId);
|
||||||
|
}
|
||||||
|
QByteArray saveSuffixBin(saveSuffix.toUtf8());
|
||||||
|
VFile* save = mDirectorySetOpenSuffix(&m_threadContext.core->dirs, m_threadContext.core->dirs.save, saveSuffixBin.constData(), O_CREAT | O_RDWR);
|
||||||
|
if (save) {
|
||||||
|
m_threadContext.core->loadSave(m_threadContext.core, save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CoreController::updateFastForward() {
|
void CoreController::updateFastForward() {
|
||||||
// If we have "Fast forward" checked in the menu (m_fastForwardForced)
|
// If we have "Fast forward" checked in the menu (m_fastForwardForced)
|
||||||
// or are holding the fast forward button (m_fastForward):
|
// or are holding the fast forward button (m_fastForward):
|
||||||
|
|
|
@ -142,6 +142,8 @@ public slots:
|
||||||
void setFastForward(bool);
|
void setFastForward(bool);
|
||||||
void forceFastForward(bool);
|
void forceFastForward(bool);
|
||||||
|
|
||||||
|
void changePlayer(int id);
|
||||||
|
|
||||||
void overrideMute(bool);
|
void overrideMute(bool);
|
||||||
|
|
||||||
void loadState(int slot = 0);
|
void loadState(int slot = 0);
|
||||||
|
@ -154,6 +156,7 @@ public slots:
|
||||||
void saveBackupState();
|
void saveBackupState();
|
||||||
|
|
||||||
void loadSave(const QString&, bool temporary);
|
void loadSave(const QString&, bool temporary);
|
||||||
|
void loadSave(VFile*, bool temporary);
|
||||||
void loadPatch(const QString&);
|
void loadPatch(const QString&);
|
||||||
void scanCard(const QString&);
|
void scanCard(const QString&);
|
||||||
void replaceGame(const QString&);
|
void replaceGame(const QString&);
|
||||||
|
@ -227,6 +230,8 @@ private:
|
||||||
int updateAutofire();
|
int updateAutofire();
|
||||||
void finishFrame();
|
void finishFrame();
|
||||||
|
|
||||||
|
void updatePlayerSave();
|
||||||
|
|
||||||
void updateFastForward();
|
void updateFastForward();
|
||||||
|
|
||||||
void updateROMInfo();
|
void updateROMInfo();
|
||||||
|
|
|
@ -1218,6 +1218,23 @@ void Window::setupMenu(QMenuBar* menubar) {
|
||||||
m_platformActions.insert(mPLATFORM_GBA, exportShark);
|
m_platformActions.insert(mPLATFORM_GBA, exportShark);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
m_actions.addSeparator("saves");
|
||||||
|
Action* savePlayerAction;
|
||||||
|
ConfigOption* savePlayer = m_config->addOption("savePlayerId");
|
||||||
|
savePlayerAction = savePlayer->addValue(tr("Automatically determine"), 0, &m_actions, "saves");
|
||||||
|
m_nonMpActions.append(savePlayerAction);
|
||||||
|
|
||||||
|
for (int i = 1; i < 5; ++i) {
|
||||||
|
savePlayerAction = savePlayer->addValue(tr("Use player %0 save game").arg(i), i, &m_actions, "saves");
|
||||||
|
m_nonMpActions.append(savePlayerAction);
|
||||||
|
}
|
||||||
|
savePlayer->connect([this](const QVariant& value) {
|
||||||
|
if (m_controller) {
|
||||||
|
m_controller->changePlayer(value.toInt());
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
m_config->updateOption("savePlayerId");
|
||||||
|
|
||||||
m_actions.addAction(tr("Load &patch..."), "loadPatch", this, &Window::selectPatch, "file");
|
m_actions.addAction(tr("Load &patch..."), "loadPatch", this, &Window::selectPatch, "file");
|
||||||
|
|
||||||
#ifdef M_CORE_GBA
|
#ifdef M_CORE_GBA
|
||||||
|
|
Loading…
Reference in New Issue