mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
LOAD_CONTENT, DRIVERS_REINIT and their relatives reinitialise the input driver synchronously, and the input driver owns the stdin, network and UDS command objects - so the command frees the very object whose poll is dispatching it, and control returns into freed memory: the stdin poll wrote back into its buffer, the network poll read its socket again, the parser went on to the next line with a dead handle. The input driver now bumps a generation counter whenever it tears the interfaces down; the message parser stops after any action that changed it, the network and UDS polls stop reading for that pass, and the stdin poll takes the line off its buffer first and parses from a private copy, touching nothing of its object afterwards. Found by ASan when driving a content load over stdin; the same load and a following session now run clean.
2946 lines
90 KiB
C
2946 lines
90 KiB
C
/* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "input/input_driver.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stddef.h>
|
|
#include <locale.h>
|
|
#ifdef HAVE_NETWORKING
|
|
#include <net/net_compat.h>
|
|
#include <net/net_socket.h>
|
|
#endif
|
|
#include <lists/dir_list.h>
|
|
#include <file/file_path.h>
|
|
#include <streams/stdin_stream.h>
|
|
#include <streams/file_stream.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#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 "runloop.h"
|
|
#include "verbosity.h"
|
|
#include "version.h"
|
|
#include "version_git.h"
|
|
#include "tasks/task_content.h"
|
|
#include <compat/strl.h>
|
|
#ifdef __MACH__
|
|
#include <TargetConditionals.h>
|
|
#endif
|
|
|
|
#define CMD_BUF_SIZE 4096
|
|
|
|
static void command_post_state_loaded(void)
|
|
{
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_DISABLED);
|
|
rcheevos_pause_hardcore();
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 0, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
#endif
|
|
#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_auto_reset =
|
|
config_get_ptr()->bools.frame_time_counter_auto_reset;
|
|
if (frame_time_counter_auto_reset)
|
|
video_st->frame_time_count = 0;
|
|
}
|
|
#if defined(HAVE_GFX_WIDGETS) && defined(HAVE_SCREENSHOTS)
|
|
{
|
|
gfx_widget_state_slot_show(dispwidget_get_ptr(), NULL, NULL);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#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)
|
|
return false;
|
|
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] Command \"%s\" failed.\n", arg);
|
|
}
|
|
else
|
|
{
|
|
/* For MENU_TOGGLE, bypass the press-and-release mechanism
|
|
* by directly invoking the command event. This avoids
|
|
* timing issues with runahead (single-instance) where the
|
|
* 1-frame pulse from network commands can be lost. */
|
|
if (map[index].id == RARCH_MENU_TOGGLE)
|
|
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
|
|
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);
|
|
unsigned gen = input_driver_command_generation();
|
|
|
|
while (tok)
|
|
{
|
|
command_parse_sub_msg(handle, tok);
|
|
/* The command may have reinitialised the input driver, which
|
|
* owns and has now freed 'handle'. Nothing further may use it. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
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 *s, size_t len)
|
|
{
|
|
command_network_t *netcmd = (command_network_t*)cmd->userptr;
|
|
/* Respond (fire and forget since it's UDP) */
|
|
sendto(netcmd->net_fd, s, 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)
|
|
{
|
|
ssize_t ret;
|
|
unsigned gen = input_driver_command_generation();
|
|
char buf[2048];
|
|
command_network_t *netcmd = (command_network_t*)handle->userptr;
|
|
|
|
if (netcmd->net_fd < 0)
|
|
return;
|
|
|
|
for (;;)
|
|
{
|
|
netcmd->cmd_source_len = sizeof(netcmd->cmd_source);
|
|
|
|
if ((ret = recvfrom(netcmd->net_fd, buf, sizeof(buf) - 1, 0,
|
|
(struct sockaddr*)&netcmd->cmd_source,
|
|
&netcmd->cmd_source_len)) <= 0)
|
|
return;
|
|
|
|
buf[ret] = '\0';
|
|
|
|
command_parse_msg(handle, buf);
|
|
/* See command_generation: the command may have freed this
|
|
* object. The remaining datagrams wait for the next poll. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
}
|
|
}
|
|
|
|
command_t* command_network_new(uint16_t port)
|
|
{
|
|
struct addrinfo *res = NULL;
|
|
command_t *cmd = NULL;
|
|
command_network_t *netcmd = NULL;
|
|
int fd = -1;
|
|
|
|
/* Allocate-then-check in sequence, matching command_uds_new.
|
|
* The previous code allocated cmd + netcmd back-to-back and then
|
|
* assigned netcmd->net_fd / cmd->userptr before NULL-checking
|
|
* either pointer - an OOM on either calloc would NULL-deref on
|
|
* line 252/253. It also leaked netcmd on the '!cmd' failure
|
|
* path via the 'error' label (which now cleanly frees both
|
|
* because they are initialised to NULL up-front; free(NULL) is
|
|
* a no-op). */
|
|
if (!(cmd = (command_t*)calloc(1, sizeof(*cmd))))
|
|
goto error;
|
|
|
|
if (!(netcmd = (command_network_t*)calloc(1, sizeof(command_network_t))))
|
|
goto error;
|
|
|
|
fd = socket_init((void**)&res, port, NULL,
|
|
SOCKET_TYPE_DATAGRAM, AF_INET);
|
|
|
|
RARCH_LOG("[NetCMD] %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("[NetCMD] %s\n",
|
|
msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET));
|
|
goto error;
|
|
}
|
|
|
|
freeaddrinfo_retro(res);
|
|
return cmd;
|
|
|
|
error:
|
|
if (res)
|
|
freeaddrinfo_retro(res);
|
|
/* The only path that reaches 'error' with fd >= 0 is the
|
|
* socket_nonblock / socket_bind failure after fd was already
|
|
* stored in netcmd->net_fd. Close it so we don't leak the
|
|
* socket file descriptor along with the allocations. */
|
|
if (fd >= 0)
|
|
socket_close(fd);
|
|
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 *s, size_t len)
|
|
{
|
|
/* Just write to stdout! */
|
|
fwrite(s, 1, len, stdout);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void stdin_command_free(command_t *handle)
|
|
{
|
|
free(handle->userptr);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_stdin_poll(command_t *handle)
|
|
{
|
|
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)
|
|
{
|
|
char *last_newline = NULL;
|
|
stdincmd->stdin_buf_ptr += ret;
|
|
|
|
/* Ensure we don't write past buffer bounds */
|
|
if (stdincmd->stdin_buf_ptr >= CMD_BUF_SIZE)
|
|
stdincmd->stdin_buf_ptr = CMD_BUF_SIZE - 1;
|
|
|
|
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';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ptrdiff_t msg_len;
|
|
size_t rest;
|
|
char *msg;
|
|
*last_newline++ = '\0';
|
|
msg_len = last_newline - stdincmd->stdin_buf;
|
|
rest = stdincmd->stdin_buf_ptr - msg_len;
|
|
|
|
/* A command can free this object from under us (see
|
|
* command_generation): take the message off the buffer, tidy the
|
|
* buffer, and only then run the message from a private copy,
|
|
* touching nothing of this object afterwards. */
|
|
msg = strdup(stdincmd->stdin_buf);
|
|
memmove(stdincmd->stdin_buf, last_newline, rest);
|
|
stdincmd->stdin_buf_ptr = rest;
|
|
if (msg)
|
|
{
|
|
command_parse_msg(handle, msg);
|
|
free(msg);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
command_t* command_stdin_new(void)
|
|
{
|
|
command_t *cmd;
|
|
command_stdin_t *stdincmd;
|
|
|
|
#if !(defined(_WIN32) || defined(__EMSCRIPTEN__))
|
|
#ifdef HAVE_NETWORKING
|
|
if (!socket_nonblock(STDIN_FILENO))
|
|
return NULL;
|
|
#endif
|
|
#endif
|
|
|
|
/* Allocate in order with per-step failure handling. The earlier
|
|
* form allocated both cmd and stdincmd back-to-back then checked
|
|
* them, which leaked stdincmd on the '!cmd' failure path (the
|
|
* '!cmd' return simply dropped the stdincmd pointer). Also
|
|
* matches the command_uds_new pattern further down. */
|
|
if (!(cmd = (command_t*)calloc(1, sizeof(command_t))))
|
|
return NULL;
|
|
|
|
if (!(stdincmd = (command_stdin_t*)calloc(1, sizeof(command_stdin_t))))
|
|
{
|
|
free(cmd);
|
|
return NULL;
|
|
}
|
|
|
|
cmd->userptr = stdincmd;
|
|
cmd->poll = command_stdin_poll;
|
|
cmd->replier = stdin_command_reply;
|
|
cmd->destroy = stdin_command_free;
|
|
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
#if defined(__EMSCRIPTEN__)
|
|
#include "frontend/drivers/platform_emscripten.h"
|
|
typedef struct
|
|
{
|
|
char command_buf[CMD_BUF_SIZE];
|
|
} command_emscripten_t;
|
|
|
|
static void emscripten_command_reply(command_t *_cmd,
|
|
const char *s, size_t len)
|
|
{
|
|
platform_emscripten_command_reply(s, len);
|
|
}
|
|
|
|
static void emscripten_command_free(command_t *handle)
|
|
{
|
|
free(handle->userptr);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_emscripten_poll(command_t *handle)
|
|
{
|
|
command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr;
|
|
ptrdiff_t msg_len = platform_emscripten_command_read((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE);
|
|
if (msg_len != 0)
|
|
command_parse_msg(handle, emscriptencmd->command_buf);
|
|
}
|
|
|
|
command_t* command_emscripten_new(void)
|
|
{
|
|
command_t *cmd;
|
|
command_emscripten_t *emscriptencmd;
|
|
|
|
/* Same sequential-allocation-with-per-step-cleanup pattern as
|
|
* command_stdin_new / command_uds_new. Previously allocated both
|
|
* structs before any NULL check, leaking emscriptencmd on the
|
|
* '!cmd' failure path. */
|
|
if (!(cmd = (command_t*)calloc(1, sizeof(command_t))))
|
|
return NULL;
|
|
|
|
if (!(emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t))))
|
|
{
|
|
free(cmd);
|
|
return NULL;
|
|
}
|
|
|
|
cmd->userptr = emscriptencmd;
|
|
cmd->poll = command_emscripten_poll;
|
|
cmd->replier = emscripten_command_reply;
|
|
cmd->destroy = emscripten_command_free;
|
|
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
bool command_get_config_param(command_t *cmd, const char* arg)
|
|
{
|
|
size_t _len;
|
|
char reply[8192];
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char value_dynamic[256];
|
|
#endif
|
|
const char *value = "unsupported";
|
|
settings_t *settings = config_get_ptr();
|
|
bool video_fullscreen = settings->bools.video_fullscreen;
|
|
const char *dir_runtime_log = settings->paths.directory_runtime_log;
|
|
const char *log_dir = settings->paths.log_dir;
|
|
const char *directory_cache = settings->paths.directory_cache;
|
|
const char *directory_system = settings->paths.directory_system;
|
|
const char *path_username = settings->paths.username;
|
|
if (memcmp(arg, "video_fullscreen", sizeof("video_fullscreen")) == 0)
|
|
{
|
|
if (video_fullscreen)
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
else if (memcmp(arg, "savefile_directory", sizeof("savefile_directory")) == 0)
|
|
value = dir_get_ptr(RARCH_DIR_SAVEFILE);
|
|
else if (memcmp(arg, "savestate_directory", sizeof("savestate_directory")) == 0)
|
|
value = dir_get_ptr(RARCH_DIR_SAVESTATE);
|
|
else if (memcmp(arg, "runtime_log_directory", sizeof("runtime_log_directory")) == 0)
|
|
value = dir_runtime_log;
|
|
else if (memcmp(arg, "log_dir", sizeof("log_dir")) == 0)
|
|
value = log_dir;
|
|
else if (memcmp(arg, "cache_directory", sizeof("cache_directory")) == 0)
|
|
value = directory_cache;
|
|
else if (memcmp(arg, "system_directory", sizeof("system_directory")) == 0)
|
|
value = directory_system;
|
|
else if (memcmp(arg, "netplay_nickname", sizeof("netplay_nickname")) == 0)
|
|
value = path_username;
|
|
#ifdef HAVE_BSV_MOVIE
|
|
else if (memcmp(arg, "active_replay", sizeof("active_replay")) == 0)
|
|
{
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
value = value_dynamic;
|
|
value_dynamic[0] = '\0';
|
|
if (input_st->bsv_movie_state_handle)
|
|
{
|
|
bsv_movie_t *movie = input_st->bsv_movie_state_handle;
|
|
snprintf(value_dynamic, sizeof(value_dynamic), "%lld %u %lld",
|
|
(long long)(movie->identifier),
|
|
input_st->bsv_movie_state.flags,
|
|
(long long)(movie->frame_counter));
|
|
}
|
|
else
|
|
strlcpy_lit(value_dynamic, "0 0 0", sizeof(value_dynamic));
|
|
}
|
|
#endif
|
|
#ifdef HAVE_MENU
|
|
else if (memcmp(arg, "menu_active", sizeof("menu_active")) == 0)
|
|
{
|
|
struct menu_state* menu_st = menu_state_get_ptr();
|
|
if (menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE))
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
#endif
|
|
#ifdef HAVE_CHEEVOS
|
|
else if (memcmp(arg, "cheevos_enable", sizeof("cheevos_enable")) == 0)
|
|
{
|
|
if (settings->bools.cheevos_enable)
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
#endif
|
|
/* TODO: query any string */
|
|
_len = strlcpy_lit(reply, "GET_CONFIG_PARAM ", sizeof(reply));
|
|
_len += strlcpy(reply + _len, arg, sizeof(reply) - _len);
|
|
reply[ _len] = ' ';
|
|
reply[++_len] = '\0';
|
|
_len += strlcpy(reply + _len, value, sizeof(reply) - _len);
|
|
cmd->replier(cmd, reply, _len);
|
|
return true;
|
|
}
|
|
|
|
#if defined(HAVE_LAKKA)
|
|
#include <sys/un.h>
|
|
#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 *s, size_t len)
|
|
{
|
|
command_uds_t *subcmd = (command_uds_t*)cmd->userptr;
|
|
write(subcmd->last_fd, s, 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)
|
|
{
|
|
unsigned gen = input_driver_command_generation();
|
|
int i;
|
|
int fd;
|
|
ssize_t ret;
|
|
char buf[2048];
|
|
command_uds_t *udscmd = (command_uds_t*)handle->userptr;
|
|
|
|
if (udscmd->sfd < 0)
|
|
return;
|
|
|
|
/* Read data from clients and process commands */
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
{
|
|
bool err = false;
|
|
|
|
fd = udscmd->userfd[i];
|
|
if (fd < 0)
|
|
continue;
|
|
|
|
ret = socket_receive_all_nonblocking(fd, &err, buf, sizeof(buf) - 1);
|
|
if (!ret)
|
|
continue;
|
|
|
|
if (!err)
|
|
{
|
|
buf[ret] = '\0';
|
|
udscmd->last_fd = fd;
|
|
|
|
command_parse_msg(handle, buf);
|
|
/* See command_generation: this object may be gone now. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
socket_close(fd);
|
|
udscmd->userfd[i] = -1;
|
|
}
|
|
}
|
|
|
|
/* Accepts new connections from clients */
|
|
fd = accept(udscmd->sfd, NULL, NULL);
|
|
if (fd >= 0)
|
|
{
|
|
if (socket_nonblock(fd))
|
|
{
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
{
|
|
if (udscmd->userfd[i] < 0)
|
|
{
|
|
udscmd->userfd[i] = fd;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
socket_close(fd);
|
|
}
|
|
}
|
|
|
|
command_t* command_uds_new(void)
|
|
{
|
|
int i;
|
|
command_t *cmd;
|
|
command_uds_t *subcmd;
|
|
struct sockaddr_un addr;
|
|
socklen_t addrsz = offsetof(struct sockaddr_un, sun_path) + STRLEN_CONST("retroarch/cmd") + 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;
|
|
strlcpy_lit(&addr.sun_path[1], "retroarch/cmd", sizeof(addr.sun_path) - 1);
|
|
if ( bind(fd, (struct sockaddr*)&addr, addrsz) < 0
|
|
|| listen(fd, MAX_USER_CONNECTIONS) < 0
|
|
|| !socket_nonblock(fd))
|
|
{
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
cmd = (command_t*)calloc(1, sizeof(command_t));
|
|
if (!cmd)
|
|
{
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
subcmd = (command_uds_t*)calloc(1, sizeof(command_uds_t));
|
|
if (!subcmd)
|
|
{
|
|
free(cmd);
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
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)
|
|
{
|
|
size_t i;
|
|
if (command_get_arg(cmd, NULL, NULL))
|
|
return true;
|
|
|
|
RARCH_ERR("[NetCMD] Command \"%s\" is not recognized by the program.\n", cmd);
|
|
RARCH_ERR("[NetCMD] \tValid commands:\n");
|
|
for (i = 0; i < ARRAY_SIZE(map); i++)
|
|
RARCH_ERR("[NetCMD] \t\t%s\n", map[i].str);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(action_map); i++)
|
|
RARCH_ERR("[NetCMD] \t\t%s %s\n", action_map[i].str, action_map[i].arg_desc);
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool udp_send_packet(const char *host, uint16_t port, const char *msg)
|
|
{
|
|
char port_buf[6];
|
|
const struct addrinfo *tmp_info;
|
|
struct addrinfo *addr = NULL;
|
|
struct addrinfo hints = {0};
|
|
size_t len = strlen(msg);
|
|
bool ret = false;
|
|
|
|
snprintf(port_buf, sizeof(port_buf), "%hu", (unsigned short)port);
|
|
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
hints.ai_flags = AI_NUMERICSERV;
|
|
|
|
if (getaddrinfo_retro(host, port_buf, &hints, &addr))
|
|
return false;
|
|
if (!addr)
|
|
return false;
|
|
|
|
/* Send to all possible targets. */
|
|
tmp_info = addr;
|
|
|
|
do
|
|
{
|
|
int fd = socket(tmp_info->ai_family,
|
|
tmp_info->ai_socktype, tmp_info->ai_protocol);
|
|
|
|
if (fd < 0)
|
|
continue;
|
|
|
|
if (sendto(fd, msg, len, 0, tmp_info->ai_addr, tmp_info->ai_addrlen) ==
|
|
(ssize_t)len)
|
|
ret = true;
|
|
|
|
socket_close(fd);
|
|
} while ((tmp_info = tmp_info->ai_next));
|
|
|
|
freeaddrinfo_retro(addr);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool command_network_send(const char *cmd_)
|
|
{
|
|
char buf[4096];
|
|
char *ptr;
|
|
const char *cmd = NULL;
|
|
const char *host = NULL;
|
|
const char *port_str = NULL;
|
|
uint16_t port = DEFAULT_NETWORK_CMD_PORT;
|
|
size_t len;
|
|
|
|
if (!cmd_ || !*cmd_)
|
|
return false;
|
|
|
|
len = strlen(cmd_);
|
|
if (len >= sizeof(buf))
|
|
return false;
|
|
|
|
if (!network_init())
|
|
return false;
|
|
|
|
memcpy(buf, cmd_, len + 1);
|
|
|
|
cmd = buf;
|
|
ptr = strchr(buf, ';');
|
|
if (ptr)
|
|
{
|
|
*ptr++ = '\0';
|
|
host = ptr;
|
|
ptr = strchr(ptr, ';');
|
|
if (ptr)
|
|
{
|
|
*ptr++ = '\0';
|
|
port_str = ptr;
|
|
}
|
|
}
|
|
|
|
if (!cmd || !*cmd)
|
|
return false;
|
|
|
|
if (!host || !*host)
|
|
{
|
|
#ifdef _WIN32
|
|
host = "127.0.0.1";
|
|
#else
|
|
host = "localhost";
|
|
#endif
|
|
}
|
|
|
|
if (port_str && *port_str)
|
|
{
|
|
unsigned long val = strtoul(port_str, NULL, 0);
|
|
if (val > 0 && val <= 0xFFFF)
|
|
port = (uint16_t)val;
|
|
}
|
|
|
|
RARCH_LOG("[NetCMD] %s: \"%s\" to %s:%hu.\n",
|
|
msg_hash_to_str(MSG_SENDING_COMMAND),
|
|
cmd, host, (unsigned short)port);
|
|
|
|
if (!command_verify(cmd))
|
|
return false;
|
|
|
|
return udp_send_packet(host, port, cmd);
|
|
}
|
|
#endif
|
|
|
|
bool command_show_osd_msg(command_t *cmd, const char* arg)
|
|
{
|
|
runloop_msg_queue_push(arg, strlen(arg), 1, 180, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool command_load_state_slot(command_t *cmd, const char *arg)
|
|
{
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
_len = strlcpy_lit(reply, "LOAD_STATE_SLOT ", sizeof(reply));
|
|
_len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot);
|
|
runloop_get_savestate_path(state_path, sizeof(state_path), slot);
|
|
/* For LOADING, an existing state file outranks metadata and
|
|
* save-capability probes: core_serialize_size() measures whether the
|
|
* core can SAVE right now (0 at e.g. a game's own main menu), which
|
|
* says nothing about whether it can restore. Let the load task and
|
|
* retro_unserialize() arbitrate. */
|
|
if (!savestates_enabled)
|
|
savestates_enabled = path_is_valid(state_path);
|
|
if (savestates_enabled)
|
|
{
|
|
if ((ret = content_load_state(state_path, false, false)))
|
|
command_post_state_loaded();
|
|
}
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_save_state_slot(command_t* cmd, const char* arg)
|
|
{
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
_len = strlcpy_lit(reply, "SAVE_STATE_SLOT ", sizeof(reply));
|
|
_len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot);
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_savestate_path(state_path, sizeof(state_path), slot);
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
if (savestates_enabled)
|
|
ret = content_save_state(state_path, true);
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_play_replay_slot(command_t *cmd, const char *arg)
|
|
{
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char replay_path[16384];
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
replay_path[0] = '\0';
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_replay_path(replay_path, sizeof(replay_path), slot);
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
if (savestates_enabled)
|
|
{
|
|
ret = movie_start_playback(input_state_get_ptr(), replay_path);
|
|
if (ret)
|
|
{
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
/* The reply carries the replay handle, which the movie
|
|
* task's callback installs, so this still waits - but only
|
|
* for that task. A NULL condition means "until the queue
|
|
* is empty", which made a network command block on every
|
|
* unrelated scan or download in flight. */
|
|
task_queue_wait(movie_playback_start_in_progress, NULL);
|
|
if (input_st->bsv_movie_state_next_handle)
|
|
snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT %lld", (long long)(input_st->bsv_movie_state_next_handle->identifier));
|
|
else
|
|
snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT 0");
|
|
command_post_state_loaded();
|
|
}
|
|
}
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, strlen(reply));
|
|
return ret;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool command_seek_replay(command_t *cmd, const char *arg)
|
|
{
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char reply[32];
|
|
char *endptr;
|
|
size_t _len;
|
|
bool ret = true;
|
|
int64_t frame = strtoll(arg, &endptr, 10);
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
if (!endptr)
|
|
ret = false;
|
|
if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING)))
|
|
ret = false;
|
|
#ifdef HAVE_CHEEVOS
|
|
ret = !rcheevos_hardcore_active();
|
|
#endif
|
|
if (ret)
|
|
ret = movie_seek_to_frame(input_st, frame);
|
|
if (ret)
|
|
{
|
|
_len = strlcpy_lit(reply, "OK ", sizeof(reply));
|
|
_len += snprintf(reply+_len, sizeof(reply)-_len,
|
|
"%" PRId64, input_st->bsv_movie_state.seek_target_frame);
|
|
}
|
|
else
|
|
_len = strlcpy_lit(reply, "NO", sizeof(reply));
|
|
reply[_len] = '\n';
|
|
reply[++_len] = '\0';
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
#else
|
|
cmd->replier(cmd, "NO\n", 4);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool command_save_savefiles(command_t *cmd, const char* arg)
|
|
{
|
|
char reply[4];
|
|
bool ret;
|
|
size_t _len = strlcpy_lit(reply, "OK", sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
/* In the future, this should probably send each saved file path
|
|
to the replier. */
|
|
ret = command_event(CMD_EVENT_SAVE_FILES, NULL);
|
|
if (!ret)
|
|
strlcpy_lit(reply, "NO", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_load_savefiles(command_t *cmd, const char* arg)
|
|
{
|
|
char reply[4];
|
|
bool ret;
|
|
size_t _len = strlcpy_lit(reply, "OK", sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
ret = command_event(CMD_EVENT_LOAD_FILES, NULL);
|
|
if (!ret)
|
|
strlcpy_lit(reply, "NO", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
#if defined(HAVE_CHEEVOS)
|
|
bool command_read_ram(command_t *cmd, const char *arg)
|
|
{
|
|
unsigned int nbytes = 0;
|
|
unsigned int addr = -1;
|
|
|
|
char *end = NULL;
|
|
addr = (unsigned int)strtoul(arg, &end, 16);
|
|
if (end && *end == ' ')
|
|
nbytes = (unsigned int)strtoul(end + 1, NULL, 10);
|
|
|
|
if (end && *end == ' ' && nbytes > 0)
|
|
{
|
|
size_t _len = 0;
|
|
char *reply_at = NULL;
|
|
const uint8_t *data = NULL;
|
|
/* We allocate more than needed, saving 20 bytes is not really relevant */
|
|
unsigned int alloc_size = 40 + nbytes * 3;
|
|
char *reply = (char*)malloc(alloc_size);
|
|
|
|
if (!reply)
|
|
{
|
|
cmd->replier(cmd, "READ_CORE_RAM ERROR: OUT OF MEMORY\n", 34);
|
|
return true;
|
|
}
|
|
|
|
reply[0] = '\0';
|
|
reply_at = reply + snprintf(
|
|
reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr);
|
|
|
|
if ((data = rcheevos_patch_address(addr)))
|
|
{
|
|
size_t i;
|
|
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_lit(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("[Command] 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];
|
|
size_t _len = strlcpy(reply, PACKAGE_VERSION, sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
cmd->replier(cmd, reply, _len);
|
|
return true;
|
|
}
|
|
|
|
/* LOAD_CORE <core path>
|
|
*
|
|
* Mirrors selecting a core in the menu's core list / file browser,
|
|
* action_ok_load_core() -> generic_action_ok()'s ACTION_OK_LOAD_CORE
|
|
* branch. Same task_push_load_new_core() call with the same arguments;
|
|
* the result is propagated rather than discarded so a bad core path is
|
|
* reported as a failed command instead of a silent no-op. */
|
|
bool command_load_core(command_t *cmd, const char* arg)
|
|
{
|
|
content_ctx_info_t content_info = {0};
|
|
|
|
if (!arg || !*arg)
|
|
return false;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
{
|
|
char exp[PATH_MAX_LENGTH];
|
|
fill_pathname_expand_special(exp, arg, sizeof(exp));
|
|
return task_push_load_new_core(exp, NULL,
|
|
&content_info, CORE_TYPE_PLAIN, NULL, NULL);
|
|
}
|
|
#else
|
|
return task_push_load_new_core(arg, NULL,
|
|
&content_info, CORE_TYPE_PLAIN, NULL, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* START_CORE
|
|
*
|
|
* Mirrors the Main Menu "Start Core" entry, action_ok_start_core():
|
|
* clears the content path and starts the currently loaded core without
|
|
* content. The list_cache() call that entry makes first is menu
|
|
* animation bookkeeping and has no equivalent here. */
|
|
bool command_start_core(command_t *cmd, const char* arg)
|
|
{
|
|
content_ctx_info_t content_info = {0};
|
|
|
|
path_clear(RARCH_PATH_BASENAME);
|
|
|
|
return task_push_start_current_core(&content_info);
|
|
}
|
|
|
|
/* LOAD_CONTENT <core path>|<content path>
|
|
*
|
|
* The separator is '|' rather than a space because both operands are
|
|
* filesystem paths and may legitimately contain spaces. Routed through
|
|
* the companion UI entry point so that the command interface takes the
|
|
* same content load path as any other frontend-external requester,
|
|
* rather than duplicating the menu-only variant. */
|
|
bool command_load_content(command_t *cmd, const char* arg)
|
|
{
|
|
char core_path[PATH_MAX_LENGTH];
|
|
content_ctx_info_t content_info = {0};
|
|
const char *sep = NULL;
|
|
size_t _len = 0;
|
|
|
|
if (!arg || !*arg)
|
|
return false;
|
|
if (!(sep = strchr(arg, '|')))
|
|
return false;
|
|
|
|
_len = (size_t)(sep - arg);
|
|
if (_len == 0 || _len >= sizeof(core_path))
|
|
return false;
|
|
if (!*(sep + 1))
|
|
return false;
|
|
|
|
memcpy(core_path, arg, _len);
|
|
core_path[_len] = '\0';
|
|
|
|
#if TARGET_OS_IPHONE
|
|
{
|
|
char exp[PATH_MAX_LENGTH];
|
|
fill_pathname_expand_special(exp, sep + 1, sizeof(exp));
|
|
return task_push_load_content_with_new_core_from_companion_ui(
|
|
core_path, exp, NULL, NULL, NULL, &content_info, NULL, NULL);
|
|
}
|
|
#else
|
|
return task_push_load_content_with_new_core_from_companion_ui(
|
|
core_path, sep + 1, NULL, NULL, NULL, &content_info, NULL, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* CLOSE_CONTENT
|
|
*
|
|
* On HAVE_MENU builds this is CMD_EVENT_CLOSE_CONTENT, the same event the
|
|
* Quick Menu "Close Content" entry issues.
|
|
*
|
|
* On builds without a menu, CMD_EVENT_CLOSE_CONTENT is defined as
|
|
* CMD_EVENT_QUIT -- reasonable for a hotkey on a frontend that has
|
|
* nowhere to return to, but wrong here: a command interface consumer
|
|
* asking to close content is not asking to terminate the process, and
|
|
* has no way to discover that the two are the same on this build. Use
|
|
* CMD_EVENT_UNLOAD_CORE instead, which unloads the core and starts the
|
|
* dummy core. That is as close to "content closed" as a menuless build
|
|
* gets, and it leaves the process alive to accept further commands.
|
|
*
|
|
* This was previously a map[] entry driving RARCH_CLOSE_CONTENT_KEY,
|
|
* which is not what the menu entry does either: the hotkey path in
|
|
* runloop_iterate() also applies the 'confirm_close' double press timer,
|
|
* and nothing external can produce the second press inside the window,
|
|
* so with that setting enabled the command was silently swallowed. The
|
|
* menu entry's confirmation is a dialog rather than a timer and is
|
|
* equally unreachable over the command interface, so neither form of
|
|
* confirmation applies on this path. */
|
|
bool command_close_content(command_t *cmd, const char* arg)
|
|
{
|
|
#ifdef HAVE_MENU
|
|
return command_event(CMD_EVENT_CLOSE_CONTENT, NULL);
|
|
#else
|
|
return command_event(CMD_EVENT_UNLOAD_CORE, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* UNLOAD_CORE
|
|
*
|
|
* CMD_EVENT_UNLOAD_CORE releases the core and starts the dummy core, and
|
|
* is not menu dependent. The Main Menu "Unload Core" entry issues the
|
|
* same event and then clears the last core path; that clear is a
|
|
* frontend path operation rather than a menu one, so it belongs here
|
|
* too. Only the entry refresh afterwards is menu state. */
|
|
bool command_unload_core(command_t *cmd, const char* arg)
|
|
{
|
|
if (!command_event(CMD_EVENT_UNLOAD_CORE, NULL))
|
|
return false;
|
|
|
|
path_clear(RARCH_PATH_CORE_LAST);
|
|
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
if (menu_st)
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
/* VIDEO_REINIT / AUDIO_REINIT / DRIVERS_REINIT
|
|
*
|
|
* Driver reinit, split by scope. None has a menu equivalent; they exist
|
|
* so that a driver teardown and rebuild can be triggered on its own,
|
|
* rather than only as a side effect of loading content.
|
|
*
|
|
* CMD_EVENT_REINIT takes an optional driver mask through its data
|
|
* pointer and falls back to DRIVERS_CMD_ALL when passed NULL, so
|
|
* VIDEO_REINIT passes DRIVER_VIDEO_MASK explicitly rather than relying on
|
|
* the default -- otherwise it would tear down audio, input, MIDI and the
|
|
* rest as well, and would not be a video reinit at all. A video-only
|
|
* mask through video_driver_reinit() is what the CRT switch path already
|
|
* does. DRIVERS_REINIT keeps the all-drivers behaviour under a name that
|
|
* says so. */
|
|
bool command_video_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
int flags = DRIVER_VIDEO_MASK;
|
|
command_event(CMD_EVENT_REINIT, &flags);
|
|
return true;
|
|
}
|
|
|
|
bool command_audio_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
return command_event(CMD_EVENT_AUDIO_REINIT, NULL);
|
|
}
|
|
|
|
bool command_drivers_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
command_event(CMD_EVENT_REINIT, NULL);
|
|
return true;
|
|
}
|
|
|
|
static const rarch_memory_descriptor_t* command_memory_get_descriptor(const rarch_memory_map_t* mmap, unsigned address, size_t* offset)
|
|
{
|
|
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)
|
|
{
|
|
*offset = address - desc->core.start;
|
|
return desc;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* otherwise, attempt to match the address by matching the select bits */
|
|
if (((desc->core.start ^ address) & desc->core.select) == 0)
|
|
{
|
|
/* adjust the address to the start of the descriptor */
|
|
unsigned desc_offset = address - (unsigned)desc->core.start;
|
|
|
|
/* address is unsigned. we only need that much of the disconnect mask */
|
|
unsigned mask = (unsigned)desc->core.disconnect;
|
|
|
|
/* this magic logic is copied from mmap_reduce. it removes any bits from
|
|
* address that are non-zero in the disconnect field. bits above the
|
|
* removed bits are shifted down to fill the gap. */
|
|
while (mask)
|
|
{
|
|
const unsigned tmp = (mask - 1) & ~mask;
|
|
desc_offset = (desc_offset & tmp) | ((desc_offset >> 1) & ~tmp);
|
|
mask = (mask & (mask - 1)) >> 1;
|
|
}
|
|
|
|
/* we've calculated the actual offset of the data within the descriptor */
|
|
*offset = desc_offset;
|
|
|
|
/* sanity check - make sure the descriptor is large enough to hold the target address */
|
|
if (desc_offset < desc->core.len)
|
|
return desc;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static uint8_t *command_memory_get_pointer(
|
|
const rarch_system_info_t* sys_info,
|
|
unsigned address, unsigned int* max_bytes,
|
|
int for_write, char *s, size_t len)
|
|
{
|
|
if (!sys_info || sys_info->mmaps.num_descriptors == 0)
|
|
strlcpy_lit(s, " -1 no memory map defined\n", len);
|
|
else
|
|
{
|
|
size_t offset;
|
|
const rarch_memory_descriptor_t* desc = command_memory_get_descriptor(&sys_info->mmaps, address, &offset);
|
|
if (!desc)
|
|
strlcpy_lit(s, " -1 no descriptor for address\n", len);
|
|
else if (!desc->core.ptr)
|
|
strlcpy_lit(s, " -1 no data for descriptor\n", len);
|
|
else if (for_write && (desc->core.flags & RETRO_MEMDESC_CONST))
|
|
strlcpy_lit(s, " -1 descriptor data is readonly\n", len);
|
|
else
|
|
{
|
|
*max_bytes = (unsigned int)(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)
|
|
{
|
|
size_t _len;
|
|
char reply[4096];
|
|
uint8_t flags;
|
|
|
|
if (!cmd)
|
|
return false;
|
|
|
|
if (!cmd->replier)
|
|
return false;
|
|
|
|
flags = content_get_flags();
|
|
|
|
if (flags & CONTENT_ST_FLAG_IS_INITED)
|
|
{
|
|
/* add some content info */
|
|
core_info_t *core_info = NULL;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *basename_path = NULL;
|
|
|
|
if (!runloop_st)
|
|
{
|
|
_len = strlcpy_lit(reply, "GET_STATUS ERROR", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return false;
|
|
}
|
|
|
|
core_info_get_current_core(&core_info);
|
|
|
|
_len = 0;
|
|
strlcpy_append(reply, sizeof(reply), &_len, "GET_STATUS ");
|
|
|
|
if (runloop_st->flags & RUNLOOP_FLAG_PAUSED)
|
|
strlcpy_append(reply, sizeof(reply), &_len, "PAUSED");
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "PLAYING");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, " ");
|
|
|
|
if (core_info && core_info->system_id)
|
|
strlcpy_append(reply, sizeof(reply), &_len, core_info->system_id);
|
|
else if (runloop_st->system.info.library_name)
|
|
strlcpy_append(reply, sizeof(reply), &_len,
|
|
runloop_st->system.info.library_name);
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, ",");
|
|
|
|
basename_path = path_get(RARCH_PATH_BASENAME);
|
|
if (basename_path)
|
|
{
|
|
const char *basename = path_basename(basename_path);
|
|
if (basename)
|
|
strlcpy_append(reply, sizeof(reply), &_len, basename);
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
}
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, "\n");
|
|
}
|
|
else
|
|
_len = strlcpy_lit(reply, "GET_STATUS CONTENTLESS", sizeof(reply));
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
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* sys_info= &runloop_st->system;
|
|
|
|
{
|
|
char *end = NULL;
|
|
address = (unsigned int)strtoul(arg, &end, 16);
|
|
if (!(end && *end == ' '))
|
|
return false;
|
|
nbytes = (unsigned int)strtoul(end + 1, NULL, 10);
|
|
if (nbytes == 0)
|
|
return false;
|
|
}
|
|
|
|
/* Ensure large enough to return all requested bytes or an error message */
|
|
alloc_size = 64 + nbytes * 3;
|
|
reply = (char*)malloc(alloc_size);
|
|
|
|
if (!reply)
|
|
{
|
|
cmd->replier(cmd, "READ_CORE_MEMORY ERROR: OUT OF MEMORY\n", 37);
|
|
return true;
|
|
}
|
|
|
|
reply_at = reply + snprintf(reply, alloc_size - 1, "READ_CORE_MEMORY %x", address);
|
|
|
|
if ((data = command_memory_get_pointer(
|
|
sys_info, address, &max_bytes,
|
|
0, reply_at, alloc_size - strlen(reply))))
|
|
{
|
|
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
|
|
*sys_info = &runloop_st->system;
|
|
char *reply_at = reply + snprintf(reply, sizeof(reply) - 1, "WRITE_CORE_MEMORY %x", address);
|
|
uint8_t *data = command_memory_get_pointer(sys_info, 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("[Command] 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)
|
|
{
|
|
size_t _len;
|
|
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);
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME),
|
|
sizeof(msg));
|
|
_len += strlcpy_lit(msg + _len, ": ", sizeof(msg) - _len);
|
|
_len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
|
|
new_volume);
|
|
_len += strlcpy_lit(msg + _len, " dB", sizeof(msg) - _len);
|
|
|
|
#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, _len, 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)
|
|
{
|
|
size_t _len;
|
|
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);
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME),
|
|
sizeof(msg));
|
|
_len += strlcpy_lit(msg + _len, ": ", sizeof(msg) - _len);
|
|
_len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
|
|
new_volume);
|
|
_len += strlcpy_lit(msg + _len, " dB", sizeof(msg) - _len);
|
|
runloop_msg_queue_push(msg, _len, 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 *sys_info,
|
|
settings_t *settings, unsigned num_active_users)
|
|
{
|
|
unsigned port;
|
|
unsigned num_core_ports = sys_info->ports.size;
|
|
|
|
for (port = 0; port < num_core_ports; port++)
|
|
{
|
|
size_t 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(
|
|
&sys_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
|
|
static size_t command_event_save_config(const char *config_path,
|
|
char *s, size_t len)
|
|
{
|
|
size_t _len = 0;
|
|
bool path_exists = *config_path;
|
|
const char *str = path_exists ? config_path :
|
|
path_get(RARCH_PATH_CONFIG);
|
|
|
|
/* Workaround for libdecor 0.2.0 setting unwanted locale */
|
|
#if defined(HAVE_WAYLAND) && defined(HAVE_DYNAMIC)
|
|
setlocale(LC_NUMERIC,"C");
|
|
#endif
|
|
if (path_exists && config_save_file(config_path))
|
|
{
|
|
#if TARGET_OS_IPHONE
|
|
char tmp[PATH_MAX_LENGTH] = {0};
|
|
fill_pathname_abbreviate_special(tmp, config_path, sizeof(tmp));
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO),
|
|
tmp);
|
|
#else
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO),
|
|
config_path);
|
|
#endif
|
|
RARCH_LOG("[Config] %s\n", s);
|
|
return _len;
|
|
}
|
|
|
|
if (str && *str)
|
|
{
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_FAILED_SAVING_CONFIG_TO),
|
|
str);
|
|
RARCH_ERR("[Config] %s\n", s);
|
|
}
|
|
|
|
return _len;
|
|
}
|
|
#endif
|
|
|
|
static size_t command_event_undo_save_state(char *s, size_t len)
|
|
{
|
|
if (content_undo_save_buf_is_empty())
|
|
{
|
|
enum msg_hash_enums msg = content_undo_save_disabled()
|
|
? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO
|
|
: MSG_NO_SAVE_STATE_HAS_BEEN_OVERWRITTEN_YET;
|
|
return strlcpy(s, msg_hash_to_str(msg), len);
|
|
}
|
|
if (!content_undo_save_state())
|
|
return 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);
|
|
}
|
|
|
|
static size_t command_event_undo_load_state(char *s, size_t len)
|
|
{
|
|
if (content_undo_load_buf_is_empty())
|
|
{
|
|
enum msg_hash_enums msg = content_undo_save_disabled()
|
|
? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO
|
|
: MSG_NO_STATE_HAS_BEEN_LOADED_YET;
|
|
return strlcpy(s, msg_hash_to_str(msg), len);
|
|
}
|
|
if (!content_undo_load_state())
|
|
return snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_FAILED_TO_UNDO_LOAD_STATE),
|
|
"RAM");
|
|
#ifdef HAVE_NETWORKING
|
|
netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL);
|
|
#endif
|
|
return 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_uint(settings, settings->uints.video_scale, window_scale);
|
|
|
|
if (!video_fullscreen)
|
|
command_event(CMD_EVENT_REINIT, NULL);
|
|
|
|
retroarch_ctl(RARCH_CTL_SET_WINDOWED_SCALE, &idx);
|
|
|
|
return true;
|
|
}
|
|
|
|
size_t command_event_save_auto_state(void)
|
|
{
|
|
size_t _len;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_savestate = runloop_st->name.savestate;
|
|
char savestate_name_auto[PATH_MAX_LENGTH];
|
|
const char *a = NULL;
|
|
|
|
if (!core_info_current_supports_savestate())
|
|
return 0;
|
|
a = path_basename(path_get(RARCH_PATH_BASENAME));
|
|
if (!a || !*a)
|
|
return 0;
|
|
_len = strlcpy(savestate_name_auto, name_savestate,
|
|
sizeof(savestate_name_auto));
|
|
_len += strlcpy_lit(savestate_name_auto + _len, ".auto",
|
|
sizeof(savestate_name_auto) - _len);
|
|
if (content_auto_save_state((const char*)savestate_name_auto))
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO),
|
|
savestate_name_auto, "succeeded");
|
|
else
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO),
|
|
savestate_name_auto, "failed");
|
|
return _len;
|
|
}
|
|
|
|
#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
|
|
allow_cheats &= !(bsv_movie_data != 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(
|
|
config_get_ptr()->bools.notification_show_cheats_applied);
|
|
}
|
|
#endif
|
|
|
|
bool command_event_load_entry_state(settings_t *settings)
|
|
{
|
|
char entry_state_path[PATH_MAX_LENGTH] = "";
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
bool ret = false;
|
|
|
|
/* No early save-capability gate here: content_load_state() decides,
|
|
* and an existing entry-state file outranks stale metadata. */
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
return false;
|
|
#endif
|
|
#ifdef HAVE_NETWORKING
|
|
if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
|
|
return false;
|
|
#endif
|
|
|
|
if (!runloop_get_entry_state_path(
|
|
entry_state_path, sizeof(entry_state_path),
|
|
runloop_st->entry_state_slot))
|
|
return false;
|
|
|
|
if (!path_is_valid(entry_state_path))
|
|
{
|
|
if (!runloop_get_savestate_path(
|
|
entry_state_path, sizeof(entry_state_path),
|
|
runloop_st->entry_state_slot))
|
|
return false;
|
|
}
|
|
|
|
if (!path_is_valid(entry_state_path))
|
|
return false;
|
|
|
|
ret = content_load_state(entry_state_path, false, true);
|
|
|
|
RARCH_LOG("[State] %s \"%s\".\n",
|
|
msg_hash_to_str(MSG_FOUND_ENTRY_STATE_IN),
|
|
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");
|
|
|
|
return ret;
|
|
}
|
|
|
|
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;
|
|
|
|
/* No early save-capability gate here: content_load_state() decides,
|
|
* and an existing .auto state file outranks stale metadata. */
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
return false;
|
|
#endif
|
|
#ifdef HAVE_NETWORKING
|
|
if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
|
|
return false;
|
|
#endif
|
|
|
|
_len = strlcpy(savestate_name_auto, name_savestate,
|
|
sizeof(savestate_name_auto));
|
|
strlcpy_lit(savestate_name_auto + _len, ".auto",
|
|
sizeof(savestate_name_auto) - _len);
|
|
|
|
if (!path_is_valid(savestate_name_auto))
|
|
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");
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* 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 @s Return value for file name that should be removed.
|
|
*/
|
|
static void command_scan_states(
|
|
bool show_hidden_files,
|
|
unsigned savestate_max_keep,
|
|
int curr_state_slot,
|
|
unsigned *last_index, char *s)
|
|
{
|
|
/* Base name of 128 may be too short for some (<<1%) of the
|
|
tosec-based file names, but in practice truncating will not
|
|
lead to mismatch */
|
|
char state_base[128];
|
|
char state_dir[DIR_MAX_LENGTH];
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_savestate = runloop_st->name.savestate;
|
|
|
|
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;
|
|
|
|
fill_pathname_basedir(state_dir, name_savestate,
|
|
sizeof(state_dir));
|
|
|
|
if (!(dir_list = dir_list_new_special(state_dir,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(state_base, name_savestate, sizeof(state_base));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
size_t _len;
|
|
char elem_base[128];
|
|
const char *ext = NULL;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
|
|
if (!dir_elem || !*dir_elem)
|
|
continue;
|
|
|
|
_len = strlen(dir_elem);
|
|
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 ( (!ext || !*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 savestate */
|
|
/* Save filename root and length (once) */
|
|
if (savefile_root_length == 0)
|
|
{
|
|
savefile_root = dir_elem;
|
|
savefile_root_length = _len;
|
|
}
|
|
|
|
/* Decode the savestate index */
|
|
end = dir_elem + _len;
|
|
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
|
{
|
|
end--;
|
|
if (savefile_root == dir_elem)
|
|
savefile_root_length--;
|
|
}
|
|
idx = string_to_unsigned(end);
|
|
|
|
/* Simple administration: max, total. */
|
|
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)))
|
|
{
|
|
/* Gap index: lowest free slot in the wraparound range */
|
|
if (gap_idx == UINT_MAX)
|
|
gap_idx = (unsigned)i;
|
|
}
|
|
else /* Occupied save slots */
|
|
{
|
|
/* Del index: first occupied slot in the wraparound range,
|
|
after gap index */
|
|
if ( gap_idx < UINT_MAX && del_idx == UINT_MAX)
|
|
del_idx = (unsigned)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 */
|
|
/* Either gap is at the end of the range: wraparound.
|
|
or there is no better idea than the lowest index */
|
|
if (del_idx == UINT_MAX)
|
|
del_idx = 0;
|
|
/* Adjust load index */
|
|
if (gap_idx == 0)
|
|
loa_idx = savestate_max_keep;
|
|
else
|
|
loa_idx = gap_idx - 1;
|
|
}
|
|
|
|
RARCH_DBG("[State] Save state 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)
|
|
*last_index = loa_idx;
|
|
|
|
if ( s
|
|
&& cnt_in_range >= savestate_max_keep)
|
|
{
|
|
strlcpy(s, savefile_root, savefile_root_length + 1);
|
|
/* ".state0" is just ".state" instead, so don't print that. */
|
|
if (del_idx > 0)
|
|
snprintf(s + 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)
|
|
return 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)
|
|
{
|
|
int prev_slot = settings->ints.state_slot;
|
|
command_scan_states(
|
|
settings->bools.show_hidden_files,
|
|
settings->uints.savestate_max_keep,
|
|
settings->ints.state_slot, &max_idx, NULL);
|
|
configuration_set_int(settings, settings->ints.state_slot, max_idx);
|
|
RARCH_LOG("[State] %s: #%d (slot reset %d -> %u from on-disk scan, "
|
|
"max_keep %u). If the previous slot was higher, earlier saves "
|
|
"may be missing on disk.\n",
|
|
msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT),
|
|
max_idx, prev_slot, max_idx,
|
|
settings->uints.savestate_max_keep);
|
|
}
|
|
else
|
|
/* Reset savestate index to 0 when loading content. */
|
|
configuration_set_int(settings, settings->ints.state_slot, 0);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
size_t i;
|
|
char state_to_delete[PATH_MAX_LENGTH] = {0};
|
|
command_scan_states(
|
|
settings->bools.show_hidden_files,
|
|
settings->uints.savestate_max_keep,
|
|
settings->ints.state_slot, NULL, state_to_delete);
|
|
/* 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 (*state_to_delete)
|
|
{
|
|
filestream_delete(state_to_delete);
|
|
RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete);
|
|
/* Construct the save state thumbnail name
|
|
* and delete that one as well. */
|
|
i = strlen(state_to_delete);
|
|
strlcpy_lit(state_to_delete + i,".png",STRLEN_CONST(".png")+1);
|
|
filestream_delete(state_to_delete);
|
|
RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete);
|
|
}
|
|
}
|
|
|
|
void command_event_set_replay_auto_index(settings_t *settings)
|
|
{
|
|
size_t i;
|
|
char elem_base[128];
|
|
char state_base[128];
|
|
char state_dir[DIR_MAX_LENGTH];
|
|
|
|
struct string_list *dir_list = NULL;
|
|
unsigned max_idx = 0;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_replay = runloop_st->name.replay;
|
|
bool replay_auto_index = settings->bools.replay_auto_index;
|
|
bool show_hidden_files = settings->bools.show_hidden_files;
|
|
|
|
if (!replay_auto_index)
|
|
return;
|
|
/* Find the file in the same directory as runloop_st->name.replay
|
|
* with the largest numeral suffix.
|
|
*
|
|
* E.g. /foo/path/content.replay will try to find
|
|
* /foo/path/content.replay%d, where %d is the largest number available.
|
|
*/
|
|
fill_pathname_basedir(state_dir, name_replay,
|
|
sizeof(state_dir));
|
|
|
|
if (!(dir_list = dir_list_new_special(state_dir,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(state_base, name_replay, sizeof(state_base));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
size_t _len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
|
|
if (strstr(elem_base, state_base) != elem_base)
|
|
continue;
|
|
|
|
end = dir_elem + _len;
|
|
|
|
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.replay_slot, max_idx);
|
|
|
|
if (max_idx)
|
|
RARCH_LOG("[Replay] %s: #%d\n",
|
|
msg_hash_to_str(MSG_FOUND_LAST_REPLAY_SLOT),
|
|
max_idx);
|
|
}
|
|
|
|
void command_event_set_replay_garbage_collect(
|
|
unsigned max_to_keep,
|
|
bool show_hidden_files
|
|
)
|
|
{
|
|
/* TODO: debugme */
|
|
size_t i, cnt = 0;
|
|
char tmp[DIR_MAX_LENGTH];
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_replay = runloop_st->name.replay;
|
|
struct string_list *dir_list = NULL;
|
|
unsigned min_idx = UINT_MAX;
|
|
const char *oldest_save = NULL;
|
|
|
|
/* Similar to command_event_set_replay_auto_index(),
|
|
* this will find the lowest numbered replay */
|
|
fill_pathname_basedir(tmp, name_replay, sizeof(tmp));
|
|
|
|
if (!(dir_list = dir_list_new_special(tmp,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(tmp, name_replay, sizeof(tmp));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
size_t _len;
|
|
char elem_base[128];
|
|
const char *ext = NULL;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
|
|
if (!dir_elem || !*dir_elem)
|
|
continue;
|
|
|
|
_len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
|
|
/* Only consider files with a '.replayXX' extension
|
|
* > i.e. Ignore '.replay.auto', '.replay.bak', etc. */
|
|
ext = path_get_extension(elem_base);
|
|
if ( (!ext || !*ext)
|
|
|| !string_starts_with_size(ext, "replay", STRLEN_CONST("REPLAY")))
|
|
continue;
|
|
|
|
/* Check whether this file is associated with
|
|
* the current content */
|
|
if (!string_starts_with(elem_base, tmp))
|
|
continue;
|
|
|
|
/* This looks like a valid save */
|
|
cnt++;
|
|
|
|
/* > Get index */
|
|
end = dir_elem + _len;
|
|
|
|
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 (oldest_save && *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();
|
|
bool apply_new_shader = arg && *arg;
|
|
|
|
configuration_set_bool(settings, settings->bools.video_shader_enable, apply_new_shader);
|
|
if (apply_new_shader)
|
|
{
|
|
gfx_ctx_flags_t flags;
|
|
flags.flags = 0;
|
|
video_context_driver_get_flags(&flags);
|
|
|
|
if (!BIT32_GET(flags.flags, video_shader_type_to_flag(type)))
|
|
return false;
|
|
|
|
/* rebase on shader directory */
|
|
if (!path_is_absolute(arg))
|
|
{
|
|
char abs_arg[PATH_MAX_LENGTH];
|
|
const char *ref_path = settings->paths.directory_video_shader;
|
|
fill_pathname_join_special(abs_arg, ref_path, arg, sizeof(abs_arg));
|
|
return video_shader_apply_shader(settings, type, abs_arg, true);
|
|
}
|
|
}
|
|
|
|
return video_shader_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_dir[DIR_MAX_LENGTH];
|
|
char config_path[PATH_MAX_LENGTH];
|
|
char config_name[NAME_MAX_LENGTH];
|
|
size_t _len = 0;
|
|
bool new_path_available = false;
|
|
bool overrides_active = false;
|
|
const char *core_path = NULL;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
|
|
msg[0] = '\0';
|
|
|
|
if (dir_menu_config && *dir_menu_config)
|
|
_len = strlcpy(config_dir, dir_menu_config, sizeof(config_dir));
|
|
else if (rarch_path_config && *rarch_path_config) /* Fallback */
|
|
_len = fill_pathname_basedir(config_dir, rarch_path_config,
|
|
sizeof(config_dir));
|
|
|
|
if (_len == 0)
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_CONFIG_DIRECTORY_NOT_SET);
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
RARCH_ERR("[Config] %s\n", _msg);
|
|
return false;
|
|
}
|
|
|
|
core_path = path_get(RARCH_PATH_CORE);
|
|
|
|
/* Infer file name based on libretro core. */
|
|
if (path_is_valid(core_path))
|
|
{
|
|
unsigned i;
|
|
char tmp[PATH_MAX_LENGTH + 8];
|
|
RARCH_LOG("[Config] %s\n", msg_hash_to_str(MSG_USING_CORE_NAME_FOR_NEW_CONFIG));
|
|
|
|
fill_pathname(config_name, path_basename(core_path), "",
|
|
sizeof(config_name));
|
|
fill_pathname_join_special(config_path, config_dir, config_name,
|
|
sizeof(config_path));
|
|
|
|
/* In case of collision, find an alternative name. */
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
size_t __len = strlcpy(tmp, config_path, sizeof(tmp));
|
|
|
|
if (i)
|
|
__len += snprintf(tmp + __len, sizeof(tmp) - __len, "-%u", i);
|
|
strlcpy_lit(tmp + __len, ".cfg", sizeof(tmp) - __len);
|
|
|
|
if (!path_is_valid(tmp))
|
|
{
|
|
strlcpy(config_path, tmp, sizeof(config_path));
|
|
new_path_available = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!new_path_available)
|
|
{
|
|
/* 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_special(config_path, config_dir, config_name,
|
|
sizeof(config_path));
|
|
}
|
|
|
|
if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE)
|
|
{
|
|
/* Overrides block config file saving,
|
|
* make it appear as overrides weren't enabled
|
|
* for a manual save. */
|
|
runloop_st->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
overrides_active = true;
|
|
}
|
|
|
|
#ifdef HAVE_CONFIGFILE
|
|
_len = command_event_save_config(config_path, msg, sizeof(msg));
|
|
#endif
|
|
|
|
if (_len > 0)
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
if (overrides_active)
|
|
runloop_st->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
else
|
|
runloop_st->flags &= ~RUNLOOP_FLAG_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)
|
|
{
|
|
default:
|
|
case OVERRIDE_NONE:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
msg[0] = '\0';
|
|
|
|
if (path_is_empty(RARCH_PATH_CONFIG))
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy_lit(msg, "Config directory not set, cannot save configuration.", sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE)
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ACTIVE_NOT_SAVING), sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = command_event_save_config(path_get(RARCH_PATH_CONFIG), msg, sizeof(msg));
|
|
}
|
|
}
|
|
|
|
/* command_event_save_config() does its own logging */
|
|
if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR)
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
}
|
|
break;
|
|
case OVERRIDE_GAME:
|
|
case OVERRIDE_CORE:
|
|
case OVERRIDE_CONTENT_DIR:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
int8_t ret = config_save_overrides(type, &runloop_st->system, false, NULL);
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
switch (ret)
|
|
{
|
|
case 1:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = 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->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
break;
|
|
case -1:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_WARNING;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_NOT_SAVED), sizeof(msg));
|
|
break;
|
|
default:
|
|
case 0:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_SAVING), sizeof(msg));
|
|
break;
|
|
}
|
|
|
|
switch (msg_cat)
|
|
{
|
|
case MESSAGE_QUEUE_CATEGORY_ERROR:
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
break;
|
|
case MESSAGE_QUEUE_CATEGORY_WARNING:
|
|
RARCH_WARN("[Override] %s\n", msg);
|
|
break;
|
|
case MESSAGE_QUEUE_CATEGORY_SUCCESS:
|
|
default:
|
|
RARCH_LOG("[Override] %s\n", msg);
|
|
break;
|
|
}
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void command_event_remove_current_config(enum override_type type)
|
|
{
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
|
|
switch (type)
|
|
{
|
|
default:
|
|
case OVERRIDE_NONE:
|
|
break;
|
|
case OVERRIDE_GAME:
|
|
case OVERRIDE_CORE:
|
|
case OVERRIDE_CONTENT_DIR:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
if (config_save_overrides(type, &runloop_st->system, true, NULL))
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_REMOVED_SUCCESSFULLY), sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_REMOVING), sizeof(msg));
|
|
}
|
|
|
|
if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR)
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
else
|
|
RARCH_LOG("[Override] %s\n", msg);
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool command_event_main_state(unsigned cmd)
|
|
{
|
|
char msg[128] = "";
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
settings_t *settings = config_get_ptr();
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_current_savestate_path(state_path,
|
|
sizeof(state_path));
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
|
|
/* TODO: Load state should act in one of three ways:
|
|
- [X] Not during recording or playback: normally
|
|
- [-] During playback: If the state is part of this replay, go back to
|
|
that state and rewind the replay (not yet implemented); otherwise
|
|
halt playback and go to that state normally.
|
|
- [-] During recording: If the state is part of this replay, go back to
|
|
that state and rewind the replay, clobbering the stuff in between
|
|
then and now (not yet implemented); if the state is not part of
|
|
the replay, do nothing and log a warning.
|
|
*/
|
|
if (savestates_enabled)
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case CMD_EVENT_SAVE_STATE:
|
|
case CMD_EVENT_SAVE_STATE_TO_RAM:
|
|
{
|
|
/* TODO: Saving state during recording should associate
|
|
* the state with the replay. */
|
|
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_auto_reset =
|
|
settings->bools.frame_time_counter_auto_reset;
|
|
|
|
if (cmd == CMD_EVENT_SAVE_STATE)
|
|
{
|
|
bool queued = content_save_state(state_path, true);
|
|
RARCH_LOG("[State] save dispatch for slot %d, path "
|
|
"\"%s\": content_save_state queued=%s "
|
|
"(auto_index=%s, max_keep=%u). NOTE: actual disk "
|
|
"write is asynchronous; success is only known when "
|
|
"the save task completes.\n",
|
|
settings->ints.state_slot, state_path,
|
|
queued ? "yes" : "NO",
|
|
savestate_auto_index ? "on" : "off",
|
|
savestate_max_keep);
|
|
}
|
|
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(settings);
|
|
|
|
if (frame_time_counter_auto_reset)
|
|
video_st->frame_time_count = 0;
|
|
|
|
ret = true;
|
|
}
|
|
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)
|
|
{
|
|
command_post_state_loaded();
|
|
ret = true;
|
|
}
|
|
}
|
|
break;
|
|
case CMD_EVENT_UNDO_LOAD_STATE:
|
|
{
|
|
/* TODO: To support this through re-recording would take some
|
|
* care around moving the replay recording forward to the time
|
|
* when the undo happened, which would need undo support for
|
|
* replays. For now, forbid it during recording and halt
|
|
* playback. */
|
|
#ifdef HAVE_BSV_MOVIE
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING)
|
|
{
|
|
RARCH_ERR("[State] Can't undo load state during movie record.\n");
|
|
return false;
|
|
}
|
|
if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK)
|
|
{
|
|
RARCH_LOG("[State] Undo load state during movie playback, halting playback.\n");
|
|
movie_stop(input_st);
|
|
}
|
|
#endif
|
|
_len = command_event_undo_load_state(msg, sizeof(msg));
|
|
ret = true;
|
|
break;
|
|
}
|
|
case CMD_EVENT_UNDO_SAVE_STATE:
|
|
_len = command_event_undo_save_state(msg, sizeof(msg));
|
|
ret = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
_len = strlcpy(msg, msg_hash_to_str(
|
|
MSG_CORE_DOES_NOT_SUPPORT_SAVESTATES), sizeof(msg));
|
|
|
|
if (_len > 0)
|
|
{
|
|
runloop_msg_queue_push(msg, _len, 2, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
RARCH_LOG("[State] %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->flags & RUNLOOP_FLAG_USE_SRAM)
|
|
autosave_deinit();
|
|
#endif
|
|
|
|
command_event(CMD_EVENT_AUTOSAVE_INIT, NULL);
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Read-side callback for the snapshot phase of command_event_reinit.
|
|
* Receives the cached frame's pixels (or NULL if HW-render / no
|
|
* cached frame), copies into the static reuse buffer, and reports
|
|
* dims back to the caller via userdata. Skips the work entirely
|
|
* if data is NULL -- caller checks the reported size to know
|
|
* whether a snapshot was actually taken. */
|
|
struct command_reinit_snapshot_ctx
|
|
{
|
|
void **buf_p; /* static cached_snapshot in the caller */
|
|
size_t *cap_p; /* static cached_snapshot_cap in the caller */
|
|
unsigned w, h;
|
|
size_t p, size;
|
|
};
|
|
|
|
static void command_reinit_snapshot_cb(void *userdata,
|
|
const void *data,
|
|
unsigned width, unsigned height, size_t pitch)
|
|
{
|
|
struct command_reinit_snapshot_ctx *ctx
|
|
= (struct command_reinit_snapshot_ctx*)userdata;
|
|
size_t want;
|
|
|
|
if (!ctx || !data || !width || !height || !pitch)
|
|
return;
|
|
|
|
want = pitch * height;
|
|
if (want > *ctx->cap_p)
|
|
{
|
|
void *tmp = realloc(*ctx->buf_p, want);
|
|
if (!tmp)
|
|
return;
|
|
*ctx->buf_p = tmp;
|
|
*ctx->cap_p = want;
|
|
}
|
|
if (!*ctx->buf_p)
|
|
return;
|
|
|
|
memcpy(*ctx->buf_p, data, want);
|
|
ctx->w = width;
|
|
ctx->h = height;
|
|
ctx->p = pitch;
|
|
ctx->size = want;
|
|
}
|
|
|
|
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_config = 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
|
|
/* Snapshot the last cached core frame before tearing the video
|
|
* driver down. video_driver_free() invalidates the cache as
|
|
* part of the reinit cycle (the pointer was borrowed from the
|
|
* core's own framebuffer and isn't guaranteed to stay live
|
|
* across the driver swap), so without a snapshot the new
|
|
* driver would come up with no core image to replay. Restored
|
|
* + replayed below so the paused-core background remains
|
|
* visible when reinit is triggered from inside the menu (e.g.
|
|
* HDR mode toggle).
|
|
*
|
|
* The snapshot buffer is static and reused across reinits — we
|
|
* need it to outlive command_event_reinit because we publish
|
|
* the pointer back through video_driver_cached_frame_publish
|
|
* for the new driver to read via video_driver_cached_frame(),
|
|
* and the core's next real frame will replace the pointer at
|
|
* its leisure. Resizing in place on each call keeps it
|
|
* bounded at one buffer's worth.
|
|
*
|
|
* The actual copy happens inside the cached_frame_read
|
|
* callback (which is what this function was hand-rolling
|
|
* before the unified read API existed -- exactly the same
|
|
* pattern, lifted into one place). HW-render frames are
|
|
* skipped: the callback receives data == NULL for those, so
|
|
* we don't even allocate. */
|
|
static void *cached_snapshot = NULL;
|
|
static size_t cached_snapshot_cap = 0;
|
|
unsigned cached_snapshot_w = 0;
|
|
unsigned cached_snapshot_h = 0;
|
|
size_t cached_snapshot_p = 0;
|
|
size_t cached_snapshot_size = 0;
|
|
|
|
{
|
|
struct command_reinit_snapshot_ctx ctx;
|
|
ctx.buf_p = &cached_snapshot;
|
|
ctx.cap_p = &cached_snapshot_cap;
|
|
ctx.w = 0;
|
|
ctx.h = 0;
|
|
ctx.p = 0;
|
|
ctx.size = 0;
|
|
video_driver_cached_frame_read(&ctx, command_reinit_snapshot_cb);
|
|
cached_snapshot_w = ctx.w;
|
|
cached_snapshot_h = ctx.h;
|
|
cached_snapshot_p = ctx.p;
|
|
cached_snapshot_size = ctx.size;
|
|
}
|
|
(void)cached_snapshot_size;
|
|
|
|
video_driver_reinit(flags);
|
|
|
|
/* Restore the snapshot and ask the new driver to replay it so the
|
|
* paused-core background appears in the first post-reinit frame.
|
|
* The buffer stays live across subsequent frame_cb calls from the
|
|
* core (which overwrite the pointer) and is reused on the next
|
|
* reinit. The static cached_snapshot itself is not freed at
|
|
* shutdown - it's a one-shot leak bounded at one framebuffer's
|
|
* worth of memory, reclaimed by the OS on process exit. Adding
|
|
* a teardown hook would mean wiring command_event_reinit's
|
|
* statics into retroarch_deinit_drivers; the size cap makes the
|
|
* leak benign in practice, so we leave it. */
|
|
if (cached_snapshot_p && cached_snapshot_h)
|
|
{
|
|
video_driver_cached_frame_publish(cached_snapshot,
|
|
cached_snapshot_w, cached_snapshot_h, cached_snapshot_p);
|
|
|
|
#ifdef HAVE_MENU
|
|
/* If the menu is alive across the reinit, the runloop's
|
|
* pre-frame menu work (driver_ctx->render +
|
|
* set_texture_enable) doesn't get a chance to run before
|
|
* the cached_frame() replay below. Two consequences if we
|
|
* don't reproduce that work here:
|
|
*
|
|
* 1. The new video driver instance starts with its
|
|
* menu-texture flag (D3D11_ST_FLAG_MENU_ENABLE,
|
|
* GL2_FLAG_MENU_TEXTURE_ENABLE, etc.) cleared, so the
|
|
* replay frame falls into the driver's "else if
|
|
* (statistics_show)" branch and draws OSD stats on the
|
|
* bare core framebuffer instead of the menu overlay.
|
|
*
|
|
* 2. Menu drivers like ozone cache layout/font dimensions
|
|
* keyed on the previous viewport size and only
|
|
* recompute them when their render() callback notices a
|
|
* width/height change. If we replay before render()
|
|
* runs, the menu draws at the old (windowed) scale into
|
|
* the new (fullscreen) viewport, so fonts and widgets
|
|
* appear undersized for one frame until the next
|
|
* runloop iteration corrects them.
|
|
*
|
|
* Mirroring the runloop's pre-frame menu sequence here --
|
|
* render with the new dimensions, then raise the
|
|
* texture-enable flag -- keeps the snapshot replay
|
|
* visually consistent with subsequent frames.
|
|
*
|
|
* Gated on a valid snapshot: without one we won't be
|
|
* pushing a replay frame anyway, and calling render() on a
|
|
* freshly-(re)initialised menu driver before the runloop
|
|
* has had a chance to populate it can leave it in a
|
|
* partially-computed state for the next real frame
|
|
* (e.g. ozone clears OZONE_FLAG_NEED_COMPUTE after a
|
|
* premature render). */
|
|
if (menu_st->flags & MENU_ST_FLAG_ALIVE)
|
|
{
|
|
if ( menu_st->driver_ctx
|
|
&& menu_st->driver_ctx->render)
|
|
menu_st->driver_ctx->render(
|
|
menu_st->userdata,
|
|
video_st->width,
|
|
video_st->height,
|
|
false);
|
|
|
|
if ( video_st->poke
|
|
&& video_st->poke->set_texture_enable)
|
|
video_st->poke->set_texture_enable(video_st->data,
|
|
true, false);
|
|
}
|
|
#endif
|
|
|
|
video_driver_cached_frame();
|
|
}
|
|
|
|
/* 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->flags |= GFX_DISP_FLAG_FB_DIRTY;
|
|
if (video_fullscreen)
|
|
{
|
|
if ( video_st->poke
|
|
&& video_st->poke->show_mouse)
|
|
video_st->poke->show_mouse(video_st->data, false);
|
|
}
|
|
if ( (menu_st->flags & MENU_ST_FLAG_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,
|
|
runloop_get_video_swap_interval(swap_interval_config));
|
|
#endif
|
|
}
|