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
This commit is contained in:
sonninnos 2025-08-03 00:38:18 +03:00 committed by GitHub
parent ecbc05de6b
commit 405c3476f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 176 additions and 92 deletions

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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));

View File

@ -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,

View File

@ -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);

View File

@ -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:

124
runloop.c
View File

@ -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;
}

View File

@ -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 */

View File

@ -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;