From 09bc80556cf160fdf9de7740fd8f546380f5f14b Mon Sep 17 00:00:00 2001 From: duonqfs Date: Sun, 26 Jul 2026 18:27:16 +0700 Subject: [PATCH 1/2] feat(minarch): undo load state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds undo for an accidental load state, mirroring RetroArch's undo_load_state. Loading a state through the in-game menu or the Load State shortcut now snapshots the running state to memory first. Undoing swaps that snapshot with the current state, so pressing undo a second time returns to the state that was loaded — the same swap semantics RetroArch uses. Nothing is written to the SD card. Reachable two ways: an "Undo" row in the in-game menu, under Load, and a bindable "Undo Load State" shortcut. The menu row greys out via State_hasUndo() whenever no snapshot is available and does nothing when confirmed, so the menu never offers an action it can't perform. Availability is evaluated on menu redraw rather than every frame, keeping core.serialize_size() off the hot path. The snapshot is only kept when the load actually succeeded, and it is dropped if the core starts reporting a different serialize size. Disc changes drop it too: Menu_loadState() swaps discs before reading the state, so a snapshot taken afterwards would hold the previous disc's machine state while the new disc is inserted. A load that changed discs skips the capture entirely, and changing discs from the menu's Continue row invalidates a stale snapshot. Undo is blocked in RetroAchievements hardcore mode, same as loading a state. --- workspace/all/minarch/ma_config.c | 1 + workspace/all/minarch/ma_game.c | 4 ++ workspace/all/minarch/ma_input.c | 1 + workspace/all/minarch/ma_internal.h | 1 + workspace/all/minarch/ma_menu.c | 45 ++++++++++++-- workspace/all/minarch/ma_menu.h | 1 + workspace/all/minarch/ma_saves.c | 94 +++++++++++++++++++++++++++++ workspace/all/minarch/ma_saves.h | 5 ++ workspace/all/minarch/minarch.c | 1 + 9 files changed, 148 insertions(+), 5 deletions(-) diff --git a/workspace/all/minarch/ma_config.c b/workspace/all/minarch/ma_config.c index 438a0a65b..2d478a996 100644 --- a/workspace/all/minarch/ma_config.c +++ b/workspace/all/minarch/ma_config.c @@ -1707,6 +1707,7 @@ struct Config config = { .shortcuts = (ButtonMapping[]){ [SHORTCUT_SAVE_STATE] = {"Save State", -1, BTN_ID_NONE, 0}, [SHORTCUT_LOAD_STATE] = {"Load State", -1, BTN_ID_NONE, 0}, + [SHORTCUT_UNDO_LOAD_STATE] = {"Undo Load State", -1, BTN_ID_NONE, 0}, [SHORTCUT_RESET_GAME] = {"Reset Game", -1, BTN_ID_NONE, 0}, [SHORTCUT_SAVE_QUIT] = {"Save & Quit", -1, BTN_ID_NONE, 0}, [SHORTCUT_CYCLE_SCALE] = {"Cycle Scaling", -1, BTN_ID_NONE, 0}, diff --git a/workspace/all/minarch/ma_game.c b/workspace/all/minarch/ma_game.c index d971c388f..858a0a9e5 100644 --- a/workspace/all/minarch/ma_game.c +++ b/workspace/all/minarch/ma_game.c @@ -8,6 +8,7 @@ #include "ma_internal.h" #include "ma_game.h" +#include "ma_saves.h" struct Game game; struct retro_disk_control_ext_callback disk_control_ext; @@ -158,6 +159,9 @@ void Game_changeDisc(char* path) { disk_control_ext.replace_image_index(0, &game_info); putFile(CHANGE_DISC_PATH, path); // NextUI still needs to know this to update recents.txt + + // any undo snapshot belongs to the disc we just ejected + State_invalidateUndo(); } int extract_zip(char** extensions) diff --git a/workspace/all/minarch/ma_input.c b/workspace/all/minarch/ma_input.c index 3233b214d..4e9b5542b 100644 --- a/workspace/all/minarch/ma_input.c +++ b/workspace/all/minarch/ma_input.c @@ -144,6 +144,7 @@ void input_poll_callback(void) { Menu_saveState(); break; case SHORTCUT_LOAD_STATE: Menu_loadState(); break; + case SHORTCUT_UNDO_LOAD_STATE: Menu_undoLoadState(); break; case SHORTCUT_SCREENSHOT: Menu_screenshot(); break; diff --git a/workspace/all/minarch/ma_internal.h b/workspace/all/minarch/ma_internal.h index 49d4a8935..a734db5cf 100644 --- a/workspace/all/minarch/ma_internal.h +++ b/workspace/all/minarch/ma_internal.h @@ -235,6 +235,7 @@ enum { enum { SHORTCUT_SAVE_STATE, SHORTCUT_LOAD_STATE, + SHORTCUT_UNDO_LOAD_STATE, SHORTCUT_RESET_GAME, SHORTCUT_SAVE_QUIT, SHORTCUT_CYCLE_SCALE, diff --git a/workspace/all/minarch/ma_menu.c b/workspace/all/minarch/ma_menu.c index 5a5c130d0..563e0839e 100644 --- a/workspace/all/minarch/ma_menu.c +++ b/workspace/all/minarch/ma_menu.c @@ -113,13 +113,14 @@ void MSG_quit(void) { /////////////////////////////////////// -#define MENU_ITEM_COUNT 5 +#define MENU_ITEM_COUNT 6 #define MENU_SLOT_COUNT 8 enum { ITEM_CONT, ITEM_SAVE, ITEM_LOAD, + ITEM_UNDO, ITEM_OPTS, ITEM_QUIT, }; @@ -161,6 +162,7 @@ static struct { [ITEM_CONT] = "Continue", [ITEM_SAVE] = "Save", [ITEM_LOAD] = "Load", + [ITEM_UNDO] = "Undo", [ITEM_OPTS] = "Options", [ITEM_QUIT] = "Quit", } @@ -1688,6 +1690,7 @@ void Menu_loadState(void) { Menu_updateState(); if (menu.save_exists) { + int disc_changed = 0; if (menu.total_discs) { char slot_disc_name[256]; getFile(menu.txt_path, slot_disc_name, 256); @@ -1699,12 +1702,22 @@ void Menu_loadState(void) { char* disc_path = menu.disc_paths[menu.disc]; if (!exactMatch(slot_disc_path, disc_path)) { Game_changeDisc(slot_disc_path); + disc_changed = 1; } } state_slot = menu.slot; putInt(menu.slot_path, menu.slot); - int success = State_read(); + int success; + if (disc_changed) { + // the state we'd be undoing back to belongs to the disc that was just + // ejected, so there's nothing safe to offer an undo for + State_invalidateUndo(); + success = State_read(); + } + else { + success = State_readWithUndo(); + } Rewind_on_state_change(); // Show notification if enabled @@ -1716,6 +1729,15 @@ void Menu_loadState(void) { } } } +void Menu_undoLoadState(void) { + int success = State_undoLoad(); + + // Show notification if enabled (hardcore mode pushes its own message) + if (CFG_getNotifyLoad() && !RA_isHardcoreModeActive()) { + Notification_push(NOTIFICATION_LOAD_STATE, + success ? "Load State Undone" : "Nothing To Undo", NULL); + } +} void Menu_loop(void) { @@ -1864,6 +1886,15 @@ void Menu_loop(void) { show_menu = 0; } break; + case ITEM_UNDO: { + // inert while there's nothing to undo, matching the greyed out label + if (State_hasUndo()) { + Menu_undoLoadState(); + status = STATUS_LOAD; + show_menu = 0; + } + } + break; case ITEM_OPTS: { if (simple_mode) { core.reset(); @@ -1936,11 +1967,12 @@ void Menu_loop(void) { GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OKAY", NULL }, 1, screen, 1); // list + int can_undo = State_hasUndo(); oy = (((DEVICE_HEIGHT / FIXED_SCALE) - PADDING * 2) - (MENU_ITEM_COUNT * PILL_SIZE)) / 2; for (int i=0; i Date: Tue, 28 Jul 2026 13:34:09 +0700 Subject: [PATCH 2/2] feat(menu): remove undo option and update load state handling --- workspace/all/minarch/ma_menu.c | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/workspace/all/minarch/ma_menu.c b/workspace/all/minarch/ma_menu.c index 563e0839e..978112fd1 100644 --- a/workspace/all/minarch/ma_menu.c +++ b/workspace/all/minarch/ma_menu.c @@ -113,14 +113,13 @@ void MSG_quit(void) { /////////////////////////////////////// -#define MENU_ITEM_COUNT 6 +#define MENU_ITEM_COUNT 5 #define MENU_SLOT_COUNT 8 enum { ITEM_CONT, ITEM_SAVE, ITEM_LOAD, - ITEM_UNDO, ITEM_OPTS, ITEM_QUIT, }; @@ -162,7 +161,6 @@ static struct { [ITEM_CONT] = "Continue", [ITEM_SAVE] = "Save", [ITEM_LOAD] = "Load", - [ITEM_UNDO] = "Undo", [ITEM_OPTS] = "Options", [ITEM_QUIT] = "Quit", } @@ -1860,6 +1858,13 @@ void Menu_loop(void) { status = STATUS_CONT; show_menu = 0; } + else if (PAD_justPressed(BTN_X)) { + if (selected==ITEM_LOAD && State_hasUndo()) { + Menu_undoLoadState(); + status = STATUS_LOAD; + show_menu = 0; + } + } else if (PAD_justPressed(BTN_A)) { switch(selected) { case ITEM_CONT: @@ -1886,15 +1891,6 @@ void Menu_loop(void) { show_menu = 0; } break; - case ITEM_UNDO: { - // inert while there's nothing to undo, matching the greyed out label - if (State_hasUndo()) { - Menu_undoLoadState(); - status = STATUS_LOAD; - show_menu = 0; - } - } - break; case ITEM_OPTS: { if (simple_mode) { core.reset(); @@ -1964,10 +1960,14 @@ void Menu_loop(void) { if (show_setting && !GetHDMI()) GFX_blitHardwareHints(screen, show_setting); else GFX_blitButtonGroup((char*[]){ BTN_SLEEP==BTN_POWER?"POWER":"MENU","SLEEP", NULL }, 0, screen, 0); - GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OKAY", NULL }, 1, screen, 1); + if (selected==ITEM_LOAD && State_hasUndo()) { + GFX_blitButtonGroup((char*[]){ "X","UNDO LOAD", "B","BACK", "A","LOAD", NULL }, 1, screen, 1); + } + else { + GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OKAY", NULL }, 1, screen, 1); + } // list - int can_undo = State_hasUndo(); oy = (((DEVICE_HEIGHT / FIXED_SCALE) - PADDING * 2) - (MENU_ITEM_COUNT * PILL_SIZE)) / 2; for (int i=0; i