/* RetroArch - A frontend for libretro. * Copyright (C) 2010-2014 - Hans-Kristian Arntzen * Copyright (C) 2011-2017 - Daniel De Matteis * Copyright (C) 2021 - David G.F. * * 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 #ifdef HAVE_NETWORKING #include #include #endif #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_CHEEVOS #include "cheevos/cheevos.h" #endif #ifdef HAVE_GFX_WIDGETS #include "gfx/gfx_widgets.h" #endif #ifdef HAVE_MENU #include "menu/menu_driver.h" #endif #ifdef HAVE_NETWORKING #include "network/netplay/netplay.h" #endif #include "audio/audio_driver.h" #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) #include "gfx/video_shader_parse.h" #endif #include "autosave.h" #include "command.h" #include "core_info.h" #include "cheat_manager.h" #include "content.h" #include "dynamic.h" #include "list_special.h" #include "paths.h" #include "retroarch.h" #include "verbosity.h" #include "version.h" #include "version_git.h" #define CMD_BUF_SIZE 4096 #if defined(HAVE_COMMAND) /* Generic command parse utilities */ static bool command_get_arg(const char *tok, const char **arg, unsigned *index) { unsigned i; for (i = 0; i < ARRAY_SIZE(map); i++) { if (string_is_equal(tok, map[i].str)) { if (arg) *arg = NULL; if (index) *index = i; return true; } } for (i = 0; i < ARRAY_SIZE(action_map); i++) { const char *str = strstr(tok, action_map[i].str); if (str == tok) { const char *argument = str + strlen(action_map[i].str); if (*argument != ' ' && *argument != '\0') return false; if (arg) *arg = argument + 1; if (index) *index = i; return true; } } return false; } static void command_parse_sub_msg(command_t *handle, const char *tok) { const char *arg = NULL; unsigned index = 0; if (command_get_arg(tok, &arg, &index)) { if (arg) { if (!action_map[index].action(handle, arg)) RARCH_ERR("Command \"%s\" failed.\n", arg); } else handle->state[map[index].id] = true; } else RARCH_WARN(msg_hash_to_str(MSG_UNRECOGNIZED_COMMAND), tok); } static void command_parse_msg( command_t *handle, char *buf) { char *save = NULL; const char *tok = strtok_r(buf, "\n", &save); while (tok) { command_parse_sub_msg(handle, tok); tok = strtok_r(NULL, "\n", &save); } } #if defined(HAVE_NETWORK_CMD) typedef struct { /* Network socket FD */ int net_fd; /* Source address for the command received */ struct sockaddr_storage cmd_source; /* Size of the previous structure in use */ socklen_t cmd_source_len; } command_network_t; static void network_command_reply( command_t *cmd, const char * data, size_t len) { command_network_t *netcmd = (command_network_t*)cmd->userptr; /* Respond (fire and forget since it's UDP) */ sendto(netcmd->net_fd, data, len, 0, (struct sockaddr*)&netcmd->cmd_source, netcmd->cmd_source_len); } static void network_command_free(command_t *handle) { command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd >= 0) socket_close(netcmd->net_fd); free(netcmd); free(handle); } static void command_network_poll(command_t *handle) { fd_set fds; struct timeval tmp_tv = {0}; command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd < 0) return; FD_ZERO(&fds); FD_SET(netcmd->net_fd, &fds); if (socket_select(netcmd->net_fd + 1, &fds, NULL, NULL, &tmp_tv) <= 0) return; if (!FD_ISSET(netcmd->net_fd, &fds)) return; for (;;) { ssize_t ret; char buf[1024]; buf[0] = '\0'; netcmd->cmd_source_len = sizeof(struct sockaddr_storage); ret = recvfrom(netcmd->net_fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&netcmd->cmd_source, &netcmd->cmd_source_len); if (ret <= 0) break; buf[ret] = '\0'; command_parse_msg(handle, buf); } } command_t* command_network_new(uint16_t port) { struct addrinfo *res = NULL; command_t *cmd = (command_t*)calloc(1, sizeof(command_t)); command_network_t *netcmd = (command_network_t*)calloc( 1, sizeof(command_network_t)); int fd = socket_init( (void**)&res, port, NULL, SOCKET_TYPE_DATAGRAM); RARCH_LOG("%s %hu.\n", msg_hash_to_str(MSG_BRINGING_UP_COMMAND_INTERFACE_ON_PORT), (unsigned short)port); if (fd < 0) goto error; netcmd->net_fd = fd; cmd->userptr = netcmd; cmd->poll = command_network_poll; cmd->replier = network_command_reply; cmd->destroy = network_command_free; if (!socket_nonblock(netcmd->net_fd)) goto error; if (!socket_bind(netcmd->net_fd, (void*)res)) { RARCH_ERR("%s.\n", msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET)); goto error; } freeaddrinfo_retro(res); return cmd; error: if (res) freeaddrinfo_retro(res); free(netcmd); free(cmd); return NULL; } #endif #if defined(HAVE_STDIN_CMD) typedef struct { /* Buffer and pointer for stdin reads */ size_t stdin_buf_ptr; char stdin_buf[CMD_BUF_SIZE]; } command_stdin_t; static void stdin_command_reply( command_t *cmd, const char * data, size_t len) { /* Just write to stdout! */ fwrite(data, 1, len, stdout); } static void stdin_command_free(command_t *handle) { free(handle->userptr); free(handle); } static void command_stdin_poll(command_t *handle) { ptrdiff_t msg_len; char *last_newline = NULL; command_stdin_t *stdincmd = (command_stdin_t*)handle->userptr; ssize_t ret = read_stdin( stdincmd->stdin_buf + stdincmd->stdin_buf_ptr, CMD_BUF_SIZE - stdincmd->stdin_buf_ptr - 1); if (ret == 0) return; stdincmd->stdin_buf_ptr += ret; stdincmd->stdin_buf[stdincmd->stdin_buf_ptr] = '\0'; last_newline = strrchr(stdincmd->stdin_buf, '\n'); if (!last_newline) { /* We're receiving bogus data in pipe * (no terminating newline), flush out the buffer. */ if (stdincmd->stdin_buf_ptr + 1 >= CMD_BUF_SIZE) { stdincmd->stdin_buf_ptr = 0; stdincmd->stdin_buf[0] = '\0'; } return; } *last_newline++ = '\0'; msg_len = last_newline - stdincmd->stdin_buf; command_parse_msg(handle, stdincmd->stdin_buf); memmove(stdincmd->stdin_buf, last_newline, stdincmd->stdin_buf_ptr - msg_len); stdincmd->stdin_buf_ptr -= msg_len; } command_t* command_stdin_new(void) { command_t *cmd; command_stdin_t *stdincmd; #ifndef _WIN32 #ifdef HAVE_NETWORKING if (!socket_nonblock(STDIN_FILENO)) return NULL; #endif #endif cmd = (command_t*)calloc(1, sizeof(command_t)); stdincmd = (command_stdin_t*)calloc(1, sizeof(command_stdin_t)); cmd->userptr = stdincmd; cmd->poll = command_stdin_poll; cmd->replier = stdin_command_reply; cmd->destroy = stdin_command_free; return cmd; } #endif #if defined(HAVE_LAKKA) #include #define MAX_USER_CONNECTIONS 4 typedef struct { /* File descriptor for the domain socket */ int sfd; /* Client sockets */ int userfd[MAX_USER_CONNECTIONS]; /* Last received user socket */ int last_fd; } command_uds_t; static void uds_command_reply( command_t *cmd, const char * data, size_t len) { command_uds_t *subcmd = (command_uds_t*)cmd->userptr; write(subcmd->last_fd, data, len); } static void uds_command_free(command_t *handle) { int i; command_uds_t *udscmd = (command_uds_t*)handle->userptr; for (i = 0; i < MAX_USER_CONNECTIONS; i++) if (udscmd->userfd[i] >= 0) socket_close(udscmd->userfd[i]); socket_close(udscmd->sfd); free(handle->userptr); free(handle); } static void command_uds_poll(command_t *handle) { int i; fd_set fds; command_uds_t *udscmd = (command_uds_t*)handle->userptr; int maxfd = udscmd->sfd; struct timeval tmp_tv = {0}; if (udscmd->sfd < 0) return; FD_ZERO(&fds); FD_SET(udscmd->sfd, &fds); for (i = 0; i < MAX_USER_CONNECTIONS; i++) { if (udscmd->userfd[i] >= 0) { maxfd = MAX(udscmd->userfd[i], maxfd); FD_SET(udscmd->userfd[i], &fds); } } if (socket_select(maxfd + 1, &fds, NULL, NULL, &tmp_tv) <= 0) return; /* Read data from clients and process commands */ for (i = 0; i < MAX_USER_CONNECTIONS; i++) { if (udscmd->userfd[i] >= 0 && FD_ISSET(udscmd->userfd[i], &fds)) { while (1) { char buf[2048]; ssize_t ret = recv(udscmd->userfd[i], buf, sizeof(buf) - 1, 0); if (ret < 0) break; /* no more data */ if (!ret) { socket_close(udscmd->userfd[i]); udscmd->userfd[i] = -1; break; } buf[ret] = 0; udscmd->last_fd = udscmd->userfd[i]; command_parse_msg(handle, buf); } } } if (FD_ISSET(udscmd->sfd, &fds)) { /* Accepts new connections from clients */ int cfd = accept(udscmd->sfd, NULL, NULL); if (cfd >= 0) { if (!socket_nonblock(cfd)) socket_close(cfd); else { for (i = 0; i < MAX_USER_CONNECTIONS; i++) if (udscmd->userfd[i] < 0) { udscmd->userfd[i] = cfd; break; } } } } } command_t* command_uds_new(void) { int i; command_t *cmd; command_uds_t *subcmd; struct sockaddr_un addr; const char *sp = "retroarch/cmd"; socklen_t addrsz = offsetof(struct sockaddr_un, sun_path) + strlen(sp) + 1; int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return NULL; /* use an abstract socket for simplicity */ memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strcpy(&addr.sun_path[1], sp); if (bind(fd, (struct sockaddr*)&addr, addrsz) < 0 || listen(fd, MAX_USER_CONNECTIONS) < 0) { socket_close(fd); return NULL; } if (!socket_nonblock(fd)) { socket_close(fd); return NULL; } cmd = (command_t*)calloc(1, sizeof(command_t)); subcmd = (command_uds_t*)calloc(1, sizeof(command_uds_t)); subcmd->sfd = fd; subcmd->last_fd = -1; for (i = 0; i < MAX_USER_CONNECTIONS; i++) subcmd->userfd[i] = -1; cmd->userptr = subcmd; cmd->poll = command_uds_poll; cmd->replier = uds_command_reply; cmd->destroy = uds_command_free; return cmd; } #endif /* Routines used to invoke retroarch command ... */ #ifdef HAVE_NETWORK_CMD static bool command_verify(const char *cmd) { unsigned i; if (command_get_arg(cmd, NULL, NULL)) return true; RARCH_ERR("Command \"%s\" is not recognized by the program.\n", cmd); RARCH_ERR("\tValid commands:\n"); for (i = 0; i < ARRAY_SIZE(map); i++) RARCH_ERR("\t\t%s\n", map[i].str); for (i = 0; i < ARRAY_SIZE(action_map); i++) RARCH_ERR("\t\t%s %s\n", action_map[i].str, action_map[i].arg_desc); return false; } bool command_network_send(const char *cmd_) { char *command = NULL; char *save = NULL; const char *cmd = NULL; if (!network_init()) return false; if (!(command = strdup(cmd_))) return false; cmd = strtok_r(command, ";", &save); if (cmd) { uint16_t port = DEFAULT_NETWORK_CMD_PORT; const char *port_ = NULL; const char *host = strtok_r(NULL, ";", &save); if (host) port_ = strtok_r(NULL, ";", &save); else { #ifdef _WIN32 host = "127.0.0.1"; #else host = "localhost"; #endif } if (port_) port = strtoul(port_, NULL, 0); RARCH_LOG("%s: \"%s\" to %s:%hu\n", msg_hash_to_str(MSG_SENDING_COMMAND), cmd, host, (unsigned short)port); if (command_verify(cmd) && udp_send_packet(host, port, cmd)) { free(command); return true; } } free(command); return false; } #endif bool command_show_osd_msg(command_t *cmd, const char* arg) { runloop_msg_queue_push(arg, 1, 180, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return true; } #if defined(HAVE_CHEEVOS) bool command_read_ram(command_t *cmd, const char *arg) { unsigned i; char *reply = NULL; const uint8_t *data = NULL; char *reply_at = NULL; unsigned int nbytes = 0; unsigned int alloc_size = 0; unsigned int addr = -1; unsigned int len = 0; if (sscanf(arg, "%x %u", &addr, &nbytes) != 2) return true; /* We allocate more than needed, saving 20 bytes is not really relevant */ alloc_size = 40 + nbytes * 3; reply = (char*)malloc(alloc_size); reply[0] = '\0'; reply_at = reply + snprintf( reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr); if ((data = rcheevos_patch_address(addr))) { for (i = 0; i < nbytes; i++) snprintf(reply_at + 3 * i, 4, " %.2X", data[i]); reply_at[3 * nbytes] = '\n'; len = reply_at + 3 * nbytes + 1 - reply; } else { strlcpy(reply_at, " -1\n", sizeof(reply) - strlen(reply)); len = reply_at + STRLEN_CONST(" -1\n") - reply; } cmd->replier(cmd, reply, len); free(reply); return true; } bool command_write_ram(command_t *cmd, const char *arg) { unsigned int addr = (unsigned int)strtoul(arg, (char**)&arg, 16); uint8_t *data = (uint8_t *)rcheevos_patch_address(addr); if (!data) return false; if (rcheevos_hardcore_active()) { RARCH_LOG("Achievements hardcore mode disabled by WRITE_CORE_RAM\n"); rcheevos_pause_hardcore(); } while (*arg) { *data = strtoul(arg, (char**)&arg, 16); data++; } return true; } #endif bool command_version(command_t *cmd, const char* arg) { char reply[256] = {0}; snprintf(reply, sizeof(reply), "%s\n", PACKAGE_VERSION); cmd->replier(cmd, reply, strlen(reply)); return true; } static const rarch_memory_descriptor_t* command_memory_get_descriptor(const rarch_memory_map_t* mmap, unsigned address) { const rarch_memory_descriptor_t* desc = mmap->descriptors; const rarch_memory_descriptor_t* end = desc + mmap->num_descriptors; for (; desc < end; desc++) { if (desc->core.select == 0) { /* if select is 0, attempt to explicitly match the address */ if (address >= desc->core.start && address < desc->core.start + desc->core.len) return desc; } else { /* otherwise, attempt to match the address by matching the select bits */ if (((desc->core.start ^ address) & desc->core.select) == 0) { /* sanity check - make sure the descriptor is large enough to hold the target address */ if (address - desc->core.start < desc->core.len) return desc; } } } return NULL; } uint8_t *command_memory_get_pointer( const rarch_system_info_t* system, unsigned address, unsigned int* max_bytes, int for_write, char *reply_at, size_t len) { if (!system || system->mmaps.num_descriptors == 0) strlcpy(reply_at, " -1 no memory map defined\n", len); else { const rarch_memory_descriptor_t* desc = command_memory_get_descriptor(&system->mmaps, address); if (!desc) strlcpy(reply_at, " -1 no descriptor for address\n", len); else if (!desc->core.ptr) strlcpy(reply_at, " -1 no data for descriptor\n", len); else if (for_write && (desc->core.flags & RETRO_MEMDESC_CONST)) strlcpy(reply_at, " -1 descriptor data is readonly\n", len); else { const size_t offset = address - desc->core.start; *max_bytes = (desc->core.len - offset); return (uint8_t*)desc->core.ptr + desc->core.offset + offset; } } *max_bytes = 0; return NULL; } bool command_get_status(command_t *cmd, const char* arg) { char reply[4096] = {0}; bool contentless = false; bool is_inited = false; runloop_state_t *runloop_st = runloop_state_get_ptr(); content_get_status(&contentless, &is_inited); if (!is_inited) strcpy_literal(reply, "GET_STATUS CONTENTLESS"); else { /* add some content info */ const char *status = "PLAYING"; const char *content_name = path_basename(path_get(RARCH_PATH_BASENAME)); /* filename only without ext */ int content_crc32 = content_get_crc(); const char* system_id = NULL; core_info_t *core_info = NULL; core_info_get_current_core(&core_info); if (runloop_st->paused) status = "PAUSED"; if (core_info) system_id = core_info->system_id; if (!system_id) system_id = runloop_st->system.info.library_name; snprintf(reply, sizeof(reply), "GET_STATUS %s %s,%s,crc32=%x\n", status, system_id, content_name, content_crc32); } cmd->replier(cmd, reply, strlen(reply)); return true; } bool command_read_memory(command_t *cmd, const char *arg) { unsigned i; char* reply = NULL; char* reply_at = NULL; const uint8_t* data = NULL; unsigned int nbytes = 0; unsigned int alloc_size = 0; unsigned int address = -1; size_t len = 0; unsigned int max_bytes = 0; runloop_state_t *runloop_st = runloop_state_get_ptr(); const rarch_system_info_t* system = &runloop_st->system; if (sscanf(arg, "%x %u", &address, &nbytes) != 2) return false; /* Ensure large enough to return all requested bytes or an error message */ alloc_size = 64 + nbytes * 3; reply = (char*)malloc(alloc_size); reply_at = reply + snprintf(reply, alloc_size - 1, "READ_CORE_MEMORY %x", address); data = command_memory_get_pointer(system, address, &max_bytes, 0, reply_at, alloc_size - strlen(reply)); if (data) { if (nbytes > max_bytes) nbytes = max_bytes; for (i = 0; i < nbytes; i++) snprintf(reply_at + 3 * i, 4, " %02X", data[i]); reply_at[3 * nbytes] = '\n'; len = reply_at + 3 * nbytes + 1 - reply; } else len = strlen(reply); cmd->replier(cmd, reply, len); free(reply); return true; } bool command_write_memory(command_t *cmd, const char *arg) { unsigned int address = (unsigned int)strtoul(arg, (char**)&arg, 16); unsigned int max_bytes = 0; char reply[128] = ""; runloop_state_t *runloop_st = runloop_state_get_ptr(); const rarch_system_info_t *system = &runloop_st->system; char *reply_at = reply + snprintf(reply, sizeof(reply) - 1, "WRITE_CORE_MEMORY %x", address); uint8_t *data = command_memory_get_pointer(system, address, &max_bytes, 1, reply_at, sizeof(reply) - strlen(reply) - 1); if (data) { uint8_t* start = data; while (*arg && max_bytes > 0) { --max_bytes; *data = strtoul(arg, (char**)&arg, 16); data++; } snprintf(reply_at, sizeof(reply) - strlen(reply) - 1, " %u\n", (unsigned)(data - start)); #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { RARCH_LOG("Achievements hardcore mode disabled by WRITE_CORE_MEMORY\n"); rcheevos_pause_hardcore(); } #endif } cmd->replier(cmd, reply, strlen(reply)); return true; } #endif void command_event_set_volume( settings_t *settings, float gain, bool widgets_active, bool audio_driver_mute_enable) { char msg[128]; float new_volume = settings->floats.audio_volume + gain; new_volume = MAX(new_volume, -80.0f); new_volume = MIN(new_volume, 12.0f); configuration_set_float(settings, settings->floats.audio_volume, new_volume); snprintf(msg, sizeof(msg), "%s: %.1f dB", msg_hash_to_str(MSG_AUDIO_VOLUME), new_volume); #if defined(HAVE_GFX_WIDGETS) if (widgets_active) gfx_widget_volume_update_and_show(new_volume, audio_driver_mute_enable); else #endif runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_LOG("[Audio]: %s\n", msg); audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume); } /** * event_set_mixer_volume: * @gain : amount of gain to be applied to current volume level. * * Adjusts the current audio volume level. * **/ void command_event_set_mixer_volume( settings_t *settings, float gain) { char msg[128]; float new_volume = settings->floats.audio_mixer_volume + gain; new_volume = MAX(new_volume, -80.0f); new_volume = MIN(new_volume, 12.0f); configuration_set_float(settings, settings->floats.audio_mixer_volume, new_volume); snprintf(msg, sizeof(msg), "%s: %.1f dB", msg_hash_to_str(MSG_AUDIO_VOLUME), new_volume); runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_LOG("[Audio]: %s\n", msg); audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume); } void command_event_init_controllers(rarch_system_info_t *info, settings_t *settings, unsigned num_active_users) { unsigned port; unsigned num_core_ports = info->ports.size; for (port = 0; port < num_core_ports; port++) { unsigned i; retro_ctx_controller_info_t pad; unsigned device = RETRO_DEVICE_NONE; const struct retro_controller_description *desc = NULL; /* Check whether current core port is mapped * to an input device * > If is not, leave 'device' set to * 'RETRO_DEVICE_NONE' * > For example: if input ports 0 and 1 are * mapped to core port 0, core port 1 will * be unmapped and should be disabled */ for (i = 0; i < num_active_users; i++) { if (i >= MAX_USERS) break; if (port == settings->uints.input_remap_ports[i]) { device = input_config_get_device(port); break; } } desc = libretro_find_controller_description( &info->ports.data[port], device); if (desc && !desc->desc) { /* If we're trying to connect a completely unknown device, * revert back to JOYPAD. */ if (device != RETRO_DEVICE_JOYPAD && device != RETRO_DEVICE_NONE) { /* Do not fix device, * because any use of dummy core will reset this, * which is not a good idea. */ RARCH_WARN("[Input]: Input device ID %u is unknown to this " "libretro implementation. Using RETRO_DEVICE_JOYPAD.\n", device); device = RETRO_DEVICE_JOYPAD; } } pad.device = device; pad.port = port; core_set_controller_port_device(&pad); } } #ifdef HAVE_CONFIGFILE bool command_event_save_config( const char *config_path, char *s, size_t len) { bool path_exists = !string_is_empty(config_path); const char *str = path_exists ? config_path : path_get(RARCH_PATH_CONFIG); if (path_exists && config_save_file(config_path)) { snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO), config_path); RARCH_LOG("[Config]: %s\n", s); return true; } if (!string_is_empty(str)) { snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_FAILED_SAVING_CONFIG_TO), str); RARCH_ERR("[Config]: %s\n", s); } return false; } #endif void command_event_undo_save_state(char *s, size_t len) { if (content_undo_save_buf_is_empty()) { strlcpy(s, msg_hash_to_str(MSG_NO_SAVE_STATE_HAS_BEEN_OVERWRITTEN_YET), len); return; } if (!content_undo_save_state()) { strlcpy(s, msg_hash_to_str(MSG_FAILED_TO_UNDO_SAVE_STATE), len); return; } strlcpy(s, msg_hash_to_str(MSG_UNDOING_SAVE_STATE), len); } void command_event_undo_load_state(char *s, size_t len) { if (content_undo_load_buf_is_empty()) { strlcpy(s, msg_hash_to_str(MSG_NO_STATE_HAS_BEEN_LOADED_YET), len); return; } if (!content_undo_load_state()) { snprintf(s, len, "%s \"%s\".", msg_hash_to_str(MSG_FAILED_TO_UNDO_LOAD_STATE), "RAM"); return; } #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL); #endif strlcpy(s, msg_hash_to_str(MSG_UNDID_LOAD_STATE), len); } bool command_event_resize_windowed_scale(settings_t *settings, unsigned window_scale) { unsigned idx = 0; bool video_fullscreen = settings->bools.video_fullscreen; if (window_scale == 0) return false; configuration_set_float(settings, settings->floats.video_scale, (float)window_scale); if (!video_fullscreen) command_event(CMD_EVENT_REINIT, NULL); retroarch_ctl(RARCH_CTL_SET_WINDOWED_SCALE, &idx); return true; } bool command_event_save_auto_state( bool savestate_auto_save, global_t *global, const enum rarch_core_type current_core_type) { bool ret = false; char savestate_name_auto[PATH_MAX_LENGTH]; if (!global || !savestate_auto_save) return false; if (current_core_type == CORE_TYPE_DUMMY) return false; if (string_is_empty(path_basename(path_get(RARCH_PATH_BASENAME)))) return false; #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) return false; #endif savestate_name_auto[0] = '\0'; fill_pathname_noext(savestate_name_auto, global->name.savestate, ".auto", sizeof(savestate_name_auto)); ret = content_save_state((const char*)savestate_name_auto, true, true); RARCH_LOG("%s \"%s\" %s.\n", msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO), savestate_name_auto, ret ? "succeeded" : "failed"); return true; } #ifdef HAVE_CHEATS void command_event_init_cheats( bool apply_cheats_after_load, const char *path_cheat_db, void *bsv_movie_data) { #ifdef HAVE_NETWORKING bool allow_cheats = !netplay_driver_ctl( RARCH_NETPLAY_CTL_IS_DATA_INITED, NULL); #else bool allow_cheats = true; #endif #ifdef HAVE_BSV_MOVIE bsv_movie_t * bsv_movie_state_handle = (bsv_movie_t*)bsv_movie_data; allow_cheats &= !(bsv_movie_state_handle != NULL); #endif if (!allow_cheats) return; cheat_manager_alloc_if_empty(); cheat_manager_load_game_specific_cheats(path_cheat_db); if (apply_cheats_after_load) cheat_manager_apply_cheats(); } #endif void command_event_load_auto_state(global_t *global) { char savestate_name_auto[PATH_MAX_LENGTH]; bool ret = false; #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) return; #endif #ifdef HAVE_NETWORKING if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) return; #endif savestate_name_auto[0] = '\0'; fill_pathname_noext(savestate_name_auto, global->name.savestate, ".auto", sizeof(savestate_name_auto)); if (!path_is_valid(savestate_name_auto)) return; ret = content_load_state(savestate_name_auto, false, true); RARCH_LOG("%s: %s\n%s \"%s\" %s.\n", msg_hash_to_str(MSG_FOUND_AUTO_SAVESTATE_IN), savestate_name_auto, msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM), savestate_name_auto, ret ? "succeeded" : "failed" ); } void command_event_set_savestate_auto_index( settings_t *settings, const global_t *global) { size_t i; char state_dir[PATH_MAX_LENGTH]; char state_base[PATH_MAX_LENGTH]; struct string_list *dir_list = NULL; unsigned max_idx = 0; bool savestate_auto_index = settings->bools.savestate_auto_index; bool show_hidden_files = settings->bools.show_hidden_files; if (!global || !savestate_auto_index) return; state_dir[0] = state_base[0] = '\0'; /* Find the file in the same directory as global->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, global->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, global->name.savestate, sizeof(state_base)); for (i = 0; i < dir_list->size; i++) { unsigned idx; char elem_base[128] = {0}; 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("%s: #%d\n", msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT), max_idx); } void command_event_set_savestate_garbage_collect( const global_t *global, unsigned max_to_keep, bool show_hidden_files ) { size_t i, cnt = 0; char state_dir[PATH_MAX_LENGTH]; char state_base[PATH_MAX_LENGTH]; struct string_list *dir_list = NULL; unsigned min_idx = UINT_MAX; const char *oldest_save = NULL; state_dir[0] = '\0'; state_base[0] = '\0'; /* Similar to command_event_set_savestate_auto_index(), * this will find the lowest numbered save-state */ fill_pathname_basedir(state_dir, global->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, global->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; elem_base[0] = '\0'; if (string_is_empty(dir_elem)) continue; fill_pathname_base(elem_base, dir_elem, sizeof(elem_base)); /* Only consider files with a '.state' extension * > i.e. Ignore '.state.auto', '.state.bak', etc. */ ext = path_get_extension(elem_base); if (string_is_empty(ext) || !string_starts_with_size(ext, "state", STRLEN_CONST("state"))) continue; /* Check whether this file is associated with * the current content */ if (!string_starts_with(elem_base, state_base)) continue; /* This looks like a valid save */ cnt++; /* > Get index */ end = dir_elem + strlen(dir_elem); while ((end > dir_elem) && ISDIGIT((int)end[-1])) end--; idx = string_to_unsigned(end); /* > Check if this is the lowest index so far */ if (idx < min_idx) { min_idx = idx; oldest_save = dir_elem; } } /* Only delete one save state per save action * > Conservative behaviour, designed to minimise * the risk of deleting multiple incorrect files * in case of accident */ if (!string_is_empty(oldest_save) && (cnt > max_to_keep)) filestream_delete(oldest_save); dir_list_free(dir_list); } #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) bool command_set_shader(command_t *cmd, const char *arg) { enum rarch_shader_type type = video_shader_parse_type(arg); settings_t *settings = config_get_ptr(); if (!string_is_empty(arg)) { if (!video_shader_is_supported(type)) return false; /* rebase on shader directory */ if (!path_is_absolute(arg)) { static char abs_arg[PATH_MAX_LENGTH]; const char *ref_path = settings->paths.directory_video_shader; fill_pathname_join(abs_arg, ref_path, arg, sizeof(abs_arg)); /* TODO/FIXME - pointer to local variable - * making abs_arg static for now to workaround this */ arg = abs_arg; } } return apply_shader(settings, type, arg, true); } #endif #ifdef HAVE_CONFIGFILE bool command_event_save_core_config( const char *dir_menu_config, const char *rarch_path_config) { char msg[128]; char config_name[PATH_MAX_LENGTH]; char config_path[PATH_MAX_LENGTH]; char config_dir[PATH_MAX_LENGTH]; bool found_path = false; bool overrides_active = false; const char *core_path = NULL; runloop_state_t *runloop_st = runloop_state_get_ptr(); msg[0] = '\0'; config_dir[0] = '\0'; if (!string_is_empty(dir_menu_config)) strlcpy(config_dir, dir_menu_config, sizeof(config_dir)); else if (!string_is_empty(rarch_path_config)) /* Fallback */ fill_pathname_basedir(config_dir, rarch_path_config, sizeof(config_dir)); if (string_is_empty(config_dir)) { runloop_msg_queue_push(msg_hash_to_str(MSG_CONFIG_DIRECTORY_NOT_SET), 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_ERR("[Config]: %s\n", msg_hash_to_str(MSG_CONFIG_DIRECTORY_NOT_SET)); return false; } core_path = path_get(RARCH_PATH_CORE); config_name[0] = '\0'; config_path[0] = '\0'; /* Infer file name based on libretro core. */ if (path_is_valid(core_path)) { unsigned i; RARCH_LOG("%s\n", msg_hash_to_str(MSG_USING_CORE_NAME_FOR_NEW_CONFIG)); /* In case of collision, find an alternative name. */ for (i = 0; i < 16; i++) { char tmp[64]; fill_pathname_base_noext( config_name, core_path, sizeof(config_name)); fill_pathname_join(config_path, config_dir, config_name, sizeof(config_path)); if (i) snprintf(tmp, sizeof(tmp), "-%u.cfg", i); else { tmp[0] = '\0'; strlcpy(tmp, ".cfg", sizeof(tmp)); } strlcat(config_path, tmp, sizeof(config_path)); if (!path_is_valid(config_path)) { found_path = true; break; } } } if (!found_path) { /* Fallback to system time... */ RARCH_WARN("[Config]: %s\n", msg_hash_to_str(MSG_CANNOT_INFER_NEW_CONFIG_PATH)); fill_dated_filename(config_name, ".cfg", sizeof(config_name)); fill_pathname_join(config_path, config_dir, config_name, sizeof(config_path)); } if (runloop_st->overrides_active) { /* Overrides block config file saving, * make it appear as overrides weren't enabled * for a manual save. */ runloop_st->overrides_active = false; overrides_active = true; } #ifdef HAVE_CONFIGFILE command_event_save_config(config_path, msg, sizeof(msg)); #endif if (!string_is_empty(msg)) runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); runloop_st->overrides_active = overrides_active; return true; } void command_event_save_current_config(enum override_type type) { runloop_state_t *runloop_st = runloop_state_get_ptr(); switch (type) { case OVERRIDE_NONE: { if (path_is_empty(RARCH_PATH_CONFIG)) { char msg[128]; msg[0] = '\0'; strcpy_literal(msg, "[Config]: Config directory not set, cannot save configuration."); runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } else { char msg[256]; msg[0] = '\0'; command_event_save_config(path_get(RARCH_PATH_CONFIG), msg, sizeof(msg)); runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } } break; case OVERRIDE_GAME: case OVERRIDE_CORE: case OVERRIDE_CONTENT_DIR: { char msg[128]; msg[0] = '\0'; if (config_save_overrides(type, &runloop_st->system)) { strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_SAVED_SUCCESSFULLY), sizeof(msg)); /* set overrides to active so the original config can be restored after closing content */ runloop_st->overrides_active = true; } else strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_SAVING), sizeof(msg)); RARCH_LOG("[Config - Overrides]: %s\n", msg); runloop_msg_queue_push(msg, 1, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } break; } } #endif bool command_event_main_state(unsigned cmd) { retro_ctx_size_info_t info; char msg[128]; char state_path[16384]; const global_t *global = global_get_ptr(); settings_t *settings = config_get_ptr(); bool ret = false; bool push_msg = true; state_path[0] = msg[0] = '\0'; retroarch_get_current_savestate_path(state_path, sizeof(state_path)); core_serialize_size(&info); if (info.size) { switch (cmd) { case CMD_EVENT_SAVE_STATE: case CMD_EVENT_SAVE_STATE_TO_RAM: { video_driver_state_t *video_st = video_state_get_ptr(); bool savestate_auto_index = settings->bools.savestate_auto_index; unsigned savestate_max_keep = settings->uints.savestate_max_keep; bool frame_time_counter_reset_after_save_state = settings->bools.frame_time_counter_reset_after_save_state; if (cmd == CMD_EVENT_SAVE_STATE) content_save_state(state_path, true, false); else content_save_state_to_ram(); /* Clean up excess savestates if necessary */ if (savestate_auto_index && (savestate_max_keep > 0)) command_event_set_savestate_garbage_collect(global, settings->uints.savestate_max_keep, settings->bools.show_hidden_files ); if (frame_time_counter_reset_after_save_state) video_st->frame_time_count = 0; ret = true; push_msg = false; } break; case CMD_EVENT_LOAD_STATE: case CMD_EVENT_LOAD_STATE_FROM_RAM: { bool res = false; if (cmd == CMD_EVENT_LOAD_STATE) res = content_load_state(state_path, false, false); else res = content_load_state_from_ram(); if (res) { #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { rcheevos_pause_hardcore(); runloop_msg_queue_push(msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_DISABLED), 0, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } #endif ret = true; #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL); #endif { video_driver_state_t *video_st = video_state_get_ptr(); bool frame_time_counter_reset_after_load_state = settings->bools.frame_time_counter_reset_after_load_state; if (frame_time_counter_reset_after_load_state) video_st->frame_time_count = 0; } } } push_msg = false; break; case CMD_EVENT_UNDO_LOAD_STATE: command_event_undo_load_state(msg, sizeof(msg)); ret = true; break; case CMD_EVENT_UNDO_SAVE_STATE: command_event_undo_save_state(msg, sizeof(msg)); ret = true; break; } } else strlcpy(msg, msg_hash_to_str( MSG_CORE_DOES_NOT_SUPPORT_SAVESTATES), sizeof(msg)); if (push_msg) runloop_msg_queue_push(msg, 2, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); if (!string_is_empty(msg)) RARCH_LOG("%s\n", msg); return ret; } bool command_event_disk_control_append_image( const char *path) { runloop_state_t *runloop_st = runloop_state_get_ptr(); rarch_system_info_t *sys_info = runloop_st ? (rarch_system_info_t*)&runloop_st->system : NULL; if ( !sys_info || !disk_control_append_image(&sys_info->disk_control, path)) return false; #ifdef HAVE_THREADS if (runloop_st->use_sram) autosave_deinit(); #endif /* TODO/FIXME: Need to figure out what to do with subsystems case. */ if (path_is_empty(RARCH_PATH_SUBSYSTEM)) { /* Update paths for our new image. * If we actually use append_image, we assume that we * started out in a single disk case, and that this way * of doing it makes the most sense. */ path_set(RARCH_PATH_NAMES, path); runloop_path_fill_names(); } command_event(CMD_EVENT_AUTOSAVE_INIT, NULL); return true; } void command_event_reinit(const int flags) { settings_t *settings = config_get_ptr(); input_driver_state_t *input_st = input_state_get_ptr(); video_driver_state_t *video_st = video_state_get_ptr(); #ifdef HAVE_MENU gfx_display_t *p_disp = disp_get_ptr(); struct menu_state *menu_st = menu_state_get_ptr(); bool video_fullscreen = settings->bools.video_fullscreen; bool adaptive_vsync = settings->bools.video_adaptive_vsync; unsigned swap_interval = settings->uints.video_swap_interval; #endif enum input_game_focus_cmd_type game_focus_cmd = GAME_FOCUS_CMD_REAPPLY; const input_device_driver_t *joypad = input_st->primary_joypad; #ifdef HAVE_MFI const input_device_driver_t *sec_joypad = input_st->secondary_joypad; #else const input_device_driver_t *sec_joypad = NULL; #endif video_driver_reinit(flags); /* Poll input to avoid possibly stale data to corrupt things. */ if ( joypad && joypad->poll) joypad->poll(); if ( sec_joypad && sec_joypad->poll) sec_joypad->poll(); if ( input_st->current_driver && input_st->current_driver->poll) input_st->current_driver->poll(input_st->current_data); command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &game_focus_cmd); #ifdef HAVE_MENU p_disp->framebuf_dirty = true; if (video_fullscreen) video_driver_hide_mouse(); if ( menu_st->alive && video_st->current_video->set_nonblock_state) video_st->current_video->set_nonblock_state( video_st->data, false, video_driver_test_all_flags(GFX_CTX_FLAGS_ADAPTIVE_VSYNC) && adaptive_vsync, swap_interval); #endif }