diff --git a/docs/10_getting_started_sd.md b/docs/10_getting_started_sd.md index 0c5a8daaa..3865f3912 100644 --- a/docs/10_getting_started_sd.md +++ b/docs/10_getting_started_sd.md @@ -88,6 +88,9 @@ the "cartridge save memory". └── b_rom_whatever.sav ``` +> [!TIP] +> To keep every save in one place instead, turn on **Global Saves Folder** in the [menu settings](./32_menu_settings.md). Saves are then stored as `/saves/.sav` at the root of the SD card, and every copy of a ROM with the same file name shares that save. Turn on **Migrate Old Saves** as well to move your existing saves there as you launch each game. + ### Transferring saves from an ED64 or emulator If you are transferring a file from a different flashcart, such as the ED64, or an emulator you must change the file extension to `sav`. For example, a save file called `Glover (USA).eep` should have its extension changed to `Glover (USA).sav` to work with N64FlashcartMenu. diff --git a/docs/32_menu_settings.md b/docs/32_menu_settings.md index a5c753a3a..3cfa195b5 100644 --- a/docs/32_menu_settings.md +++ b/docs/32_menu_settings.md @@ -73,6 +73,19 @@ Controls whether N64FlashcartMenu should use `/saves` folders to store ROM save ON: ROM saves are saved in separate subfolders (called `\saves`, will create one `\saves` subfolder per folder). OFF: ROM saves are saved alongside the ROM file. +### Global Saves Folder + +Controls whether N64FlashcartMenu keeps every save in one global `/saves` folder at the root of the SD card. This setting is OFF by default. +ON: saves are stored as `/saves/.sav`, whichever folder the ROM is launched from. Copies of the same ROM file in different folders (for example a favourites folder and an A-Z folder) therefore share one save. +OFF: saves are stored per folder, as described in Use Save Folders, so each copy of a ROM keeps its own save. + +### Migrate Old Saves + +Only used when Global Saves Folder is ON. This setting is OFF by default. +ON: when a ROM has no global save yet but still has one in its own location (see Use Save Folders), that save is moved into the global folder the first time the ROM is launched, so existing progress carries over. +OFF: saves in the per-folder locations are left where they are, and a ROM without a global save starts a new one. +Saves that were moved to the global folder are not moved back when either setting is turned OFF. + ### Sound Effects The menu has default sound effects to improve the user experience. See the [sound documentation](./40_sound.md) for details. This setting is OFF by default. diff --git a/src/menu/cart_load.c b/src/menu/cart_load.c index 87622540d..e5d0ab4c0 100644 --- a/src/menu/cart_load.c +++ b/src/menu/cart_load.c @@ -6,6 +6,7 @@ #include #include +#include #include "cart_load.h" #include "ini_parser.h" #include "path.h" @@ -48,6 +49,50 @@ static bool create_saves_subdirectory (path_t *path) { return error; } +/** + * @brief Get the path of a ROM's save in the global saves directory. + * + * Every save lives in /saves on the SD card, named after the ROM file ("sd:/any/directory/Game.z64" uses + * "sd:/saves/Game.sav"), so copies of the same ROM in different directories share one save. + * With "Migrate Old Saves" on, a save still stored in the ROM's own location (its "saves" subdirectory, or next + * to the ROM) is moved into the global directory the first time, so existing progress carries over. If that move + * fails, the existing save is used where it is rather than starting a new, empty one. With it off, saves in the + * per-folder locations are left untouched. + * + * @param menu Pointer to the menu structure. + * @param save_path The save path next to the ROM (with the "sav" extension already applied). + * @param save_type The save type (nothing is created or moved when the ROM has no save). + * @return path_t* The save path to use (to be freed by the caller), or NULL if the directory could not be created. + */ +static path_t *global_save_path (menu_t *menu, path_t *save_path, flashcart_save_type_t save_type) { + path_t *global_save = path_init(menu->storage_prefix, "/" SAVE_DIRECTORY_NAME); + if ((save_type != FLASHCART_SAVE_TYPE_NONE) && directory_create(path_get(global_save))) { + path_free(global_save); + return NULL; + } + path_push(global_save, path_last_get(save_path)); + + if (!menu->settings.migrate_saves_to_global_folder || (save_type == FLASHCART_SAVE_TYPE_NONE)) { + return global_save; + } + + path_t *local_save = path_clone(save_path); + if (menu->settings.use_saves_folder) { + path_push_subdir(local_save, SAVE_DIRECTORY_NAME); + } + + if (!path_are_match(global_save, local_save) && !file_exists(path_get(global_save)) && + file_exists(path_get(local_save))) { + if (f_rename(strip_fs_prefix(path_get(local_save)), strip_fs_prefix(path_get(global_save))) != FR_OK) { + path_free(global_save); + return local_save; + } + } + + path_free(local_save); + return global_save; +} + /** * @brief Convert the ROM save type to the flashcart save type. * @@ -113,7 +158,14 @@ cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, flashcart_progress_cal } path_ext_replace(path, "sav"); - if (menu->settings.use_saves_folder) { + if (menu->settings.use_global_saves_folder) { + path_t *save_path = global_save_path(menu, path, save_type); + path_free(path); + if (save_path == NULL) { + return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL; + } + path = save_path; + } else if (menu->settings.use_saves_folder) { if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(path)) { path_free(path); return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL; @@ -359,7 +411,14 @@ cart_load_err_t cart_load_emulator (menu_t *menu, cart_load_emu_type_t emu_type, } path_ext_replace(path, "sav"); - if (menu->settings.use_saves_folder) { + if (menu->settings.use_global_saves_folder) { + path_t *save_path = global_save_path(menu, path, save_type); + path_free(path); + if (save_path == NULL) { + return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL; + } + path = save_path; + } else if (menu->settings.use_saves_folder) { if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(path)) { path_free(path); return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL; diff --git a/src/menu/settings.c b/src/menu/settings.c index f530a7ddf..5a668b1ea 100644 --- a/src/menu/settings.c +++ b/src/menu/settings.c @@ -42,6 +42,8 @@ static settings_t init = { .boot_into_grid = false, .grid_directory = "/", .use_saves_folder = true, + .use_global_saves_folder = false, + .migrate_saves_to_global_folder = false, .show_saves_folder = false, .show_save_files = false, .show_cheat_files = false, @@ -91,6 +93,8 @@ void settings_load (settings_t *settings) { free(settings->grid_directory); settings->grid_directory = strdup(ini_get_string(ini, "menu", "grid_directory", settings->default_directory)); settings->use_saves_folder = ini_get_bool(ini, "menu", "use_saves_folder", init.use_saves_folder); + settings->use_global_saves_folder = ini_get_bool(ini, "menu", "use_global_saves_folder", init.use_global_saves_folder); + settings->migrate_saves_to_global_folder = ini_get_bool(ini, "menu", "migrate_saves_to_global_folder", init.migrate_saves_to_global_folder); settings->show_saves_folder = ini_get_bool(ini, "menu", "show_saves_folder", init.show_saves_folder); settings->show_save_files = ini_get_bool(ini, "menu", "show_save_files", init.show_save_files); settings->show_cheat_files = ini_get_bool(ini, "menu", "show_cheat_files", init.show_cheat_files); @@ -137,6 +141,8 @@ void settings_save (settings_t *settings) { ini_set_bool(ini, "menu", "boot_into_grid", settings->boot_into_grid); ini_set_string(ini, "menu", "grid_directory", settings->grid_directory); ini_set_bool(ini, "menu", "use_saves_folder", settings->use_saves_folder); + ini_set_bool(ini, "menu", "use_global_saves_folder", settings->use_global_saves_folder); + ini_set_bool(ini, "menu", "migrate_saves_to_global_folder", settings->migrate_saves_to_global_folder); ini_set_bool(ini, "menu", "show_saves_folder", settings->show_saves_folder); ini_set_bool(ini, "menu", "show_save_files", settings->show_save_files); ini_set_bool(ini, "menu", "show_cheat_files", settings->show_cheat_files); diff --git a/src/menu/settings.h b/src/menu/settings.h index cf2b186a3..142cb16b8 100644 --- a/src/menu/settings.h +++ b/src/menu/settings.h @@ -39,6 +39,12 @@ typedef struct { /** @brief Put saves into separate directory */ bool use_saves_folder; + /** @brief Keep every save in one global folder (/saves), shared by all copies of a ROM */ + bool use_global_saves_folder; + + /** @brief Move a ROM's existing per-folder save into the global saves folder the first time the ROM is launched */ + bool migrate_saves_to_global_folder; + /** @brief Show saves folder in file browser */ bool show_saves_folder; diff --git a/src/menu/views/settings_editor.c b/src/menu/views/settings_editor.c index 9a9f2f12a..33d428049 100644 --- a/src/menu/views/settings_editor.c +++ b/src/menu/views/settings_editor.c @@ -110,6 +110,8 @@ static const setting_descriptor_t settings[] = { BOOL_SETTING("Sound Effects", soundfx_enabled, SETTING_UPDATE_SFX), BOOL_SETTING("Background Music", bgm_enabled, SETTING_UPDATE_BGM), BOOL_SETTING("Use Saves Folder", use_saves_folder, 0), + BOOL_SETTING("Global Saves Folder", use_global_saves_folder, 0), + BOOL_SETTING("Migrate Old Saves", migrate_saves_to_global_folder, 0), BOOL_SETTING("Show Saves Folder", show_saves_folder, SETTING_RELOAD_BROWSER), BOOL_SETTING("Show Save Files", show_save_files, SETTING_RELOAD_BROWSER), BOOL_SETTING("Show Cheat Files", show_cheat_files, SETTING_RELOAD_BROWSER),