mirror of
https://github.com/libretro/RetroArch
synced 2025-04-09 21:45:45 +00:00
Add savestate wraparound. (#16947)
When save state auto indexing is enabled, and maximum kept states are limited, wrap around after reaching the configured maximum. A gap in the indexing is used to keep track of most recent state. If e.g. maximum kept amount is 5, then indexes 0..5 will be used, if 3 is empty, most recent state is 2.
This commit is contained in:
parent
cbfe2a7279
commit
98c79b3f14
322
command.c
322
command.c
@ -1419,27 +1419,43 @@ void command_event_load_auto_state(void)
|
|||||||
savestate_name_auto, "failed");
|
savestate_name_auto, "failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
void command_event_set_savestate_auto_index(settings_t *settings)
|
/**
|
||||||
|
* Scans existing states to determine which one should be loaded
|
||||||
|
* and which one can be deleted, using savestate wraparound if
|
||||||
|
* enabled.
|
||||||
|
*
|
||||||
|
* @param settings The usual RetroArch settings ptr.
|
||||||
|
* @param last_index Return value for load slot.
|
||||||
|
* @param file_to_delete Return value for file name that should be removed.
|
||||||
|
*/
|
||||||
|
static void scan_states(settings_t *settings,
|
||||||
|
unsigned *last_index, char *file_to_delete)
|
||||||
{
|
{
|
||||||
size_t i;
|
|
||||||
char state_base[128];
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
||||||
|
bool show_hidden_files = settings->bools.show_hidden_files;
|
||||||
|
unsigned savestate_max_keep = settings->uints.savestate_max_keep;
|
||||||
|
int curr_state_slot = settings->ints.state_slot;
|
||||||
|
|
||||||
|
unsigned max_idx = 0;
|
||||||
|
unsigned loa_idx = 0;
|
||||||
|
unsigned gap_idx = UINT_MAX;
|
||||||
|
unsigned del_idx = UINT_MAX;
|
||||||
|
retro_bits_512_t slot_mapping_low = {0};
|
||||||
|
retro_bits_512_t slot_mapping_high = {0};
|
||||||
|
|
||||||
|
struct string_list *dir_list = NULL;
|
||||||
|
const char *savefile_root = NULL;
|
||||||
|
size_t savefile_root_length = 0;
|
||||||
|
|
||||||
|
size_t i, cnt = 0;
|
||||||
|
size_t cnt_in_range = 0;
|
||||||
char state_dir[PATH_MAX_LENGTH];
|
char state_dir[PATH_MAX_LENGTH];
|
||||||
|
/* Base name of 128 may be too short for some (<<1%) of the
|
||||||
struct string_list *dir_list = NULL;
|
tosec-based file names, but in practice truncating will not
|
||||||
unsigned max_idx = 0;
|
lead to mismatch */
|
||||||
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
char state_base[128];
|
||||||
bool savestate_auto_index = settings->bools.savestate_auto_index;
|
|
||||||
bool show_hidden_files = settings->bools.show_hidden_files;
|
|
||||||
|
|
||||||
if (!savestate_auto_index)
|
|
||||||
return;
|
|
||||||
|
|
||||||
/* Find the file in the same directory as runloop_st->savestate_name
|
|
||||||
* with the largest numeral suffix.
|
|
||||||
*
|
|
||||||
* E.g. /foo/path/content.state, will try to find
|
|
||||||
* /foo/path/content.state%d, where %d is the largest number available.
|
|
||||||
*/
|
|
||||||
fill_pathname_basedir(state_dir, runloop_st->name.savestate,
|
fill_pathname_basedir(state_dir, runloop_st->name.savestate,
|
||||||
sizeof(state_dir));
|
sizeof(state_dir));
|
||||||
|
|
||||||
@ -1455,68 +1471,10 @@ void command_event_set_savestate_auto_index(settings_t *settings)
|
|||||||
for (i = 0; i < dir_list->size; i++)
|
for (i = 0; i < dir_list->size; i++)
|
||||||
{
|
{
|
||||||
unsigned idx;
|
unsigned idx;
|
||||||
char elem_base[128] = {0};
|
char elem_base[128] = {0};
|
||||||
const char *end = NULL;
|
const char *ext = NULL;
|
||||||
const char *dir_elem = dir_list->elems[i].data;
|
const char *end = NULL;
|
||||||
|
const char *dir_elem = dir_list->elems[i].data;
|
||||||
fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
||||||
|
|
||||||
if (strstr(elem_base, state_base) != elem_base)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
end = dir_elem + strlen(dir_elem);
|
|
||||||
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
|
||||||
end--;
|
|
||||||
|
|
||||||
idx = (unsigned)strtoul(end, NULL, 0);
|
|
||||||
if (idx > max_idx)
|
|
||||||
max_idx = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
dir_list_free(dir_list);
|
|
||||||
|
|
||||||
configuration_set_int(settings, settings->ints.state_slot, max_idx);
|
|
||||||
|
|
||||||
RARCH_LOG("[State]: %s: #%d\n",
|
|
||||||
msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT),
|
|
||||||
max_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void command_event_set_savestate_garbage_collect(
|
|
||||||
unsigned max_to_keep,
|
|
||||||
bool show_hidden_files
|
|
||||||
)
|
|
||||||
{
|
|
||||||
size_t i, cnt = 0;
|
|
||||||
char state_dir[PATH_MAX_LENGTH];
|
|
||||||
char state_base[128];
|
|
||||||
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
||||||
|
|
||||||
struct string_list *dir_list = NULL;
|
|
||||||
unsigned min_idx = UINT_MAX;
|
|
||||||
const char *oldest_save = NULL;
|
|
||||||
|
|
||||||
/* Similar to command_event_set_savestate_auto_index(),
|
|
||||||
* this will find the lowest numbered save-state */
|
|
||||||
fill_pathname_basedir(state_dir, runloop_st->name.savestate,
|
|
||||||
sizeof(state_dir));
|
|
||||||
|
|
||||||
dir_list = dir_list_new_special(state_dir, DIR_LIST_PLAIN, NULL,
|
|
||||||
show_hidden_files);
|
|
||||||
|
|
||||||
if (!dir_list)
|
|
||||||
return;
|
|
||||||
|
|
||||||
fill_pathname_base(state_base, runloop_st->name.savestate,
|
|
||||||
sizeof(state_base));
|
|
||||||
|
|
||||||
for (i = 0; i < dir_list->size; i++)
|
|
||||||
{
|
|
||||||
unsigned idx;
|
|
||||||
char elem_base[128];
|
|
||||||
const char *ext = NULL;
|
|
||||||
const char *end = NULL;
|
|
||||||
const char *dir_elem = dir_list->elems[i].data;
|
|
||||||
|
|
||||||
if (string_is_empty(dir_elem))
|
if (string_is_empty(dir_elem))
|
||||||
continue;
|
continue;
|
||||||
@ -1535,39 +1493,206 @@ void command_event_set_savestate_garbage_collect(
|
|||||||
if (!string_starts_with(elem_base, state_base))
|
if (!string_starts_with(elem_base, state_base))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* This looks like a valid save */
|
/* This looks like a valid savestate */
|
||||||
cnt++;
|
/* Save filename root and length (once) */
|
||||||
|
if (savefile_root_length == 0)
|
||||||
|
{
|
||||||
|
savefile_root = dir_elem;
|
||||||
|
savefile_root_length = strlen(dir_elem);
|
||||||
|
}
|
||||||
|
|
||||||
/* > Get index */
|
/* Decode the savestate index */
|
||||||
end = dir_elem + strlen(dir_elem);
|
end = dir_elem + strlen(dir_elem);
|
||||||
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
||||||
|
{
|
||||||
end--;
|
end--;
|
||||||
|
if (savefile_root == dir_elem)
|
||||||
|
savefile_root_length--;
|
||||||
|
}
|
||||||
idx = string_to_unsigned(end);
|
idx = string_to_unsigned(end);
|
||||||
|
|
||||||
/* > Check if this is the lowest index so far */
|
/* Simple administration: max, total. */
|
||||||
if (idx < min_idx)
|
if (idx > max_idx)
|
||||||
|
max_idx = idx;
|
||||||
|
cnt++;
|
||||||
|
if (idx <= savestate_max_keep)
|
||||||
|
cnt_in_range++;
|
||||||
|
|
||||||
|
/* Maintain a 2x512 bit map of occupied save states */
|
||||||
|
if (idx<512)
|
||||||
|
BIT512_SET(slot_mapping_low,idx);
|
||||||
|
else if (idx<1024)
|
||||||
|
BIT512_SET(slot_mapping_high,idx-512);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Next loop on the bitmap, since the file system may have presented the files in any order above */
|
||||||
|
for(i=0 ; i <= savestate_max_keep ; i++)
|
||||||
|
{
|
||||||
|
/* Unoccupied save slots */
|
||||||
|
if ((i < 512 && !BIT512_GET(slot_mapping_low, i)) ||
|
||||||
|
(i > 511 && !BIT512_GET(slot_mapping_high, i-512)) )
|
||||||
{
|
{
|
||||||
min_idx = idx;
|
/* Gap index: lowest free slot in the wraparound range */
|
||||||
oldest_save = dir_elem;
|
if (gap_idx == UINT_MAX)
|
||||||
|
gap_idx = i;
|
||||||
|
}
|
||||||
|
/* Occupied save slots */
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Del index: first occupied slot in the wraparound range,
|
||||||
|
after gap index */
|
||||||
|
if (gap_idx < UINT_MAX &&
|
||||||
|
del_idx == UINT_MAX)
|
||||||
|
del_idx = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Special cases of wraparound */
|
||||||
|
|
||||||
|
/* No previous savestate - set to end, so that first save
|
||||||
|
goes to 0 */
|
||||||
|
if (cnt_in_range == 0)
|
||||||
|
{
|
||||||
|
if (cnt == 0)
|
||||||
|
loa_idx = savestate_max_keep;
|
||||||
|
/* Transient: nothing in current range, but something is present
|
||||||
|
* higher up -> load that */
|
||||||
|
else
|
||||||
|
loa_idx = max_idx;
|
||||||
|
gap_idx = savestate_max_keep;
|
||||||
|
del_idx = savestate_max_keep;
|
||||||
|
}
|
||||||
|
/* No gap was found - deduct from current index or default
|
||||||
|
and set (missing) gap index to be deleted */
|
||||||
|
else if (gap_idx == UINT_MAX)
|
||||||
|
{
|
||||||
|
/* Transient: no gap, and max is higher than currently
|
||||||
|
* allowed -> load that, but wrap around so that next
|
||||||
|
* time gap will be present */
|
||||||
|
if (max_idx > savestate_max_keep)
|
||||||
|
{
|
||||||
|
loa_idx = max_idx;
|
||||||
|
gap_idx = 1;
|
||||||
|
}
|
||||||
|
/* Current index is in range, so let's assume it is correct */
|
||||||
|
else if ( (unsigned)curr_state_slot < savestate_max_keep)
|
||||||
|
{
|
||||||
|
loa_idx = curr_state_slot;
|
||||||
|
gap_idx = curr_state_slot + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loa_idx = savestate_max_keep;
|
||||||
|
gap_idx = 0;
|
||||||
|
}
|
||||||
|
del_idx = gap_idx;
|
||||||
|
}
|
||||||
|
/* Gap was found */
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* No candidate to delete */
|
||||||
|
if (del_idx == UINT_MAX)
|
||||||
|
{
|
||||||
|
/* Either gap is at the end of the range: wraparound.
|
||||||
|
or there is no better idea than the lowest index */
|
||||||
|
del_idx = 0;
|
||||||
|
}
|
||||||
|
/* Adjust load index */
|
||||||
|
if (gap_idx == 0)
|
||||||
|
loa_idx = savestate_max_keep;
|
||||||
|
else
|
||||||
|
loa_idx = gap_idx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RARCH_DBG("[State]: savestate scanning finished, used slots (in range): "
|
||||||
|
"%d (%d), max:%d, load index %d, gap index %d, delete index %d\n",
|
||||||
|
cnt, cnt_in_range, max_idx, loa_idx, gap_idx, del_idx);
|
||||||
|
|
||||||
|
if (last_index != NULL)
|
||||||
|
{
|
||||||
|
*last_index = loa_idx;
|
||||||
|
}
|
||||||
|
if (file_to_delete != NULL && cnt_in_range >= savestate_max_keep)
|
||||||
|
{
|
||||||
|
strlcpy(file_to_delete, savefile_root, savefile_root_length + 1);
|
||||||
|
/* ".state0" is just ".state" instead, so don't print that. */
|
||||||
|
if (del_idx > 0)
|
||||||
|
snprintf(file_to_delete+savefile_root_length, 5, "%d", del_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
dir_list_free(dir_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines next savestate slot in case of auto-increment,
|
||||||
|
* i.e. save state scanning was done already earlier.
|
||||||
|
* Logic moved here so that all save state wraparound code is
|
||||||
|
* in this file.
|
||||||
|
*
|
||||||
|
* @param settings The usual RetroArch settings ptr.
|
||||||
|
* @return \c The next savestate slot.
|
||||||
|
*/
|
||||||
|
int command_event_get_next_savestate_auto_index(settings_t *settings)
|
||||||
|
{
|
||||||
|
unsigned savestate_max_keep = settings->uints.savestate_max_keep;
|
||||||
|
int new_state_slot = settings->ints.state_slot + 1;
|
||||||
|
|
||||||
|
/* If previous save was above the wraparound range, or it overflows,
|
||||||
|
return to the start of the range. */
|
||||||
|
if( savestate_max_keep > 0 && (unsigned)new_state_slot > savestate_max_keep)
|
||||||
|
new_state_slot = 0;
|
||||||
|
|
||||||
|
return new_state_slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines most recent savestate slot in case of content load.
|
||||||
|
*
|
||||||
|
* @param settings The usual RetroArch settings ptr.
|
||||||
|
* @return \c The most recent savestate slot.
|
||||||
|
*/
|
||||||
|
void command_event_set_savestate_auto_index(settings_t *settings)
|
||||||
|
{
|
||||||
|
unsigned max_idx = 0;
|
||||||
|
bool savestate_auto_index = settings->bools.savestate_auto_index;
|
||||||
|
|
||||||
|
if (!savestate_auto_index)
|
||||||
|
return;
|
||||||
|
|
||||||
|
scan_states(settings, &max_idx, NULL);
|
||||||
|
configuration_set_int(settings, settings->ints.state_slot, max_idx);
|
||||||
|
|
||||||
|
RARCH_LOG("[State]: %s: #%d\n",
|
||||||
|
msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT),
|
||||||
|
max_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the oldest save state and its thumbnail, if needed.
|
||||||
|
*
|
||||||
|
* @param settings The usual RetroArch settings ptr.
|
||||||
|
*/
|
||||||
|
static void command_event_set_savestate_garbage_collect(settings_t *settings)
|
||||||
|
{
|
||||||
|
char state_to_delete[PATH_MAX_LENGTH] = {0};
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
scan_states(settings, NULL, state_to_delete);
|
||||||
/* Only delete one save state per save action
|
/* Only delete one save state per save action
|
||||||
* > Conservative behaviour, designed to minimise
|
* > Conservative behaviour, designed to minimise
|
||||||
* the risk of deleting multiple incorrect files
|
* the risk of deleting multiple incorrect files
|
||||||
* in case of accident */
|
* in case of accident */
|
||||||
if (!string_is_empty(oldest_save) && (cnt > max_to_keep))
|
if (!string_is_empty(state_to_delete))
|
||||||
{
|
{
|
||||||
filestream_delete(oldest_save);
|
filestream_delete(state_to_delete);
|
||||||
|
RARCH_DBG("[State]: garbage collect, deleting \"%s\" \n",state_to_delete);
|
||||||
/* Construct the save state thumbnail name
|
/* Construct the save state thumbnail name
|
||||||
* and delete that one as well. */
|
* and delete that one as well. */
|
||||||
i = strlcpy(state_dir,oldest_save,PATH_MAX_LENGTH);
|
i = strlen(state_to_delete);
|
||||||
strlcpy(state_dir + i,".png",STRLEN_CONST(".png")+1);
|
strlcpy(state_to_delete + i,".png",STRLEN_CONST(".png")+1);
|
||||||
filestream_delete(state_dir);
|
filestream_delete(state_to_delete);
|
||||||
|
RARCH_DBG("[State]: garbage collect, deleting \"%s\" \n",state_to_delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
dir_list_free(dir_list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void command_event_set_replay_auto_index(settings_t *settings)
|
void command_event_set_replay_auto_index(settings_t *settings)
|
||||||
@ -1998,10 +2123,7 @@ bool command_event_main_state(unsigned cmd)
|
|||||||
|
|
||||||
/* Clean up excess savestates if necessary */
|
/* Clean up excess savestates if necessary */
|
||||||
if (savestate_auto_index && (savestate_max_keep > 0))
|
if (savestate_auto_index && (savestate_max_keep > 0))
|
||||||
command_event_set_savestate_garbage_collect(
|
command_event_set_savestate_garbage_collect(settings);
|
||||||
settings->uints.savestate_max_keep,
|
|
||||||
settings->bools.show_hidden_files
|
|
||||||
);
|
|
||||||
|
|
||||||
if (frame_time_counter_reset_after_save_state)
|
if (frame_time_counter_reset_after_save_state)
|
||||||
video_st->frame_time_count = 0;
|
video_st->frame_time_count = 0;
|
||||||
|
@ -381,10 +381,8 @@ void command_event_load_auto_state(void);
|
|||||||
void command_event_set_savestate_auto_index(
|
void command_event_set_savestate_auto_index(
|
||||||
settings_t *settings);
|
settings_t *settings);
|
||||||
|
|
||||||
void command_event_set_savestate_garbage_collect(
|
int command_event_get_next_savestate_auto_index(
|
||||||
unsigned max_to_keep,
|
settings_t *settings);
|
||||||
bool show_hidden_files
|
|
||||||
);
|
|
||||||
|
|
||||||
void command_event_set_replay_auto_index(
|
void command_event_set_replay_auto_index(
|
||||||
settings_t *settings);
|
settings_t *settings);
|
||||||
|
@ -3558,12 +3558,10 @@ bool command_event(enum event_command cmd, void *data)
|
|||||||
case CMD_EVENT_SAVE_STATE:
|
case CMD_EVENT_SAVE_STATE:
|
||||||
case CMD_EVENT_SAVE_STATE_TO_RAM:
|
case CMD_EVENT_SAVE_STATE_TO_RAM:
|
||||||
{
|
{
|
||||||
int state_slot = settings->ints.state_slot;
|
|
||||||
|
|
||||||
if (settings->bools.savestate_auto_index)
|
if (settings->bools.savestate_auto_index)
|
||||||
{
|
{
|
||||||
int new_state_slot = state_slot + 1;
|
configuration_set_int(settings, settings->ints.state_slot,
|
||||||
configuration_set_int(settings, settings->ints.state_slot, new_state_slot);
|
command_event_get_next_savestate_auto_index(settings));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!command_event_main_state(cmd))
|
if (!command_event_main_state(cmd))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user