diff --git a/snes/const.a65 b/snes/const.a65 index ed9712153..a925c3ca4 100644 --- a/snes/const.a65 +++ b/snes/const.a65 @@ -259,7 +259,7 @@ mdesc_browser_screensaver .byt "Fade-out the screen after some time", 0 mdesc_browser_screensaver_timeout .byt "Set time after which to fade the screen", 0 mdesc_browser_ledbright .byt "Change brightness of LEDs", 0 mdesc_ingame_cheats .byt "Whether to start out with cheats enabled. Can be changed using in-game buttons", 0 -mdesc_ingame_shortreset .byt "Go back to menu on short reset if enabled", 0 +mdesc_ingame_shortreset .byt "Go back to menu on short reset (Off/On/Folder/Rom). Folder: return to last game folder. Rom: also pre-select the ROM.", 0 mdesc_ingame_enable .byt "Required for in-game buttons and 7E/7F cheats", 0 mdesc_ingame_buttons .byt "Enable in-game buttons (reset, en/disable cheats", 127, 128, "). Requires in-game hook to be enabled.", 0 mdesc_ingame_holdoff .byt "Wait 10 seconds before enabling hook (helps some games boot with hook enabled)", 0 @@ -312,6 +312,8 @@ text_on_p1 .byt "On: P1", 0 text_on_p2 .byt "On: P2", 0 text_on .byt "On", 0 text_off .byt "Off", 0 +text_nav_folder .byt "Folder", 0 +text_nav_rom .byt "Rom", 0 text_enabled .byt "Enabled", 0 text_disabled .byt "Disabled", 0 text_normal .byt "Normal", 0 diff --git a/snes/filesel.a65 b/snes/filesel.a65 index 6d02db66f..c29c39ace 100644 --- a/snes/filesel.a65 +++ b/snes/filesel.a65 @@ -1315,3 +1315,394 @@ filesel_request_filelist_wait_mcu: plb plp rts + +; ========================================================= +; filesel_nav_last: +; Navigate the file browser to the folder of the most +; recently played game. +; Folder mode (CFG_ENABLE_RST_TO_MENU == 2): folder only. +; Rom mode (CFG_ENABLE_RST_TO_MENU == 3): folder + pre-select ROM. +; Called after filesel_init when reset_to_menu >= 2 AND +; ST_RESET_TO_MENU_ACTIVE is set (i.e. the console returned to +; the menu via a short reset, not via a cold power-on). +; Precondition: filesel_init has already been called. +; Uses scratch variables: testvar[0..5], longptr, +; dirent_addr, dirent_bank, mm_tmp. +; ========================================================= +filesel_nav_last: + php + sep #$20 : .as + rep #$10 : .xl + ; Bail out early if no directory path is stored + lda @LAST_GAME_DIR + beq fnl_out + ; For Rom mode (3) also bail if no last game filename is stored + lda @CFG_ENABLE_RST_TO_MENU + cmp #$03 + bne + + lda @LAST_GAME + beq fnl_out ++ + ; Navigate path components one by one. + ; LAST_GAME_DIR = "/A/B/C" — start after leading '/' + ldx #$0001 +fnl_comp_loop: + lda @LAST_GAME_DIR, x + beq fnl_find_file ; end of path → already in correct dir + stx testvar ; fnl_comp_start = x +fnl_find_comp_end: + lda @LAST_GAME_DIR, x + beq fnl_got_comp_eos ; null = last component + cmp #$2f ; '/' + beq fnl_got_comp_slash + inx + bra fnl_find_comp_end +fnl_got_comp_slash: + stx testvar+2 ; fnl_comp_end = x (exclusive, '/' position) + jsr fnl_enter_subdir + bcc fnl_out ; not found — bail out + ldx testvar+2 ; restore x — fnl_enter_subdir clobbers it + inx ; skip '/' + bra fnl_comp_loop +fnl_got_comp_eos: + stx testvar+2 ; fnl_comp_end = x (at null) + jsr fnl_enter_subdir ; navigate into last path component + ; fall through to file scan +fnl_find_file: + ; Only pre-select ROM when mode is Rom (3); Folder (2) skips this + sep #$20 : .as + lda @CFG_ENABLE_RST_TO_MENU + cmp #$03 + bne fnl_out + jsr fnl_select_file +fnl_out: + sep #$20 : .as + lda #$01 + sta filesel_dirty + plp + rts + +; ========================================================= +; fnl_enter_subdir: +; Scan current directory for a TYPE_SUBDIR entry whose +; name matches LAST_GAME_DIR[comp_start..comp_end). +; On match: saves parent state to dirlog, appends the +; component to FILESEL_CWD, requests the new listing, +; and resets dir pointers. +; Returns carry set = found, carry clear = not found. +; ========================================================= +fnl_enter_subdir: + rep #$20 : .al + lda #$0000 + sta testvar+4 ; current entry byte offset = 0 +fnl_es_loop: + ; Point longptr at current entry in $C1 + lda testvar+4 + sta longptr + sep #$20 : .as + lda #$C1 + sta longptr+2 + ; bank offset of entry (= 0 → end of list) + rep #$10 : .xl + ldy #$0002 + sep #$20 : .as + lda [longptr], y + beq fnl_es_not_found + ; check type == TYPE_SUBDIR + iny + lda [longptr], y + cmp #TYPE_SUBDIR + bne fnl_es_next + ; Read name address: entry[0-1] → dirent_addr, entry[2]+$C0 → dirent_bank + ldy #$0000 + rep #$20 : .al + lda [longptr], y + sta dirent_addr + ldy #$0002 + sep #$20 : .as + lda [longptr], y + clc + adc #$C0 + sta dirent_bank + ; Point longptr at name start (dirent_addr + 6 skips size string) + rep #$20 : .al + lda dirent_addr + clc + adc #$0006 + sta longptr + sep #$20 : .as + lda dirent_bank + sta longptr+2 + ; Compare name against path component + jsr fnl_cmp_subdir + bcc fnl_es_next + ; ---- Match: save dirlog entry ---- + ; Layout (8 bytes): dirstart_addr(2), dirstart_bank(1), + ; dirptr_addr(2), dirptr_bank(1), filesel_sel(2) + rep #$20 : .al + ldy #$0000 + lda #$0000 + sta [dirlog_idx], y ; dirstart_addr = 0 + iny + iny + lda #$00C1 + sta [dirlog_idx], y ; dirstart_bank lo = $C1 (hi byte overwritten next) + iny + lda testvar+4 ; dirptr_addr = entry offset (screen starts here on B) + sta [dirlog_idx], y + iny + iny + lda #$00C1 + sta [dirlog_idx], y ; dirptr_bank lo = $C1 + iny + lda #$0000 + sta [dirlog_idx], y ; filesel_sel = 0 + ; Advance dirlog_idx by 8 + lda dirlog_idx + clc + adc #$0008 + sta dirlog_idx + ; Append component to FILESEL_CWD and reload directory + jsr fnl_append_cwd + jsr filesel_request_filelist + rep #$20 : .al + lda #$0000 + sta @dirptr_addr + sta @dirstart_addr + sep #$20 : .as + sta @filesel_sel + sta @direntry_xscroll + sta @direntry_xscroll_state + sec ; carry set = found + rts +fnl_es_next: + rep #$20 : .al + lda testvar+4 + clc + adc #$0004 + sta testvar+4 + bra fnl_es_loop +fnl_es_not_found: + clc ; carry clear = not found + rts + +; ========================================================= +; fnl_append_cwd: +; Append '/' + LAST_GAME_DIR[comp_start..comp_end) to +; FILESEL_CWD and null-terminate. +; ========================================================= +fnl_append_cwd: + sep #$20 : .as + rep #$10 : .xl + ; Point longptr at LAST_GAME_DIR ($FF1F00) for [longptr],y access + rep #$20 : .al + lda #$1F00 + sta longptr + sep #$20 : .as + lda #$FF + sta longptr+2 + ; Find the null-terminator of FILESEL_CWD using X (long,X) + ldx #$0000 +fnl_ac_find_end: + lda @FILESEL_CWD, x + beq fnl_ac_at_end + inx + bra fnl_ac_find_end +fnl_ac_at_end: + ; x is at null. Back up to last char and check for '/' + dex + lda @FILESEL_CWD, x + cmp #$2f + beq fnl_ac_has_slash + inx ; move x past last non-slash char +fnl_ac_has_slash: + lda #$2f + sta @FILESEL_CWD, x + inx + ; Copy path component via indirect long Y ([longptr],y = $B7, valid) + ldy testvar ; Y = comp_start index +fnl_ac_copy: + cpy testvar+2 ; Y == comp_end? + beq fnl_ac_done + lda [longptr], y ; LAST_GAME_DIR[Y] — indirect long Y, valid + sta @FILESEL_CWD, x ; FILESEL_CWD[X] — long,X, valid + inx + iny + bra fnl_ac_copy +fnl_ac_done: + lda #$00 + sta @FILESEL_CWD, x + rts + +; ========================================================= +; fnl_select_file: +; Scan the current directory for a TYPE_ROM or TYPE_SPC +; entry whose name matches LAST_GAME[0] (just filename). +; On match: sets filesel_sel and adjusts dirptr_addr so +; the entry is visible. +; ========================================================= +fnl_select_file: + rep #$20 : .al + lda #$0000 + sta testvar+4 ; current entry byte offset = 0 +fnl_sf_loop: + lda testvar+4 + sta longptr + sep #$20 : .as + lda #$C1 + sta longptr+2 + rep #$10 : .xl + ldy #$0002 + sep #$20 : .as + lda [longptr], y + beq fnl_sf_done ; end of list + iny + lda [longptr], y + cmp #TYPE_ROM + beq fnl_sf_check + cmp #TYPE_SPC + beq fnl_sf_check + bra fnl_sf_next +fnl_sf_check: + ldy #$0000 + rep #$20 : .al + lda [longptr], y + sta dirent_addr + ldy #$0002 + sep #$20 : .as + lda [longptr], y + clc + adc #$C0 + sta dirent_bank + rep #$20 : .al + lda dirent_addr + clc + adc #$0006 + sta longptr + sep #$20 : .as + lda dirent_bank + sta longptr+2 + jsr fnl_cmp_file + bcc fnl_sf_next + ; Match: compute entry index = testvar+4 / 4 + rep #$20 : .al + lda testvar+4 + lsr + lsr ; A = entry index + cmp listdisp + bcs fnl_sf_scroll + ; index < listdisp: show from top, cursor at index + sta @filesel_sel + lda #$0000 + sta @dirptr_addr + sta @dirstart_addr + bra fnl_sf_done +fnl_sf_scroll: + ; index >= listdisp: scroll so entry appears last on screen + pha + sec + sbc listdisp + inc + asl + asl ; dirptr_addr = (index - listdisp + 1) * 4 + sta @dirptr_addr + sta @dirstart_addr + pla + lda listdisp + dec + sta @filesel_sel + bra fnl_sf_done +fnl_sf_next: + rep #$20 : .al + lda testvar+4 + clc + adc #$0004 + sta testvar+4 + bra fnl_sf_loop +fnl_sf_done: + rts + +; ========================================================= +; fnl_cmp_subdir: +; Case-insensitive compare of name at longptr[0..] against +; LAST_GAME_DIR[comp_start..comp_end). +; Subdir entry names end with '/' in PSRAM. +; Returns carry set = match, carry clear = no match. +; ========================================================= +fnl_cmp_subdir: + php + sep #$20 : .as + rep #$10 : .xl + ldy #$0000 + ldx testvar ; X = comp_start +fnl_cs_loop: + cpx testvar+2 ; end of target component? + beq fnl_cs_eot + ; Read and uppercase entry char + lda [longptr], y + and #$DF + sta mm_tmp + ; Read and uppercase target char + lda @LAST_GAME_DIR, x + and #$DF + cmp mm_tmp + bne fnl_cs_no + inx + iny + bra fnl_cs_loop +fnl_cs_eot: + ; Target exhausted: entry must have trailing '/' (or null) + lda [longptr], y + cmp #$2f + beq fnl_cs_yes + cmp #$00 + beq fnl_cs_yes +fnl_cs_no: + plp + clc + rts +fnl_cs_yes: + plp + sec + rts + +; ========================================================= +; fnl_cmp_file: +; Case-insensitive compare of name at longptr[0..] against +; LAST_GAME[0] (null-terminated filename). +; Handles hide_extensions: $01 in entry name treated as '.'. +; Returns carry set = match, carry clear = no match. +; ========================================================= +fnl_cmp_file: + php + sep #$20 : .as + rep #$10 : .xl + ldy #$0000 ; Y indexes entry string via [longptr] + ldx #$0000 ; X indexes LAST_GAME via long,X +fnl_cf_loop: + ; Read entry char; $01 = hidden extension dot + lda [longptr], y + cmp #$01 + bne fnl_cf_not_ext + lda #$2e ; treat $01 as '.' +fnl_cf_not_ext: + and #$DF + sta mm_tmp + ; Read LAST_GAME[X] char — long,X ($BF), valid in 65816 + lda @LAST_GAME, x + and #$DF + cmp mm_tmp + bne fnl_cf_no + cmp #$00 ; both null = full match + beq fnl_cf_yes + iny + inx + bra fnl_cf_loop +fnl_cf_yes: + plp + sec + rts +fnl_cf_no: + plp + clc + rts diff --git a/snes/main.a65 b/snes/main.a65 index 1504cd02c..67ff5cb57 100644 --- a/snes/main.a65 +++ b/snes/main.a65 @@ -111,6 +111,16 @@ coldboot_bypass_autoboot: ; clear autoboot flag, continue to menu coldboot_autoboot_out: jsr screen_on jsr filesel_init + ; If reset_to_menu >= 2 (Folder/Rom) AND this was a reset-to-menu (not cold power-on): + ; navigate to last game's directory and optionally pre-select the ROM. + sep #$20 : .as + lda @CFG_ENABLE_RST_TO_MENU + cmp #$02 + bcc + ; < 2 → Off or On, skip nav + lda @ST_RESET_TO_MENU_ACTIVE + beq + ; not a reset-to-menu boot → skip nav + jsr filesel_nav_last ++ sep #$20 : .as lda @ST_RTC_VALID beq + diff --git a/snes/memmap.i65 b/snes/memmap.i65 index 178bada84..4ca1716ec 100644 --- a/snes/memmap.i65 +++ b/snes/memmap.i65 @@ -57,6 +57,7 @@ #define ST_SNES_ADDR $FF1110 #define SYSINFO_BLK $FF1200 #define LAST_GAME $FF1420 +#define LAST_GAME_DIR $FF1F00 #define WRAM_BAK $FF2000 #define FAVORITE_GAMES $FF4000 @@ -146,6 +147,7 @@ #define ST_PAIRMODE ST_MCU_ADDR+$0002 #define ST_NUM_FAVORITE_GAMES ST_MCU_ADDR+$0003 #define ST_AUTOBOOT_ENABLED ST_MCU_ADDR+$0004 +#define ST_RESET_TO_MENU_ACTIVE ST_MCU_ADDR+$0005 #define ST_IS_U16 ST_SNES_ADDR+$0000 #define ST_U16_CFG ST_SNES_ADDR+$0001 @@ -180,4 +182,4 @@ #define TYPE_SUBDIR $40 #define TYPE_PARENT $80 -#define DIM_BRIGHTNESS $03 \ No newline at end of file +#define DIM_BRIGHTNESS $03 diff --git a/snes/menudata.a65 b/snes/menudata.a65 index fdc84568a..6fba2dbcc 100644 --- a/snes/menudata.a65 +++ b/snes/menudata.a65 @@ -555,8 +555,8 @@ menu_enttab_ingame: .byt MTYPE_VALUE .word !mtext_ingame_shortreset .byt ^mtext_ingame_shortreset - .word !kv_onoff - .byt ^kv_onoff + .word !kv_rst_to_menu + .byt ^kv_rst_to_menu .byt OPTTYPE_KVBYTE .word !CFG_ENABLE_RST_TO_MENU .byt ^CFG_ENABLE_RST_TO_MENU @@ -917,6 +917,13 @@ kv_onoff: .word $0001, !text_on : .byt ^text_on .word $ffff +kv_rst_to_menu: +.word $0000, !text_off : .byt ^text_off +.word $0001, !text_on : .byt ^text_on +.word $0002, !text_nav_folder : .byt ^text_nav_folder +.word $0003, !text_nav_rom : .byt ^text_nav_rom +.word $ffff + kv_enabledisable: .word $0000, !text_enabled : .byt ^text_enabled .word $0001, !text_disabled : .byt ^text_disabled diff --git a/src/cfg.c b/src/cfg.c index 2221137e6..1600b5367 100644 --- a/src/cfg.c +++ b/src/cfg.c @@ -80,8 +80,8 @@ int cfg_save() { f_printf(&file_handle, "%s: %s\n", CFG_1CHIP_TRANSIENT_FIXES, CFG.onechip_transient_fixes ? "true" : "false"); f_puts("\n# Brightness limit - can be used to limit RGB output levels on S-CPUN based consoles\n", &file_handle); f_printf(&file_handle, "%s: %d\n", CFG_BRIGHTNESS_LIMIT, CFG.brightness_limit); - f_puts("\n# Reset to menu on short reset\n", &file_handle); - f_printf(&file_handle, "%s: %s\n", CFG_ENABLE_RST_TO_MENU, CFG.reset_to_menu ? "true" : "false"); + f_puts("\n# Reset to menu on short reset (0: off, 1: on, 2: on+return to last folder, 3: on+return to folder and pre-select ROM)\n", &file_handle); + f_printf(&file_handle, "%s: %d\n", CFG_ENABLE_RST_TO_MENU, CFG.reset_to_menu); f_puts("\n# Initial cheats state when loading a game (true: enabled, false: disabled)\n", &file_handle); f_printf(&file_handle, "%s: %s\n", CFG_ENABLE_CHEATS, CFG.enable_cheats ? "true" : "false"); f_puts("\n\n# IRQ hook related settings\n", &file_handle); @@ -221,7 +221,11 @@ int cfg_load() { CFG.brightness_limit = tok.longvalue & 0xf; } if(yaml_get_itemvalue(CFG_ENABLE_RST_TO_MENU, &tok)) { - CFG.reset_to_menu = tok.boolvalue ? 1 : 0; + if(tok.type == YAML_BOOL) { + CFG.reset_to_menu = tok.boolvalue ? 1 : 0; + } else { + CFG.reset_to_menu = tok.longvalue > 3 ? 1 : (uint8_t)tok.longvalue; + } } if(yaml_get_itemvalue(CFG_LED_BRIGHTNESS, &tok)) { CFG.led_brightness = tok.longvalue; @@ -381,11 +385,27 @@ int cfg_get_last_game(uint8_t *fn, uint8_t index) { void cfg_dump_recent_games_for_snes(uint32_t address) { TCHAR fntmp[256]; + TCHAR dirtmp[256]; int index; + sram_writebyte(0, SRAM_LASTGAME_DIR_ADDR); /* default: empty dir path */ file_open(LAST_FILE, FA_READ); for(index = 0; index < 10 && !f_eof(&file_handle); index++) { f_gets(fntmp, 255, &file_handle); sram_writestrn(strrchr((const char*)fntmp, '/')+1, address+256*index, 256); + if(index == 0) { + /* write directory of most recent game for reset_to_menu >= 2 (Folder/Rom) navigation */ + char *slash = strrchr((const char*)fntmp, '/'); + if(slash != NULL) { + size_t dir_len = slash - fntmp; + if(dir_len == 0) { + sram_writestrn((uint8_t*)"/", SRAM_LASTGAME_DIR_ADDR, 256); + } else { + strncpy(dirtmp, fntmp, dir_len); + dirtmp[dir_len] = '\0'; + sram_writestrn((uint8_t*)dirtmp, SRAM_LASTGAME_DIR_ADDR, 256); + } + } + } } STM.num_recent_games = index; file_close(); @@ -679,4 +699,4 @@ int cfg_get_stringvalue(const char *key, char *target, size_t count) { strncpy(target, tok.stringvalue, count); yaml_file_close(); return found; -} \ No newline at end of file +} diff --git a/src/cfg.h b/src/cfg.h index 956feb5cd..db673f4cc 100644 --- a/src/cfg.h +++ b/src/cfg.h @@ -79,7 +79,7 @@ typedef struct __attribute__ ((__packed__)) _cfg_block { uint8_t onechip_transient_fixes; /* override register 2100 bits 3-0 */ uint8_t brightness_limit; /* limit brightness set by register 2100 */ uint8_t gsu_speed; /* GSU speed (0: original, 1: no waitstates */ - uint8_t reset_to_menu; /* Go back to menu on short reset */ + uint8_t reset_to_menu; /* Go back to menu on short reset (0=off, 1=on, 2=folder, 3=rom) */ uint8_t led_brightness; /* LED brightness (0..15) */ uint8_t enable_cheats; /* initial cheat enable state */ uint8_t reset_patch; /* enable reset patch */ diff --git a/src/main.c b/src/main.c index cc98a29ca..a166c49fa 100644 --- a/src/main.c +++ b/src/main.c @@ -269,6 +269,7 @@ int main(void) { } STM.autoboot_enabled = cfg_is_autoboot_enabled(); status_load_to_menu(); + STM.reset_to_menu_active = 0; /* SRAM now holds the flag for the SNES; zero in RAM so later status_load_to_menu() calls don't re-broadcast it */ uint8_t cmd = 0; uint64_t btime = 0; @@ -477,6 +478,7 @@ int main(void) { } uint8_t resetState = get_snes_reset_state(); if(resetState == SNES_RESET_LONG) { + STM.reset_to_menu_active = (CFG.reset_to_menu >= 2) ? 1 : 0; prepare_reset(); break; } else { @@ -504,6 +506,7 @@ int main(void) { break; case SNES_CMD_RESET_TO_MENU: usb_cmd = 0; + STM.reset_to_menu_active = (CFG.reset_to_menu >= 2) ? 1 : 0; prepare_reset(); goto snes_loop_out; case SNES_CMD_SAVESTATE: diff --git a/src/memory.h b/src/memory.h index c75382913..9b5b7a79c 100644 --- a/src/memory.h +++ b/src/memory.h @@ -58,6 +58,7 @@ extern char current_filename[]; #define SRAM_SNES_STATUS_ADDR (0xFF1110L) #define SRAM_SYSINFO_ADDR (0xFF1200L) #define SRAM_LASTGAME_ADDR (0xFF1420L) +#define SRAM_LASTGAME_DIR_ADDR (0xFF1F00L) #define SRAM_FAVORITEGAMES_ADDR (0xFF4000L) #define SRAM_SCRATCHPAD (0xFFFF00L) #define SRAM_DIRID (0xFFFFF0L) diff --git a/src/snes.h b/src/snes.h index 7e05e8990..fbd562981 100644 --- a/src/snes.h +++ b/src/snes.h @@ -147,7 +147,8 @@ typedef struct __attribute__ ((__packed__)) _mcu_status { uint8_t num_recent_games; uint8_t pairmode; uint8_t num_favorite_games; - uint8_t autoboot_enabled; /* 1 if an autoboot ROM is configured */ + uint8_t autoboot_enabled; /* 1 if an autoboot ROM is configured */ + uint8_t reset_to_menu_active; /* 1 if this boot is a reset-to-menu (not cold power-on) */ } mcu_status_t; typedef struct __attribute__ ((__packed__)) _snes_status {