From 405c3476f5c041f9666f29760020125f2b5f1b7a Mon Sep 17 00:00:00 2001 From: sonninnos <45124675+sonninnos@users.noreply.github.com> Date: Sun, 3 Aug 2025 00:38:18 +0300 Subject: [PATCH] Save state slot improvements (#18140) * Stop polluting playlists with redundant entry_slot numbers * Playlist entry_slot improvements and cleanups * Save current state slot to runtime log --- command.c | 39 ++++++------- command.h | 2 +- menu/cbs/menu_cbs_ok.c | 2 - menu/menu_driver.c | 3 - playlist.c | 32 +++++++---- retroarch.c | 6 +- runloop.c | 124 ++++++++++++++++++++++++++--------------- runtime_file.c | 59 ++++++++++++++++++-- runtime_file.h | 1 + 9 files changed, 176 insertions(+), 92 deletions(-) diff --git a/command.c b/command.c index cef69e46ce..faf2ceacfc 100644 --- a/command.c +++ b/command.c @@ -1388,7 +1388,6 @@ void command_event_init_cheats( bool command_event_load_entry_state(settings_t *settings) { char entry_state_path[PATH_MAX_LENGTH] = ""; - int entry_path_stats; runloop_state_t *runloop_st = runloop_state_get_ptr(); bool ret = false; @@ -1417,10 +1416,7 @@ bool command_event_load_entry_state(settings_t *settings) return false; } - entry_path_stats = path_stat(entry_state_path); - - if ((entry_path_stats & RETRO_VFS_STAT_IS_VALID) == 0 - || (entry_path_stats & RETRO_VFS_STAT_IS_DIRECTORY) != 0) + if (!path_is_valid(entry_state_path)) return false; ret = content_load_state(entry_state_path, false, true); @@ -1430,32 +1426,30 @@ bool command_event_load_entry_state(settings_t *settings) entry_state_path); RARCH_LOG("[State] %s \"%s\" %s.\n", msg_hash_to_str(MSG_LOADING_ENTRY_STATE_FROM), - entry_state_path, ret ? "succeeded" : "failed" - ); - - if (ret) - configuration_set_int(settings, settings->ints.state_slot, runloop_st->entry_state_slot); + entry_state_path, + ret ? "succeeded" : "failed"); return ret; } -void command_event_load_auto_state(void) +bool command_event_load_auto_state(void) { size_t _len; char savestate_name_auto[PATH_MAX_LENGTH]; runloop_state_t *runloop_st = runloop_state_get_ptr(); const char *name_savestate = runloop_st->name.savestate; + bool ret = false; if (!core_info_current_supports_savestate()) - return; + return false; #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) - return; + return false; #endif #ifdef HAVE_NETWORKING if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) - return; + return false; #endif _len = strlcpy(savestate_name_auto, name_savestate, @@ -1464,20 +1458,19 @@ void command_event_load_auto_state(void) sizeof(savestate_name_auto) - _len); if (!path_is_valid(savestate_name_auto)) - return; + return false; + + ret = content_load_state(savestate_name_auto, false, true); RARCH_LOG("[State] %s \"%s\".\n", msg_hash_to_str(MSG_FOUND_AUTO_SAVESTATE_IN), savestate_name_auto); + RARCH_LOG("[State] %s \"%s\" %s.\n", + msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM), + savestate_name_auto, + ret ? "succeeded" : "failed"); - if ((content_load_state(savestate_name_auto, false, true))) - RARCH_LOG("[State] %s \"%s\" %s.\n", - msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM), - savestate_name_auto, "succeeded"); - else - RARCH_LOG("[State] %s \"%s\" %s.\n", - msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM), - savestate_name_auto, "failed"); + return ret; } /** diff --git a/command.h b/command.h index e63a0bf54d..6f4564acb4 100644 --- a/command.h +++ b/command.h @@ -380,7 +380,7 @@ void command_event_init_controllers(rarch_system_info_t *info, bool command_event_load_entry_state(settings_t *settings); -void command_event_load_auto_state(void); +bool command_event_load_auto_state(void); void command_event_set_savestate_auto_index( settings_t *settings); diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 94557fecfb..69422b34bf 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -2762,8 +2762,6 @@ static int action_ok_playlist_entry_collection(const char *path, playlist_resolve_path(PLAYLIST_LOAD, false, content_path, sizeof(content_path)); } - runloop_st->entry_state_slot = entry->entry_slot; - /* Cache entry label */ if (!string_is_empty(entry->label)) strlcpy(content_label, entry->label, sizeof(content_label)); diff --git a/menu/menu_driver.c b/menu/menu_driver.c index cc8217fd20..ac16bceb47 100644 --- a/menu/menu_driver.c +++ b/menu/menu_driver.c @@ -4665,9 +4665,6 @@ bool menu_driver_init(bool video_is_threaded) settings_t *settings = config_get_ptr(); struct menu_state *menu_st = &menu_driver_state; - command_event(CMD_EVENT_CORE_INFO_INIT, NULL); - command_event(CMD_EVENT_LOAD_CORE_PERSIST, NULL); - if ( menu_st->driver_data || menu_driver_init_internal( menu_st, p_disp, settings, diff --git a/playlist.c b/playlist.c index b3674239a5..0038da959b 100644 --- a/playlist.c +++ b/playlist.c @@ -1406,7 +1406,9 @@ bool playlist_push(playlist_t *playlist, continue; } - if (playlist->entries[i].entry_slot != entry->entry_slot) + /* Only write non-redundant entry slot numbers */ + if ( playlist->entries[i].entry_slot != entry->entry_slot + && (int)entry->entry_slot > 0) { playlist->entries[i].entry_slot = entry->entry_slot; entry_updated = true; @@ -1962,17 +1964,6 @@ void playlist_write_file(playlist_t *playlist) rjsonwriter_add_string(writer, playlist->entries[i].path); rjsonwriter_raw(writer, ",", 1); - if (playlist->entries[i].entry_slot) - { - rjsonwriter_raw(writer, "\n", 1); - rjsonwriter_add_spaces(writer, 6); - rjsonwriter_add_string(writer, "entry_slot"); - rjsonwriter_raw(writer, ":", 1); - rjsonwriter_raw(writer, " ", 1); - rjsonwriter_rawf(writer, "%d", (int)playlist->entries[i].entry_slot); - rjsonwriter_raw(writer, ",", 1); - } - rjsonwriter_raw(writer, "\n", 1); rjsonwriter_add_spaces(writer, 6); rjsonwriter_add_string(writer, "label"); @@ -2012,6 +2003,23 @@ void playlist_write_file(playlist_t *playlist) rjsonwriter_raw(writer, " ", 1); rjsonwriter_add_string(writer, playlist->entries[i].db_name); + /* Conditional rows must add "," first */ + + /* Typecast required because playlist_entry.entry_slot is unsigned, + * and 0 and -1 are redundant, but runloop.entry_state_slot is int16_t + * and must be able to be negative, because 0 is a valid slot */ + if ( (int)playlist->entries[i].entry_slot > 0 + && !strstr(playlist->config.path, FILE_PATH_BUILTIN)) + { + rjsonwriter_raw(writer, ",", 1); + rjsonwriter_raw(writer, "\n", 1); + rjsonwriter_add_spaces(writer, 6); + rjsonwriter_add_string(writer, "entry_slot"); + rjsonwriter_raw(writer, ":", 1); + rjsonwriter_raw(writer, " ", 1); + rjsonwriter_rawf(writer, "%d", (int)playlist->entries[i].entry_slot); + } + if (!string_is_empty(playlist->entries[i].subsystem_ident)) { rjsonwriter_raw(writer, ",", 1); diff --git a/retroarch.c b/retroarch.c index 8d25313eb2..bbacf8e237 100644 --- a/retroarch.c +++ b/retroarch.c @@ -7203,6 +7203,9 @@ static bool retroarch_parse_input_and_config( runloop_st->current_core.flags &= ~(RETRO_CORE_FLAG_HAS_SET_INPUT_DESCRIPTORS | RETRO_CORE_FLAG_HAS_SET_SUBSYSTEMS); + /* Reset entry slot */ + runloop_st->entry_state_slot = -1; + /* Load the config file now that we know what it is */ #ifdef HAVE_CONFIGFILE if (!(p_rarch->flags & RARCH_FLAGS_BLOCK_CONFIG_READ)) @@ -7773,7 +7776,6 @@ bool retroarch_main_init(int argc, char *argv[]) input_st->osk_idx = OSK_LOWERCASE_LATIN; video_st->flags |= VIDEO_FLAG_ACTIVE; audio_state_get_ptr()->flags |= AUDIO_FLAG_ACTIVE; - runloop_st->entry_state_slot = -1; if (setjmp(global->error_sjlj_context) > 0) { @@ -8105,8 +8107,6 @@ bool retroarch_main_init(int argc, char *argv[]) game_ai_init(); #endif - - return true; error: diff --git a/runloop.c b/runloop.c index d6470b267f..3dfb1074b3 100644 --- a/runloop.c +++ b/runloop.c @@ -655,6 +655,9 @@ static void runloop_update_runtime_log( /* Update 'last played' entry */ runtime_log_set_last_played_now(runtime_log); + /* Update state slot */ + runtime_log->state_slot = config_get_ptr()->ints.state_slot; + /* Save runtime log file */ runtime_log_save(runtime_log); @@ -4282,43 +4285,64 @@ static bool event_init_content( runloop_path_init_savefile(runloop_st); - if (!event_load_save_files(runloop_st->flags & - RUNLOOP_FLAG_IS_SRAM_LOAD_DISABLED)) - RARCH_LOG("[SRAM] %s\n", - msg_hash_to_str(MSG_SKIPPING_SRAM_LOAD)); + if (!event_load_save_files(runloop_st->flags & RUNLOOP_FLAG_IS_SRAM_LOAD_DISABLED)) + RARCH_LOG("[SRAM] %s\n", msg_hash_to_str(MSG_SKIPPING_SRAM_LOAD)); -/* - Since the operations are asynchronous we can't - guarantee users will not use auto_load_state to cheat on - achievements so we forbid auto_load_state from happening - if cheevos_enable and cheevos_hardcode_mode_enable - are true. -*/ + /* Set entry slot from playlist entry if available */ + { + playlist_t *playlist = playlist_get_cached(); + + if (playlist) + { + struct menu_state *menu_st = menu_state_get_ptr(); + const struct playlist_entry *entry = NULL; + + if (menu_st->driver_data) + playlist_get_index(playlist, menu_st->driver_data->rpl_entry_selection_ptr, &entry); + + if (entry) + runloop_st->entry_state_slot = entry->entry_slot; + } + + /* Set current active state slot */ + if (runloop_st->entry_state_slot > -1) + configuration_set_int(settings, settings->ints.state_slot, runloop_st->entry_state_slot); + } + + /* + * Since the operations are asynchronous we can't + * guarantee users will not use auto_load_state to cheat on + * achievements so we forbid auto_load_state from happening + * if cheevos_enable and cheevos_hardcode_mode_enable + * are true. + */ #ifdef HAVE_CHEEVOS if ( !cheevos_enable || !cheevos_hardcore_mode_enable) #endif { #ifdef HAVE_BSV_MOVIE - /* ignore entry state if we're doing bsv playback (we do want it - for bsv recording though) */ - if (!(input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_START_PLAYBACK)) + /* Ignore entry state if we're doing bsv playback (we do want it + for bsv recording though) */ + if (!(input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_START_PLAYBACK)) #endif { - if ( runloop_st->entry_state_slot > -1 + if ( runloop_st->entry_state_slot > -1 && !command_event_load_entry_state(settings)) { - /* loading the state failed, reset entry slot */ + /* Loading the state failed, reset entry slot */ runloop_st->entry_state_slot = -1; } } + #ifdef HAVE_BSV_MOVIE - /* ignore autoload state if we're doing bsv playback or recording */ - if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_START_RECORDING | BSV_FLAG_MOVIE_START_PLAYBACK))) + /* Ignore autoload state if we're doing bsv playback or recording */ + if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_START_RECORDING | BSV_FLAG_MOVIE_START_PLAYBACK))) #endif { - if (runloop_st->entry_state_slot < 0 && settings->bools.savestate_auto_load) - command_event_load_auto_state(); + if ( runloop_st->entry_state_slot < 0 + && settings->bools.savestate_auto_load) + command_event_load_auto_state(); } } @@ -4326,25 +4350,25 @@ static bool event_init_content( movie_stop(input_st); if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_START_RECORDING) { - configuration_set_uint(settings, settings->uints.rewind_granularity, 1); + configuration_set_uint(settings, settings->uints.rewind_granularity, 1); #ifndef HAVE_THREADS - /* Hack: the regular scheduler doesn't do the right thing here at - least in emscripten builds. I would expect that the check in - task_movie.c:343 should defer recording until the movie task - is done, but maybe that task isn't enqueued again yet when the - movie-record task is checked? Or the finder call in - content_load_state_in_progress is not correct? Either way, - the load happens after the recording starts rather than the - right way around. - */ - task_queue_wait(NULL,NULL); + /* Hack: the regular scheduler doesn't do the right thing here at + least in emscripten builds. I would expect that the check in + task_movie.c:343 should defer recording until the movie task + is done, but maybe that task isn't enqueued again yet when the + movie-record task is checked? Or the finder call in + content_load_state_in_progress is not correct? Either way, + the load happens after the recording starts rather than the + right way around. + */ + task_queue_wait(NULL, NULL); #endif - movie_start_record(input_st, input_st->bsv_movie_state.movie_start_path); + movie_start_record(input_st, input_st->bsv_movie_state.movie_start_path); } else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_START_PLAYBACK) { - configuration_set_uint(settings, settings->uints.rewind_granularity, 1); - movie_start_playback(input_st, input_st->bsv_movie_state.movie_start_path); + configuration_set_uint(settings, settings->uints.rewind_granularity, 1); + movie_start_playback(input_st, input_st->bsv_movie_state.movie_start_path); } #endif @@ -4355,6 +4379,7 @@ static bool event_init_content( static void runloop_runtime_log_init(runloop_state_t *runloop_st) { + settings_t *settings = config_get_ptr(); const char *content_path = path_get(RARCH_PATH_CONTENT); const char *core_path = path_get(RARCH_PATH_CORE); @@ -4386,6 +4411,19 @@ static void runloop_runtime_log_init(runloop_state_t *runloop_st) strlcpy(runloop_st->runtime_core_path, core_path, sizeof(runloop_st->runtime_core_path)); + + if ( !settings->bools.content_runtime_log + && !settings->bools.content_runtime_log_aggregate) + return; + + if ( !string_is_empty(content_path) + && !string_is_empty(core_path)) + runtime_log_init( + runloop_st->runtime_content_path, + runloop_st->runtime_core_path, + settings->paths.directory_runtime_log, + settings->paths.directory_playlist, + true); } void runloop_set_frame_limit( @@ -4634,16 +4672,9 @@ bool runloop_event_init_core( float fastforward_ratio = 0.0f; rarch_system_info_t *sys_info = &runloop_st->system; -#ifdef HAVE_NETWORKING - if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) - { - /* We need this in order for core_info_current_supports_netplay - to work correctly at init_netplay, - called later at event_init_content. */ - command_event(CMD_EVENT_CORE_INFO_INIT, NULL); - command_event(CMD_EVENT_LOAD_CORE_PERSIST, NULL); - } -#endif + /* Init core info files */ + command_event(CMD_EVENT_CORE_INFO_INIT, NULL); + command_event(CMD_EVENT_LOAD_CORE_PERSIST, NULL); /* Load symbols */ if (!runloop_init_libretro_symbols(runloop_st, @@ -4771,7 +4802,12 @@ bool runloop_event_init_core( runloop_set_frame_limit(&video_st->av_info, fastforward_ratio); runloop_st->frame_limit_last_time = cpu_features_get_time_usec(); + /* Init runtime log and read current state slot */ runloop_runtime_log_init(runloop_st); + + if (runloop_st->entry_state_slot > -1) + configuration_set_int(settings, settings->ints.state_slot, runloop_st->entry_state_slot); + return true; } diff --git a/runtime_file.c b/runtime_file.c index 18246ba3d1..a65698a474 100644 --- a/runtime_file.c +++ b/runtime_file.c @@ -55,6 +55,7 @@ typedef struct char **current_entry_val; char *runtime_string; char *last_played_string; + char *state_slot; } RtlJSONContext; static bool RtlJSONObjectMemberHandler(void *ctx, const char *s, size_t len) @@ -71,6 +72,8 @@ static bool RtlJSONObjectMemberHandler(void *ctx, const char *s, size_t len) p_ctx->current_entry_val = &p_ctx->runtime_string; else if (string_is_equal(s, "last_played")) p_ctx->current_entry_val = &p_ctx->last_played_string; + else if (string_is_equal(s, "state_slot")) + p_ctx->current_entry_val = &p_ctx->state_slot; /* Ignore unknown members */ } @@ -113,6 +116,8 @@ static void runtime_log_read_file(runtime_log_t *runtime_log) unsigned last_played_minute = 0; unsigned last_played_second = 0; + unsigned state_slot = 0; + RtlJSONContext context = {0}; /* Attempt to open log file */ RFILE *file = filestream_open(runtime_log->path, @@ -193,6 +198,24 @@ static void runtime_log_read_file(runtime_log_t *runtime_log) } } + /* State slot */ + if (!string_is_empty(context.state_slot)) + { + if (sscanf(context.state_slot, + "%04u", + &state_slot) != 1) + { + RARCH_ERR("[Runtime] Invalid \"state slot\" entry detected: \"%s\".\n", runtime_log->path); + goto end; + } + } + + if (state_slot > 0) + { + runloop_state_t *runloop_st = runloop_state_get_ptr(); + runloop_st->entry_state_slot = state_slot; + } + /* If we reach this point then all is well * > Assign values to runtime_log object */ runtime_log->runtime.hours = runtime_hours; @@ -206,12 +229,16 @@ static void runtime_log_read_file(runtime_log_t *runtime_log) runtime_log->last_played.minute = last_played_minute; runtime_log->last_played.second = last_played_second; + runtime_log->state_slot = state_slot; + end: /* Clean up leftover strings */ if (context.runtime_string) free(context.runtime_string); if (context.last_played_string) free(context.last_played_string); + if (context.state_slot) + free(context.state_slot); /* Close log file */ filestream_close(file); @@ -317,8 +344,10 @@ runtime_log_t *runtime_log_init( * no content is provided, 'content' is simply * the name of the core itself */ if (supports_no_game) - fill_pathname(content_name, core_name, - ".lrtl", sizeof(content_name)); + fill_pathname(content_name, + core_name, + FILE_PATH_RUNTIME_EXTENSION, + sizeof(content_name)); } /* NOTE: TyrQuake requires a specific hack, since all * content has the same name... */ @@ -334,12 +363,16 @@ runtime_log_t *runtime_log_init( strlcpy(tmp_buf, content_path, _len * sizeof(char)); fill_pathname(content_name, - path_basename(tmp_buf), ".lrtl", sizeof(content_name)); + path_basename(tmp_buf), + FILE_PATH_RUNTIME_EXTENSION, + sizeof(content_name)); } } } else - fill_pathname(content_name, path_basename(content_path), ".lrtl", + fill_pathname(content_name, + path_basename(content_path), + FILE_PATH_RUNTIME_EXTENSION, sizeof(content_name)); if (string_is_empty(content_name)) @@ -369,6 +402,8 @@ runtime_log_t *runtime_log_init( runtime_log->last_played.minute = 0; runtime_log->last_played.second = 0; + runtime_log->state_slot = 0; + runtime_log->path[0] = '\0'; strlcpy(runtime_log->path, log_file_path, sizeof(runtime_log->path)); @@ -468,6 +503,8 @@ void runtime_log_reset(runtime_log_t *runtime_log) runtime_log->last_played.hour = 0; runtime_log->last_played.minute = 0; runtime_log->last_played.second = 0; + + runtime_log->state_slot = 0; } /* Getters */ @@ -1112,6 +1149,20 @@ void runtime_log_save(runtime_log_t *runtime_log) rjsonwriter_raw(writer, ":", 1); rjsonwriter_raw(writer, " ", 1); rjsonwriter_add_string(writer, value_string); + rjsonwriter_raw(writer, ",", 1); + rjsonwriter_raw(writer, "\n", 1); + + /* > Current state slot */ + value_string[0] = '\0'; + snprintf(value_string, sizeof(value_string), + "%u", + runtime_log->state_slot); + + rjsonwriter_add_spaces(writer, 2); + rjsonwriter_add_string(writer, "state_slot"); + rjsonwriter_raw(writer, ":", 1); + rjsonwriter_raw(writer, " ", 1); + rjsonwriter_add_string(writer, value_string); rjsonwriter_raw(writer, "\n", 1); /* > Finalise */ diff --git a/runtime_file.h b/runtime_file.h index e485e686e0..c694e7ff21 100644 --- a/runtime_file.h +++ b/runtime_file.h @@ -57,6 +57,7 @@ typedef struct { rtl_runtime_t runtime; /* unsigned alignment */ rtl_last_played_t last_played; /* unsigned alignment */ + unsigned state_slot; char path[PATH_MAX_LENGTH]; } runtime_log_t;