diff --git a/Core/Config.cpp b/Core/Config.cpp index d97e2509cd..9e83f27e48 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -532,6 +532,7 @@ static ConfigSetting generalSettings[] = { ConfigSetting("SaveLoadResetsAVdumping", &g_Config.bSaveLoadResetsAVdumping, false), ConfigSetting("StateSlot", &g_Config.iCurrentStateSlot, 0, true, true), ConfigSetting("EnableStateUndo", &g_Config.bEnableStateUndo, &DefaultEnableStateUndo, true, true), + ConfigSetting("StateLoadUndoGame", &g_Config.sStateLoadUndoGame, "NA", true, false), ConfigSetting("RewindFlipFrequency", &g_Config.iRewindFlipFrequency, 0, true, true), ConfigSetting("ShowOnScreenMessage", &g_Config.bShowOnScreenMessages, true, true, false), diff --git a/Core/Config.h b/Core/Config.h index a07a6182d8..5e29220fc7 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -209,6 +209,7 @@ public: int iRewindFlipFrequency; bool bUISound; bool bEnableStateUndo; + std::string sStateLoadUndoGame; int iAutoLoadSaveState; // 0 = off, 1 = oldest, 2 = newest, >2 = slot number + 3 bool bEnableCheats; bool bReloadCheats; diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index ac94c52399..2df8b9e6f3 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -412,18 +412,19 @@ namespace SaveState return filename.GetFilename() + " " + sy->T("(broken)"); } - Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension) - { + std::string GenerateFullDiskId(const Path &gameFilename) { std::string discId = g_paramSFO.GetValueString("DISC_ID"); std::string discVer = g_paramSFO.GetValueString("DISC_VERSION"); - std::string fullDiscId; if (discId.empty()) { discId = g_paramSFO.GenerateFakeID(); discVer = "1.00"; } - fullDiscId = StringFromFormat("%s_%s", discId.c_str(), discVer.c_str()); + return StringFromFormat("%s_%s", discId.c_str(), discVer.c_str()); + } - std::string filename = StringFromFormat("%s_%d.%s", fullDiscId.c_str(), slot, extension); + Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension) + { + std::string filename = StringFromFormat("%s_%d.%s", GenerateFullDiskId(gameFilename).c_str(), slot, extension); return GetSysDirectory(DIRECTORY_SAVESTATE) / filename; } @@ -437,18 +438,6 @@ namespace SaveState g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot + 1) % NUM_SLOTS; } - void LoadSlot(const Path &gameFilename, int slot, Callback callback, void *cbUserData) - { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); - if (!fn.empty()) { - Load(fn, slot, callback, cbUserData); - } else { - auto sy = GetI18NCategory("System"); - if (callback) - callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system."), cbUserData); - } - } - static void DeleteIfExists(const Path &fn) { // Just avoiding error messages. if (File::Exists(fn)) { @@ -471,6 +460,63 @@ namespace SaveState } } + void LoadSlot(const Path &gameFilename, int slot, Callback callback, void *cbUserData) + { + Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); + if (!fn.empty()) { + // This add only 1 extra state, should we just always enable it? + if (g_Config.bEnableStateUndo) { + Path backup = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME; + + auto saveCallback = [=](Status status, const std::string &message, void *data) { + if (status != Status::FAILURE) { + DeleteIfExists(backup); + File::Rename(backup.WithExtraExtension(".tmp"), backup); + g_Config.sStateLoadUndoGame = GenerateFullDiskId(gameFilename); + Load(fn, slot, callback, cbUserData); + } else if (callback) { + callback(status, message, data); + } + }; + + if (!backup.empty()) { + Save(backup.WithExtraExtension(".tmp"), -2, saveCallback, cbUserData); + } else { + auto sy = GetI18NCategory("System"); + if (callback) + callback(Status::FAILURE, sy->T("Failed to save state for load undo. Error in the file system."), cbUserData); + } + } else { + Load(fn, slot, callback, cbUserData); + } + } else { + auto sy = GetI18NCategory("System"); + if (callback) + callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system."), cbUserData); + } + } + + bool UndoLoad(const Path &gameFilename, Callback callback, void *cbUserData) + { + if (g_Config.sStateLoadUndoGame != GenerateFullDiskId(gameFilename)) { + auto sy = GetI18NCategory("System"); + if (callback) + callback(Status::FAILURE, sy->T("Error: load undo state is from a different game"), cbUserData); + return false; + } + + Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME; + if (!fn.empty()) { + Load(fn, -2, callback, cbUserData); // Slot number is visual only, -2 will display special message (kinda have to find a better way) + return true; + } else { + auto sy = GetI18NCategory("System"); + if (callback) + callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system."), cbUserData); + return false; + } + } + void SaveSlot(const Path &gameFilename, int slot, Callback callback, void *cbUserData) { Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); @@ -541,6 +587,12 @@ namespace SaveState return File::Exists(fn); } + bool HasUndoLoad(const Path &gameFilename) + { + Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME; + return File::Exists(fn) && g_Config.sStateLoadUndoGame == GenerateFullDiskId(gameFilename); + } + bool operator < (const tm &t1, const tm &t2) { if (t1.tm_year < t2.tm_year) return true; if (t1.tm_year > t2.tm_year) return false; @@ -752,7 +804,7 @@ namespace SaveState // Use the state's latest version as a guess for saveStateInitialGitVersion. result = CChunkFileReader::Load(op.filename, &saveStateInitialGitVersion, state, &errorString); if (result == CChunkFileReader::ERROR_NONE) { - callbackMessage = slot_prefix + sc->T("Loaded State"); + callbackMessage = op.slot != -2 ? slot_prefix + sc->T("Loaded State") : sc->T("State load undone"); callbackResult = Status::SUCCESS; hasLoadedState = true; diff --git a/Core/SaveState.h b/Core/SaveState.h index d7bb0142fe..7936fd32d6 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -37,6 +37,8 @@ namespace SaveState static const char *UNDO_STATE_EXTENSION = "undo.ppst"; static const char *UNDO_SCREENSHOT_EXTENSION = "undo.jpg"; + static const char *LOAD_UNDO_NAME = "load_undo.ppst"; + void Init(); void Shutdown(); @@ -45,9 +47,11 @@ namespace SaveState void SaveSlot(const Path &gameFilename, int slot, Callback callback, void *cbUserData = 0); void LoadSlot(const Path &gameFilename, int slot, Callback callback, void *cbUserData = 0); bool UndoSaveSlot(const Path &gameFilename, int slot); + bool UndoLoad(const Path &gameFilename, Callback callback, void *cbUserData = 0); // Checks whether there's an existing save in the specified slot. bool HasSaveInSlot(const Path &gameFilename, int slot); bool HasUndoSaveInSlot(const Path &gameFilename, int slot); + bool HasUndoLoad(const Path &gameFilename); bool HasScreenshotInSlot(const Path &gameFilename, int slot); int GetCurrentSlot(); @@ -57,6 +61,7 @@ namespace SaveState int GetOldestSlot(const Path &gameFilename); std::string GetSlotDateAsString(const Path &gameFilename, int slot); + std::string GenerateFullDiskId(const Path &gameFilename); Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension); std::string GetTitle(const Path &filename); diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index cb1fd33506..ec135051d8 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -396,6 +396,12 @@ void GamePauseScreen::CreateViews() { rewindButton->OnClick.Handle(this, &GamePauseScreen::OnRewind); } + if (g_Config.bEnableStateUndo) { + UI::Choice *loadUndoButton = leftColumnItems->Add(new Choice(pa->T("Undo last state load"))); + loadUndoButton->SetEnabled(SaveState::HasUndoLoad(gamePath_)); + loadUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLoadUndo); + } + ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); root_->Add(rightColumn); @@ -492,6 +498,13 @@ UI::EventReturn GamePauseScreen::OnRewind(UI::EventParams &e) { return UI::EVENT_DONE; } +UI::EventReturn GamePauseScreen::OnLoadUndo(UI::EventParams &e) { + SaveState::UndoLoad(gamePath_, &AfterSaveStateAction); + + TriggerFinish(DR_CANCEL); + return UI::EVENT_DONE; +} + UI::EventReturn GamePauseScreen::OnCwCheat(UI::EventParams &e) { screenManager()->push(new CwCheatScreen(gamePath_)); return UI::EVENT_DONE; diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index e45a1a93ec..6d4acbd961 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -44,6 +44,7 @@ private: UI::EventReturn OnReportFeedback(UI::EventParams &e); UI::EventReturn OnRewind(UI::EventParams &e); + UI::EventReturn OnLoadUndo(UI::EventParams &e); UI::EventReturn OnScreenshotClicked(UI::EventParams &e); UI::EventReturn OnCwCheat(UI::EventParams &e);