/* RetroArch - A frontend for libretro. * Copyright (C) 2011-2022 - Daniel De Matteis * Copyright (C) 2019-2022 - James Leaver * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include #include #include #include #include #include #include "menu_driver.h" #include "menu_displaylist.h" #include "../file_path_special.h" #include "../retroarch.h" #include "../core_info.h" #include "../configuration.h" #define CONTENTLESS_CORE_ICON_DEFAULT "default.png" typedef struct { uintptr_t **system; uintptr_t fallback; } contentless_core_icons_t; typedef struct { contentless_core_info_entry_t **info_entries; contentless_core_icons_t *icons; bool icons_enabled; } contentless_cores_state_t; static contentless_cores_state_t *contentless_cores_state = NULL; static void contentless_cores_free_runtime_info( contentless_core_runtime_info_t *runtime_info) { if (!runtime_info) return; if (runtime_info->runtime_str) { free(runtime_info->runtime_str); runtime_info->runtime_str = NULL; } if (runtime_info->last_played_str) { free(runtime_info->last_played_str); runtime_info->last_played_str = NULL; } runtime_info->status = CONTENTLESS_CORE_RUNTIME_UNKNOWN; } static void contentless_cores_free_info_entries( contentless_cores_state_t *state) { size_t i, cap; if (!state || !state->info_entries) return; for (i = 0, cap = RHMAP_CAP(state->info_entries); i != cap; i++) { if (RHMAP_KEY(state->info_entries, i)) { contentless_core_info_entry_t *entry = state->info_entries[i]; if (!entry) continue; if (entry->licenses_str) free(entry->licenses_str); entry->licenses_str = NULL; contentless_cores_free_runtime_info(&entry->runtime); free(entry); } } RHMAP_FREE(state->info_entries); } static void contentless_cores_init_info_entries( contentless_cores_state_t *state) { core_info_list_t *core_info_list = NULL; size_t i; if (!state) return; /* Free any existing entries */ contentless_cores_free_info_entries(state); /* Create an entry for each contentless core */ core_info_get_list(&core_info_list); if (!core_info_list) return; for (i = 0; i < core_info_list->count; i++) { core_info_t *core_info = core_info_get(core_info_list, i); if ( core_info && core_info->supports_no_game) { char licenses_str[MENU_SUBLABEL_MAX_LENGTH]; contentless_core_info_entry_t *entry = (contentless_core_info_entry_t*)malloc(sizeof(*entry)); size_t _len = strlcpy(licenses_str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_CORE_INFO_LICENSES), sizeof(licenses_str)); licenses_str[ _len] = ':'; licenses_str[++_len] = ' '; licenses_str[++_len] = '\0'; /* Populate licences string */ if (core_info->licenses_list) { char tmp_str[MENU_SUBLABEL_MAX_LENGTH - 2]; tmp_str[0] = '\0'; string_list_join_concat(tmp_str, sizeof(tmp_str), core_info->licenses_list, ", "); strlcpy(licenses_str + _len, tmp_str, sizeof(licenses_str) - _len); } /* No license found - set to N/A */ else strlcpy(licenses_str + _len, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE), sizeof(licenses_str) - _len); entry->licenses_str = strdup(licenses_str); /* Initialise runtime info */ entry->runtime.runtime_str = NULL; entry->runtime.last_played_str = NULL; entry->runtime.status = CONTENTLESS_CORE_RUNTIME_UNKNOWN; /* Add entry to hash map */ RHMAP_SET_STR(state->info_entries, core_info->core_file_id.str, entry); } } } void menu_contentless_cores_set_runtime(const char *core_id, const contentless_core_runtime_info_t *runtime_info) { contentless_core_info_entry_t *info_entry = NULL; if ( !contentless_cores_state || !contentless_cores_state->info_entries || !runtime_info || string_is_empty(core_id)) return; info_entry = RHMAP_GET_STR(contentless_cores_state->info_entries, core_id); if (!info_entry) return; if (!string_is_empty(runtime_info->runtime_str)) { if (info_entry->runtime.runtime_str) free(info_entry->runtime.runtime_str); info_entry->runtime.runtime_str = strdup(runtime_info->runtime_str); } if (!string_is_empty(runtime_info->last_played_str)) { if (info_entry->runtime.last_played_str) free(info_entry->runtime.last_played_str); info_entry->runtime.last_played_str = strdup(runtime_info->last_played_str); } info_entry->runtime.status = runtime_info->status; } void menu_contentless_cores_get_info(const char *core_id, const contentless_core_info_entry_t **info) { if (!info) return; if ( !contentless_cores_state || !contentless_cores_state->info_entries || string_is_empty(core_id)) *info = NULL; *info = RHMAP_GET_STR(contentless_cores_state->info_entries, core_id); } void menu_contentless_cores_flush_runtime(void) { contentless_cores_state_t *state = contentless_cores_state; size_t i, cap; if (!state || !state->info_entries) return; for (i = 0, cap = RHMAP_CAP(state->info_entries); i != cap; i++) { if (RHMAP_KEY(state->info_entries, i)) { contentless_core_info_entry_t *entry = state->info_entries[i]; if (!entry) continue; contentless_cores_free_runtime_info(&entry->runtime); } } } static void contentless_cores_unload_icons(contentless_cores_state_t *state) { size_t i, cap; if (!state || !state->icons) return; if (state->icons->fallback) video_driver_texture_unload(&state->icons->fallback); for (i = 0, cap = RHMAP_CAP(state->icons->system); i != cap; i++) { if (RHMAP_KEY(state->icons->system, i)) { uintptr_t *icon = state->icons->system[i]; if (!icon) continue; video_driver_texture_unload(icon); free(icon); } } RHMAP_FREE(state->icons->system); free(state->icons); state->icons = NULL; } static void contentless_cores_load_icons(contentless_cores_state_t *state) { size_t i; char icon_path[PATH_MAX_LENGTH]; char icon_directory[PATH_MAX_LENGTH]; bool rgba_supported = video_driver_supports_rgba(); core_info_list_t *core_info_list = NULL; if (!state) return; /* Unload any existing icons */ contentless_cores_unload_icons(state); if (!state->icons_enabled) return; /* Create new icon container */ state->icons = (contentless_core_icons_t*)calloc( 1, sizeof(*state->icons)); /* Get icon directory */ fill_pathname_application_special(icon_directory, sizeof(icon_directory), APPLICATION_SPECIAL_DIRECTORY_ASSETS_SYSICONS); if (string_is_empty(icon_directory)) return; /* Load fallback icon */ fill_pathname_join_special(icon_path, icon_directory, CONTENTLESS_CORE_ICON_DEFAULT, sizeof(icon_path)); if (path_is_valid(icon_path)) { struct texture_image ti = {0}; ti.supports_rgba = rgba_supported; if (image_texture_load(&ti, icon_path)) { if (ti.pixels) video_driver_texture_load(&ti, TEXTURE_FILTER_MIPMAP_LINEAR, &state->icons->fallback); image_texture_free(&ti); } } /* Get icons for all contentless cores */ core_info_get_list(&core_info_list); if (!core_info_list) return; for (i = 0; i < core_info_list->count; i++) { core_info_t *core_info = core_info_get(core_info_list, i); /* Icon name is the first entry in the core * info database list */ if ( core_info && core_info->supports_no_game && core_info->databases_list && (core_info->databases_list->size > 0)) { const char *icon_name = core_info->databases_list->elems[0].data; struct texture_image ti = {0}; size_t len = fill_pathname_join_special( icon_path, icon_directory, icon_name, sizeof(icon_path)); icon_path[ len] = '.'; icon_path[++len] = 'p'; icon_path[++len] = 'n'; icon_path[++len] = 'g'; icon_path[++len] = '\0'; ti.supports_rgba = rgba_supported; if (!path_is_valid(icon_path)) continue; if (image_texture_load(&ti, icon_path)) { if (ti.pixels) { uintptr_t *icon = (uintptr_t*)calloc(1, sizeof(*icon)); video_driver_texture_load(&ti, TEXTURE_FILTER_MIPMAP_LINEAR, icon); /* Add icon to hash map */ RHMAP_SET_STR(state->icons->system, core_info->core_file_id.str, icon); } image_texture_free(&ti); } } } } uintptr_t menu_contentless_cores_get_entry_icon(const char *core_id) { contentless_cores_state_t *state = contentless_cores_state; uintptr_t *icon = NULL; if ( !state || !state->icons_enabled || !state->icons || string_is_empty(core_id)) return 0; if ((icon = RHMAP_GET_STR(state->icons->system, core_id))) return *icon; return state->icons->fallback; } void menu_contentless_cores_context_init(void) { if (contentless_cores_state) contentless_cores_load_icons(contentless_cores_state); } void menu_contentless_cores_context_deinit(void) { if (contentless_cores_state) contentless_cores_unload_icons(contentless_cores_state); } void menu_contentless_cores_free(void) { if (!contentless_cores_state) return; contentless_cores_free_info_entries(contentless_cores_state); contentless_cores_unload_icons(contentless_cores_state); free(contentless_cores_state); contentless_cores_state = NULL; } unsigned menu_displaylist_contentless_cores(file_list_t *list, settings_t *settings) { unsigned count = 0; enum menu_contentless_cores_display_type core_display_type = (enum menu_contentless_cores_display_type) settings->uints.menu_content_show_contentless_cores; core_info_list_t *core_info_list = NULL; /* Get core list */ core_info_get_list(&core_info_list); if (core_info_list) { size_t i; size_t menu_index = 0; /* Sort cores alphabetically */ core_info_qsort(core_info_list, CORE_INFO_LIST_SORT_DISPLAY_NAME); /* Loop through cores */ for (i = 0; i < core_info_list->count; i++) { core_info_t *core_info = core_info_get(core_info_list, i); if (core_info) { switch (core_display_type) { case MENU_CONTENTLESS_CORES_DISPLAY_ALL: if (!( core_info->supports_no_game)) continue; break; case MENU_CONTENTLESS_CORES_DISPLAY_SINGLE_PURPOSE: if (!( core_info->supports_no_game && core_info->single_purpose)) continue; break; case MENU_CONTENTLESS_CORES_DISPLAY_CUSTOM: if (!( core_info->supports_no_game && !core_info->is_standalone_exempt)) continue; break; default: break; } /* Valid core if we have reached here */ if (menu_entries_append(list, core_info->path, core_info->core_file_id.str, MENU_ENUM_LABEL_CONTENTLESS_CORE, MENU_SETTING_ACTION_CONTENTLESS_CORE_RUN, 0, 0, NULL)) { file_list_set_alt_at_offset( list, menu_index, core_info->display_name); menu_index++; count++; } } } } /* Initialise global state, if required */ if (!contentless_cores_state && (count > 0)) { contentless_cores_state = (contentless_cores_state_t*)calloc(1, sizeof(*contentless_cores_state)); /* Disable icons when using menu drivers without * icon support */ contentless_cores_state->icons_enabled = !string_is_equal(menu_driver_ident(), "rgui"); contentless_cores_init_info_entries(contentless_cores_state); contentless_cores_load_icons(contentless_cores_state); } if ( (count == 0) && menu_entries_append(list, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_CORES_AVAILABLE), msg_hash_to_str(MENU_ENUM_LABEL_NO_CORES_AVAILABLE), MENU_ENUM_LABEL_NO_CORES_AVAILABLE, 0, 0, 0, NULL)) count++; return count; }