From c8af7551856d60045348139df0a9c0269e36af81 Mon Sep 17 00:00:00 2001 From: charveey Date: Sat, 22 Nov 2025 20:46:30 +0000 Subject: [PATCH] feat: port game switcher from NextUI with preview path fix Port game switcher feature from NextUI by @frysee with modifications to properly build preview_path for save state display. Original work: https://github.com/LoveRetro/NextUI/pull/27 Changes from original: - Fix preview_path building in readyResumePath() to support switcher - Build state path and preview path based on selected slot number - Set has_preview flag when preview file exists - Use getStatePath() to determine correct preview location --- workspace/all/common/defines.h | 1 + workspace/all/minarch/minarch.c | 17 ++- workspace/all/minui/minui.c | 239 ++++++++++++++++++++++++++++++-- 3 files changed, 241 insertions(+), 16 deletions(-) diff --git a/workspace/all/common/defines.h b/workspace/all/common/defines.h index 57eb13e2e..201f9055d 100644 --- a/workspace/all/common/defines.h +++ b/workspace/all/common/defines.h @@ -31,6 +31,7 @@ #define HIDE_BOXART_IFSTATE_PATH SHARED_USERDATA_PATH "/hide-boxart-ifstate" #define AUTO_RESUME_PATH SHARED_USERDATA_PATH "/.minui/auto_resume.txt" #define AUTO_RESUME_SLOT 0 +#define GAME_SWITCHER_PERSIST_PATH SHARED_USERDATA_PATH "/.minui/game_switcher.txt" #define TOOLBOXART_CFGFILE SDCARD_PATH "/Tools/" THISPLATFORM "/Convert BoxArt.pak/toolboxart.cfg" #define GAMEBOXART_CFGFILE SDCARD_PATH "/Tools/" THISPLATFORM "/Convert BoxArt.pak/gameboxart.cfg" #define AUTO_RESUME_MODIFIER_PATH SHARED_USERDATA_PATH "/.minui/auto_resume_modifier.txt" diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index 44b35dd1f..f74b4bd5c 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -772,6 +772,7 @@ enum { SHORTCUT_CYCLE_EFFECT, SHORTCUT_TOGGLE_FF, SHORTCUT_HOLD_FF, + SHORTCUT_GAMESWITCHER, SHORTCUT_COUNT, }; @@ -1047,6 +1048,7 @@ static struct Config { [SHORTCUT_CYCLE_EFFECT] = {"Cycle Effect", -1, BTN_ID_NONE, 0}, [SHORTCUT_TOGGLE_FF] = {"Toggle FF", -1, BTN_ID_NONE, 0}, [SHORTCUT_HOLD_FF] = {"Hold FF", -1, BTN_ID_NONE, 0}, + [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, {NULL} }, .controller_map_abxy_to_rstick = 0, @@ -1757,6 +1759,14 @@ static void input_poll_callback(void) { ignore_menu = 1; } + if (PAD_isPressed(BTN_MENU) && PAD_isPressed(BTN_SELECT)) { + ignore_menu = 1; + Menu_saveState(); + putFile(GAME_SWITCHER_PERSIST_PATH, game.path + strlen(SDCARD_PATH)); + GFX_clear(screen); + quit = 1; + } + if (PAD_justPressed(BTN_POWER)) { if (thread_video) { @@ -1812,7 +1822,12 @@ static void input_poll_callback(void) { Menu_saveState(); quit = 1; break; - case SHORTCUT_CYCLE_SCALE: + case SHORTCUT_GAMESWITCHER: + Menu_saveState(); + putFile(GAME_SWITCHER_PERSIST_PATH, game.path + strlen(SDCARD_PATH)); + quit = 1; + break; + case SHORTCUT_CYCLE_SCALE: screen_scaling += 1; if (screen_scaling>=SCALE_COUNT) screen_scaling -= SCALE_COUNT; Config_syncFrontend(config.frontend.options[FE_OPT_SCALING].key, screen_scaling); diff --git a/workspace/all/minui/minui.c b/workspace/all/minui/minui.c index f15e1d8dc..25b0930f9 100644 --- a/workspace/all/minui/minui.c +++ b/workspace/all/minui/minui.c @@ -52,6 +52,15 @@ static void* Array_pop(Array* self) { if (self->count==0) return NULL; return self->items[--self->count]; } +static void Array_remove(Array* self, void* item) { + if (self->count==0 || item == NULL) + return; + int i = 0; + while (self->items[i] != item) i++; + for (int j = i; j < self->count-1; j++) + self->items[j] = self->items[j+1]; + self->count--; +} static void Array_reverse(Array* self) { int end = self->count-1; int mid = self->count/2; @@ -534,10 +543,14 @@ static Array* hiddens; // HiddenArray static int quit = 0; static int can_resume = 0; static int should_resume = 0; // set to 1 on BTN_RESUME but only if can_resume==1 +static int has_preview = 0; static int last_selected_slot = 0; static int simple_mode = 0; +static int show_switcher = 0; +static int switcher_selected = 0; static char slot_path[256]; static char slot_path_rom[256]; +static char preview_path[256]; static int restore_depth = -1; static int restore_relative = -1; @@ -1026,21 +1039,29 @@ static Array* getRoot(void) { if (hasHiddens() && exists(SHOW_HIDDEN_FOLDER_PATH)) Array_push(root, Entry_new(FAUX_HIDDEN_PATH, ENTRY_DIR)); return root; } + +static Entry* entryFromRecent(Recent* recent) +{ + if(!recent || !recent->available) + return NULL; + + char sd_path[256]; + sprintf(sd_path, "%s%s", SDCARD_PATH, recent->path); + int type = suffixMatch(".pak", sd_path) ? ENTRY_PAK : ENTRY_ROM; // ??? + Entry* entry = Entry_new(sd_path, type); + if (recent->alias) { + free(entry->name); + entry->name = strdup(recent->alias); + } + return entry; +} static Array* getRecents(void) { Array* entries = Array_new(); for (int i=0; icount; i++) { Recent* recent = recents->items[i]; - if (!recent->available) continue; - - char sd_path[256]; - sprintf(sd_path, "%s%s", SDCARD_PATH, recent->path); - int type = suffixMatch(".pak", sd_path) ? ENTRY_PAK : ENTRY_ROM; // ??? - Entry* entry = Entry_new(sd_path, type); - if (recent->alias) { - free(entry->name); - entry->name = strdup(recent->alias); - } - Array_push(entries, entry); + Entry* entry = entryFromRecent(recent); + if (entry) + Array_push(entries, entry); } return entries; } @@ -1299,6 +1320,7 @@ static char* escapeSingleQuotes(char* str) { static void readyResumePath(char* rom_path, int type) { char* tmp; can_resume = 0; + has_preview = 0; char path[256]; strcpy(path, rom_path); @@ -1332,17 +1354,33 @@ static void readyResumePath(char* rom_path, int type) { sprintf(slot_path, "%s/.minui/%s/%s.txt", SHARED_USERDATA_PATH, emu_name, rom_file); // /.userdata/shared/.minui//.txt //sprintf(slot_path_rom, "%s/%s/%s", MYSAVESTATE_PATH, emu_name, rom_file); // /.userdata/shared/.minui//.ext //sprintf(slot_path, "%s.txt", slot_path_rom); // /.userdata/.minui//.ext.txt + + // Build the state path for preview + char state_path[256]; + getStatePath(path, state_path); + int last_know_slot = 0; - //can_resume = exists(slot_path); if (exists(slot_path)) { char slot[16]; getFile(slot_path, slot, 16); - if (slot[0]!='\0') { + if (slot[0] != '\0') { last_know_slot = atoi(slot); - } + } } + last_selected_slot = canResume(path, last_know_slot); - if (last_selected_slot) can_resume = 1; + if (last_selected_slot) { + can_resume = 1; + + // Build preview_path based on the slot + if (last_selected_slot > 0) { + sprintf(preview_path, "%s/%s.state%d.png", state_path, rom_file, last_selected_slot); + } else { + sprintf(preview_path, "%s/%s.state.png", state_path, rom_file); + } + + has_preview = exists(preview_path); + } } static void readyResume(Entry* entry) { @@ -1704,6 +1742,33 @@ static void Menu_quit(void) { /////////////////////////////////////// +static SDL_Rect GFX_scaled_rect(SDL_Rect preview_rect, SDL_Rect image_rect) { + SDL_Rect scaled_rect; + + // Calculate the aspect ratios + float image_aspect = (float)image_rect.w / (float)image_rect.h; + float preview_aspect = (float)preview_rect.w / (float)preview_rect.h; + + // Determine scaling factor + if (image_aspect > preview_aspect) { + // Image is wider than the preview area + scaled_rect.w = preview_rect.w; + scaled_rect.h = (int)(preview_rect.w / image_aspect); + } else { + // Image is taller than or equal to the preview area + scaled_rect.h = preview_rect.h; + scaled_rect.w = (int)(preview_rect.h * image_aspect); + } + + // Center the scaled rectangle within preview_rect + scaled_rect.x = preview_rect.x + (preview_rect.w - scaled_rect.w) / 2; + scaled_rect.y = preview_rect.y + (preview_rect.h - scaled_rect.h) / 2; + + return scaled_rect; +} + +/////////////////////////////////////// + int main (int argc, char *argv[]) { selected_modifier = 0; if (autoResume()) return 0; // nothing to do @@ -1756,9 +1821,17 @@ int main (int argc, char *argv[]) { if (!HAS_POWER_BUTTON && !simple_mode) PWR_disableSleep(); SDL_Surface* version = NULL; + SDL_Surface* preview = NULL; Menu_init(); + show_switcher = exists(GAME_SWITCHER_PERSIST_PATH); + if (show_switcher) { + // consider this "consumed", dont bring up the switcher next time we regularly exit a game + unlink(GAME_SWITCHER_PERSIST_PATH); + // todo: map recent slot to last used game + } + // now that (most of) the heavy lifting is done, take a load off PWR_setCPUSpeed(CPU_SPEED_MENU); GFX_setVsync(VSYNC_STRICT); @@ -1818,12 +1891,61 @@ int main (int argc, char *argv[]) { dirty = 1; } } + else if(show_switcher) { + if (PAD_justPressed(BTN_B) || PAD_justReleased(BTN_SELECT)) { + show_switcher = 0; + switcher_selected = 0; + dirty = 1; + } + else if (recents->count > 0 && PAD_justReleased(BTN_A)) { + // TODO: This is crappy af - putting this here since it works, but + // super inefficient. Why are Recents not decorated with type, and need + // to be remade into Entries via getRecents()? - need to understand the + // architecture more... + Entry *selectedEntry = entryFromRecent(recents->items[switcher_selected]); + should_resume = can_resume; + Entry_open(selectedEntry); + dirty = 1; + Entry_free(selectedEntry); + } + else if (recents->count > 0 && PAD_justReleased(BTN_Y)) { + // remove + Recent* recentEntry = recents->items[switcher_selected--]; + Array_remove(recents, recentEntry); + Recent_free(recentEntry); + saveRecents(); + if(switcher_selected < 0) + switcher_selected = recents->count - 1; // wrap + dirty = 1; + } + else if (PAD_justPressed(BTN_RIGHT)) { + switcher_selected++; + if(switcher_selected == recents->count) + switcher_selected = 0; // wrap + dirty = 1; + } + else if (PAD_justPressed(BTN_LEFT)) { + switcher_selected--; + if(switcher_selected < 0) + switcher_selected = recents->count - 1; // wrap + + + dirty = 1; + } + } else { if (PAD_tappedMenu(now)) { show_version = 1; + show_switcher = 0; dirty = 1; if (!HAS_POWER_BUTTON && !simple_mode) PWR_enableSleep(); } + else if (PAD_justReleased(BTN_SELECT)) { + show_switcher = 1; + switcher_selected = 0; + show_version = 0; // just to be sure + dirty = 1; + } else if (total>0) { if (PAD_justRepeated(BTN_UP)) { itemnotchanged = 0; @@ -2143,6 +2265,92 @@ int main (int argc, char *argv[]) { GFX_blitButtonGroup((char*[]){ "UP/DOWN", "MODE", "B","BACK", NULL }, 0, screen, 1, fancy_mode); } + else if(show_switcher) { + // For all recents with resumable state (i.e. has savegame), show game switcher carousel + + #define WINDOW_RADIUS 0 // TODO: this logic belongs in blitRect? + #define PAGINATION_HEIGHT 0 + // unscaled + int hw = screen->w; + int hh = screen->h; + int pw = hw + SCALE1(WINDOW_RADIUS*2); + int ph = hh + SCALE1(WINDOW_RADIUS*2 + PAGINATION_HEIGHT + WINDOW_RADIUS); + ox = 0; // screen->w - pw - SCALE1(PADDING); + oy = 0; // (screen->h - ph) / 2; + + // window + GFX_blitRect(ASSET_STATE_BG, screen, &(SDL_Rect){ox,oy,pw,ph}); + + if(recents->count > 0) { + Entry *selectedEntry = entryFromRecent(recents->items[switcher_selected]); + readyResume(selectedEntry); + + if(has_preview) { + // lotta memory churn here + SDL_Surface* bmp = IMG_Load(preview_path); + SDL_Surface* raw_preview = SDL_ConvertSurface(bmp, screen->format, SDL_SWSURFACE); + SDL_Rect image_rect = {0, 0, raw_preview->w, raw_preview->h}; + SDL_Rect preview_rect = {ox, oy, hw, hh}; + SDL_Rect scaled_rect = GFX_scaled_rect(preview_rect, image_rect); + SDL_FillRect(screen, NULL, 0); + SDL_BlitScaled(raw_preview, NULL, screen, &scaled_rect); + SDL_FreeSurface(raw_preview); + SDL_FreeSurface(bmp); + } + else { + SDL_Rect preview_rect = {ox,oy,hw,hh}; + SDL_FillRect(screen, &preview_rect, 0); + GFX_blitMessage(font.large, "No Preview", screen, &preview_rect); + } + + // title pill + { + int ow = GFX_blitHardwareGroup(screen, show_setting, fancy_mode); + int max_width = screen->w - SCALE1(PADDING * 2) - ow; + + char display_name[256]; + int text_width = GFX_truncateText(font.large, selectedEntry->name, display_name, max_width, SCALE1(BUTTON_PADDING*2)); + max_width = MIN(max_width, text_width); + + SDL_Surface* text; + text = TTF_RenderUTF8_Blended(font.large, display_name, COLOR_WHITE); + GFX_blitPill(ASSET_BLACK_PILL, screen, &(SDL_Rect){ + SCALE1(PADDING), + SCALE1(PADDING), + max_width, + SCALE1(PILL_SIZE) + }); + SDL_BlitSurface(text, &(SDL_Rect){ + 0, + 0, + max_width-SCALE1(BUTTON_PADDING*2), + text->h + }, screen, &(SDL_Rect){ + SCALE1(PADDING+BUTTON_PADDING), + SCALE1(PADDING+4) + }); + SDL_FreeSurface(text); + } + + // pagination + { + + } + + if(can_resume) GFX_blitButtonGroup((char*[]){ "X","RESUME", NULL }, 0, screen, 0, fancy_mode); + else GFX_blitButtonGroup((char*[]){ BTN_SLEEP==BTN_POWER?"POWER":"MENU","SLEEP", NULL }, 0, screen, 0, fancy_mode); + + GFX_blitButtonGroup((char*[]){ "B","BACK", "A", "OPEN", NULL }, 1, screen, 1, fancy_mode); + + Entry_free(selectedEntry); + } + else { + SDL_Rect preview_rect = {ox,oy,hw,hh}; + SDL_FillRect(screen, &preview_rect, 0); + GFX_blitMessage(font.large, "No Recents", screen, &preview_rect); + GFX_blitButtonGroup((char*[]){ "B","BACK", NULL }, 1, screen, 1, fancy_mode); + } + } else { // list if (total>0) { @@ -2405,6 +2613,7 @@ int main (int argc, char *argv[]) { } if (version) SDL_FreeSurface(version); + if (preview) SDL_FreeSurface(preview); Menu_quit(); PWR_quit();