From 0b043124120be9dda9f7e81841cb6d0748639d4d Mon Sep 17 00:00:00 2001 From: jdgleaver Date: Wed, 20 Feb 2019 16:42:55 +0000 Subject: [PATCH 1/2] Add optional playlist sublabels (associated core + play time, where available) --- command.c | 2 + config.def.h | 2 + configuration.c | 1 + configuration.h | 1 + intl/msg_hash_lbl.h | 2 + intl/msg_hash_us.h | 16 ++++++ menu/cbs/menu_cbs_sublabel.c | 107 +++++++++++++++++++++++++++++++++++ menu/menu_displaylist.c | 3 + menu/menu_setting.c | 16 ++++++ msg_hash.h | 4 ++ retroarch.c | 6 +- 11 files changed, 157 insertions(+), 3 deletions(-) diff --git a/command.c b/command.c index 6820a27226..b6a2957864 100644 --- a/command.c +++ b/command.c @@ -1961,6 +1961,8 @@ bool command_event(enum event_command cmd, void *data) content_get_status(&contentless, &is_inited); + if (!contentless) + rarch_ctl(RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT, NULL); command_event(CMD_EVENT_AUTOSAVE_STATE, NULL); command_event(CMD_EVENT_DISABLE_OVERRIDES, NULL); command_event(CMD_EVENT_RESTORE_DEFAULT_SHADER_PRESET, NULL); diff --git a/config.def.h b/config.def.h index 6f0f957f0c..17c8838d7e 100644 --- a/config.def.h +++ b/config.def.h @@ -714,6 +714,8 @@ static const bool playlist_use_old_format = false; * (RGUI only) */ static const bool playlist_show_core_name = true; +static const bool playlist_show_sublabels = false; + /* Show Menu start-up screen on boot. */ static const bool default_menu_show_start_screen = true; diff --git a/configuration.c b/configuration.c index c499dad7f7..05ea7b3f79 100644 --- a/configuration.c +++ b/configuration.c @@ -1575,6 +1575,7 @@ static struct config_bool_setting *populate_settings_bool(settings_t *settings, SETTING_BOOL("playlist_use_old_format", &settings->bools.playlist_use_old_format, true, playlist_use_old_format, false); SETTING_BOOL("content_runtime_log", &settings->bools.content_runtime_log, true, content_runtime_log, false); + SETTING_BOOL("playlist_show_sublabels", &settings->bools.playlist_show_sublabels, true, playlist_show_sublabels, false); SETTING_BOOL("playlist_sort_alphabetical", &settings->bools.playlist_sort_alphabetical, true, playlist_sort_alphabetical, false); diff --git a/configuration.h b/configuration.h index 5cab0195ea..995e41fb3a 100644 --- a/configuration.h +++ b/configuration.h @@ -311,6 +311,7 @@ typedef struct settings bool playlist_show_core_name; bool playlist_sort_alphabetical; + bool playlist_show_sublabels; } bools; struct diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index 961872a2ff..afd9c20522 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -1789,3 +1789,5 @@ MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SHOW_CORE_NAME, "playlist_show_core_name") MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SORT_ALPHABETICAL, "playlist_sort_alphabetical") +MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS, + "playlist_show_sublabels") diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index f584789602..d13a55c5af 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -8298,3 +8298,19 @@ MSG_HASH( MENU_ENUM_SUBLABEL_CONTENT_RUNTIME_LOG, "Keeps track of how long your content has been running over time." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_SHOW_SUBLABELS, + "Show playlist sublabels" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_SHOW_SUBLABELS, + "Shows additional information for each playlist entry, such as current core association and play time (if available). Has a variable performance impact." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_CORE, + "Core:" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME, + "Play Time:" + ) diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 94de0b6865..67bbec6bfc 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -47,6 +47,8 @@ #include "../input/input_driver.h" #include "../tasks/tasks_internal.h" +#include "../../defaults.h" + #define default_sublabel_macro(func_name, lbl) \ static int (func_name)(file_list_t *list, unsigned type, unsigned i, const char *label, const char *path, char *s, size_t len) \ { \ @@ -509,6 +511,7 @@ default_sublabel_macro(action_bind_sublabel_switch_gpu_profile, MENU default_sublabel_macro(action_bind_sublabel_switch_backlight_control, MENU_ENUM_SUBLABEL_SWITCH_BACKLIGHT_CONTROL) #endif +default_sublabel_macro(action_bind_sublabel_playlist_show_sublabels, MENU_ENUM_SUBLABEL_PLAYLIST_SHOW_SUBLABELS) default_sublabel_macro(action_bind_sublabel_menu_rgui_border_filler_enable, MENU_ENUM_SUBLABEL_MENU_RGUI_BORDER_FILLER_ENABLE) default_sublabel_macro(action_bind_sublabel_menu_rgui_border_filler_thickness_enable, MENU_ENUM_SUBLABEL_MENU_RGUI_BORDER_FILLER_THICKNESS_ENABLE) default_sublabel_macro(action_bind_sublabel_menu_rgui_background_filler_thickness_enable, MENU_ENUM_SUBLABEL_MENU_RGUI_BACKGROUND_FILLER_THICKNESS_ENABLE) @@ -809,6 +812,104 @@ static int action_bind_sublabel_netplay_room( } #endif +static int action_bind_sublabel_playlist_entry( + file_list_t *list, + unsigned type, unsigned i, + const char *label, const char *path, + char *s, size_t len) +{ + settings_t *settings = config_get_ptr(); + playlist_t *playlist = NULL; + const char *playlist_path = NULL; + const char *core_path = NULL; + const char *core_name = NULL; + + if (!settings->bools.playlist_show_sublabels) + return 0; + + /* Get current playlist */ + playlist = playlist_get_cached(); + if (!playlist) + return 0; + if (i >= playlist_get_size(playlist)) + return 0; + + /* Read playlist entry */ + playlist_get_index(playlist, i, &playlist_path, NULL, &core_path, &core_name, NULL, NULL); + + /* Only add sublabel if a core is currently assigned */ + if (string_is_empty(core_name) || string_is_equal(core_name, file_path_str(FILE_PATH_DETECT))) + return 0; + + /* Add core name */ + snprintf(s, len, "%s %s", + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_CORE), + core_name); + + /* Get runtime *if* 'content_runtime_log' is enabled + * NB: Runtime is currently stored in an independent + * 'content_runtime.lpl' file, similar to the content + * history. It therefore only really makes sense to + * check runtime when viewing the content history + * playlist. If runtime were added to all playlists + * (would be nice), we could do this trivially for all + * content. */ + if (!settings->bools.content_runtime_log) + return 0; + + if (!string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_LOAD_CONTENT_HISTORY)) + && !string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_HISTORY_TAB))) + return 0; + + /* Check that 'content_runtime.lpl' exists, and playlist has + * non-null path/core_path values... */ + if (g_defaults.content_runtime && !string_is_empty(playlist_path) && !string_is_empty(core_path)) + { + unsigned runtime_hours; + unsigned runtime_minutes; + unsigned runtime_seconds; + unsigned j; + + /* This is lame, but unless runtime is added to all playlists + * we can't really do it any other way... + * Search 'content_runtime.lpl' until we find the current + * content+core combo. */ + for (j = 0; j < playlist_get_size(g_defaults.content_runtime); j++) + { + const char *runtime_path = NULL; + const char *runtime_core_path = NULL; + + playlist_get_runtime_index(g_defaults.content_runtime, j, &runtime_path, &runtime_core_path, + &runtime_hours, &runtime_minutes, &runtime_seconds); + + if (string_is_equal(playlist_path, runtime_path) && string_is_equal(core_path, runtime_core_path)) + { + int n = 0; + char tmp[64]; + tmp[0] = '\0'; + + n = snprintf(tmp, sizeof(tmp), "\n%s %02u:%02u:%02u", + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME), + runtime_hours, runtime_minutes, runtime_seconds); + + /* Stupid nonsense... GCC will generate warnings if we + * don't do something here... */ + if ((n < 0) || (n >= 64)) + { + n = 0; + } + + if (!string_is_empty(tmp)) + strlcat(s, tmp, len); + + break; + } + } + } + + return 0; +} + static int action_bind_sublabel_generic( file_list_t *list, unsigned type, unsigned i, @@ -2236,6 +2337,12 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_DISCORD_ALLOW: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_discord_allow); break; + case MENU_ENUM_LABEL_PLAYLIST_ENTRY: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_entry); + break; + case MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_show_sublabels); + break; case MENU_ENUM_LABEL_MENU_RGUI_BORDER_FILLER_ENABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_rgui_border_filler_enable); break; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 6d992d7f97..f81f17e6b4 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -5309,6 +5309,9 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type, menu_displaylist ret = menu_displaylist_parse_settings_enum(menu, info, MENU_ENUM_LABEL_PLAYLIST_USE_OLD_FORMAT, PARSE_ONLY_BOOL, false); + ret = menu_displaylist_parse_settings_enum(menu, info, + MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS, + PARSE_ONLY_BOOL, false); if (string_is_equal(settings->arrays.menu_driver, "rgui")) { ret = menu_displaylist_parse_settings_enum(menu, info, diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 91229eed7e..85febb512e 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -9836,6 +9836,22 @@ static bool setting_append_list( SD_FLAG_NONE ); + CONFIG_BOOL( + list, list_info, + &settings->bools.playlist_show_sublabels, + MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS, + MENU_ENUM_LABEL_VALUE_PLAYLIST_SHOW_SUBLABELS, + playlist_show_sublabels, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE + ); + if (string_is_equal(settings->arrays.menu_driver, "rgui")) { CONFIG_BOOL( diff --git a/msg_hash.h b/msg_hash.h index a5dd828d55..6bdf894e81 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2275,6 +2275,10 @@ enum msg_hash_enums MENU_LABEL(PLAYLIST_SHOW_CORE_NAME), MENU_LABEL(PLAYLIST_SORT_ALPHABETICAL), + MENU_LABEL(PLAYLIST_SHOW_SUBLABELS), + + MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_CORE, + MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME, MSG_LAST }; diff --git a/retroarch.c b/retroarch.c index 218b237565..6d300c11cf 100644 --- a/retroarch.c +++ b/retroarch.c @@ -1862,9 +1862,9 @@ bool rarch_ctl(enum rarch_ctl_state state, void *data) const char *path = path_get(RARCH_PATH_CONTENT); const char *core_path = path_get(RARCH_PATH_CORE); - if (!string_is_empty(path) && !string_is_empty(core_path)) + if (!string_is_empty(path) && !string_is_empty(core_path) && !string_is_equal(core_path, "builtin")) { - playlist_push_runtime(g_defaults.content_runtime, path_get(RARCH_PATH_CONTENT), path_get(RARCH_PATH_CORE), 0, 0, 0); + playlist_push_runtime(g_defaults.content_runtime, path, core_path, 0, 0, 0); /* if entry already existed, the runtime won't be updated, so manually update it again */ if (playlist_get_size(g_defaults.content_runtime) > 0) @@ -1893,7 +1893,7 @@ bool rarch_ctl(enum rarch_ctl_state state, void *data) runtime_hours += hours; - playlist_update_runtime(g_defaults.content_runtime, 0, path_get(RARCH_PATH_CONTENT), path_get(RARCH_PATH_CORE), runtime_hours, runtime_minutes, runtime_seconds); + playlist_update_runtime(g_defaults.content_runtime, 0, path, core_path, runtime_hours, runtime_minutes, runtime_seconds); } } } From 1d0cee5fe5fef861e1967f988c38b20b243f92ba Mon Sep 17 00:00:00 2001 From: jdgleaver Date: Thu, 21 Feb 2019 15:48:55 +0000 Subject: [PATCH 2/2] Only parse runtime playlist when loading content history playlist (+ bugfixes to original runtime logging) --- command.c | 3 +- menu/cbs/menu_cbs_sublabel.c | 74 ++++++++++++++---------------------- menu/menu_displaylist.c | 36 +++++++++++++++++- retroarch.c | 21 +++++----- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/command.c b/command.c index b6a2957864..0df46885c9 100644 --- a/command.c +++ b/command.c @@ -1961,8 +1961,7 @@ bool command_event(enum event_command cmd, void *data) content_get_status(&contentless, &is_inited); - if (!contentless) - rarch_ctl(RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT, NULL); + rarch_ctl(RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT, NULL); command_event(CMD_EVENT_AUTOSAVE_STATE, NULL); command_event(CMD_EVENT_DISABLE_OVERRIDES, NULL); command_event(CMD_EVENT_RESTORE_DEFAULT_SHADER_PRESET, NULL); diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 67bbec6bfc..75cb730eab 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -47,7 +47,7 @@ #include "../input/input_driver.h" #include "../tasks/tasks_internal.h" -#include "../../defaults.h" +#include "../../playlist.h" #define default_sublabel_macro(func_name, lbl) \ static int (func_name)(file_list_t *list, unsigned type, unsigned i, const char *label, const char *path, char *s, size_t len) \ @@ -820,9 +820,10 @@ static int action_bind_sublabel_playlist_entry( { settings_t *settings = config_get_ptr(); playlist_t *playlist = NULL; - const char *playlist_path = NULL; - const char *core_path = NULL; const char *core_name = NULL; + unsigned runtime_hours = 0; + unsigned runtime_minutes = 0; + unsigned runtime_seconds = 0; if (!settings->bools.playlist_show_sublabels) return 0; @@ -835,7 +836,7 @@ static int action_bind_sublabel_playlist_entry( return 0; /* Read playlist entry */ - playlist_get_index(playlist, i, &playlist_path, NULL, &core_path, &core_name, NULL, NULL); + playlist_get_index(playlist, i, NULL, NULL, NULL, &core_name, NULL, NULL); /* Only add sublabel if a core is currently assigned */ if (string_is_empty(core_name) || string_is_equal(core_name, file_path_str(FILE_PATH_DETECT))) @@ -861,50 +862,31 @@ static int action_bind_sublabel_playlist_entry( && !string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_HISTORY_TAB))) return 0; - /* Check that 'content_runtime.lpl' exists, and playlist has - * non-null path/core_path values... */ - if (g_defaults.content_runtime && !string_is_empty(playlist_path) && !string_is_empty(core_path)) - { - unsigned runtime_hours; - unsigned runtime_minutes; - unsigned runtime_seconds; - unsigned j; - - /* This is lame, but unless runtime is added to all playlists - * we can't really do it any other way... - * Search 'content_runtime.lpl' until we find the current - * content+core combo. */ - for (j = 0; j < playlist_get_size(g_defaults.content_runtime); j++) - { - const char *runtime_path = NULL; - const char *runtime_core_path = NULL; - - playlist_get_runtime_index(g_defaults.content_runtime, j, &runtime_path, &runtime_core_path, + /* Any available runtime values are now copied to the content + * history playlist when it is parsed by menu_displaylist, so + * we can extract them directly via index */ + playlist_get_runtime_index(playlist, i, NULL, NULL, &runtime_hours, &runtime_minutes, &runtime_seconds); - - if (string_is_equal(playlist_path, runtime_path) && string_is_equal(core_path, runtime_core_path)) - { - int n = 0; - char tmp[64]; - tmp[0] = '\0'; - - n = snprintf(tmp, sizeof(tmp), "\n%s %02u:%02u:%02u", - msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME), - runtime_hours, runtime_minutes, runtime_seconds); - - /* Stupid nonsense... GCC will generate warnings if we - * don't do something here... */ - if ((n < 0) || (n >= 64)) - { - n = 0; - } - - if (!string_is_empty(tmp)) - strlcat(s, tmp, len); - - break; - } + + if ((runtime_hours > 0) || (runtime_minutes > 0) || (runtime_seconds > 0)) + { + int n = 0; + char tmp[64]; + tmp[0] = '\0'; + + n = snprintf(tmp, sizeof(tmp), "\n%s %02u:%02u:%02u", + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME), + runtime_hours, runtime_minutes, runtime_seconds); + + /* Stupid nonsense... GCC will generate warnings if we + * don't do something here... */ + if ((n < 0) || (n >= 64)) + { + n = 0; } + + if (!string_is_empty(tmp)) + strlcat(s, tmp, len); } return 0; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index f81f17e6b4..7be1fa2e84 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -1310,6 +1310,8 @@ static int menu_displaylist_parse_playlist(menu_displaylist_info_t *info, size_t selection = menu_navigation_get_selection(); size_t list_size = playlist_size(playlist); settings_t *settings = config_get_ptr(); + bool is_rgui = string_is_equal(settings->arrays.menu_driver, "rgui"); + bool get_runtime = string_is_equal(path_playlist, "history") && g_defaults.content_runtime && settings->bools.content_runtime_log; if (list_size == 0) goto error; @@ -1333,6 +1335,7 @@ static int menu_displaylist_parse_playlist(menu_displaylist_info_t *info, size_t path_size = PATH_MAX_LENGTH * sizeof(char); char *path_copy = (char*)malloc(path_size); char *fill_buf = (char*)malloc(path_size); + const char *core_path = NULL; const char *core_name = NULL; const char *path = NULL; const char *label = NULL; @@ -1345,7 +1348,36 @@ static int menu_displaylist_parse_playlist(menu_displaylist_info_t *info, path = path_copy; playlist_get_index(playlist, i, - &path, &label, NULL, &core_name, NULL, NULL); + &path, &label, &core_path, &core_name, NULL, NULL); + + /* If this is the content history playlist and runtime logging + * is enabled, extract any available runtime values */ + if (get_runtime) + { + unsigned j; + + /* Search 'content_runtime.lpl' until we find the current + * content+core combo */ + for (j = 0; j < playlist_get_size(g_defaults.content_runtime); j++) + { + const char *runtime_path = NULL; + const char *runtime_core_path = NULL; + unsigned runtime_hours; + unsigned runtime_minutes; + unsigned runtime_seconds; + + playlist_get_runtime_index(g_defaults.content_runtime, j, &runtime_path, &runtime_core_path, + &runtime_hours, &runtime_minutes, &runtime_seconds); + + if (string_is_equal(path, runtime_path) && string_is_equal(core_path, runtime_core_path)) + { + playlist_update_runtime(playlist, i, NULL, NULL, + runtime_hours, runtime_minutes, runtime_seconds); + + break; + } + } + } if (core_name) strlcpy(fill_buf, core_name, path_size); @@ -1355,7 +1387,7 @@ static int menu_displaylist_parse_playlist(menu_displaylist_info_t *info, * But if it ever were then we must ensure that thumbnail * updates are omitted (since this functionality is * handled elsewhere). */ - if (!is_history && i == selection && !string_is_empty(label) && !string_is_equal(settings->arrays.menu_driver, "rgui")) + if (!is_history && i == selection && !string_is_empty(label) && !is_rgui) { char *content_basename = strdup(label); diff --git a/retroarch.c b/retroarch.c index 6d300c11cf..99115a14cb 100644 --- a/retroarch.c +++ b/retroarch.c @@ -1837,17 +1837,17 @@ bool rarch_ctl(enum rarch_ctl_state state, void *data) size_t pos = 0; seconds -= minutes * 60; - seconds -= hours * 60 *60; + minutes -= hours * 60; pos = strlcpy(log, "Content ran for a total of", sizeof(log)); if (hours > 0) - pos += snprintf(log + pos, sizeof(log) - pos, ", %d hours", hours); + pos += snprintf(log + pos, sizeof(log) - pos, ", %u hours", hours); if (minutes > 0) - pos += snprintf(log + pos, sizeof(log) - pos, ", %d minutes", minutes); + pos += snprintf(log + pos, sizeof(log) - pos, ", %u minutes", minutes); - pos += snprintf(log + pos, sizeof(log) - pos, ", %d seconds", seconds); + pos += snprintf(log + pos, sizeof(log) - pos, ", %u seconds", seconds); if (pos < sizeof(log) - 2) { @@ -1873,22 +1873,25 @@ bool rarch_ctl(enum rarch_ctl_state state, void *data) unsigned runtime_minutes = 0; unsigned runtime_seconds = 0; - playlist_get_runtime_index(g_defaults.content_runtime, 0, NULL, NULL, &runtime_hours, &runtime_minutes, &runtime_seconds); + playlist_get_runtime_index(g_defaults.content_runtime, 0, NULL, NULL, + &runtime_hours, &runtime_minutes, &runtime_seconds); runtime_seconds += seconds; if (runtime_seconds >= 60) { - runtime_minutes += runtime_seconds / 60; - runtime_seconds -= runtime_minutes * 60; + unsigned new_minutes = runtime_seconds / 60; + runtime_minutes += new_minutes; + runtime_seconds -= new_minutes * 60; } runtime_minutes += minutes; if (runtime_minutes >= 60) { - runtime_hours += runtime_minutes / 60; - runtime_minutes -= runtime_hours * 60; + unsigned new_hours = runtime_minutes / 60; + runtime_hours += new_hours; + runtime_minutes -= new_hours * 60; } runtime_hours += hours;